mirror of
https://github.com/owntracks/frontend.git
synced 2026-02-21 21:39:50 +00:00
Compare commits
26 Commits
v2.0.0-bet
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e63f01438 | ||
|
|
fb97cd080f | ||
|
|
f2d461d019 | ||
|
|
798a0af97d | ||
|
|
3c92a77847 | ||
|
|
b252a6580a | ||
|
|
9ce9933d11 | ||
|
|
f22db6301b | ||
|
|
3b18ab58ed | ||
|
|
bb81daaea4 | ||
|
|
f491d63eb9 | ||
|
|
e923fdc6c7 | ||
|
|
b1ce1297ed | ||
|
|
f4262efaa4 | ||
|
|
a2109a5802 | ||
|
|
00fbb7cd7c | ||
|
|
4078597f7a | ||
|
|
8dc9611a77 | ||
|
|
c1f58c992e | ||
|
|
6631929d6f | ||
|
|
36281db2e3 | ||
|
|
5a8d261943 | ||
|
|
7b83349dc8 | ||
|
|
a20fbf7e10 | ||
|
|
d2eafd5a4a | ||
|
|
871f9f0cb2 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: owntracks.org
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
Dates are in UTC.
|
||||
|
||||
## 2.0.0 (2020-03-01)
|
||||
|
||||
Stable release of v2, finally! 🎉
|
||||
|
||||
_This is just a version bump, see all the beta releases below, esspecially the first one, for a list of changes._
|
||||
|
||||
## 2.0.0-beta.11 (2020-03-01)
|
||||
|
||||
- Add Spanish translations ([#25](https://github.com/owntracks/frontend/pull/25), [@dtorner](https://github.com/dtorner))
|
||||
- Change "distance travelled" label to `title`
|
||||
- Replace map initial center/zoom config with auto fitting ([#23](https://github.com/owntracks/frontend/issues/23))
|
||||
- Enhance code type definitions using TypeScript features ([#20](https://github.com/owntracks/frontend/pull/20))
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.0.0-beta.10 (2020-02-07)
|
||||
|
||||
- Add "distance travelled" feature
|
||||
|
||||
## 2.0.0-beta.9 (2020-02-06)
|
||||
|
||||
- Support locale with language and region part (`en-GB`)
|
||||
- Update docs (screenshot, changelog improvements, typo fix)
|
||||
- Add funding information
|
||||
|
||||
## 2.0.0-beta.8 (2020-01-26)
|
||||
|
||||
- Add friendly device name and face images to location history popups
|
||||
@@ -22,19 +48,19 @@ This release doesn't really affect end-users but greatly improves the developmen
|
||||
|
||||
## 2.0.0-beta.6 (2019-12-14)
|
||||
|
||||
- Fix heatmap - the upgrade of `vue2-leaflet` from 2.2.1 to 2.3.0 added an `activated` attribute to layers causing the heatmap to not show
|
||||
- Fix heatmap - the upgrade of `vue2-leaflet` from 2.2.1 to 2.3.0 added an `activated` attribute to layers causing the heatmap to not show ([#18](https://github.com/owntracks/frontend/issues/18))
|
||||
|
||||
## 2.0.0-beta.5 (2019-12-14)
|
||||
|
||||
- Add Leaflet popup close button background color transition
|
||||
- Add `$config` Vue instance property
|
||||
- Improve accessibility
|
||||
- Improve accessibility ([#9](https://github.com/owntracks/frontend/issues/9))
|
||||
- Use configured locale for timestamp formatting
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.0.0-beta.4 (2019-12-14)
|
||||
|
||||
- Add support for time selection
|
||||
- Add support for time selection ([#10](https://github.com/owntracks/frontend/issues/10))
|
||||
- New date/time picker component is properly translated/localised and keyboard accessible
|
||||
- Config options are now `startDateTime`/`endDateTime` and format of URL parameters changed
|
||||
- Changed default start/end date and time to use local timezone
|
||||
@@ -45,7 +71,7 @@ This release doesn't really affect end-users but greatly improves the developmen
|
||||
|
||||
- Add i18 support (currently English and German, `locale` config option)
|
||||
- Add custom checkbox focus style
|
||||
- Fix layer dropdown issues
|
||||
- Fix layer dropdown issues ([#1](https://github.com/owntracks/frontend/issues/1))
|
||||
- Fix checkbox style issues
|
||||
- Fix hover/focus inconsistencies
|
||||
- Fix Docker image labels
|
||||
@@ -77,21 +103,21 @@ This release doesn't really affect end-users but greatly improves the developmen
|
||||
- Custom checkbox styles
|
||||
- Switch from Font Awesome 4 to Feather Icons
|
||||
- Application now uses Vuex and Vue Router
|
||||
- Add URL query parameters to load and preserve application state: `lat`, `lng`, `zoom`, `start`, `end`, `user`, `device` and `layers`.
|
||||
- Add a loading indicator.
|
||||
- Add 'download data' modal, currently supporting formatted and minified JSON.
|
||||
- Add a verbose mode.
|
||||
- Add CORS proxy script toeasily use a production instance of the OwnTracks recorder in development.
|
||||
- Add unit tests for util and API functions.
|
||||
- Add URL query parameters to load and preserve application state: `lat`, `lng`, `zoom`, `start`, `end`, `user`, `device` and `layers`
|
||||
- Add a loading indicator
|
||||
- Add 'download data' modal, currently supporting formatted and minified JSON
|
||||
- Add a verbose mode
|
||||
- Add CORS proxy script to easily use a production instance of the OwnTracks recorder in development
|
||||
- Add unit tests for util and API functions
|
||||
- Add documentation for all public funtions
|
||||
- Add documentation for all configuration options.
|
||||
- Add more configuration options, including setting the API base URL ([#4](https://github.com/owntracks/frontend/issues/4)) and hiding the `ping/ping` location ([#12](https://github.com/owntracks/frontend/issues/12)).
|
||||
- Add documentation for all configuration options
|
||||
- Add more configuration options, including setting the API base URL ([#4](https://github.com/owntracks/frontend/issues/4)) and hiding the `ping/ping` location ([#12](https://github.com/owntracks/frontend/issues/12))
|
||||
|
||||
## 1.1.0 (2019-10-26)
|
||||
|
||||
- Add support for Docker. [#7](https://github.com/owntracks/frontend/pull/7), [@sharkoz](https://github.com/sharkoz)
|
||||
- Move project to the OwnTracks organisation on GitHub. [#8](https://github.com/owntracks/frontend/pull/8), [@jpmens](https://github.com/jpmens)
|
||||
- Enable compression in nginx configuration used in Docker image. [#11](https://github.com/owntracks/frontend/pull/11), [@sharkoz](https://github.com/sharkoz)
|
||||
- Add support for Docker ([#7](https://github.com/owntracks/frontend/pull/7), [@sharkoz](https://github.com/sharkoz))
|
||||
- Move project to the OwnTracks organisation on GitHub ([#8](https://github.com/owntracks/frontend/pull/8), [@jpmens](https://github.com/jpmens))
|
||||
- Enable compression in nginx configuration used in Docker image ([#11](https://github.com/owntracks/frontend/pull/11), [@sharkoz](https://github.com/sharkoz))
|
||||
|
||||
## 1.0.0 (2019-06-18)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ COPY . ./
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:1.17-alpine
|
||||
LABEL version="2.0.0-beta.8"
|
||||
LABEL version="2.0.0"
|
||||
LABEL description="OwnTracks UI"
|
||||
LABEL maintainer="Linus Groh <mail@linusgroh.de>"
|
||||
ENV LISTEN_PORT=80 \
|
||||
|
||||
@@ -29,9 +29,6 @@ window.owntracks.config = {};
|
||||
- [`locale`](#locale)
|
||||
- `map`
|
||||
- [`attribution`](#mapattribution)
|
||||
- `center`
|
||||
- [`lat`](#mapcenterlat)
|
||||
- [`lng`](#mapcenterlng)
|
||||
- [`circle`](#mapcircle)
|
||||
- [`circleMarker`](#mapcirclemarker)
|
||||
- `controls`
|
||||
@@ -59,12 +56,12 @@ window.owntracks.config = {};
|
||||
- [`maxZoom`](#mapmaxzoom)
|
||||
- [`polyline`](#mappolyline)
|
||||
- [`url`](#mapurl)
|
||||
- [`zoom`](#mapzoom)
|
||||
- `onLocationChange`
|
||||
- [`reloadHistory`](#onlocationchangereloadhistory)
|
||||
- [`primaryColor`](#primarycolor)
|
||||
- [`selectedDevice`](#selecteddevice)
|
||||
- [`selectedUser`](#selecteduser)
|
||||
- [`showDistanceTravelled`](#showdistancetravelled)
|
||||
- [`startDateTime`](#startdatetime)
|
||||
- [`verbose`](#verbose)
|
||||
|
||||
@@ -123,7 +120,16 @@ Remove the `ping/ping` location from the fetched data. This is useful when using
|
||||
|
||||
### `locale`
|
||||
|
||||
The language to use for the user interface. Available: `de` (German), `en` (English).
|
||||
The locale to use for the user interface, this affects the language and date/time
|
||||
formats.
|
||||
|
||||
Available languages:
|
||||
|
||||
- `de` (German)
|
||||
- `en` (English)
|
||||
- `es` (Spanish)
|
||||
|
||||
You can use formats like `en-GB`, `en-US`, `de-DE`.
|
||||
|
||||
- Type: [`String`]
|
||||
- Default: `"en"`
|
||||
@@ -144,20 +150,6 @@ Attribution for map tiles.
|
||||
};
|
||||
```
|
||||
|
||||
### `map.center.lat`
|
||||
|
||||
Initial map center latitude.
|
||||
|
||||
- Type: [`Number`]
|
||||
- Default: `0`
|
||||
|
||||
### `map.center.lng`
|
||||
|
||||
Initial map center longitude.
|
||||
|
||||
- Type: [`Number`]
|
||||
- Default: `0`
|
||||
|
||||
### `map.circle`
|
||||
|
||||
Location accuracy indicator configuation. `color` and `fillColor` default to
|
||||
@@ -368,13 +360,6 @@ and [this Wikipedia article](https://en.wikipedia.org/wiki/Tiled_web_map).
|
||||
};
|
||||
```
|
||||
|
||||
### `map.zoom`
|
||||
|
||||
Initial map zoom level.
|
||||
|
||||
- Type: [`Number`]
|
||||
- Default: `19`
|
||||
|
||||
### `onLocationChange.reloadHistory`
|
||||
|
||||
Whether to reload the location history (of selected date range) or not when a location
|
||||
@@ -400,7 +385,7 @@ Primary color for the user interface (navigation bar and various map elements).
|
||||
### `selectedDevice`
|
||||
|
||||
Initial selected device. All devices will be shown by default if `null`. Will be ignored
|
||||
if [`selectedUser`](#selectedUser) is `null`;
|
||||
if [`selectedUser`](#selectedUser) is `null`.
|
||||
|
||||
Only data for the selected user/device will be fetched, so you can use this to limit the
|
||||
amount of data fetched after page load.
|
||||
@@ -433,6 +418,16 @@ amount of data fetched after page load.
|
||||
};
|
||||
```
|
||||
|
||||
### `showDistanceTravelled`
|
||||
|
||||
Whether to calculate and show the travelled distance of the last fetched data in the
|
||||
header bar. `maxPointDistance` is being takein into account, if a distance between two
|
||||
subsequent points is greater than `maxPointDistance`, it will not contibute to the
|
||||
calculated travelled distance.
|
||||
|
||||
- Type: [`Boolean`]
|
||||
- Default: `true`
|
||||
|
||||
### `startDateTime`
|
||||
|
||||
Initial start date and time (browser timezone) for fetched data.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 452 KiB After Width: | Height: | Size: 653 KiB |
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owntracks-ui",
|
||||
"version": "2.0.0-beta.8",
|
||||
"version": "2.0.0",
|
||||
"author": {
|
||||
"name": "Linus Groh",
|
||||
"email": "mail@linusgroh.de"
|
||||
@@ -26,29 +26,29 @@
|
||||
"vue": "^2.6.11",
|
||||
"vue-ctk-date-time-picker": "^2.4.0",
|
||||
"vue-feather-icons": "^5.0.0",
|
||||
"vue-i18n": "^8.15.3",
|
||||
"vue-js-modal": "^1.3.31",
|
||||
"vue-i18n": "^8.15.4",
|
||||
"vue-js-modal": "^1.3.33",
|
||||
"vue-outside-events": "^1.1.3",
|
||||
"vue-router": "^3.1.5",
|
||||
"vue2-leaflet": "^2.4.2",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue2-leaflet": "^2.5.2",
|
||||
"vuex": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.1.2",
|
||||
"@vue/cli-plugin-eslint": "^4.1.2",
|
||||
"@vue/cli-plugin-unit-jest": "^4.1.2",
|
||||
"@vue/cli-service": "^4.1.2",
|
||||
"@vue/cli-plugin-babel": "^4.2.3",
|
||||
"@vue/cli-plugin-eslint": "^4.2.3",
|
||||
"@vue/cli-plugin-unit-jest": "^4.2.3",
|
||||
"@vue/cli-service": "^4.2.3",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/test-utils": "1.0.0-beta.31",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^25.1.0",
|
||||
"cors-anywhere": "^0.4.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-vue": "^6.1.2",
|
||||
"eslint-plugin-vue": "^6.2.1",
|
||||
"jest-fetch-mock": "^3.0.1",
|
||||
"lint-staged": "^10.0.2",
|
||||
"lint-staged": "^10.0.8",
|
||||
"moment-locales-webpack-plugin": "^1.1.2",
|
||||
"node-sass": "^4.13.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
|
||||
31
src/api.js
31
src/api.js
@@ -1,23 +1,11 @@
|
||||
import { log, logLevels } from "@/logging";
|
||||
import { getApiUrl } from "@/util";
|
||||
|
||||
/** @typedef {import("./types").QueryParams} QueryParams */
|
||||
/** @typedef {import("./types").User} User */
|
||||
/** @typedef {import("./types").Device} Device */
|
||||
/** @typedef {import("./types").LastLocation} LastLocation */
|
||||
/** @typedef {import("./types").LocationHistory} LocationHistory */
|
||||
|
||||
/**
|
||||
* Callback for new WebSocket location messages.
|
||||
*
|
||||
* @callback webSocketLocationCallback
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetch an API resource.
|
||||
*
|
||||
* @param {String} path API resource path
|
||||
* @param {QueryParams} [params] Query parameters
|
||||
* @param {Object} [params] Query parameters
|
||||
* @returns {Promise} Promise returned by the fetch function
|
||||
*/
|
||||
const fetchApi = (path, params = {}) => {
|
||||
@@ -42,7 +30,7 @@ export const getVersion = async () => {
|
||||
/**
|
||||
* Get all users.
|
||||
*
|
||||
* @returns {Array.<User>} Array of usernames
|
||||
* @returns {User[]} Array of usernames
|
||||
*/
|
||||
export const getUsers = async () => {
|
||||
const response = await fetchApi("/api/0/list");
|
||||
@@ -54,8 +42,8 @@ export const getUsers = async () => {
|
||||
/**
|
||||
* Get all devices for the provided users.
|
||||
*
|
||||
* @param {Array.<User>} users Array of usernames
|
||||
* @returns {Object.<User, Array.<Device>>}
|
||||
* @param {User[]} users Array of usernames
|
||||
* @returns {{User: Device[]}}
|
||||
* Object mapping each username to an array of device names
|
||||
*/
|
||||
export const getDevices = async users => {
|
||||
@@ -76,7 +64,7 @@ export const getDevices = async users => {
|
||||
*
|
||||
* @param {User} [user] Get last locations of all devices from this user
|
||||
* @param {Device} [device] Get last location of specific device
|
||||
* @returns {Array.<LastLocation>} Array of last location objects
|
||||
* @returns {OTLocation[]} Array of last location objects
|
||||
*/
|
||||
export const getLastLocations = async (user, device) => {
|
||||
const params = {};
|
||||
@@ -98,7 +86,7 @@ export const getLastLocations = async (user, device) => {
|
||||
* @param {Device} device Device name
|
||||
* @param {String} start Start date and time in UTC
|
||||
* @param {String} end End date and time in UTC
|
||||
* @returns {LocationHistory} Array of location history objects
|
||||
* @returns {OTLocation[]} Array of location history objects
|
||||
*/
|
||||
export const getUserDeviceLocationHistory = async (
|
||||
user,
|
||||
@@ -120,12 +108,11 @@ export const getUserDeviceLocationHistory = async (
|
||||
/**
|
||||
* Get the location history of multiple devices.
|
||||
*
|
||||
* @param {Object.<User, Array.<Device>>} devices
|
||||
* @param {{User: Device[]}} devices
|
||||
* Devices of which the history should be fetched
|
||||
* @param {String} start Start date and time in UTC
|
||||
* @param {String} end End date and time in UTC
|
||||
* @returns {Object.<User, Object.<Device, LocationHistory>>}
|
||||
* Array of location history objects
|
||||
* @returns {LocationHistory} Location history
|
||||
*/
|
||||
export const getLocationHistory = async (devices, start, end) => {
|
||||
const locationHistory = {};
|
||||
@@ -151,7 +138,7 @@ export const getLocationHistory = async (devices, start, end) => {
|
||||
* Connect to the WebSocket API, reconnect when necessary and handle received
|
||||
* messages.
|
||||
*
|
||||
* @param {webSocketLocationCallback} [callback] Callback for location messages
|
||||
* @param {WebSocketLocationCallback} [callback] Callback for location messages
|
||||
*/
|
||||
export const connectWebsocket = async callback => {
|
||||
let url = getApiUrl("/ws/last");
|
||||
|
||||
@@ -98,6 +98,13 @@
|
||||
</div>
|
||||
</nav>
|
||||
<nav class="nav-shrink">
|
||||
<div
|
||||
v-if="$config.showDistanceTravelled && distanceTravelled"
|
||||
class="nav-item"
|
||||
:title="$t('Distance travelled')"
|
||||
>
|
||||
{{ humanReadableDistance(distanceTravelled) }}
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<button
|
||||
class="button button-flat button-icon"
|
||||
@@ -141,6 +148,7 @@ import "vue-ctk-date-time-picker/dist/vue-ctk-date-time-picker.css";
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
import { DATE_TIME_FORMAT } from "@/constants";
|
||||
import * as types from "@/store/mutation-types";
|
||||
import { humanReadableDistance } from "@/util";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -165,7 +173,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["users", "devices", "map"]),
|
||||
...mapState(["users", "devices", "map", "distanceTravelled"]),
|
||||
selectedUser: {
|
||||
get() {
|
||||
return this.$store.state.selectedUser;
|
||||
@@ -224,6 +232,7 @@ export default {
|
||||
"setStartDateTime",
|
||||
"setEndDateTime",
|
||||
]),
|
||||
humanReadableDistance,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -17,10 +17,6 @@ const DEFAULT_CONFIG = {
|
||||
map: {
|
||||
attribution:
|
||||
'© <a href="https://osm.org/copyright">OpenStreetMap</a> contributors',
|
||||
center: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
circle: {
|
||||
color: null,
|
||||
fillColor: null,
|
||||
@@ -65,7 +61,6 @@ const DEFAULT_CONFIG = {
|
||||
fillColor: "transparent",
|
||||
},
|
||||
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
zoom: 19,
|
||||
},
|
||||
onLocationChange: {
|
||||
reloadHistory: false,
|
||||
@@ -73,6 +68,7 @@ const DEFAULT_CONFIG = {
|
||||
primaryColor: "#3f51b5",
|
||||
selectedDevice: null,
|
||||
selectedUser: null,
|
||||
showDistanceTravelled: true,
|
||||
startDateTime,
|
||||
verbose: false,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ locales.keys().forEach(key => {
|
||||
});
|
||||
|
||||
export default new VueI18n({
|
||||
locale: config.locale,
|
||||
locale: config.locale.split("-")[0],
|
||||
fallbackLocale: "en",
|
||||
formatFallbackMessages: true,
|
||||
messages,
|
||||
|
||||
220
src/index.d.ts
vendored
Normal file
220
src/index.d.ts
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
/** Configuration object. */
|
||||
interface Config {
|
||||
api: {
|
||||
baseUrl: string;
|
||||
};
|
||||
endDateTime: Date;
|
||||
ignorePingLocation: boolean;
|
||||
locale: string;
|
||||
map: {
|
||||
attribution: string;
|
||||
circle: {
|
||||
color: OptionalColor;
|
||||
fillColor: OptionalColor;
|
||||
fillOpacity: number;
|
||||
};
|
||||
circleMarker: {
|
||||
color: OptionalColor;
|
||||
fillColor: OptionalColor;
|
||||
fillOpacity: number;
|
||||
radius: number;
|
||||
};
|
||||
controls: {
|
||||
scale: {
|
||||
display: boolean;
|
||||
imperial: boolean;
|
||||
maxWidth: number;
|
||||
metric: boolean;
|
||||
position: string;
|
||||
};
|
||||
zoom: {
|
||||
display: boolean;
|
||||
position: string;
|
||||
};
|
||||
};
|
||||
heatmap: {
|
||||
blur: number;
|
||||
gradient: { number: Color } | null;
|
||||
max: number;
|
||||
radius: number;
|
||||
};
|
||||
layers: {
|
||||
heatmap: boolean;
|
||||
last: boolean;
|
||||
line: boolean;
|
||||
points: boolean;
|
||||
};
|
||||
maxNativeZoom: number;
|
||||
maxPointDistance: number | null;
|
||||
maxZoom: number;
|
||||
polyline: {
|
||||
color: OptionalColor;
|
||||
fillColor: OptionalColor;
|
||||
};
|
||||
url: string;
|
||||
};
|
||||
onLocationChange: {
|
||||
reloadHistory: boolean;
|
||||
};
|
||||
primaryColor: Color;
|
||||
selectedUser: User| null;
|
||||
selectedDevice: Device| null;
|
||||
startDateTime: Date;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
/** Vuex state. */
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
frontendVersion: string;
|
||||
recorderVersion: string;
|
||||
users: User[];
|
||||
devices: { User: Device[] };
|
||||
lastLocations: OTLocation[];
|
||||
locationHistory: LocationHistory;
|
||||
selectedUser: User| null;
|
||||
selectedDevice: Device| null;
|
||||
startDateTime: string;
|
||||
endDateTime: string;
|
||||
map: {
|
||||
center: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
layers: {
|
||||
heatmap: boolean;
|
||||
last: boolean;
|
||||
line: boolean;
|
||||
points: boolean;
|
||||
};
|
||||
zoom: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A location object as returned by the OwnTracks recorder.
|
||||
* https://owntracks.org/booklet/tech/json/#_typelocation
|
||||
*/
|
||||
interface OTLocation {
|
||||
_http: boolean;
|
||||
/**
|
||||
* In this case always "location"
|
||||
* https://owntracks.org/booklet/tech/json/#types
|
||||
*/
|
||||
_type: string;
|
||||
/** Accuracy in meters */
|
||||
acc?: number;
|
||||
/** Altitude above sea level in meters */
|
||||
alt?: number;
|
||||
/** Device battery level in percent */
|
||||
batt?: number;
|
||||
/**
|
||||
* Battery status (iOS only)
|
||||
*
|
||||
* - `0` = unknown
|
||||
* - `1` = unplugged
|
||||
* - `2` = charging
|
||||
* - `3` = full
|
||||
*/
|
||||
bs?: number;
|
||||
/** Course over ground in degrees (iOS only) */
|
||||
cog?: number;
|
||||
/**
|
||||
* Internet connectivity status (route to host) when the message is created
|
||||
*
|
||||
* - `"w"` = phone is connected to a WiFi connection
|
||||
* - `"o"` = phone is offline
|
||||
* - `"m"` = mobile data
|
||||
*/
|
||||
conn?: string;
|
||||
/** Device name */
|
||||
device?: Device;
|
||||
/** Timestamp in a readable format */
|
||||
disptst: string;
|
||||
/** Base64-encoded face image (device icon) */
|
||||
face?: string;
|
||||
/**
|
||||
* Geohash of the location
|
||||
* https://en.wikipedia.org/wiki/Geohash
|
||||
*/
|
||||
ghash?: string;
|
||||
/** Regions the device is currently in (e.g. `["Home", "Garage"]`). Might be empty. */
|
||||
inregions?: string[];
|
||||
/**
|
||||
* No idea; some kind of timestamp as well - figure it out yourself. :)
|
||||
* https://github.com/owntracks/recorder/blob/df009f791a845012e9cce24923e6203a079ca1ed/storage.c#L659
|
||||
* https://github.com/owntracks/recorder/blob/df009f791a845012e9cce24923e6203a079ca1ed/storage.c#L704
|
||||
*/
|
||||
isorcv?: string;
|
||||
/** ISO 8601 timestamp */
|
||||
isotst?: string;
|
||||
/** Latitude in degrees */
|
||||
lat: number;
|
||||
/** Longitude in degrees */
|
||||
lon: number;
|
||||
/** Friendly device name */
|
||||
name?: string;
|
||||
/**
|
||||
* Trigger for the location report
|
||||
*
|
||||
* - `"p"` = ping issued randomly by background task
|
||||
* - `"c"` = circular region enter/leave event
|
||||
* - `"b"` = beacon region enter/leave event (iOS only)
|
||||
* - `"r"` = response to a reportLocation cmd message
|
||||
* - `"u"` = manual publish requested by the user
|
||||
* - `"t"` = timer based publish in move move (iOS only)
|
||||
* - `"v"` = updated by Settings/Privacy/Locations Services/System Services/Frequent Locations monitoring (iOS only)
|
||||
*/
|
||||
t?: string;
|
||||
/** Tracker ID used to display the initials of a user */
|
||||
tid?: string;
|
||||
/**
|
||||
* Original publish topic
|
||||
* https://owntracks.org/booklet/tech/json/#topics
|
||||
*/
|
||||
topic?: string;
|
||||
/** UNIX epoch timestamp of the location fix in seconds */
|
||||
tst: number;
|
||||
/** User */
|
||||
username?: User;
|
||||
/** Vertical accuracy of the alt element in meters */
|
||||
vac?: number;
|
||||
/** Velocity in km/h */
|
||||
vel?: number;
|
||||
}
|
||||
|
||||
/** URL query parameters (prior to any parsing so it's all strings). */
|
||||
interface QueryParams {
|
||||
/** Map center latitude */
|
||||
lat?: string;
|
||||
/** Map center longitude */
|
||||
lng?: string;
|
||||
/** Start date and time of selected time range */
|
||||
start?: string;
|
||||
/** End date and time of selected time range */
|
||||
end?: string;
|
||||
/** Selected user */
|
||||
user?: string;
|
||||
/** Selected device */
|
||||
device?: string;
|
||||
/** Comma-separated list of active layers */
|
||||
layers?: string;
|
||||
}
|
||||
|
||||
/** Callback for new WebSocket location messages. */
|
||||
interface WebSocketLocationCallback { (): void }
|
||||
|
||||
/** A CSS color. */
|
||||
type Color = string;
|
||||
|
||||
/** A CSS color that will use `primaryColor` as fallback. */
|
||||
type OptionalColor = Color | null;
|
||||
|
||||
/** A user's name. */
|
||||
type User = string;
|
||||
|
||||
/** A device's name. */
|
||||
type Device = string;
|
||||
|
||||
/** Multiple location histories mapped to user and devices. */
|
||||
type LocationHistory = { User: { Device: OTLocation[] } };
|
||||
@@ -10,6 +10,7 @@
|
||||
"Select user": "Benutzer auswählen",
|
||||
"Show all": "Alle anzeigen",
|
||||
"Select device": "Gerät auswählen",
|
||||
"Distance travelled": "Gereiste Entfernung",
|
||||
"Download raw data": "Rohdaten herunterladen",
|
||||
"Information": "Information",
|
||||
"Show last known locations": "Zeige letzte bekannte Standorte",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"Select user": "Select user",
|
||||
"Show all": "Show all",
|
||||
"Select device": "Select device",
|
||||
"Distance travelled": "Distance travelled",
|
||||
"Download raw data": "Download raw data",
|
||||
"Information": "Information",
|
||||
"Show last known locations": "Show last known locations",
|
||||
|
||||
33
src/locales/es.json
Normal file
33
src/locales/es.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Automatically center the map view and zoom in to relevant data": "Centrar automáticamente el zoom y la vista del mapa a los datos",
|
||||
"Fit view": "Ajustar vista",
|
||||
"Layer settings": "Configuración de capas",
|
||||
"Show/hide layers": "Mostrar/ocultar capas",
|
||||
"Now": "Ahora",
|
||||
"Select start date": "Seleccionar fecha inicio",
|
||||
"to": "hasta",
|
||||
"Select end date": "Seleccionar fecha fin",
|
||||
"Select user": "Seleccionar usuario",
|
||||
"Show all": "Mostrar todos",
|
||||
"Select device": "Seleccionar dispositivo",
|
||||
"Distance travelled": "Distancia recorrida",
|
||||
"Download raw data": "Descargar datos en crudo",
|
||||
"Information": "Información",
|
||||
"Show last known locations": "Mostrar última ubicación conocida",
|
||||
"Show location history (line)": "Mostrar historial (línea)",
|
||||
"Show location history (points)": "Mostrar historial (puntos)",
|
||||
"Show location heatmap": "Mostra mapa de calor",
|
||||
"Minify JSON": "Reducir JSON",
|
||||
"Copy to clipboard": "Copiar al portapapeles",
|
||||
"Download": "Descarga",
|
||||
"OwnTracks website": "OwnTracks - Sitio web",
|
||||
"OwnTracks documentation": "OwnTracks - documentación",
|
||||
"OwnTracks on Twitter": "OwnTracks en Twitter",
|
||||
"Loading data, please wait...": "Cargando datos, por favor, espera...",
|
||||
"Image of {deviceName}": "Imágen de {deviceName}",
|
||||
"Timestamp": "Fecha / Hora",
|
||||
"Location": "Ubicación",
|
||||
"Address": "Dirección",
|
||||
"Battery": "Bateria",
|
||||
"Speed": "Velocidad"
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
import * as types from "@/store/mutation-types";
|
||||
import * as api from "@/api";
|
||||
import config from "@/config";
|
||||
import { isIsoDateTime } from "@/util";
|
||||
|
||||
/** @typedef {import("./types").QueryParams} QueryParams */
|
||||
/** @typedef {import("./types").User} User */
|
||||
/** @typedef {import("./types").Device} Device */
|
||||
import { distanceBetweenCoordinates, isIsoDateTime } from "@/util";
|
||||
|
||||
/**
|
||||
* Populate the state from URL query parameters.
|
||||
@@ -121,6 +117,35 @@ const getLastLocations = async ({ commit, state }) => {
|
||||
commit(types.SET_LAST_LOCATIONS, lastLocations);
|
||||
};
|
||||
|
||||
const _getDistanceTravelled = locationHistory => {
|
||||
let distanceTravelled = 0;
|
||||
Object.keys(locationHistory).forEach(user => {
|
||||
Object.keys(locationHistory[user]).forEach(device => {
|
||||
let lastLatLng = null;
|
||||
locationHistory[user][device].forEach(coordinate => {
|
||||
const latLng = L.latLng(coordinate.lat, coordinate.lon);
|
||||
if (lastLatLng !== null) {
|
||||
const distance = distanceBetweenCoordinates(lastLatLng, latLng);
|
||||
if (
|
||||
typeof config.map.maxPointDistance === "number" &&
|
||||
config.map.maxPointDistance > 0
|
||||
) {
|
||||
if (distance <= config.map.maxPointDistance) {
|
||||
// Part of the current group, add calculated distance to total
|
||||
distanceTravelled += distance;
|
||||
}
|
||||
} else {
|
||||
// If grouping is disabled always add calculated distance to total
|
||||
distanceTravelled += distance;
|
||||
}
|
||||
}
|
||||
lastLatLng = latLng;
|
||||
});
|
||||
});
|
||||
});
|
||||
return distanceTravelled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load location history of all devices, in the selected date range.
|
||||
*/
|
||||
@@ -136,15 +161,19 @@ const getLocationHistory = async ({ commit, state }) => {
|
||||
} else {
|
||||
devices = state.devices;
|
||||
}
|
||||
commit(
|
||||
types.SET_LOCATION_HISTORY,
|
||||
await api.getLocationHistory(
|
||||
devices,
|
||||
state.startDateTime,
|
||||
state.endDateTime
|
||||
)
|
||||
const locationHistory = await api.getLocationHistory(
|
||||
devices,
|
||||
state.startDateTime,
|
||||
state.endDateTime
|
||||
);
|
||||
commit(types.SET_IS_LOADING, false);
|
||||
commit(types.SET_LOCATION_HISTORY, locationHistory);
|
||||
if (config.showDistanceTravelled) {
|
||||
commit(
|
||||
types.SET_DISTANCE_TRAVELLED,
|
||||
_getDistanceTravelled(locationHistory)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,10 +3,6 @@ import L from "leaflet";
|
||||
import config from "@/config";
|
||||
import { distanceBetweenCoordinates } from "@/util";
|
||||
|
||||
/** @typedef {import("./types").State} State */
|
||||
/** @typedef {import("./types").MultiLocationHistory} MultiLocationHistory */
|
||||
/** @typedef {import("./types").DatepickerConfig} DatepickerConfig */
|
||||
|
||||
/**
|
||||
* From the selected users' and devices' location histories, create an
|
||||
* array of all coordinates.
|
||||
@@ -14,7 +10,7 @@ import { distanceBetweenCoordinates } from "@/util";
|
||||
* @param {State} state
|
||||
* @param {MultiLocationHistory} state.locationHistory
|
||||
* Location history of selected users and devices
|
||||
* @returns {Array.<L.LatLng>} All coordinates
|
||||
* @returns {L.LatLng[]} All coordinates
|
||||
*/
|
||||
const locationHistoryLatLngs = state => {
|
||||
const latLngs = [];
|
||||
@@ -36,7 +32,7 @@ const locationHistoryLatLngs = state => {
|
||||
* @param {State} state
|
||||
* @param {MultiLocationHistory} state.locationHistory
|
||||
* Location history of selected users and devices
|
||||
* @returns {Array.<Array.<L.LatLng>>} Groups of coherent coordinates
|
||||
* @returns {L.LatLng[][]} Groups of coherent coordinates
|
||||
*/
|
||||
const locationHistoryLatLngGroups = state => {
|
||||
const groups = [];
|
||||
@@ -47,7 +43,11 @@ const locationHistoryLatLngGroups = state => {
|
||||
const latLng = L.latLng(coordinate.lat, coordinate.lon);
|
||||
// Skip if group splitting is disabled or this is the first
|
||||
// coordinate in the current group
|
||||
if (config.map.maxPointDistance !== null && latLngs.length > 0) {
|
||||
if (
|
||||
typeof config.map.maxPointDistance === "number" &&
|
||||
config.map.maxPointDistance > 0 &&
|
||||
latLngs.length > 0
|
||||
) {
|
||||
const lastLatLng = latLngs.slice(-1)[0];
|
||||
if (
|
||||
distanceBetweenCoordinates(lastLatLng, latLng) >
|
||||
|
||||
@@ -23,10 +23,14 @@ export default new Vuex.Store({
|
||||
startDateTime: config.startDateTime.toISOString().slice(0, 19),
|
||||
endDateTime: config.endDateTime.toISOString().slice(0, 19),
|
||||
map: {
|
||||
center: config.map.center,
|
||||
zoom: config.map.zoom,
|
||||
center: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
zoom: 19,
|
||||
layers: config.map.layers,
|
||||
},
|
||||
distanceTravelled: null,
|
||||
},
|
||||
getters,
|
||||
mutations,
|
||||
|
||||
@@ -11,3 +11,4 @@ export const SET_END_DATE_TIME = "SET_END_DATE_TIME";
|
||||
export const SET_MAP_CENTER = "SET_MAP_CENTER";
|
||||
export const SET_MAP_ZOOM = "SET_MAP_ZOOM";
|
||||
export const SET_MAP_LAYER_VISIBILITY = "SET_MAP_LAYER_VISIBILITY";
|
||||
export const SET_DISTANCE_TRAVELLED = "SET_DISTANCE_TRAVELLED";
|
||||
|
||||
@@ -40,4 +40,7 @@ export default {
|
||||
[types.SET_MAP_LAYER_VISIBILITY](state, { layer, visibility }) {
|
||||
state.map.layers[layer] = visibility;
|
||||
},
|
||||
[types.SET_DISTANCE_TRAVELLED](state, distanceTravelled) {
|
||||
state.distanceTravelled = distanceTravelled;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -108,6 +108,7 @@ pre {
|
||||
nav {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 20px;
|
||||
|
||||
57
src/types.js
57
src/types.js
@@ -1,57 +0,0 @@
|
||||
/* eslint max-len: 0 */
|
||||
|
||||
/**
|
||||
* A coordinate with latitude and longitude.
|
||||
*
|
||||
* @typedef Coordinate
|
||||
* @type {(Object|L.LatLng)}
|
||||
* @property {Number} lat Latitude
|
||||
* @property {Number} lng Longitude
|
||||
*/
|
||||
|
||||
/**
|
||||
* Vuex state.
|
||||
*
|
||||
* @typedef {Object.<String, *>} State
|
||||
*/
|
||||
|
||||
/**
|
||||
* URL query parameter object.
|
||||
*
|
||||
* @typedef {Object.<String, *>} QueryParams
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DatepickerConfig
|
||||
* @property {Function} DatepickerConfig.customPredictor Custom predictor function
|
||||
*/
|
||||
|
||||
/**
|
||||
* A user's name.
|
||||
*
|
||||
* @typedef {String} User
|
||||
*/
|
||||
|
||||
/**
|
||||
* A device's name.
|
||||
*
|
||||
* @typedef {String} Device
|
||||
*/
|
||||
|
||||
/**
|
||||
* A last location object.
|
||||
*
|
||||
* @typedef {Object.<String, *>} LastLocation
|
||||
*/
|
||||
|
||||
/**
|
||||
* An array of location history objects
|
||||
*
|
||||
* @typedef {Array.<Object.<String, *>>} LocationHistory
|
||||
*/
|
||||
|
||||
/**
|
||||
* Multiple arrays of location history objects mapped to user and devices.
|
||||
*
|
||||
* @typedef {Object.<User, Object.<Device, LocationHistory>>} MultiLocationHistory
|
||||
*/
|
||||
22
src/util.js
22
src/util.js
@@ -3,8 +3,6 @@ import moment from "moment";
|
||||
import config from "@/config";
|
||||
import { DATE_TIME_FORMAT, EARTH_RADIUS_IN_KM } from "@/constants";
|
||||
|
||||
/** @typedef {import("./types").Coordinate} Coordinate */
|
||||
|
||||
/**
|
||||
* Get a complete URL for any API resource, taking the
|
||||
* base URL configuration into account.
|
||||
@@ -83,3 +81,23 @@ export const download = (text, filename, mimeType = "text/plain") => {
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a distance in meters into a human-readable string with unit.
|
||||
*
|
||||
* This only supports m / km for now, but could read a config option and return
|
||||
* ft / mi.
|
||||
*
|
||||
* @param {Number} distance Distance in meters
|
||||
* @returns {String} Formatted string including unit
|
||||
*/
|
||||
export const humanReadableDistance = distance => {
|
||||
let unit = "m";
|
||||
if (Math.abs(distance) >= 1000) {
|
||||
distance = distance / 1000;
|
||||
unit = "km";
|
||||
}
|
||||
return `${distance.toLocaleString(config.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
})} ${unit}`;
|
||||
};
|
||||
|
||||
@@ -232,5 +232,13 @@ export default {
|
||||
}));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
lastLocations() {
|
||||
this.fitView();
|
||||
},
|
||||
locationHistory() {
|
||||
this.fitView();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isIsoDateTime,
|
||||
degreesToRadians,
|
||||
distanceBetweenCoordinates,
|
||||
humanReadableDistance,
|
||||
} from "@/util";
|
||||
|
||||
describe("getApiUrl", () => {
|
||||
@@ -102,3 +103,22 @@ describe("distanceBetweenCoordinates", () => {
|
||||
).toBe(9105627.810109457);
|
||||
});
|
||||
});
|
||||
|
||||
describe("humanReadableDistance", () => {
|
||||
test("expected results", () => {
|
||||
expect(humanReadableDistance(0)).toBe("0 m");
|
||||
expect(humanReadableDistance(1)).toBe("1 m");
|
||||
expect(humanReadableDistance(123)).toBe("123 m");
|
||||
expect(humanReadableDistance(123.4567)).toBe("123.5 m");
|
||||
expect(humanReadableDistance(999)).toBe("999 m");
|
||||
expect(humanReadableDistance(1000)).toBe("1 km");
|
||||
expect(humanReadableDistance(9000)).toBe("9 km");
|
||||
expect(humanReadableDistance(9900)).toBe("9.9 km");
|
||||
expect(humanReadableDistance(9990)).toBe("10 km");
|
||||
expect(humanReadableDistance(9999)).toBe("10 km");
|
||||
expect(humanReadableDistance(9999.0)).toBe("10 km");
|
||||
expect(humanReadableDistance(9999.9999)).toBe("10 km");
|
||||
expect(humanReadableDistance(100000)).toBe("100 km");
|
||||
expect(humanReadableDistance(-42)).toBe("-42 m");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user