mirror of
https://github.com/owntracks/frontend.git
synced 2024-11-15 17:28:19 -07:00
Add "distance travelled" feature
This commit is contained in:
parent
8dc9611a77
commit
4078597f7a
@ -65,6 +65,7 @@ window.owntracks.config = {};
|
|||||||
- [`primaryColor`](#primarycolor)
|
- [`primaryColor`](#primarycolor)
|
||||||
- [`selectedDevice`](#selecteddevice)
|
- [`selectedDevice`](#selecteddevice)
|
||||||
- [`selectedUser`](#selecteduser)
|
- [`selectedUser`](#selecteduser)
|
||||||
|
- [`showDistanceTravelled`](#showdistancetravelled)
|
||||||
- [`startDateTime`](#startdatetime)
|
- [`startDateTime`](#startdatetime)
|
||||||
- [`verbose`](#verbose)
|
- [`verbose`](#verbose)
|
||||||
|
|
||||||
@ -435,6 +436,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`
|
### `startDateTime`
|
||||||
|
|
||||||
Initial start date and time (browser timezone) for fetched data.
|
Initial start date and time (browser timezone) for fetched data.
|
||||||
|
@ -98,6 +98,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="nav-shrink">
|
<nav class="nav-shrink">
|
||||||
|
<div
|
||||||
|
class="nav-item"
|
||||||
|
v-if="$config.showDistanceTravelled && distanceTravelled"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t("Distance travelled: {distance}", {
|
||||||
|
distance: humanReadableDistance(distanceTravelled),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
<div class="nav-item">
|
<div class="nav-item">
|
||||||
<button
|
<button
|
||||||
class="button button-flat button-icon"
|
class="button button-flat button-icon"
|
||||||
@ -141,6 +151,7 @@ import "vue-ctk-date-time-picker/dist/vue-ctk-date-time-picker.css";
|
|||||||
import Dropdown from "@/components/Dropdown";
|
import Dropdown from "@/components/Dropdown";
|
||||||
import { DATE_TIME_FORMAT } from "@/constants";
|
import { DATE_TIME_FORMAT } from "@/constants";
|
||||||
import * as types from "@/store/mutation-types";
|
import * as types from "@/store/mutation-types";
|
||||||
|
import { humanReadableDistance } from "@/util";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -165,7 +176,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["users", "devices", "map"]),
|
...mapState(["users", "devices", "map", "distanceTravelled"]),
|
||||||
selectedUser: {
|
selectedUser: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.state.selectedUser;
|
return this.$store.state.selectedUser;
|
||||||
@ -224,6 +235,7 @@ export default {
|
|||||||
"setStartDateTime",
|
"setStartDateTime",
|
||||||
"setEndDateTime",
|
"setEndDateTime",
|
||||||
]),
|
]),
|
||||||
|
humanReadableDistance,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -73,6 +73,7 @@ const DEFAULT_CONFIG = {
|
|||||||
primaryColor: "#3f51b5",
|
primaryColor: "#3f51b5",
|
||||||
selectedDevice: null,
|
selectedDevice: null,
|
||||||
selectedUser: null,
|
selectedUser: null,
|
||||||
|
showDistanceTravelled: true,
|
||||||
startDateTime,
|
startDateTime,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"Select user": "Benutzer auswählen",
|
"Select user": "Benutzer auswählen",
|
||||||
"Show all": "Alle anzeigen",
|
"Show all": "Alle anzeigen",
|
||||||
"Select device": "Gerät auswählen",
|
"Select device": "Gerät auswählen",
|
||||||
|
"Distance travelled: {distance}": "Gereiste Entfernung: {distance}",
|
||||||
"Download raw data": "Rohdaten herunterladen",
|
"Download raw data": "Rohdaten herunterladen",
|
||||||
"Information": "Information",
|
"Information": "Information",
|
||||||
"Show last known locations": "Zeige letzte bekannte Standorte",
|
"Show last known locations": "Zeige letzte bekannte Standorte",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"Select user": "Select user",
|
"Select user": "Select user",
|
||||||
"Show all": "Show all",
|
"Show all": "Show all",
|
||||||
"Select device": "Select device",
|
"Select device": "Select device",
|
||||||
|
"Distance travelled: {distance}": "Distance travelled: {distance}",
|
||||||
"Download raw data": "Download raw data",
|
"Download raw data": "Download raw data",
|
||||||
"Information": "Information",
|
"Information": "Information",
|
||||||
"Show last known locations": "Show last known locations",
|
"Show last known locations": "Show last known locations",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as types from "@/store/mutation-types";
|
import * as types from "@/store/mutation-types";
|
||||||
import * as api from "@/api";
|
import * as api from "@/api";
|
||||||
import config from "@/config";
|
import config from "@/config";
|
||||||
import { isIsoDateTime } from "@/util";
|
import { distanceBetweenCoordinates, isIsoDateTime } from "@/util";
|
||||||
|
|
||||||
/** @typedef {import("./types").QueryParams} QueryParams */
|
/** @typedef {import("./types").QueryParams} QueryParams */
|
||||||
/** @typedef {import("./types").User} User */
|
/** @typedef {import("./types").User} User */
|
||||||
@ -121,6 +121,35 @@ const getLastLocations = async ({ commit, state }) => {
|
|||||||
commit(types.SET_LAST_LOCATIONS, lastLocations);
|
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.
|
* Load location history of all devices, in the selected date range.
|
||||||
*/
|
*/
|
||||||
@ -136,15 +165,19 @@ const getLocationHistory = async ({ commit, state }) => {
|
|||||||
} else {
|
} else {
|
||||||
devices = state.devices;
|
devices = state.devices;
|
||||||
}
|
}
|
||||||
commit(
|
const locationHistory = await api.getLocationHistory(
|
||||||
types.SET_LOCATION_HISTORY,
|
devices,
|
||||||
await api.getLocationHistory(
|
state.startDateTime,
|
||||||
devices,
|
state.endDateTime
|
||||||
state.startDateTime,
|
|
||||||
state.endDateTime
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
commit(types.SET_IS_LOADING, false);
|
commit(types.SET_IS_LOADING, false);
|
||||||
|
commit(types.SET_LOCATION_HISTORY, locationHistory);
|
||||||
|
if (config.showDistanceTravelled) {
|
||||||
|
commit(
|
||||||
|
types.SET_DISTANCE_TRAVELLED,
|
||||||
|
_getDistanceTravelled(locationHistory)
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +47,11 @@ const locationHistoryLatLngGroups = state => {
|
|||||||
const latLng = L.latLng(coordinate.lat, coordinate.lon);
|
const latLng = L.latLng(coordinate.lat, coordinate.lon);
|
||||||
// Skip if group splitting is disabled or this is the first
|
// Skip if group splitting is disabled or this is the first
|
||||||
// coordinate in the current group
|
// 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];
|
const lastLatLng = latLngs.slice(-1)[0];
|
||||||
if (
|
if (
|
||||||
distanceBetweenCoordinates(lastLatLng, latLng) >
|
distanceBetweenCoordinates(lastLatLng, latLng) >
|
||||||
|
@ -27,6 +27,7 @@ export default new Vuex.Store({
|
|||||||
zoom: config.map.zoom,
|
zoom: config.map.zoom,
|
||||||
layers: config.map.layers,
|
layers: config.map.layers,
|
||||||
},
|
},
|
||||||
|
distanceTravelled: null,
|
||||||
},
|
},
|
||||||
getters,
|
getters,
|
||||||
mutations,
|
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_CENTER = "SET_MAP_CENTER";
|
||||||
export const SET_MAP_ZOOM = "SET_MAP_ZOOM";
|
export const SET_MAP_ZOOM = "SET_MAP_ZOOM";
|
||||||
export const SET_MAP_LAYER_VISIBILITY = "SET_MAP_LAYER_VISIBILITY";
|
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 }) {
|
[types.SET_MAP_LAYER_VISIBILITY](state, { layer, visibility }) {
|
||||||
state.map.layers[layer] = visibility;
|
state.map.layers[layer] = visibility;
|
||||||
},
|
},
|
||||||
|
[types.SET_DISTANCE_TRAVELLED](state, distanceTravelled) {
|
||||||
|
state.distanceTravelled = distanceTravelled;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -108,6 +108,7 @@ pre {
|
|||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
20
src/util.js
20
src/util.js
@ -83,3 +83,23 @@ export const download = (text, filename, mimeType = "text/plain") => {
|
|||||||
element.click();
|
element.click();
|
||||||
document.body.removeChild(element);
|
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
|
||||||
|
* @param {String} [mimeType] 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}`;
|
||||||
|
};
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
isIsoDateTime,
|
isIsoDateTime,
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
distanceBetweenCoordinates,
|
distanceBetweenCoordinates,
|
||||||
|
humanReadableDistance,
|
||||||
} from "@/util";
|
} from "@/util";
|
||||||
|
|
||||||
describe("getApiUrl", () => {
|
describe("getApiUrl", () => {
|
||||||
@ -102,3 +103,22 @@ describe("distanceBetweenCoordinates", () => {
|
|||||||
).toBe(9105627.810109457);
|
).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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user