14 Commits

Author SHA1 Message Date
Linus Groh
00fbb7cd7c Release 2.0.0-beta.10 2020-02-07 21:42:03 +00:00
Linus Groh
4078597f7a Add "distance travelled" feature 2020-02-07 21:20:38 +00:00
Linus Groh
8dc9611a77 Release 2.0.0-beta.9 2020-02-06 21:30:41 +00:00
Linus Groh
c1f58c992e Support locale with language and region part 2020-02-06 21:26:21 +00:00
Linus Groh
6631929d6f Update screenshot 2020-02-06 21:13:11 +00:00
Linus Groh
36281db2e3 Add .github/FUNDING.yml 2020-02-06 20:35:08 +00:00
Linus Groh
5a8d261943 Fix typo in docs/config.md 2020-02-06 20:23:08 +00:00
Linus Groh
7b83349dc8 Improve CHANGELOG.md 2020-02-06 20:22:48 +00:00
Linus Groh
bc3670df99 Release 2.0.0-beta.8 2020-01-26 00:57:08 +00:00
Linus Groh
95613753a9 Enable ESLint max-len rule 2020-01-26 00:49:15 +00:00
Linus Groh
cfa3052a0a Show name and face on location history popups 2020-01-26 00:40:30 +00:00
Linus Groh
0bd84f4de5 s/@return/@returns 2020-01-26 00:38:36 +00:00
Linus Groh
85e51643bf Add missing alt/title to device face image 2020-01-25 23:37:41 +00:00
Linus Groh
6cbdf30580 Use computed prop for device name in location popup 2020-01-25 23:33:10 +00:00
24 changed files with 242 additions and 53 deletions

View File

@@ -7,6 +7,12 @@ module.exports = {
rules: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"max-len": [
"error",
{
ignoreUrls: true,
},
],
"prettier/prettier": [
"error",
{

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
liberapay: owntracks.org

View File

@@ -1,5 +1,23 @@
# Changelog
## 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
- Add missing `alt`/`title` to device face image
- Fix all JSDoc `@return` directives to `@returns`
- Use computed prop for device name in location popup
- Enable ESLint `max-len` rule
## 2.0.0-beta.7 (2020-01-24)
This release doesn't really affect end-users but greatly improves the development experience.
@@ -14,19 +32,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
@@ -37,7 +55,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
@@ -69,21 +87,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)

View File

@@ -6,7 +6,7 @@ COPY . ./
RUN yarn build
FROM nginx:1.17-alpine
LABEL version="2.0.0-beta.7"
LABEL version="2.0.0-beta.10"
LABEL description="OwnTracks UI"
LABEL maintainer="Linus Groh <mail@linusgroh.de>"
ENV LISTEN_PORT=80 \

View File

@@ -65,6 +65,7 @@ window.owntracks.config = {};
- [`primaryColor`](#primarycolor)
- [`selectedDevice`](#selecteddevice)
- [`selectedUser`](#selecteduser)
- [`showDistanceTravelled`](#showdistancetravelled)
- [`startDateTime`](#startdatetime)
- [`verbose`](#verbose)
@@ -123,7 +124,9 @@ 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 are `de` (German), `en` (English). You can use formats
like `en-GB`, `en-US`, `de-DE`.
- Type: [`String`]
- Default: `"en"`
@@ -400,7 +403,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 +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`
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

View File

@@ -1,6 +1,6 @@
{
"name": "owntracks-ui",
"version": "2.0.0-beta.7",
"version": "2.0.0-beta.10",
"author": {
"name": "Linus Groh",
"email": "mail@linusgroh.de"

View File

@@ -18,7 +18,7 @@ import { getApiUrl } from "@/util";
*
* @param {String} path API resource path
* @param {QueryParams} [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);
@@ -30,7 +30,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");
@@ -42,7 +42,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");
@@ -55,7 +55,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 = {};
@@ -75,7 +76,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 = {};
@@ -97,7 +98,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,
@@ -119,10 +120,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 = {};
@@ -145,7 +148,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
*/

View File

@@ -98,6 +98,16 @@
</div>
</nav>
<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">
<button
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 { DATE_TIME_FORMAT } from "@/constants";
import * as types from "@/store/mutation-types";
import { humanReadableDistance } from "@/util";
export default {
components: {
@@ -165,7 +176,7 @@ export default {
};
},
computed: {
...mapState(["users", "devices", "map"]),
...mapState(["users", "devices", "map", "distanceTravelled"]),
selectedUser: {
get() {
return this.$store.state.selectedUser;
@@ -224,6 +235,7 @@ export default {
"setStartDateTime",
"setEndDateTime",
]),
humanReadableDistance,
},
};
</script>

View File

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

View File

@@ -73,6 +73,7 @@ const DEFAULT_CONFIG = {
primaryColor: "#3f51b5",
selectedDevice: null,
selectedUser: null,
showDistanceTravelled: true,
startDateTime,
verbose: false,
};

View File

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

View File

@@ -10,6 +10,7 @@
"Select user": "Benutzer auswählen",
"Show all": "Alle anzeigen",
"Select device": "Gerät auswählen",
"Distance travelled: {distance}": "Gereiste Entfernung: {distance}",
"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",

View File

@@ -10,6 +10,7 @@
"Select user": "Select user",
"Show all": "Show all",
"Select device": "Select device",
"Distance travelled: {distance}": "Distance travelled: {distance}",
"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",

View File

@@ -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";
/** @typedef {import("./types").QueryParams} QueryParams */
/** @typedef {import("./types").User} User */
@@ -121,6 +121,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 +165,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)
);
}
};
/**

View File

@@ -12,8 +12,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 = [];
@@ -33,8 +34,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 = [];
@@ -45,7 +47,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) >

View File

@@ -27,6 +27,7 @@ export default new Vuex.Store({
zoom: config.map.zoom,
layers: config.map.layers,
},
distanceTravelled: null,
},
getters,
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_ZOOM = "SET_MAP_ZOOM";
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 }) {
state.map.layers[layer] = visibility;
},
[types.SET_DISTANCE_TRAVELLED](state, distanceTravelled) {
state.distanceTravelled = distanceTravelled;
},
};

View File

@@ -108,6 +108,7 @@ pre {
nav {
display: flex;
flex: 1;
align-items: center;
&:not(:first-child) {
margin-left: 20px;

View File

@@ -1,3 +1,5 @@
/* eslint max-len: 0 */
/**
* A coordinate with latitude and longitude.
*

View File

@@ -10,7 +10,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("/")
@@ -24,7 +24,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();
@@ -32,7 +32,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;
@@ -44,7 +44,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;
@@ -83,3 +83,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
* @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

@@ -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,29 @@ 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,
}));
},
},
};
</script>

View File

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