mirror of
https://github.com/owntracks/frontend.git
synced 2026-02-15 21:09:50 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a47fd1b6c | ||
|
|
163e0e3ec7 | ||
|
|
8d8664a338 | ||
|
|
045e635c21 | ||
|
|
7db7837dfd | ||
|
|
beb522c03e | ||
|
|
658cb6b223 | ||
|
|
7ab98be4ad | ||
|
|
6d4d47b5a1 | ||
|
|
159470181c | ||
|
|
b53a0be707 | ||
|
|
4e70d3a3ad | ||
|
|
b42f6db024 | ||
|
|
3fbf0a2ff1 | ||
|
|
206eb268fa | ||
|
|
d7266f48f1 | ||
|
|
9ec17e3e9c | ||
|
|
e320441b5e | ||
|
|
1282d93769 | ||
|
|
b8661b11fb | ||
|
|
4f5bfefc36 | ||
|
|
a1faf8153b | ||
|
|
23d73461bc | ||
|
|
90dac4022a | ||
|
|
58b22aeebe | ||
|
|
cb79a4de81 | ||
|
|
c5f491b6fb | ||
|
|
49a9b54a5e | ||
|
|
c449fcaf21 | ||
|
|
1746c359f4 | ||
|
|
6180e52f71 | ||
|
|
0f0f29fcee | ||
|
|
cb0694b032 | ||
|
|
d0f2a99302 | ||
|
|
c8b0ec8b9e | ||
|
|
906eb2a1b4 | ||
|
|
005aab715f | ||
|
|
b76cbdc2e6 |
17
.github/workflows/build.yml
vendored
Normal file
17
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Build
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: install
|
||||
- name: Run production build
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: build
|
||||
75
.github/workflows/docker.yml
vendored
Normal file
75
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Build Docker images
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # everyday at 3am
|
||||
pull_request:
|
||||
branches: master
|
||||
push:
|
||||
branches: master
|
||||
tags:
|
||||
- v*
|
||||
release:
|
||||
types: [published]
|
||||
branches: master
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
DOCKER_IMAGE=owntracks/frontend
|
||||
DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64
|
||||
VERSION=master
|
||||
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
fi
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
VERSION=nightly
|
||||
fi
|
||||
|
||||
TAGS="--tag ${DOCKER_IMAGE}:${VERSION}"
|
||||
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
|
||||
echo ::set-output name=docker_image::${DOCKER_IMAGE}
|
||||
echo ::set-output name=version::${VERSION}
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||
--build-arg VCS_REF=${GITHUB_SHA::8} \
|
||||
${TAGS} --file ./docker/Dockerfile .
|
||||
- name: Set up Docker Buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v3
|
||||
- name: Docker Buildx (build)
|
||||
run: |
|
||||
docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }}
|
||||
- name: Docker Login
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
||||
- name: Docker Buildx (push)
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
run: |
|
||||
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }}
|
||||
- name: Docker Check Manifest
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
run: |
|
||||
docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}
|
||||
- name: Clear
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
run: |
|
||||
rm -f ${HOME}/.docker/config.json
|
||||
21
.github/workflows/lint.yml
vendored
Normal file
21
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Lint
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: install
|
||||
- name: Lint JavaScript/Vue files
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: lint:js
|
||||
- name: Lint Markdown files
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: lint:md
|
||||
17
.github/workflows/test.yml
vendored
Normal file
17
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Tests
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: install
|
||||
- name: Run unit tests
|
||||
uses: borales/actions-yarn@v2.0.0
|
||||
with:
|
||||
cmd: test
|
||||
@@ -1,8 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
cache: yarn
|
||||
script:
|
||||
- yarn lint:js
|
||||
- yarn lint:md
|
||||
- yarn test
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -2,6 +2,34 @@
|
||||
|
||||
Dates are in UTC.
|
||||
|
||||
## 2.6.0 (2020-12-29)
|
||||
|
||||
- Add `router.basePath` config option for non-webroot deployments
|
||||
- Configure Vue to not assume it's on the web root ([#47](https://github.com/owntracks/frontend/pull/47), [@growse](https://github.com/growse))
|
||||
- Update Docker NGINX config to listen on IPv6 as well ([#46](https://github.com/owntracks/frontend/pull/46), [@growse](https://github.com/growse))
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.5.1 (2020-10-27)
|
||||
|
||||
- Fix incorrect handling of `api.baseUrl` with trailing slash ([#44](https://github.com/owntracks/frontend/pull/44), [@karmanyaahm](https://github.com/karmanyaahm))
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.5.0 (2020-09-07)
|
||||
|
||||
- Add `filters.fitView` config option - this will prevent the map from re-fitting automatically by default when a live location changes ([#41](https://github.com/owntracks/frontend/issues/41))
|
||||
- Show regions for location on popup
|
||||
- Fix vertical offset of non-pin popups
|
||||
- Build Docker images for multiple architectures (linux/amd64, linux/arm/v7, linux/arm64) using GitHub Actions ([#38](https://github.com/owntracks/frontend/pull/38), [@wollew](https://github.com/wollew))
|
||||
- Replace Travis CI with GitHub Actions build/lint/test workflows ([#39](https://github.com/owntracks/frontend/pull/39))
|
||||
- Replace node-sass with sass (dart-sass)
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.4.0 (2020-06-01)
|
||||
|
||||
- Add `filters.minAccuracy` config option - this allows ignoring location points which do
|
||||
not meet the configured accuracy requirement ([#35](https://github.com/owntracks/frontend/issues/35))
|
||||
- Upgrade dependencies
|
||||
|
||||
## 2.3.1 (2020-05-09)
|
||||
|
||||
- Fix linting issue in `config.md`
|
||||
|
||||
26
README.md
26
README.md
@@ -2,7 +2,10 @@
|
||||
|
||||

|
||||
[](https://hub.docker.com/r/owntracks/frontend)
|
||||
[](https://travis-ci.org/owntracks/frontend)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ABuild+branch%3Amaster)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ATests+branch%3Amaster)
|
||||
[](https://github.com/owntracks/frontend/actions?query=workflow%3ALint+branch%3Amaster)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://github.com/owntracks/frontend/blob/master/LICENSE)
|
||||
|
||||

|
||||
@@ -20,13 +23,14 @@ pages, this is a more advanced interface with more functionality, all in one pla
|
||||
- Last known (i.e. live) locations:
|
||||
- Accuracy visualization (circle)
|
||||
- Device friendly name and icon
|
||||
- Detailed information (if available): time, latitude, longitude, height, battery and
|
||||
speed
|
||||
- Detailed information (if available): time, latitude, longitude, height, battery,
|
||||
speed and regions
|
||||
- Location history (data points, line or both)
|
||||
- Location heatmap
|
||||
- Quickly fit all shown objects on the map into view
|
||||
- Display data in a specific date range
|
||||
- Filter by user and device
|
||||
- Display data in a specific date and time range
|
||||
- Filter by user or specific device
|
||||
- Calculation of distance travelled
|
||||
- Download selected location data as JSON
|
||||
- Highly customisable
|
||||
|
||||
@@ -144,18 +148,6 @@ and start translating the individual strings. Make sure to [mention the new loca
|
||||
For a specific example see commit [`b2edda4`](https://github.com/owntracks/frontend/commit/b2edda410f16633aa6fd9cd4e5250f2031536c7d)
|
||||
where German translations were added.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/loading.gif" alt="Loading...">
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/downloader.png" alt="Download location data">
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://raw.githubusercontent.com/owntracks/frontend/master/docs/images/info.png" alt="Info">
|
||||
</p>
|
||||
|
||||
## Contributing
|
||||
|
||||
Please feel free to open an issue and discuss your ideas and report bugs. If you think
|
||||
|
||||
@@ -6,7 +6,7 @@ COPY . ./
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:1.17-alpine
|
||||
LABEL version="2.3.1"
|
||||
LABEL version="2.6.0"
|
||||
LABEL description="OwnTracks UI"
|
||||
LABEL maintainer="Linus Groh <mail@linusgroh.de>"
|
||||
ENV LISTEN_PORT=80 \
|
||||
|
||||
@@ -8,6 +8,7 @@ http {
|
||||
}
|
||||
server {
|
||||
listen ${LISTEN_PORT};
|
||||
listen [::]:${LISTEN_PORT};
|
||||
root /usr/share/nginx/html;
|
||||
location /api {
|
||||
proxy_pass http://otrecorder/api/;
|
||||
|
||||
@@ -26,6 +26,8 @@ window.owntracks.config = {};
|
||||
- [`baseUrl`](#apibaseurl)
|
||||
- [`fetchOptions`](#apifetchoptions)
|
||||
- [`endDateTime`](#enddatetime)
|
||||
- `filters`
|
||||
- [`minAccuracy`](#filtersminaccuracy)
|
||||
- [`ignorePingLocation`](#ignorepinglocation)
|
||||
- [`locale`](#locale)
|
||||
- `map`
|
||||
@@ -58,8 +60,11 @@ window.owntracks.config = {};
|
||||
- [`polyline`](#mappolyline)
|
||||
- [`url`](#mapurl)
|
||||
- `onLocationChange`
|
||||
- [`fitView`](#onlocationchangefitview)
|
||||
- [`reloadHistory`](#onlocationchangereloadhistory)
|
||||
- [`primaryColor`](#primarycolor)
|
||||
- `router`
|
||||
- [`basePath`](#routerbasepath)
|
||||
- [`selectedDevice`](#selecteddevice)
|
||||
- [`selectedUser`](#selecteduser)
|
||||
- [`showDistanceTravelled`](#showdistancetravelled)
|
||||
@@ -77,16 +82,16 @@ Base URL for the recorder's HTTP and WebSocket API. Keep CORS in mind.
|
||||
// API requests will be made to https://owntracks.example.com/api/0/...
|
||||
window.owntracks.config = {
|
||||
api: {
|
||||
baseUrl: "https://owntracks.example.com"
|
||||
}
|
||||
baseUrl: "https://owntracks.example.com",
|
||||
},
|
||||
};
|
||||
```
|
||||
```js
|
||||
// API requests will be made to https://example.com/owntracks/api/0/...
|
||||
window.owntracks.config = {
|
||||
api: {
|
||||
baseUrl: "https://example.com/owntracks/"
|
||||
}
|
||||
baseUrl: "https://example.com/owntracks/",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -104,9 +109,9 @@ You can use this for example to send custom HTTP headers or to include cookies i
|
||||
window.owntracks.config = {
|
||||
api: {
|
||||
fetchOptions: {
|
||||
credentials: "include"
|
||||
}
|
||||
}
|
||||
credentials: "include",
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -120,7 +125,27 @@ Initial end date and time (browser timezone) for fetched data.
|
||||
```js
|
||||
// Data will be fetched up to 1970-01-01
|
||||
window.owntracks.config = {
|
||||
endDateTime: new Date(1970, 1, 1)
|
||||
endDateTime: new Date(1970, 1, 1),
|
||||
};
|
||||
```
|
||||
|
||||
### `filters.minAccuracy`
|
||||
|
||||
Minimum accuracy in meters for location points to be rendered & included in the travelled distance.
|
||||
|
||||
This filter is disabled by default as accuracies can vary across devices an locations, but you're
|
||||
encouraged to set it as it can be a simple way to remove outliers and vastly improve the travelled
|
||||
distance calculation.
|
||||
|
||||
- Type: [`Number`] or `null`
|
||||
- Default: `null`
|
||||
- Example:
|
||||
```js
|
||||
// Don't include location points with an accuracy exceeding 100 meters
|
||||
window.owntracks.config = {
|
||||
filters: {
|
||||
minAccuracy: 100,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -135,7 +160,7 @@ Remove the `ping/ping` location from the fetched data. This is useful when using
|
||||
```js
|
||||
// Don't ignore ping/ping location. Not sure why you'd do this :)
|
||||
window.owntracks.config = {
|
||||
ignorePingLocation: false
|
||||
ignorePingLocation: false,
|
||||
};
|
||||
```
|
||||
|
||||
@@ -166,8 +191,8 @@ Attribution for map tiles.
|
||||
// Make sure to add proper attribution!
|
||||
window.owntracks.config = {
|
||||
map: {
|
||||
attribution: "Map tiles © MyTileServerProvider"
|
||||
}
|
||||
attribution: "Map tiles © MyTileServerProvider",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -334,8 +359,8 @@ splitting into separate lines.
|
||||
// Don't connect points with a distance of more than 1km
|
||||
window.owntracks.config = {
|
||||
map: {
|
||||
maxPointDistance: 1000
|
||||
}
|
||||
maxPointDistance: 1000,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -376,11 +401,20 @@ and [this Wikipedia article](https://en.wikipedia.org/wiki/Tiled_web_map).
|
||||
window.owntracks.config = {
|
||||
map: {
|
||||
url:
|
||||
"https://api.mapbox.com/v4/mapbox.dark/{z}/{x}/{y}@2x.png?access_token=xxxxxxxxxxxxxxxx"
|
||||
}
|
||||
"https://api.mapbox.com/v4/mapbox.dark/{z}/{x}/{y}@2x.png?access_token=xxxxxxxxxxxxxxxx",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### `onLocationChange.fitView`
|
||||
|
||||
Whether to re-fit the map's content into view or not when a location update is received.
|
||||
|
||||
This can be useful if you're showing live locations and don't want them to "leave" the map.
|
||||
|
||||
- Type: [`Boolean`]
|
||||
- Default: `false`
|
||||
|
||||
### `onLocationChange.reloadHistory`
|
||||
|
||||
Whether to reload the location history (of selected date range) or not when a location
|
||||
@@ -399,7 +433,23 @@ Primary color for the user interface (navigation bar and various map elements).
|
||||
```js
|
||||
// Set the UI's primary color to 'rebeccapurple'
|
||||
window.owntracks.config = {
|
||||
primaryColor: "rebeccapurple"
|
||||
primaryColor: "rebeccapurple",
|
||||
};
|
||||
```
|
||||
|
||||
### `router.basePath`
|
||||
|
||||
Base path of the application deployment.
|
||||
|
||||
- Type: [`String`]
|
||||
- Default: `"/"`
|
||||
- Example:
|
||||
```js
|
||||
// Frontend will be reachable at https://example.com/owntracks
|
||||
window.owntracks.config = {
|
||||
router: {
|
||||
basePath: "/owntracks",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -418,7 +468,7 @@ amount of data fetched after page load.
|
||||
// Select the device 'phone' from user 'foo' by default
|
||||
window.owntracks.config = {
|
||||
selectedUser: "foo",
|
||||
selectedDevice: "phone"
|
||||
selectedDevice: "phone",
|
||||
};
|
||||
```
|
||||
|
||||
@@ -435,7 +485,7 @@ amount of data fetched after page load.
|
||||
```js
|
||||
// Select all devices from user 'foo' by default
|
||||
window.owntracks.config = {
|
||||
selectedUser: "foo"
|
||||
selectedUser: "foo",
|
||||
};
|
||||
```
|
||||
|
||||
@@ -462,7 +512,7 @@ Initial start date and time (browser timezone) for fetched data.
|
||||
startDateTime.setHours(0, 0, 0, 0);
|
||||
startDateTime.setDate(1);
|
||||
window.owntracks.config = {
|
||||
startDateTime
|
||||
startDateTime,
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 370 KiB |
51
package.json
51
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owntracks-ui",
|
||||
"version": "2.3.1",
|
||||
"version": "2.6.0",
|
||||
"author": {
|
||||
"name": "Linus Groh",
|
||||
"email": "mail@linusgroh.de"
|
||||
@@ -17,44 +17,45 @@
|
||||
"test": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard-copy": "^3.1.0",
|
||||
"core-js": "^3.6.5",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"core-js": "^3.8.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"leaflet": "^1.6.0",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet.heat": "^0.2.0",
|
||||
"moment": "^2.25.3",
|
||||
"vue": "^2.6.11",
|
||||
"vue-ctk-date-time-picker": "^2.4.0",
|
||||
"vue-feather-icons": "^5.0.0",
|
||||
"vue-i18n": "^8.17.4",
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.12",
|
||||
"vue-ctk-date-time-picker": "^2.5.0",
|
||||
"vue-feather-icons": "^5.1.0",
|
||||
"vue-i18n": "^8.22.2",
|
||||
"vue-js-modal": "^1.3.33",
|
||||
"vue-mq": "^1.0.1",
|
||||
"vue-outside-events": "^1.1.3",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue2-leaflet": "^2.5.2",
|
||||
"vuex": "^3.3.0"
|
||||
"vue-router": "^3.4.9",
|
||||
"vue2-leaflet": "^2.6.0",
|
||||
"vuex": "^3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.3.1",
|
||||
"@vue/cli-plugin-eslint": "~4.3.1",
|
||||
"@vue/cli-plugin-unit-jest": "~4.3.1",
|
||||
"@vue/cli-service": "~4.3.1",
|
||||
"@vue/cli-plugin-babel": "~4.5.9",
|
||||
"@vue/cli-plugin-eslint": "~4.5.9",
|
||||
"@vue/cli-plugin-unit-jest": "~4.5.9",
|
||||
"@vue/cli-service": "~4.5.9",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/test-utils": "1.0.2",
|
||||
"@vue/test-utils": "1.1.2",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.0.1",
|
||||
"babel-jest": "^26.6.3",
|
||||
"cors-anywhere": "^0.4.3",
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"eslint": "^7.16.0",
|
||||
"eslint-plugin-prettier": "^3.3.0",
|
||||
"eslint-plugin-vue": "^7.4.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"lint-staged": "^10.2.2",
|
||||
"lint-staged": "^10.5.3",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"prettier": "^2.2.1",
|
||||
"sass": "^1.30.0",
|
||||
"sass-loader": "^10.1.0",
|
||||
"vue-cli-plugin-i18n": "~1.0.1",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
this.populateStateFromQuery(this.$route.query);
|
||||
this.loadData();
|
||||
// Update URL query params when relevant values changes
|
||||
this.$store.subscribe(mutation => {
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (
|
||||
[
|
||||
types.SET_SELECTED_USER,
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
selectedDevice: device,
|
||||
} = this.$store.state;
|
||||
const activeLayers = Object.keys(map.layers).filter(
|
||||
key => map.layers[key] === true
|
||||
(key) => map.layers[key] === true
|
||||
);
|
||||
const query = {
|
||||
lat: map.center.lat,
|
||||
|
||||
27
src/api.js
27
src/api.js
@@ -11,9 +11,11 @@ import { getApiUrl, getLocationHistoryCount } from "@/util";
|
||||
*/
|
||||
const fetchApi = (path, params = {}) => {
|
||||
const url = getApiUrl(path);
|
||||
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
|
||||
Object.keys(params).forEach((key) =>
|
||||
url.searchParams.append(key, params[key])
|
||||
);
|
||||
log("HTTP", `GET ${url.href}`);
|
||||
return fetch(url.href, config.api.fetchOptions).catch(error =>
|
||||
return fetch(url.href, config.api.fetchOptions).catch((error) =>
|
||||
log("HTTP", error, logLevels.ERROR)
|
||||
);
|
||||
};
|
||||
@@ -51,10 +53,10 @@ export const getUsers = async () => {
|
||||
* @returns {{User: Device[]}}
|
||||
* Object mapping each username to an array of device names
|
||||
*/
|
||||
export const getDevices = async users => {
|
||||
export const getDevices = async (users) => {
|
||||
const devices = {};
|
||||
await Promise.all(
|
||||
users.map(async user => {
|
||||
users.map(async (user) => {
|
||||
const response = await fetchApi(`/api/0/list`, { user });
|
||||
const json = await response.json();
|
||||
const userDevices = json.results;
|
||||
@@ -63,7 +65,7 @@ export const getDevices = async users => {
|
||||
);
|
||||
log("API", () => {
|
||||
const devicesCount = Object.keys(devices)
|
||||
.map(user => devices[user].length)
|
||||
.map((user) => devices[user].length)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
return (
|
||||
`[getDevices] Fetched ${devicesCount} ` +
|
||||
@@ -144,10 +146,10 @@ export const getUserDeviceLocationHistory = async (
|
||||
export const getLocationHistory = async (devices, start, end) => {
|
||||
const locationHistory = {};
|
||||
await Promise.all(
|
||||
Object.keys(devices).map(async user => {
|
||||
Object.keys(devices).map(async (user) => {
|
||||
locationHistory[user] = {};
|
||||
await Promise.all(
|
||||
devices[user].map(async device => {
|
||||
devices[user].map(async (device) => {
|
||||
locationHistory[user][device] = await getUserDeviceLocationHistory(
|
||||
user,
|
||||
device,
|
||||
@@ -174,7 +176,7 @@ export const getLocationHistory = async (devices, start, end) => {
|
||||
*
|
||||
* @param {WebSocketLocationCallback} [callback] Callback for location messages
|
||||
*/
|
||||
export const connectWebsocket = async callback => {
|
||||
export const connectWebsocket = async (callback) => {
|
||||
let url = getApiUrl("/ws/last");
|
||||
url.protocol = url.protocol.replace("http", "ws");
|
||||
url = url.href;
|
||||
@@ -184,16 +186,17 @@ export const connectWebsocket = async callback => {
|
||||
log("WS", "Connected");
|
||||
ws.send("LAST");
|
||||
};
|
||||
ws.onclose = event => {
|
||||
ws.onclose = (event) => {
|
||||
log(
|
||||
"WS",
|
||||
`Disconnected unexpectedly (reason: ${event.reason ||
|
||||
"unknown"}). Reconnecting in one second.`,
|
||||
`Disconnected unexpectedly (reason: ${
|
||||
event.reason || "unknown"
|
||||
}). Reconnecting in one second.`,
|
||||
logLevels.WARNING
|
||||
);
|
||||
setTimeout(connectWebsocket, 1000);
|
||||
};
|
||||
ws.onmessage = async msg => {
|
||||
ws.onmessage = async (msg) => {
|
||||
if (msg.data) {
|
||||
try {
|
||||
const data = JSON.parse(msg.data);
|
||||
|
||||
@@ -222,9 +222,7 @@ export default {
|
||||
},
|
||||
set(value) {
|
||||
this.setStartDateTime(
|
||||
moment(value, DATE_TIME_FORMAT)
|
||||
.utc()
|
||||
.format(DATE_TIME_FORMAT)
|
||||
moment(value, DATE_TIME_FORMAT).utc().format(DATE_TIME_FORMAT)
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<LPopup>
|
||||
<LPopup :options="options">
|
||||
<div class="device">{{ deviceName }}</div>
|
||||
<div class="wrapper">
|
||||
<img
|
||||
@@ -35,6 +35,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="regions.length" class="regions">
|
||||
{{ $t("Regions:") }}
|
||||
{{ regions.join(", ") }}
|
||||
</div>
|
||||
</LPopup>
|
||||
</template>
|
||||
|
||||
@@ -56,6 +60,11 @@
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
.regions {
|
||||
border-top: 1px solid var(--color-separator);
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -123,6 +132,14 @@ export default {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
regions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
this.parentContainer.addLayer(this, !this.visible);
|
||||
this.$watch(
|
||||
"latLng",
|
||||
newVal => {
|
||||
(newVal) => {
|
||||
this.mapObject.setLatLngs(newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
|
||||
@@ -3,16 +3,12 @@
|
||||
<ul class="info-list">
|
||||
<li>
|
||||
<GithubIcon size="1x" aria-hidden="true" role="img" />
|
||||
<a href="https://github.com/owntracks/frontend">
|
||||
owntracks/frontend
|
||||
</a>
|
||||
<a href="https://github.com/owntracks/frontend">owntracks/frontend</a>
|
||||
({{ frontendVersion }})
|
||||
</li>
|
||||
<li>
|
||||
<GithubIcon size="1x" aria-hidden="true" role="img" />
|
||||
<a href="https://github.com/owntracks/recorder">
|
||||
owntracks/recorder
|
||||
</a>
|
||||
<a href="https://github.com/owntracks/recorder">owntracks/recorder</a>
|
||||
({{ recorderVersion || "Loading version..." }})
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -13,6 +13,9 @@ const DEFAULT_CONFIG = {
|
||||
fetchOptions: {},
|
||||
},
|
||||
endDateTime,
|
||||
filters: {
|
||||
minAccuracy: null,
|
||||
},
|
||||
ignorePingLocation: true,
|
||||
locale: "en",
|
||||
map: {
|
||||
@@ -64,9 +67,13 @@ const DEFAULT_CONFIG = {
|
||||
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
},
|
||||
onLocationChange: {
|
||||
fitView: false,
|
||||
reloadHistory: false,
|
||||
},
|
||||
primaryColor: "#3f51b5",
|
||||
router: {
|
||||
basePath: "/",
|
||||
},
|
||||
selectedDevice: null,
|
||||
selectedUser: null,
|
||||
showDistanceTravelled: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ Vue.use(VueI18n);
|
||||
|
||||
const locales = require.context("./locales", true, /[A-Za-z0-9-_,\s]+\.json$/i);
|
||||
const messages = {};
|
||||
locales.keys().forEach(key => {
|
||||
locales.keys().forEach((key) => {
|
||||
const matched = key.match(/([A-Za-z0-9-_]+)\./i);
|
||||
if (matched && matched.length > 1) {
|
||||
const locale = matched[1];
|
||||
|
||||
@@ -29,5 +29,6 @@
|
||||
"Location": "Standort",
|
||||
"Address": "Adresse",
|
||||
"Battery": "Akku",
|
||||
"Speed": "Geschwindigkeit"
|
||||
"Speed": "Geschwindigkeit",
|
||||
"Regions:": "Regionen:"
|
||||
}
|
||||
|
||||
@@ -29,5 +29,6 @@
|
||||
"Location": "Location",
|
||||
"Address": "Address",
|
||||
"Battery": "Battery",
|
||||
"Speed": "Speed"
|
||||
"Speed": "Speed",
|
||||
"Regions:": "Regions:"
|
||||
}
|
||||
|
||||
@@ -29,5 +29,6 @@
|
||||
"Location": "Ubicación",
|
||||
"Address": "Dirección",
|
||||
"Battery": "Bateria",
|
||||
"Speed": "Velocidad"
|
||||
"Speed": "Velocidad",
|
||||
"Regions:": "Regiones:"
|
||||
}
|
||||
|
||||
@@ -29,5 +29,5 @@ new Vue({
|
||||
i18n,
|
||||
router,
|
||||
store,
|
||||
render: h => h(App),
|
||||
render: (h) => h(App),
|
||||
}).$mount("#app");
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Vue from "vue";
|
||||
import Router from "vue-router";
|
||||
import Map from "./views/Map.vue";
|
||||
import config from "@/config";
|
||||
import Map from "@/views/Map.vue";
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
mode: "history",
|
||||
base: process.env.BASE_URL,
|
||||
base: config.router.basePath,
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
|
||||
@@ -43,7 +43,7 @@ const populateStateFromQuery = ({ state, commit }, query) => {
|
||||
}
|
||||
if (query.layers) {
|
||||
const activeLayers = query.layers.split(",");
|
||||
Object.keys(state.map.layers).forEach(layer => {
|
||||
Object.keys(state.map.layers).forEach((layer) => {
|
||||
const visibility = activeLayers.includes(layer);
|
||||
if (state.map.layers[layer] !== visibility) {
|
||||
commit(types.SET_MAP_LAYER_VISIBILITY, { layer, visibility });
|
||||
@@ -116,20 +116,25 @@ const getLastLocations = async ({ commit, state }) => {
|
||||
// Remove ping/ping from the owntracks/recorder Docker image
|
||||
// https://github.com/owntracks/frontend/issues/12
|
||||
lastLocations = lastLocations.filter(
|
||||
l => !(l.username === "ping" && l.device === "ping")
|
||||
(l) => !(l.username === "ping" && l.device === "ping")
|
||||
);
|
||||
}
|
||||
commit(types.SET_LAST_LOCATIONS, lastLocations);
|
||||
};
|
||||
|
||||
const _getDistanceTravelled = locationHistory => {
|
||||
const _getDistanceTravelled = (locationHistory) => {
|
||||
const start = Date.now();
|
||||
let distanceTravelled = 0;
|
||||
Object.keys(locationHistory).forEach(user => {
|
||||
Object.keys(locationHistory[user]).forEach(device => {
|
||||
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);
|
||||
locationHistory[user][device].forEach((location) => {
|
||||
if (
|
||||
config.filters.minAccuracy !== null &&
|
||||
location.acc > config.filters.minAccuracy
|
||||
)
|
||||
return;
|
||||
const latLng = L.latLng(location.lat, location.lon);
|
||||
if (lastLatLng !== null) {
|
||||
const distance = distanceBetweenCoordinates(lastLatLng, latLng);
|
||||
if (
|
||||
|
||||
@@ -4,20 +4,46 @@ import config from "@/config";
|
||||
import { distanceBetweenCoordinates } from "@/util";
|
||||
|
||||
/**
|
||||
* From the selected users' and devices' location histories, create an
|
||||
* array of all coordinates.
|
||||
* Apply filters to the selected users' and devices' location histories.
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {LocationHistory} state.locationHistory
|
||||
* Location history of selected users and devices
|
||||
* @returns {LocationHistory} Filtered location history
|
||||
*/
|
||||
const filteredLocationHistory = (state) => {
|
||||
const locationHistory = {};
|
||||
Object.keys(state.locationHistory).forEach((user) => {
|
||||
locationHistory[user] = {};
|
||||
Object.keys(state.locationHistory[user]).forEach((device) => {
|
||||
locationHistory[user][device] = [];
|
||||
state.locationHistory[user][device].forEach((location) => {
|
||||
if (
|
||||
config.filters.minAccuracy !== null &&
|
||||
location.acc > config.filters.minAccuracy
|
||||
)
|
||||
return;
|
||||
locationHistory[user][device].push(location);
|
||||
});
|
||||
});
|
||||
});
|
||||
return locationHistory;
|
||||
};
|
||||
|
||||
/**
|
||||
* From the selected users' and devices' location histories, create an
|
||||
* array of all coordinates.
|
||||
*
|
||||
* @param {State} state
|
||||
* @returns {L.LatLng[]} All coordinates
|
||||
*/
|
||||
const locationHistoryLatLngs = state => {
|
||||
const filteredLocationHistoryLatLngs = (state) => {
|
||||
const latLngs = [];
|
||||
Object.keys(state.locationHistory).forEach(user => {
|
||||
Object.keys(state.locationHistory[user]).forEach(device => {
|
||||
state.locationHistory[user][device].forEach(coordinate => {
|
||||
latLngs.push(L.latLng(coordinate.lat, coordinate.lon));
|
||||
const locationHistory = filteredLocationHistory(state);
|
||||
Object.keys(locationHistory).forEach((user) => {
|
||||
Object.keys(locationHistory[user]).forEach((device) => {
|
||||
locationHistory[user][device].forEach((location) => {
|
||||
latLngs.push(L.latLng(location.lat, location.lon));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,17 +56,16 @@ const locationHistoryLatLngs = state => {
|
||||
* coordinates does not exceed `config.map.maxPointDistance`.
|
||||
*
|
||||
* @param {State} state
|
||||
* @param {LocationHistory} state.locationHistory
|
||||
* Location history of selected users and devices
|
||||
* @returns {L.LatLng[][]} Groups of coherent coordinates
|
||||
*/
|
||||
const locationHistoryLatLngGroups = state => {
|
||||
const filteredLocationHistoryLatLngGroups = (state) => {
|
||||
const groups = [];
|
||||
Object.keys(state.locationHistory).forEach(user => {
|
||||
Object.keys(state.locationHistory[user]).forEach(device => {
|
||||
const locationHistory = filteredLocationHistory(state);
|
||||
Object.keys(locationHistory).forEach((user) => {
|
||||
Object.keys(locationHistory[user]).forEach((device) => {
|
||||
let latLngs = [];
|
||||
state.locationHistory[user][device].forEach(coordinate => {
|
||||
const latLng = L.latLng(coordinate.lat, coordinate.lon);
|
||||
locationHistory[user][device].forEach((location) => {
|
||||
const latLng = L.latLng(location.lat, location.lon);
|
||||
// Skip if group splitting is disabled or this is the first
|
||||
// coordinate in the current group
|
||||
if (
|
||||
@@ -68,6 +93,7 @@ const locationHistoryLatLngGroups = state => {
|
||||
};
|
||||
|
||||
export default {
|
||||
locationHistoryLatLngs,
|
||||
locationHistoryLatLngGroups,
|
||||
filteredLocationHistory,
|
||||
filteredLocationHistoryLatLngs,
|
||||
filteredLocationHistoryLatLngGroups,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
--color-background: #fff;
|
||||
--color-primary: #3f51b5;
|
||||
--color-primary-text: #fff;
|
||||
--color-separator: #ddd;
|
||||
--drop-shadow: drop-shadow(0 10px 10px rgb(0, 0, 0, 0.2));
|
||||
--dropdown-arrow: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2225%22%20height%3D%2210%22%3E%3Cpath%20fill%3D%22%23333%22%20fill-opacity%3D%221%22%20stroke%3D%22none%22%20d%3D%22M0%2C0%20L0%2C0%20L1%2C0%20L1%2C6%20L7%2C6%20L7%2C7%20L0%2C7%20z%22%20transform%3D%22rotate(-45%205%200)%22%20%2F%3E%3C%2Fsvg%3E");
|
||||
--pin-width: 32px;
|
||||
|
||||
@@ -6,14 +6,5 @@
|
||||
box-shadow: none !important;
|
||||
filter: var(--drop-shadow);
|
||||
margin-top: 5px;
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
// Fix buttons being off screen
|
||||
.datepicker-buttons-container {
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
.leaflet-popup {
|
||||
filter: var(--drop-shadow);
|
||||
margin-bottom: calc(var(--pin-width) * 1.5 + 20px);
|
||||
|
||||
&--for-pin {
|
||||
margin-bottom: calc(var(--pin-width) * 1.5 + 20px);
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 3px;
|
||||
|
||||
16
src/util.js
16
src/util.js
@@ -10,9 +10,9 @@ import { DATE_TIME_FORMAT, EARTH_RADIUS_IN_KM } from "@/constants";
|
||||
* @param {String} path Path to the API resource
|
||||
* @returns {URL} Final API URL
|
||||
*/
|
||||
export const getApiUrl = path => {
|
||||
export const getApiUrl = (path) => {
|
||||
const normalizedBaseUrl = config.api.baseUrl.endsWith("/")
|
||||
? config.api.baseUrl.slice(1)
|
||||
? config.api.baseUrl.slice(0, -1)
|
||||
: config.api.baseUrl;
|
||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return new URL(`${normalizedBaseUrl}${normalizedPath}`);
|
||||
@@ -24,7 +24,7 @@ export const getApiUrl = path => {
|
||||
* @param {String} s Input value to be tested
|
||||
* @returns {Boolean} Whether the input matches the expected format
|
||||
*/
|
||||
export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
|
||||
export const isIsoDateTime = (s) => moment(s, DATE_TIME_FORMAT, true).isValid();
|
||||
|
||||
/**
|
||||
* Convert degrees to radians.
|
||||
@@ -32,7 +32,7 @@ export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
|
||||
* @param {Number} degrees Angle in degrees
|
||||
* @returns {Number} Angle in radians
|
||||
*/
|
||||
export const degreesToRadians = degrees => (degrees * Math.PI) / 180;
|
||||
export const degreesToRadians = (degrees) => (degrees * Math.PI) / 180;
|
||||
|
||||
/**
|
||||
* Calculate the distance between two coordinates. Uses the haversine formula,
|
||||
@@ -91,7 +91,7 @@ export const download = (text, filename, mimeType = "text/plain") => {
|
||||
* @param {Number} distance Distance in meters
|
||||
* @returns {String} Formatted string including unit
|
||||
*/
|
||||
export const humanReadableDistance = distance => {
|
||||
export const humanReadableDistance = (distance) => {
|
||||
let unit = "m";
|
||||
if (Math.abs(distance) >= 1000) {
|
||||
distance = distance / 1000;
|
||||
@@ -108,11 +108,11 @@ export const humanReadableDistance = distance => {
|
||||
* @param {LocationHistory} locationHistory Location history
|
||||
* @returns {Number} Total number of locations
|
||||
*/
|
||||
export const getLocationHistoryCount = locationHistory =>
|
||||
export const getLocationHistoryCount = (locationHistory) =>
|
||||
Object.keys(locationHistory)
|
||||
.map(user =>
|
||||
.map((user) =>
|
||||
Object.keys(locationHistory[user])
|
||||
.map(device => locationHistory[user][device].length)
|
||||
.map((device) => locationHistory[user][device].length)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
@@ -50,13 +50,15 @@
|
||||
:alt="l.alt"
|
||||
:battery="l.batt"
|
||||
:speed="l.vel"
|
||||
:regions="l.inregions"
|
||||
:options="{ className: 'leaflet-popup--for-pin' }"
|
||||
/>
|
||||
</LMarker>
|
||||
</template>
|
||||
|
||||
<template v-if="map.layers.line">
|
||||
<LPolyline
|
||||
v-for="(group, i) in locationHistoryLatLngGroups"
|
||||
v-for="(group, i) in filteredLocationHistoryLatLngGroups"
|
||||
:key="i"
|
||||
:lat-lngs="group"
|
||||
v-bind="polyline"
|
||||
@@ -64,7 +66,7 @@
|
||||
</template>
|
||||
|
||||
<template v-if="map.layers.points">
|
||||
<template v-for="(userDevices, user) in locationHistory">
|
||||
<template v-for="(userDevices, user) in filteredLocationHistory">
|
||||
<template v-for="(deviceLocations, device) in userDevices">
|
||||
<LCircleMarker
|
||||
v-for="(l, n) in deviceLocationsWithNameAndFace(
|
||||
@@ -87,6 +89,7 @@
|
||||
:alt="l.alt"
|
||||
:battery="l.batt"
|
||||
:speed="l.vel"
|
||||
:regions="l.inregions"
|
||||
></LDeviceLocationPopup>
|
||||
</LCircleMarker>
|
||||
</template>
|
||||
@@ -95,8 +98,8 @@
|
||||
|
||||
<template v-if="map.layers.heatmap">
|
||||
<LHeatmap
|
||||
v-if="locationHistoryLatLngs.length"
|
||||
:lat-lng="locationHistoryLatLngs"
|
||||
v-if="filteredLocationHistoryLatLngs.length"
|
||||
:lat-lng="filteredLocationHistoryLatLngs"
|
||||
:max="heatmap.max"
|
||||
:radius="heatmap.radius"
|
||||
:blur="heatmap.blur"
|
||||
@@ -171,8 +174,12 @@ export default {
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["locationHistoryLatLngs", "locationHistoryLatLngGroups"]),
|
||||
...mapState(["lastLocations", "locationHistory", "map"]),
|
||||
...mapGetters([
|
||||
"filteredLocationHistory",
|
||||
"filteredLocationHistoryLatLngs",
|
||||
"filteredLocationHistoryLatLngGroups",
|
||||
]),
|
||||
...mapState(["lastLocations", "map"]),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
@@ -187,13 +194,13 @@ export default {
|
||||
(this.map.layers.line ||
|
||||
this.map.layers.points ||
|
||||
this.map.layers.heatmap) &&
|
||||
this.locationHistoryLatLngs.length > 0
|
||||
this.filteredLocationHistoryLatLngs.length > 0
|
||||
) {
|
||||
this.$refs.map.mapObject.fitBounds(
|
||||
new L.LatLngBounds(this.locationHistoryLatLngs)
|
||||
new L.LatLngBounds(this.filteredLocationHistoryLatLngs)
|
||||
);
|
||||
} else if (this.map.layers.last && this.lastLocations.length > 0) {
|
||||
const locations = this.lastLocations.map(l => L.latLng(l.lat, l.lon));
|
||||
const locations = this.lastLocations.map((l) => L.latLng(l.lat, l.lon));
|
||||
this.$refs.map.mapObject.fitBounds(new L.LatLngBounds(locations), {
|
||||
maxZoom: this.maxNativeZoom,
|
||||
});
|
||||
@@ -211,12 +218,12 @@ export default {
|
||||
*/
|
||||
deviceLocationsWithNameAndFace(user, device, deviceLocations) {
|
||||
const lastLocation = this.lastLocations.find(
|
||||
l => l.username === user && l.device === device
|
||||
(l) => l.username === user && l.device === device
|
||||
);
|
||||
if (!lastLocation) {
|
||||
return deviceLocations;
|
||||
}
|
||||
return deviceLocations.map(l => ({
|
||||
return deviceLocations.map((l) => ({
|
||||
...l,
|
||||
name: lastLocation.name,
|
||||
face: lastLocation.face,
|
||||
@@ -225,9 +232,11 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
lastLocations() {
|
||||
this.fitView();
|
||||
if (this.$config.onLocationChange.fitView) {
|
||||
this.fitView();
|
||||
}
|
||||
},
|
||||
locationHistory() {
|
||||
filteredLocationHistory() {
|
||||
this.fitView();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,6 +22,13 @@ describe("getApiUrl", () => {
|
||||
expect(getApiUrl("/foo/bar").href).toBe(
|
||||
"http://example.com/owntracks/foo/bar"
|
||||
);
|
||||
|
||||
config.api.baseUrl = "http://example.com/owntracks/";
|
||||
expect(getApiUrl("foo").href).toBe("http://example.com/owntracks/foo");
|
||||
expect(getApiUrl("/foo").href).toBe("http://example.com/owntracks/foo");
|
||||
expect(getApiUrl("/foo/bar").href).toBe(
|
||||
"http://example.com/owntracks/foo/bar"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const packageJson = fs.readFileSync("./package.json");
|
||||
const version = JSON.parse(packageJson).version;
|
||||
|
||||
module.exports = {
|
||||
publicPath: "",
|
||||
configureWebpack: {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
|
||||
Reference in New Issue
Block a user