Add "distance travelled" feature

This commit is contained in:
Linus Groh 2020-02-07 21:20:38 +00:00
parent 8dc9611a77
commit 4078597f7a
13 changed files with 119 additions and 10 deletions

View File

@ -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.

View File

@ -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>

View File

@ -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,
}; };

View File

@ -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",

View File

@ -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",

View File

@ -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)
);
}
}; };
/** /**

View File

@ -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) >

View File

@ -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,

View File

@ -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";

View File

@ -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;
},
}; };

View File

@ -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;

View File

@ -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}`;
};

View File

@ -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");
});
});