mirror of
https://github.com/owntracks/frontend.git
synced 2026-05-07 03:16:50 +00:00
Merge branch 'master' into clean-up-the-typing-mess
This commit is contained in:
22
src/api.js
22
src/api.js
@@ -12,7 +12,7 @@ import { getApiUrl } from "@/util";
|
||||
*
|
||||
* @param {String} path API resource path
|
||||
* @param {Object} [params] Query parameters
|
||||
* @return {Promise} Promise returned by the fetch function
|
||||
* @returns {Promise} Promise returned by the fetch function
|
||||
*/
|
||||
const fetchApi = (path, params = {}) => {
|
||||
const url = getApiUrl(path);
|
||||
@@ -24,7 +24,7 @@ const fetchApi = (path, params = {}) => {
|
||||
/**
|
||||
* Get the recorder's version.
|
||||
*
|
||||
* @return {String} Version
|
||||
* @returns {String} Version
|
||||
*/
|
||||
export const getVersion = async () => {
|
||||
const response = await fetchApi("/api/0/version");
|
||||
@@ -36,7 +36,7 @@ export const getVersion = async () => {
|
||||
/**
|
||||
* Get all users.
|
||||
*
|
||||
* @return {Array.<User>} Array of usernames
|
||||
* @returns {Array.<User>} Array of usernames
|
||||
*/
|
||||
export const getUsers = async () => {
|
||||
const response = await fetchApi("/api/0/list");
|
||||
@@ -49,7 +49,8 @@ export const getUsers = async () => {
|
||||
* Get all devices for the provided users.
|
||||
*
|
||||
* @param {Array.<User>} users Array of usernames
|
||||
* @return {Object.<User, Array.<Device>>} Object mapping each username to an array of device names
|
||||
* @returns {Object.<User, Array.<Device>>}
|
||||
* Object mapping each username to an array of device names
|
||||
*/
|
||||
export const getDevices = async users => {
|
||||
const devices = {};
|
||||
@@ -69,7 +70,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
|
||||
* @return {Array.<LastLocation>} Array of last location objects
|
||||
* @returns {Array.<LastLocation>} Array of last location objects
|
||||
*/
|
||||
export const getLastLocations = async (user, device) => {
|
||||
const params = {};
|
||||
@@ -91,7 +92,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
|
||||
* @return {LocationHistory} Array of location history objects
|
||||
* @returns {LocationHistory} Array of location history objects
|
||||
*/
|
||||
export const getUserDeviceLocationHistory = async (
|
||||
user,
|
||||
@@ -113,10 +114,12 @@ export const getUserDeviceLocationHistory = async (
|
||||
/**
|
||||
* Get the location history of multiple devices.
|
||||
*
|
||||
* @param {Object.<User, Array.<Device>>} devices Devices of which the history should be fetched
|
||||
* @param {Object.<User, Array.<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
|
||||
* @return {Object.<User, Object.<Device, LocationHistory>>} Array of location history objects
|
||||
* @returns {Object.<User, Object.<Device, LocationHistory>>}
|
||||
* Array of location history objects
|
||||
*/
|
||||
export const getLocationHistory = async (devices, start, end) => {
|
||||
const locationHistory = {};
|
||||
@@ -139,7 +142,8 @@ export const getLocationHistory = async (devices, start, end) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to the WebSocket API, reconnect when necessary and handle received messages.
|
||||
* Connect to the WebSocket API, reconnect when necessary and handle received
|
||||
* messages.
|
||||
*
|
||||
* @param {webSocketLocationCallback} [callback] Callback for location messages
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<LPopup>
|
||||
<div v-if="name" class="device">{{ name }}</div>
|
||||
<div v-else class="device">{{ user }}/{{ device }}</div>
|
||||
<div class="device">{{ deviceName }}</div>
|
||||
<div class="wrapper">
|
||||
<img v-if="face" :src="faceImageDataURI" alt="" />
|
||||
<img
|
||||
v-if="face"
|
||||
:src="faceImageDataURI"
|
||||
:alt="$t('Image of {deviceName}', { deviceName })"
|
||||
:title="$t('Image of {deviceName}', { deviceName })"
|
||||
/>
|
||||
<ul class="info-list">
|
||||
<li :title="$t('Timestamp')">
|
||||
<ClockIcon size="1x" aria-hidden="true" role="img" />
|
||||
@@ -122,13 +126,23 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Return the face image as a data URI string which can be used for an image's src attribute
|
||||
* Return the face image as a data URI string which can be used for an
|
||||
* image's src attribute.
|
||||
*
|
||||
* @return {String} base64-encoded face image data URI
|
||||
* @returns {String} base64-encoded face image data URI
|
||||
*/
|
||||
faceImageDataURI() {
|
||||
return `data:image/png;base64,${this.face}`;
|
||||
},
|
||||
/**
|
||||
* Return the device name for displaying with <user identifier>/<device
|
||||
* identifier> as fallback.
|
||||
*
|
||||
* @returns {String} device name for displaying
|
||||
*/
|
||||
deviceName() {
|
||||
return this.name ? this.name : `${this.user}/${this.device}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</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,
|
||||
|
||||
@@ -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",
|
||||
@@ -23,6 +24,7 @@
|
||||
"OwnTracks documentation": "OwnTracks Dokumentation",
|
||||
"OwnTracks on Twitter": "OwnTracks auf Twitter",
|
||||
"Loading data, please wait...": "Daten werden geladen, bitte warten...",
|
||||
"Image of {deviceName}": "Bild von {deviceName}",
|
||||
"Timestamp": "Zeitstempel",
|
||||
"Location": "Standort",
|
||||
"Address": "Adresse",
|
||||
|
||||
@@ -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",
|
||||
@@ -23,6 +24,7 @@
|
||||
"OwnTracks documentation": "OwnTracks documentation",
|
||||
"OwnTracks on Twitter": "OwnTracks on Twitter",
|
||||
"Loading data, please wait...": "Loading data, please wait...",
|
||||
"Image of {deviceName}": "Image of {deviceName}",
|
||||
"Timestamp": "Timestamp",
|
||||
"Location": "Location",
|
||||
"Address": "Address",
|
||||
|
||||
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,7 +1,7 @@
|
||||
import * as types from "@/store/mutation-types";
|
||||
import * as api from "@/api";
|
||||
import config from "@/config";
|
||||
import { isIsoDateTime } from "@/util";
|
||||
import { distanceBetweenCoordinates, isIsoDateTime } from "@/util";
|
||||
|
||||
/**
|
||||
* Populate the state from URL query parameters.
|
||||
@@ -117,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.
|
||||
*/
|
||||
@@ -132,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)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,8 +8,9 @@ import { distanceBetweenCoordinates } from "@/util";
|
||||
* array of all coordinates.
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {MultiLocationHistory} state.locationHistory Location history of selected users and devices
|
||||
* @return {Array.<L.LatLng>} All coordinates
|
||||
* @param {MultiLocationHistory} state.locationHistory
|
||||
* Location history of selected users and devices
|
||||
* @returns {Array.<L.LatLng>} All coordinates
|
||||
*/
|
||||
const locationHistoryLatLngs = state => {
|
||||
const latLngs = [];
|
||||
@@ -29,8 +30,9 @@ const locationHistoryLatLngs = state => {
|
||||
* coordinates does not exceed `config.map.maxPointDistance`.
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {MultiLocationHistory} state.locationHistory Location history of selected users and devices
|
||||
* @return {Array.<Array.<L.LatLng>>} Groups of coherent coordinates
|
||||
* @param {MultiLocationHistory} state.locationHistory
|
||||
* Location history of selected users and devices
|
||||
* @returns {Array.<Array.<L.LatLng>>} Groups of coherent coordinates
|
||||
*/
|
||||
const locationHistoryLatLngGroups = state => {
|
||||
const groups = [];
|
||||
@@ -41,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;
|
||||
|
||||
28
src/util.js
28
src/util.js
@@ -8,7 +8,7 @@ import { DATE_TIME_FORMAT, EARTH_RADIUS_IN_KM } from "@/constants";
|
||||
* base URL configuration into account.
|
||||
*
|
||||
* @param {String} path Path to the API resource
|
||||
* @return {URL} Final API URL
|
||||
* @returns {URL} Final API URL
|
||||
*/
|
||||
export const getApiUrl = path => {
|
||||
const normalizedBaseUrl = config.api.baseUrl.endsWith("/")
|
||||
@@ -22,7 +22,7 @@ export const getApiUrl = path => {
|
||||
* Check if the given string is an ISO 8601 YYYY-MM-DDTHH:MM:SS datetime.
|
||||
*
|
||||
* @param {String} s Input value to be tested
|
||||
* @return {Boolean} Whether the input matches the expected format
|
||||
* @returns {Boolean} Whether the input matches the expected format
|
||||
*/
|
||||
export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
|
||||
|
||||
@@ -30,7 +30,7 @@ export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
|
||||
* Convert degrees to radians.
|
||||
*
|
||||
* @param {Number} degrees Angle in degrees
|
||||
* @return {Number} Angle in radians
|
||||
* @returns {Number} Angle in radians
|
||||
*/
|
||||
export const degreesToRadians = degrees => (degrees * Math.PI) / 180;
|
||||
|
||||
@@ -42,7 +42,7 @@ export const degreesToRadians = degrees => (degrees * Math.PI) / 180;
|
||||
*
|
||||
* @param {Coordinate} c1 First coordinate
|
||||
* @param {Coordinate} c2 Second coordinate
|
||||
* @return {Number} Distance in meters
|
||||
* @returns {Number} Distance in meters
|
||||
*/
|
||||
export const distanceBetweenCoordinates = (c1, c2) => {
|
||||
const r = EARTH_RADIUS_IN_KM * 1000;
|
||||
@@ -81,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}`;
|
||||
};
|
||||
|
||||
@@ -66,7 +66,11 @@
|
||||
<template v-for="(userDevices, user) in locationHistory">
|
||||
<template v-for="(deviceLocations, device) in userDevices">
|
||||
<LCircleMarker
|
||||
v-for="(l, n) in deviceLocations"
|
||||
v-for="(l, n) in deviceLocationsWithNameAndFace(
|
||||
user,
|
||||
device,
|
||||
deviceLocations
|
||||
)"
|
||||
:key="`${user}-${device}-${n}`"
|
||||
:lat-lng="[l.lat, l.lon]"
|
||||
v-bind="circleMarker"
|
||||
@@ -74,6 +78,8 @@
|
||||
<LDeviceLocationPopup
|
||||
:user="user"
|
||||
:device="device"
|
||||
:name="l.name"
|
||||
:face="l.face"
|
||||
:timestamp="l.tst"
|
||||
:lat="l.lat"
|
||||
:lon="l.lon"
|
||||
@@ -202,6 +208,37 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Find a the last location object for a user/device combination from the
|
||||
* local cache and backfill name and face attributes to each item from the
|
||||
* passed array of location objects.
|
||||
*
|
||||
* @param {User} user Username
|
||||
* @param {Device} device Device name
|
||||
* @param {LocationHistory} deviceLocations Device name
|
||||
* @returns {LocationHistory} Updated location history
|
||||
*/
|
||||
deviceLocationsWithNameAndFace(user, device, deviceLocations) {
|
||||
const lastLocation = this.lastLocations.find(
|
||||
l => l.username === user && l.device === device
|
||||
);
|
||||
if (!lastLocation) {
|
||||
return deviceLocations;
|
||||
}
|
||||
return deviceLocations.map(l => ({
|
||||
...l,
|
||||
name: lastLocation.name,
|
||||
face: lastLocation.face,
|
||||
}));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
lastLocations() {
|
||||
this.fitView();
|
||||
},
|
||||
locationHistory() {
|
||||
this.fitView();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user