From fd5f383530a025eb40793cdf00a6f6b20639bd77 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 10:13:02 -0800 Subject: Display position accuracy --- modern/package.json | 1 + modern/src/MainPage.js | 2 ++ modern/src/map/AccuracyMap.js | 53 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 modern/src/map/AccuracyMap.js (limited to 'modern') diff --git a/modern/package.json b/modern/package.json index bd1dd33..e5438b5 100644 --- a/modern/package.json +++ b/modern/package.json @@ -9,6 +9,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.56", "@reduxjs/toolkit": "^1.4.0", + "@turf/turf": "^5.1.6", "mapbox-gl": "^1.12.0", "moment": "^2.28.0", "ol": "^6.4.3", diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index adb86b2..d277f28 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -9,6 +9,7 @@ import MainToolbar from './MainToolbar'; import Map from './map/Map'; import PositionsMap from './map/PositionsMap'; import SelectedDeviceMap from './map/SelectedDeviceMap'; +import AccuracyMap from './map/AccuracyMap'; import GeofenceMap from './map/GeofenceMap'; const useStyles = makeStyles(theme => ({ @@ -58,6 +59,7 @@ const MainPage = ({ width }) => { + diff --git a/modern/src/map/AccuracyMap.js b/modern/src/map/AccuracyMap.js new file mode 100644 index 0000000..e81fc8f --- /dev/null +++ b/modern/src/map/AccuracyMap.js @@ -0,0 +1,53 @@ +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import circle from '@turf/circle'; + +import { map } from './Map'; + +const AccuracyMap = () => { + const id = 'accuracy'; + + const positions = useSelector(state => ({ + type: 'FeatureCollection', + features: Object.values(state.positions.items).filter(position => position.accuracy > 0).map(position => + circle([position.longitude, position.latitude], position.accuracy * 0.001) + ), + })); + + useEffect(() => { + map.addSource(id, { + 'type': 'geojson', + 'data': { + type: 'FeatureCollection', + features: [] + } + }); + map.addLayer({ + 'source': id, + 'id': id, + 'type': 'fill', + 'filter': [ + 'all', + ['==', '$type', 'Polygon'], + ], + 'paint': { + 'fill-color':'#3bb2d0', + 'fill-outline-color':'#3bb2d0', + 'fill-opacity':0.25, + }, + }); + + return () => { + map.removeLayer(id); + map.removeSource(id); + }; + }, []); + + useEffect(() => { + map.getSource(id).setData(positions); + }, [positions]); + + return null; +} + +export default AccuracyMap; -- cgit v1.2.3 From 47fd60f2c88bd540938eb0d9ba1c6e964c27d7d9 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 11:39:57 -0800 Subject: Move map styles --- modern/src/map/Map.js | 19 ++--------------- modern/src/map/mapStyles.js | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 modern/src/map/mapStyles.js (limited to 'modern') diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index fec8d50..6cb3949 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -3,6 +3,7 @@ import mapboxgl from 'mapbox-gl'; import React, { useRef, useLayoutEffect, useEffect, useState } from 'react'; import { deviceCategories } from '../common/deviceCategories'; import { loadIcon, loadImage } from './mapUtil'; +import { styleOsm } from './mapStyles'; const element = document.createElement('div'); element.style.width = '100%'; @@ -10,23 +11,7 @@ element.style.height = '100%'; export const map = new mapboxgl.Map({ container: element, - style: { - version: 8, - sources: { - osm: { - type: 'raster', - tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"], - tileSize: 256, - attribution: '© OpenStreetMap contributors', - }, - }, - glyphs: 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf', - layers: [{ - id: 'osm', - type: 'raster', - source: 'osm', - }], - }, + style: styleOsm(), }); map.addControl(new mapboxgl.NavigationControl()); diff --git a/modern/src/map/mapStyles.js b/modern/src/map/mapStyles.js new file mode 100644 index 0000000..ff323cb --- /dev/null +++ b/modern/src/map/mapStyles.js @@ -0,0 +1,52 @@ +export const styleCustom = (url, attribution) => ({ + version: 8, + sources: { + osm: { + type: 'raster', + tiles: [url], + attribution: attribution, + }, + }, + glyphs: 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf', + layers: [{ + id: 'osm', + type: 'raster', + source: 'osm', + }], +}); + +export const styleOsm = () => styleCustom( + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + '© OpenStreetMap contributors', +); + +export const styleCarto = () => ({ + 'version': 8, + 'sources': { + 'raster-tiles': { + 'type': 'raster', + 'tiles': [ + 'https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png', + 'https://b.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png', + 'https://c.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png', + 'https://d.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png' + ], + 'tileSize': 256, + 'attribution': '© OpenStreetMap contributors, © CARTO' + } + }, + 'glyphs': 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf', + 'layers': [ + { + 'id': 'simple-tiles', + 'type': 'raster', + 'source': 'raster-tiles', + 'minzoom': 0, + 'maxzoom': 22, + } + ] +}); + +export const styleMapbox = (style) => `mapbox://styles/mapbox/${style}`; + +export const styleMapTiler = (style, key) => `https://api.maptiler.com/maps/${style}/style.json?key=${key}`; -- cgit v1.2.3 From 76ef2b38fac5c58264921479b0e7d19115bb9c7d Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 14:01:16 -0800 Subject: Fix indentation --- modern/src/MainToolbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'modern') diff --git a/modern/src/MainToolbar.js b/modern/src/MainToolbar.js index 374c694..b4757d8 100644 --- a/modern/src/MainToolbar.js +++ b/modern/src/MainToolbar.js @@ -78,7 +78,7 @@ const MainToolbar = () => { Traccar - + -- cgit v1.2.3 From e0b977f7f50b4174382df7787f63d58dac3e6ed8 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 15:37:23 -0800 Subject: Implement map switching --- modern/src/map/Map.js | 52 +++++++++++++++++------- modern/src/map/switcher/switcher.css | 40 ++++++++++++++++++ modern/src/map/switcher/switcher.js | 79 ++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 modern/src/map/switcher/switcher.css create mode 100644 modern/src/map/switcher/switcher.js (limited to 'modern') diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index 6cb3949..2617b98 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -1,9 +1,11 @@ import 'mapbox-gl/dist/mapbox-gl.css'; +import './switcher/switcher.css'; import mapboxgl from 'mapbox-gl'; +import { SwitcherControl } from './switcher/switcher'; import React, { useRef, useLayoutEffect, useEffect, useState } from 'react'; import { deviceCategories } from '../common/deviceCategories'; import { loadIcon, loadImage } from './mapUtil'; -import { styleOsm } from './mapStyles'; +import { styleCarto, styleOsm } from './mapStyles'; const element = document.createElement('div'); element.style.width = '100%'; @@ -14,17 +16,17 @@ export const map = new mapboxgl.Map({ style: styleOsm(), }); -map.addControl(new mapboxgl.NavigationControl()); +let ready = false; +const readyListeners = new Set(); -let readyListeners = []; +const addReadyListener = listener => readyListeners.add(listener); -const onMapReady = listener => { - if (!readyListeners) { - listener(); - } else { - readyListeners.push(listener); - } -}; +const removeReadyListener = listener => readyListeners.delete(listener); + +const updateReadyValue = value => { + ready = value; + readyListeners.forEach(listener => listener(value)); +} map.on('load', async () => { const background = await loadImage('images/background.svg'); @@ -32,18 +34,38 @@ map.on('load', async () => { const imageData = await loadIcon(category, background, `images/icon/${category}.svg`); map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio }); })); - if (readyListeners) { - readyListeners.forEach(listener => listener()); - readyListeners = null; - } + updateReadyValue(true); }); +map.addControl(new mapboxgl.NavigationControl({ + showCompass: false, +})); + +map.addControl(new SwitcherControl( + [{ + title: "styleOsm", + uri: styleOsm(), + }, { + title: "styleCarto", + uri: styleCarto(), + }], + 'styleOsm', + () => updateReadyValue(false), + () => updateReadyValue(true), +)); + const Map = ({ children }) => { const containerEl = useRef(null); const [mapReady, setMapReady] = useState(false); - useEffect(() => onMapReady(() => setMapReady(true)), []); + useEffect(() => { + const listener = ready => setMapReady(ready); + addReadyListener(listener); + return () => { + removeReadyListener(listener); + }; + }, []); useLayoutEffect(() => { const currentEl = containerEl.current; diff --git a/modern/src/map/switcher/switcher.css b/modern/src/map/switcher/switcher.css new file mode 100644 index 0000000..6c8fdb4 --- /dev/null +++ b/modern/src/map/switcher/switcher.css @@ -0,0 +1,40 @@ +.mapboxgl-style-list +{ + display: none; +} + +.mapboxgl-ctrl-group .mapboxgl-style-list button +{ + background: none; + border: none; + cursor: pointer; + display: block; + font-size: 14px; + padding: 8px 8px 6px; + text-align: right; + width: 100%; + height: auto; +} + +.mapboxgl-style-list button.active +{ + font-weight: bold; +} + +.mapboxgl-style-list button:hover +{ + background-color: rgba(0, 0, 0, 0.05); +} + +.mapboxgl-style-list button + button +{ + border-top: 1px solid #ddd; +} + +.mapboxgl-style-switcher +{ + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iNTQuODQ5cHgiIGhlaWdodD0iNTQuODQ5cHgiIHZpZXdCb3g9IjAgMCA1NC44NDkgNTQuODQ5IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1NC44NDkgNTQuODQ5OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PGc+PGc+PHBhdGggZD0iTTU0LjQ5NywzOS42MTRsLTEwLjM2My00LjQ5bC0xNC45MTcsNS45NjhjLTAuNTM3LDAuMjE0LTEuMTY1LDAuMzE5LTEuNzkzLDAuMzE5Yy0wLjYyNywwLTEuMjU0LTAuMTA0LTEuNzktMC4zMThsLTE0LjkyMS01Ljk2OEwwLjM1MSwzOS42MTRjLTAuNDcyLDAuMjAzLTAuNDY3LDAuNTI0LDAuMDEsMC43MTZMMjYuNTYsNTAuODFjMC40NzcsMC4xOTEsMS4yNTEsMC4xOTEsMS43MjksMEw1NC40ODgsNDAuMzNDNTQuOTY0LDQwLjEzOSw1NC45NjksMzkuODE3LDU0LjQ5NywzOS42MTR6Ii8+PHBhdGggZD0iTTU0LjQ5NywyNy41MTJsLTEwLjM2NC00LjQ5MWwtMTQuOTE2LDUuOTY2Yy0wLjUzNiwwLjIxNS0xLjE2NSwwLjMyMS0xLjc5MiwwLjMyMWMtMC42MjgsMC0xLjI1Ni0wLjEwNi0xLjc5My0wLjMyMWwtMTQuOTE4LTUuOTY2TDAuMzUxLDI3LjUxMmMtMC40NzIsMC4yMDMtMC40NjcsMC41MjMsMC4wMSwwLjcxNkwyNi41NiwzOC43MDZjMC40NzcsMC4xOSwxLjI1MSwwLjE5LDEuNzI5LDBsMjYuMTk5LTEwLjQ3OUM1NC45NjQsMjguMDM2LDU0Ljk2OSwyNy43MTYsNTQuNDk3LDI3LjUxMnoiLz48cGF0aCBkPSJNMC4zNjEsMTYuMTI1bDEzLjY2Miw1LjQ2NWwxMi41MzcsNS4wMTVjMC40NzcsMC4xOTEsMS4yNTEsMC4xOTEsMS43MjksMGwxMi41NDEtNS4wMTZsMTMuNjU4LTUuNDYzYzAuNDc3LTAuMTkxLDAuNDgtMC41MTEsMC4wMS0wLjcxNkwyOC4yNzcsNC4wNDhjLTAuNDcxLTAuMjA0LTEuMjM2LTAuMjA0LTEuNzA4LDBMMC4zNTEsMTUuNDFDLTAuMTIxLDE1LjYxNC0wLjExNiwxNS45MzUsMC4zNjEsMTYuMTI1eiIvPjwvZz48L2c+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjwvc3ZnPg==); + background-position: center; + background-repeat: no-repeat; + background-size: 70%; +} diff --git a/modern/src/map/switcher/switcher.js b/modern/src/map/switcher/switcher.js new file mode 100644 index 0000000..ff9fbe9 --- /dev/null +++ b/modern/src/map/switcher/switcher.js @@ -0,0 +1,79 @@ +export class SwitcherControl { + + constructor(styles, defaultStyle, beforeSwitch, afterSwitch) { + this.styles = styles; + this.defaultStyle = defaultStyle; + this.beforeSwitch = beforeSwitch; + this.afterSwitch = afterSwitch; + this.onDocumentClick = this.onDocumentClick.bind(this); + } + + getDefaultPosition() { + return 'top-right'; + } + + onAdd(map) { + this.map = map; + this.controlContainer = document.createElement('div'); + this.controlContainer.classList.add('mapboxgl-ctrl'); + this.controlContainer.classList.add('mapboxgl-ctrl-group'); + this.mapStyleContainer = document.createElement('div'); + this.styleButton = document.createElement('button'); + this.styleButton.type = 'button'; + this.mapStyleContainer.classList.add('mapboxgl-style-list'); + for (const style of this.styles) { + const styleElement = document.createElement('button'); + styleElement.type = 'button'; + styleElement.innerText = style.title; + styleElement.classList.add(style.title.replace(/[^a-z0-9-]/gi, '_')); + styleElement.dataset.uri = JSON.stringify(style.uri); + styleElement.addEventListener('click', event => { + const srcElement = event.srcElement; + if (srcElement.classList.contains('active')) { + return; + } + this.beforeSwitch(); + this.map.setStyle(JSON.parse(srcElement.dataset.uri)); + this.afterSwitch(); + this.mapStyleContainer.style.display = 'none'; + this.styleButton.style.display = 'block'; + const elms = this.mapStyleContainer.getElementsByClassName('active'); + while (elms[0]) { + elms[0].classList.remove('active'); + } + srcElement.classList.add('active'); + }); + if (style.title === this.defaultStyle) { + styleElement.classList.add('active'); + } + this.mapStyleContainer.appendChild(styleElement); + } + this.styleButton.classList.add('mapboxgl-ctrl-icon'); + this.styleButton.classList.add('mapboxgl-style-switcher'); + this.styleButton.addEventListener('click', () => { + this.styleButton.style.display = 'none'; + this.mapStyleContainer.style.display = 'block'; + }); + document.addEventListener('click', this.onDocumentClick); + this.controlContainer.appendChild(this.styleButton); + this.controlContainer.appendChild(this.mapStyleContainer); + return this.controlContainer; + } + + onRemove() { + if (!this.controlContainer || !this.controlContainer.parentNode || !this.map || !this.styleButton) { + return; + } + this.styleButton.removeEventListener('click', this.onDocumentClick); + this.controlContainer.parentNode.removeChild(this.controlContainer); + document.removeEventListener('click', this.onDocumentClick); + this.map = undefined; + } + + onDocumentClick(event) { + if (this.controlContainer && !this.controlContainer.contains(event.target) && this.mapStyleContainer && this.styleButton) { + this.mapStyleContainer.style.display = 'none'; + this.styleButton.style.display = 'block'; + } + } +} -- cgit v1.2.3 From 4cbb539001e8ba536674373efb60160b16a5d111 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 16:31:30 -0800 Subject: Update style names --- modern/src/map/Map.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'modern') diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index 2617b98..d5e8617 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -6,6 +6,7 @@ import React, { useRef, useLayoutEffect, useEffect, useState } from 'react'; import { deviceCategories } from '../common/deviceCategories'; import { loadIcon, loadImage } from './mapUtil'; import { styleCarto, styleOsm } from './mapStyles'; +import t from '../common/localization'; const element = document.createElement('div'); element.style.width = '100%'; @@ -43,13 +44,13 @@ map.addControl(new mapboxgl.NavigationControl({ map.addControl(new SwitcherControl( [{ - title: "styleOsm", + title: t('mapOsm'), uri: styleOsm(), }, { - title: "styleCarto", + title: t('mapCarto'), uri: styleCarto(), }], - 'styleOsm', + t('mapOsm'), () => updateReadyValue(false), () => updateReadyValue(true), )); -- cgit v1.2.3 From 47b95d6739d8bfcb2ee285ffaaffc8fc41520f1f Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 18:44:31 -0800 Subject: Improve map switching --- modern/src/map/Map.js | 33 +++++++++++++++++++++------------ modern/src/map/switcher/switcher.js | 4 ++++ 2 files changed, 25 insertions(+), 12 deletions(-) (limited to 'modern') diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index d5e8617..71fb5e5 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -5,7 +5,7 @@ import { SwitcherControl } from './switcher/switcher'; import React, { useRef, useLayoutEffect, useEffect, useState } from 'react'; import { deviceCategories } from '../common/deviceCategories'; import { loadIcon, loadImage } from './mapUtil'; -import { styleCarto, styleOsm } from './mapStyles'; +import { styleCarto, styleMapbox, styleOsm } from './mapStyles'; import t from '../common/localization'; const element = document.createElement('div'); @@ -29,30 +29,39 @@ const updateReadyValue = value => { readyListeners.forEach(listener => listener(value)); } -map.on('load', async () => { +const initMap = async () => { const background = await loadImage('images/background.svg'); await Promise.all(deviceCategories.map(async category => { const imageData = await loadIcon(category, background, `images/icon/${category}.svg`); - map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio }); + if (!map.hasImage(category)) map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio }); })); updateReadyValue(true); -}); +}; + +map.on('load', initMap); map.addControl(new mapboxgl.NavigationControl({ showCompass: false, })); map.addControl(new SwitcherControl( - [{ - title: t('mapOsm'), - uri: styleOsm(), - }, { - title: t('mapCarto'), - uri: styleCarto(), - }], + [ + { title: t('mapOsm'), uri: styleOsm() }, + { title: t('mapCarto'), uri: styleCarto() }, + { title: 'Mapbox Streets', uri: styleMapbox('streets-v11') }, + ], t('mapOsm'), () => updateReadyValue(false), - () => updateReadyValue(true), + () => { + const waiting = () => { + if (!map.loaded()) { + setTimeout(waiting, 100); + } else { + initMap(); + } + }; + waiting(); + }, )); const Map = ({ children }) => { diff --git a/modern/src/map/switcher/switcher.js b/modern/src/map/switcher/switcher.js index ff9fbe9..c2b9d6d 100644 --- a/modern/src/map/switcher/switcher.js +++ b/modern/src/map/switcher/switcher.js @@ -32,9 +32,13 @@ export class SwitcherControl { if (srcElement.classList.contains('active')) { return; } + console.log('beforeSwitch start'); this.beforeSwitch(); + console.log('beforeSwitch end'); this.map.setStyle(JSON.parse(srcElement.dataset.uri)); + console.log('afterSwitch start'); this.afterSwitch(); + console.log('afterSwitch end'); this.mapStyleContainer.style.display = 'none'; this.styleButton.style.display = 'block'; const elms = this.mapStyleContainer.getElementsByClassName('active'); -- cgit v1.2.3 From bee6677be113e4215d07abd54db32dd4fc37b3b0 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 18:46:57 -0800 Subject: Another switch fix --- modern/src/map/Map.js | 11 ++++++++--- modern/src/map/switcher/switcher.js | 4 ---- 2 files changed, 8 insertions(+), 7 deletions(-) (limited to 'modern') diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index 71fb5e5..2bcf04f 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -20,14 +20,19 @@ export const map = new mapboxgl.Map({ let ready = false; const readyListeners = new Set(); -const addReadyListener = listener => readyListeners.add(listener); +const addReadyListener = listener => { + readyListeners.add(listener); + listener(ready); +}; -const removeReadyListener = listener => readyListeners.delete(listener); +const removeReadyListener = listener => { + readyListeners.delete(listener); +}; const updateReadyValue = value => { ready = value; readyListeners.forEach(listener => listener(value)); -} +}; const initMap = async () => { const background = await loadImage('images/background.svg'); diff --git a/modern/src/map/switcher/switcher.js b/modern/src/map/switcher/switcher.js index c2b9d6d..ff9fbe9 100644 --- a/modern/src/map/switcher/switcher.js +++ b/modern/src/map/switcher/switcher.js @@ -32,13 +32,9 @@ export class SwitcherControl { if (srcElement.classList.contains('active')) { return; } - console.log('beforeSwitch start'); this.beforeSwitch(); - console.log('beforeSwitch end'); this.map.setStyle(JSON.parse(srcElement.dataset.uri)); - console.log('afterSwitch start'); this.afterSwitch(); - console.log('afterSwitch end'); this.mapStyleContainer.style.display = 'none'; this.styleButton.style.display = 'block'; const elms = this.mapStyleContainer.getElementsByClassName('active'); -- cgit v1.2.3 From a20e4b3348285607631630e6dda415a9e29e967d Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 20:31:10 -0800 Subject: Fix react warnings --- modern/src/DevicesList.js | 2 +- modern/src/map/PositionsMap.js | 19 +++++++++++-------- modern/src/map/StatusView.js | 2 +- modern/src/reactHelper.js | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) (limited to 'modern') diff --git a/modern/src/DevicesList.js b/modern/src/DevicesList.js index 15badcb..976fd84 100644 --- a/modern/src/DevicesList.js +++ b/modern/src/DevicesList.js @@ -47,7 +47,7 @@ const DeviceView = ({ updateTimestamp, onMenuClick }) => { dispatch(devicesActions.select(item))}> - + diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js index 6a7a68b..0ad9a69 100644 --- a/modern/src/map/PositionsMap.js +++ b/modern/src/map/PositionsMap.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import ReactDOM from 'react-dom'; import mapboxgl from 'mapbox-gl'; import { Provider, useSelector } from 'react-redux'; @@ -18,7 +18,7 @@ const PositionsMap = () => { return { deviceId: position.deviceId, name: device ? device.name : '', - category: device && device.category || 'default', + category: device && (device.category || 'default'), } }; @@ -37,7 +37,7 @@ const PositionsMap = () => { const onMouseEnter = () => map.getCanvas().style.cursor = 'pointer'; const onMouseLeave = () => map.getCanvas().style.cursor = ''; - const onClick = event => { + const onClickCallback = useCallback(event => { const feature = event.features[0]; let coordinates = feature.geometry.coordinates.slice(); while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) { @@ -59,12 +59,15 @@ const PositionsMap = () => { .setDOMContent(placeholder) .setLngLat(coordinates) .addTo(map); - }; + }, [history]); useEffect(() => { map.addSource(id, { 'type': 'geojson', - 'data': positions, + 'data': { + type: 'FeatureCollection', + features: [], + } }); map.addLayer({ 'id': id, @@ -88,19 +91,19 @@ const PositionsMap = () => { map.on('mouseenter', id, onMouseEnter); map.on('mouseleave', id, onMouseLeave); - map.on('click', id, onClick); + map.on('click', id, onClickCallback); return () => { Array.from(map.getContainer().getElementsByClassName('mapboxgl-popup')).forEach(el => el.remove()); map.off('mouseenter', id, onMouseEnter); map.off('mouseleave', id, onMouseLeave); - map.off('click', id, onClick); + map.off('click', id, onClickCallback); map.removeLayer(id); map.removeSource(id); }; - }, []); + }, [onClickCallback]); useEffect(() => { map.getSource(id).setData(positions); diff --git a/modern/src/map/StatusView.js b/modern/src/map/StatusView.js index 3a30426..ae049af 100644 --- a/modern/src/map/StatusView.js +++ b/modern/src/map/StatusView.js @@ -22,7 +22,7 @@ const StatusView = ({ deviceId, onShowDetails }) => { {position.attributes.batteryLevel && <>{t('positionBattery')}: {formatPosition(position.attributes.batteryLevel, 'batteryLevel')}
} - {t('sharedShowDetails')} + {t('sharedShowDetails')} ); }; diff --git a/modern/src/reactHelper.js b/modern/src/reactHelper.js index 9286c57..cd161d4 100644 --- a/modern/src/reactHelper.js +++ b/modern/src/reactHelper.js @@ -12,5 +12,5 @@ export const usePrevious = value => { export const useEffectAsync = (effect, deps) => { useEffect(() => { effect(); - }, deps); + }, [effect, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps } -- cgit v1.2.3 From fb330f777ceb0e531699ea0842c0199622114288 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 20:35:45 -0800 Subject: Optimize image loading --- modern/src/map/Map.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'modern') diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index 2bcf04f..a718798 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -37,8 +37,10 @@ const updateReadyValue = value => { const initMap = async () => { const background = await loadImage('images/background.svg'); await Promise.all(deviceCategories.map(async category => { - const imageData = await loadIcon(category, background, `images/icon/${category}.svg`); - if (!map.hasImage(category)) map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio }); + if (!map.hasImage(category)) { + const imageData = await loadIcon(category, background, `images/icon/${category}.svg`); + map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio }); + } })); updateReadyValue(true); }; -- cgit v1.2.3 From cea47047f296b25794ebb06f679b590c0b42448e Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 20:57:02 -0800 Subject: Enable mapbox maps --- modern/src/common/preferences.js | 21 +++++++++++++++++++++ modern/src/map/Map.js | 9 ++++++++- modern/src/reactHelper.js | 6 +++--- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 modern/src/common/preferences.js (limited to 'modern') diff --git a/modern/src/common/preferences.js b/modern/src/common/preferences.js new file mode 100644 index 0000000..24fe389 --- /dev/null +++ b/modern/src/common/preferences.js @@ -0,0 +1,21 @@ +import { useSelector } from 'react-redux'; + +export const usePreference = (key, defaultValue) => { + return useSelector(state => { + if (state.session.server.forceSettings) { + return state.session.server[key] || state.session.user[key] || defaultValue; + } else { + return state.session.user[key] || state.session.server[key] || defaultValue; + } + }); +}; + +export const useAttributePreference = (key, defaultValue) => { + return useSelector(state => { + if (state.session.server.forceSettings) { + return state.session.server.attributes[key] || state.session.user.attributes[key] || defaultValue; + } else { + return state.session.user.attributes[key] || state.session.server.attributes[key] || defaultValue; + } + }); +}; diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index a718798..1e75d30 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -7,6 +7,7 @@ import { deviceCategories } from '../common/deviceCategories'; import { loadIcon, loadImage } from './mapUtil'; import { styleCarto, styleMapbox, styleOsm } from './mapStyles'; import t from '../common/localization'; +import { useAttributePreference } from '../common/preferences'; const element = document.createElement('div'); element.style.width = '100%'; @@ -75,7 +76,13 @@ const Map = ({ children }) => { const containerEl = useRef(null); const [mapReady, setMapReady] = useState(false); - + + const mapboxAccessToken = useAttributePreference('mapboxAccessToken'); + + useEffect(() => { + mapboxgl.accessToken = mapboxAccessToken; + }, [mapboxAccessToken]); + useEffect(() => { const listener = ready => setMapReady(ready); addReadyListener(listener); diff --git a/modern/src/reactHelper.js b/modern/src/reactHelper.js index cd161d4..b0eb016 100644 --- a/modern/src/reactHelper.js +++ b/modern/src/reactHelper.js @@ -7,10 +7,10 @@ export const usePrevious = value => { ref.current = value; }); return ref.current; -} +}; export const useEffectAsync = (effect, deps) => { useEffect(() => { effect(); - }, [effect, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps -} + }, deps); // eslint-disable-line react-hooks/exhaustive-deps +}; -- cgit v1.2.3 From 0802e051c2a27c4df5020f2a65a57504a762ad30 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 21:00:43 -0800 Subject: More Mapbox layers --- modern/src/map/Map.js | 4 +++- web/l10n/en.json | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'modern') diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index 1e75d30..8a43e97 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -56,7 +56,9 @@ map.addControl(new SwitcherControl( [ { title: t('mapOsm'), uri: styleOsm() }, { title: t('mapCarto'), uri: styleCarto() }, - { title: 'Mapbox Streets', uri: styleMapbox('streets-v11') }, + { title: t('mapMapboxStreets'), uri: styleMapbox('streets-v11') }, + { title: t('mapMapboxOutdoors'), uri: styleMapbox('outdoors-v11') }, + { title: t('mapMapboxSatellite'), uri: styleMapbox('satellite-v9') }, ], t('mapOsm'), () => updateReadyValue(false), diff --git a/web/l10n/en.json b/web/l10n/en.json index 3867041..4f0825c 100644 --- a/web/l10n/en.json +++ b/web/l10n/en.json @@ -254,6 +254,9 @@ "mapYandexMap": "Yandex Map", "mapYandexSat": "Yandex Satellite", "mapWikimedia": "Wikimedia", + "mapMapboxStreets": "Mapbox Streets", + "mapMapboxOutdoors": "Mapbox Outdoors", + "mapMapboxSatellite": "Mapbox Satellite", "mapShapePolygon": "Polygon", "mapShapeCircle": "Circle", "mapShapePolyline": "Polyline", -- cgit v1.2.3 From 1a06115aa27a0c1545b8eb9bfb38930fcb8b56bf Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 21:25:10 -0800 Subject: Empty page for replay --- modern/src/App.js | 2 ++ modern/src/MainToolbar.js | 7 +++++++ modern/src/reports/ReplayPage.js | 38 ++++++++++++++++++++++++++++++++++++++ web/l10n/en.json | 1 + 4 files changed, 48 insertions(+) create mode 100644 modern/src/reports/ReplayPage.js (limited to 'modern') diff --git a/modern/src/App.js b/modern/src/App.js index 1f6b55a..6fd5c00 100644 --- a/modern/src/App.js +++ b/modern/src/App.js @@ -14,6 +14,7 @@ import NotificationPage from './settings/NotificationPage'; import GroupsPage from './settings/GroupsPage'; import GroupPage from './settings/GroupPage'; import PositionPage from './PositionPage'; +import ReplayPage from './reports/ReplayPage'; const App = () => { return ( @@ -23,6 +24,7 @@ const App = () => { + diff --git a/modern/src/MainToolbar.js b/modern/src/MainToolbar.js index b4757d8..942aac6 100644 --- a/modern/src/MainToolbar.js +++ b/modern/src/MainToolbar.js @@ -29,6 +29,7 @@ import NotificationsActiveIcon from '@material-ui/icons/NotificationsActive'; import FormatListBulletedIcon from '@material-ui/icons/FormatListBulleted'; import TrendingUpIcon from '@material-ui/icons/TrendingUp'; import FolderIcon from '@material-ui/icons/Folder'; +import ReplayIcon from '@material-ui/icons/Replay'; import t from './common/localization'; const useStyles = makeStyles(theme => ({ @@ -96,6 +97,12 @@ const MainToolbar = () => {
+ history.push('/replay')}> + + + + + ({ + root: { + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + content: { + flexGrow: 1, + overflow: 'hidden', + display: 'flex', + flexDirection: 'row', + [theme.breakpoints.down('xs')]: { + flexDirection: 'column-reverse', + } + }, + mapContainer: { + flexGrow: 1, + }, +})); + +const ReplayPage = () => { + const classes = useStyles(); + + return ( +
+ + + +
+ ); +} + +export default ReplayPage; diff --git a/web/l10n/en.json b/web/l10n/en.json index 4f0825c..2b24cf2 100644 --- a/web/l10n/en.json +++ b/web/l10n/en.json @@ -380,6 +380,7 @@ "notificatorSms": "SMS", "notificatorFirebase": "Firebase", "notificatorTraccar": "Traccar", + "reportReplay": "Replay", "reportRoute": "Route", "reportEvents": "Events", "reportTrips": "Trips", -- cgit v1.2.3 From 162000dc250b1be24fd7e6dd4e9c3883ca6581c5 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 21:57:47 -0800 Subject: Improve session handing --- modern/src/App.js | 34 ++++++++++++++++++++++------------ modern/src/MainPage.js | 5 +---- 2 files changed, 23 insertions(+), 16 deletions(-) (limited to 'modern') diff --git a/modern/src/App.js b/modern/src/App.js index 6fd5c00..243489e 100644 --- a/modern/src/App.js +++ b/modern/src/App.js @@ -15,26 +15,36 @@ import GroupsPage from './settings/GroupsPage'; import GroupPage from './settings/GroupPage'; import PositionPage from './PositionPage'; import ReplayPage from './reports/ReplayPage'; +import { useSelector } from 'react-redux'; +import { LinearProgress } from '@material-ui/core'; const App = () => { + const initialized = useSelector(state => !!state.session.server && !!state.session.user); + return ( <> - - - - - - - - - - - - + + {!initialized ? () : ( + + + + + + + + + + + + + + + )} + ); diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index d277f28..58bf83a 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -1,9 +1,7 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { isWidthUp, makeStyles, withWidth } from '@material-ui/core'; import Drawer from '@material-ui/core/Drawer'; import ContainerDimensions from 'react-container-dimensions'; -import LinearProgress from '@material-ui/core/LinearProgress'; import DevicesList from './DevicesList'; import MainToolbar from './MainToolbar'; import Map from './map/Map'; @@ -42,10 +40,9 @@ const useStyles = makeStyles(theme => ({ })); const MainPage = ({ width }) => { - const initialized = useSelector(state => !!state.session.server && !!state.session.user); const classes = useStyles(); - return !initialized ? () : ( + return (
-- cgit v1.2.3 From 3c9f0117b2b3cdffc98f4ea19e5dbc7ed40e9b4f Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 1 Nov 2020 22:20:36 -0800 Subject: Add panel with slider --- modern/src/reports/ReplayPage.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) (limited to 'modern') diff --git a/modern/src/reports/ReplayPage.js b/modern/src/reports/ReplayPage.js index 826b361..0988284 100644 --- a/modern/src/reports/ReplayPage.js +++ b/modern/src/reports/ReplayPage.js @@ -1,5 +1,5 @@ import React from 'react'; -import { makeStyles } from '@material-ui/core'; +import { Container, makeStyles, Paper, Slider } from '@material-ui/core'; import MainToolbar from '../MainToolbar'; import Map from '../map/Map'; @@ -9,17 +9,14 @@ const useStyles = makeStyles(theme => ({ display: 'flex', flexDirection: 'column', }, - content: { - flexGrow: 1, - overflow: 'hidden', - display: 'flex', - flexDirection: 'row', - [theme.breakpoints.down('xs')]: { - flexDirection: 'column-reverse', - } + controlPanel: { + position: 'absolute', + bottom: theme.spacing(5), + left: '50%', + transform: 'translateX(-50%)', }, - mapContainer: { - flexGrow: 1, + controlContent: { + padding: theme.spacing(2), }, })); @@ -31,6 +28,11 @@ const ReplayPage = () => { + + + + +
); } -- cgit v1.2.3 From d53632348f684f35719b035ff39744a41c088f3e Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Tue, 3 Nov 2020 13:58:37 -0800 Subject: Show replay path --- modern/src/RemoveDialog.js | 2 +- modern/src/map/ReplayPathMap.js | 59 +++++++++++++++++++++++++++ modern/src/map/mapUtil.js | 24 ----------- modern/src/reports/FilterForm.js | 88 ++++++++++++++++++++++++++++++++++++++++ modern/src/reports/ReplayPage.js | 58 +++++++++++++++++++++++++- 5 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 modern/src/map/ReplayPathMap.js create mode 100644 modern/src/reports/FilterForm.js (limited to 'modern') diff --git a/modern/src/RemoveDialog.js b/modern/src/RemoveDialog.js index 8e7d97f..bbcfb22 100644 --- a/modern/src/RemoveDialog.js +++ b/modern/src/RemoveDialog.js @@ -8,7 +8,7 @@ import DialogContentText from '@material-ui/core/DialogContentText'; const RemoveDialog = ({ open, endpoint, itemId, onResult }) => { const handleRemove = async () => { - const response = await fetch(`/api/${endpoint}/${itemId}`, { method: 'DELETE' }) + const response = await fetch(`/api/${endpoint}/${itemId}`, { method: 'DELETE' }); if (response.ok) { onResult(true); } diff --git a/modern/src/map/ReplayPathMap.js b/modern/src/map/ReplayPathMap.js new file mode 100644 index 0000000..feab071 --- /dev/null +++ b/modern/src/map/ReplayPathMap.js @@ -0,0 +1,59 @@ +import mapboxgl from 'mapbox-gl'; +import { useEffect } from 'react'; +import { map } from './Map'; + +const ReplayPathMap = ({ positions }) => { + const id = 'replay'; + + useEffect(() => { + map.addSource(id, { + 'type': 'geojson', + 'data': { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [], + }, + }, + }); + map.addLayer({ + 'source': id, + 'id': id, + 'type': 'line', + 'layout': { + 'line-join': 'round', + 'line-cap': 'round', + }, + 'paint': { + 'line-color': '#333', + 'line-width': 5, + }, + }); + + return () => { + map.removeLayer(id); + map.removeSource(id); + }; + }, []); + + useEffect(() => { + const coordinates = positions.map(item => [item.longitude, item.latitude]); + map.getSource(id).setData({ + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: coordinates, + }, + }); + if (coordinates.length) { + const bounds = coordinates.reduce((bounds, item) => bounds.extend(item), new mapboxgl.LngLatBounds(coordinates[0], coordinates[0])); + map.fitBounds(bounds, { + padding: { top: 50, bottom: 250, left: 25, right: 25 }, + }); + } + }, [positions]); + + return null; +} + +export default ReplayPathMap; diff --git a/modern/src/map/mapUtil.js b/modern/src/map/mapUtil.js index 71d7b3c..15f1620 100644 --- a/modern/src/map/mapUtil.js +++ b/modern/src/map/mapUtil.js @@ -27,30 +27,6 @@ export const loadIcon = async (key, background, url) => { return context.getImageData(0, 0, canvas.width, canvas.height); }; -export const calculateBounds = features => { - if (features && features.length) { - const first = features[0].geometry.coordinates; - const bounds = [[...first], [...first]]; - for (let feature of features) { - const longitude = feature.geometry.coordinates[0] - const latitude = feature.geometry.coordinates[1] - if (longitude < bounds[0][0]) { - bounds[0][0] = longitude; - } else if (longitude > bounds[1][0]) { - bounds[1][0] = longitude; - } - if (latitude < bounds[0][1]) { - bounds[0][1] = latitude; - } else if (latitude > bounds[1][1]) { - bounds[1][1] = latitude; - } - } - return bounds; - } else { - return null; - } -} - export const reverseCoordinates = it => { if (!it) { return it; diff --git a/modern/src/reports/FilterForm.js b/modern/src/reports/FilterForm.js new file mode 100644 index 0000000..86339d2 --- /dev/null +++ b/modern/src/reports/FilterForm.js @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from 'react'; +import { FormControl, InputLabel, Select, MenuItem, TextField } from '@material-ui/core'; +import t from '../common/localization'; +import { useSelector } from 'react-redux'; +import moment from 'moment'; + +const FilterForm = ({ deviceId, setDeviceId, from, setFrom, to, setTo }) => { + const devices = useSelector(state => Object.values(state.devices.items)); + + const [period, setPeriod] = useState('today'); + + useEffect(() => { + switch (period) { + default: + case 'today': + setFrom(moment().startOf('day')); + setTo(moment().endOf('day')); + break; + case 'yesterday': + setFrom(moment().subtract(1, 'day').startOf('day')); + setTo(moment().subtract(1, 'day').endOf('day')); + break; + case 'thisWeek': + setFrom(moment().startOf('week')); + setTo(moment().endOf('week')); + break; + case 'previousWeek': + setFrom(moment().subtract(1, 'week').startOf('week')); + setTo(moment().subtract(1, 'week').endOf('week')); + break; + case 'thisMonth': + setFrom(moment().startOf('month')); + setTo(moment().endOf('month')); + break; + case 'previousMonth': + setFrom(moment().subtract(1, 'month').startOf('month')); + setTo(moment().subtract(1, 'month').endOf('month')); + break; + } + }, [period, setFrom, setTo]); + + return ( + <> + + {t('reportDevice')} + + + + {t('reportPeriod')} + + + {period === 'custom' && + setFrom(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + fullWidth /> + } + {period === 'custom' && + setTo(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + fullWidth /> + } + + ); +} + +export default FilterForm; diff --git a/modern/src/reports/ReplayPage.js b/modern/src/reports/ReplayPage.js index 0988284..bdc9edb 100644 --- a/modern/src/reports/ReplayPage.js +++ b/modern/src/reports/ReplayPage.js @@ -1,7 +1,11 @@ -import React from 'react'; -import { Container, makeStyles, Paper, Slider } from '@material-ui/core'; +import React, { useState } from 'react'; +import { Accordion, AccordionDetails, AccordionSummary, Button, Container, FormControl, makeStyles, Paper, Slider, Typography } from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import MainToolbar from '../MainToolbar'; import Map from '../map/Map'; +import t from '../common/localization'; +import FilterForm from './FilterForm'; +import ReplayPathMap from '../map/ReplayPathMap'; const useStyles = makeStyles(theme => ({ root: { @@ -17,21 +21,71 @@ const useStyles = makeStyles(theme => ({ }, controlContent: { padding: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + configForm: { + display: 'flex', + flexDirection: 'column', }, })); const ReplayPage = () => { const classes = useStyles(); + const [expanded, setExpanded] = useState(true); + + const [deviceId, setDeviceId] = useState(); + const [from, setFrom] = useState(); + const [to, setTo] = useState(); + + const [positions, setPositions] = useState([]); + + const handleShow = async () => { + const query = new URLSearchParams({ + deviceId, + from: from.toISOString(), + to: to.toISOString(), + }); + const response = await fetch(`/api/positions?${query.toString()}`, { headers: { 'Accept': 'application/json' } }); + if (response.ok) { + setPositions(await response.json()); + setExpanded(false); + } + }; + return (
+ +
+ setExpanded(!expanded)}> + }> + + {t('reportConfigure')} + + + + + + + + + +
); -- cgit v1.2.3 From 155e3b90365994a4bfbf3b43505f0452fb3fe88a Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Tue, 3 Nov 2020 14:34:49 -0800 Subject: Show replay position --- modern/src/MainPage.js | 4 ++-- modern/src/map/CurrentPositionsMap.js | 11 +++++++++++ modern/src/map/PositionsMap.js | 33 ++++++++++++++++----------------- modern/src/reports/ReplayPage.js | 30 ++++++++++++++++++++++++++++-- 4 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 modern/src/map/CurrentPositionsMap.js (limited to 'modern') diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index 58bf83a..a5a8061 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -5,10 +5,10 @@ import ContainerDimensions from 'react-container-dimensions'; import DevicesList from './DevicesList'; import MainToolbar from './MainToolbar'; import Map from './map/Map'; -import PositionsMap from './map/PositionsMap'; import SelectedDeviceMap from './map/SelectedDeviceMap'; import AccuracyMap from './map/AccuracyMap'; import GeofenceMap from './map/GeofenceMap'; +import CurrentPositionsMap from './map/CurrentPositionsMap'; const useStyles = makeStyles(theme => ({ root: { @@ -57,7 +57,7 @@ const MainPage = ({ width }) => { - + diff --git a/modern/src/map/CurrentPositionsMap.js b/modern/src/map/CurrentPositionsMap.js new file mode 100644 index 0000000..0bfe4fc --- /dev/null +++ b/modern/src/map/CurrentPositionsMap.js @@ -0,0 +1,11 @@ +import React, { } from 'react'; +import { useSelector } from 'react-redux'; + +import PositionsMap from './PositionsMap'; + +const CurrentPositionsMap = () => { + const positions = useSelector(state => Object.values(state.positions.items)); + return (); +} + +export default CurrentPositionsMap; diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js index 0ad9a69..fa7b431 100644 --- a/modern/src/map/PositionsMap.js +++ b/modern/src/map/PositionsMap.js @@ -8,13 +8,14 @@ import store from '../store'; import { useHistory } from 'react-router-dom'; import StatusView from './StatusView'; -const PositionsMap = () => { +const PositionsMap = ({ positions }) => { const id = 'positions'; const history = useHistory(); + const devices = useSelector(state => state.devices.items); - const createFeature = (state, position) => { - const device = state.devices.items[position.deviceId] || null; + const createFeature = (devices, position) => { + const device = devices[position.deviceId] || null; return { deviceId: position.deviceId, name: device ? device.name : '', @@ -22,18 +23,6 @@ const PositionsMap = () => { } }; - const positions = useSelector(state => ({ - type: 'FeatureCollection', - features: Object.values(state.positions.items).map(position => ({ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [position.longitude, position.latitude] - }, - properties: createFeature(state, position), - })), - })); - const onMouseEnter = () => map.getCanvas().style.cursor = 'pointer'; const onMouseLeave = () => map.getCanvas().style.cursor = ''; @@ -106,8 +95,18 @@ const PositionsMap = () => { }, [onClickCallback]); useEffect(() => { - map.getSource(id).setData(positions); - }, [positions]); + map.getSource(id).setData({ + type: 'FeatureCollection', + features: positions.map(position => ({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [position.longitude, position.latitude], + }, + properties: createFeature(devices, position), + })) + }); + }, [devices, positions]); return null; } diff --git a/modern/src/reports/ReplayPage.js b/modern/src/reports/ReplayPage.js index bdc9edb..2afa1a2 100644 --- a/modern/src/reports/ReplayPage.js +++ b/modern/src/reports/ReplayPage.js @@ -1,11 +1,13 @@ import React, { useState } from 'react'; -import { Accordion, AccordionDetails, AccordionSummary, Button, Container, FormControl, makeStyles, Paper, Slider, Typography } from '@material-ui/core'; +import { Accordion, AccordionDetails, AccordionSummary, Button, Container, FormControl, makeStyles, Paper, Slider, Tooltip, Typography } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import MainToolbar from '../MainToolbar'; import Map from '../map/Map'; import t from '../common/localization'; import FilterForm from './FilterForm'; import ReplayPathMap from '../map/ReplayPathMap'; +import PositionsMap from '../map/PositionsMap'; +import { formatPosition } from '../common/formatter'; const useStyles = makeStyles(theme => ({ root: { @@ -29,6 +31,14 @@ const useStyles = makeStyles(theme => ({ }, })); +const TimeLabel = ({ children, open, value }) => { + return ( + + {children} + + ); +}; + const ReplayPage = () => { const classes = useStyles(); @@ -40,6 +50,8 @@ const ReplayPage = () => { const [positions, setPositions] = useState([]); + const [index, setIndex] = useState(0); + const handleShow = async () => { const query = new URLSearchParams({ deviceId, @@ -48,6 +60,7 @@ const ReplayPage = () => { }); const response = await fetch(`/api/positions?${query.toString()}`, { headers: { 'Accept': 'application/json' } }); if (response.ok) { + setIndex(0); setPositions(await response.json()); setExpanded(false); } @@ -58,10 +71,23 @@ const ReplayPage = () => { + {index < positions.length && + + } - + ({ value: index }))} + value={index} + onChange={(_, index) => setIndex(index)} + valueLabelDisplay="auto" + valueLabelFormat={i => i < positions.length ? formatPosition(positions[i], 'fixTime') : ''} + ValueLabelComponent={TimeLabel} + />
setExpanded(!expanded)}> -- cgit v1.2.3 From 5e96fe64b49bb857fef1a2ac4d6522db332a89a0 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Tue, 3 Nov 2020 15:10:11 -0800 Subject: Add current location --- modern/src/MainPage.js | 2 ++ modern/src/map/CurrentLocationMap.js | 21 +++++++++++++++++++++ modern/src/reports/ReplayPage.js | 27 ++++++++++++++------------- 3 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 modern/src/map/CurrentLocationMap.js (limited to 'modern') diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index a5a8061..b6b5044 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -9,6 +9,7 @@ import SelectedDeviceMap from './map/SelectedDeviceMap'; import AccuracyMap from './map/AccuracyMap'; import GeofenceMap from './map/GeofenceMap'; import CurrentPositionsMap from './map/CurrentPositionsMap'; +import CurrentLocationMap from './map/CurrentLocationMap'; const useStyles = makeStyles(theme => ({ root: { @@ -55,6 +56,7 @@ const MainPage = ({ width }) => {
+ diff --git a/modern/src/map/CurrentLocationMap.js b/modern/src/map/CurrentLocationMap.js new file mode 100644 index 0000000..31e6e28 --- /dev/null +++ b/modern/src/map/CurrentLocationMap.js @@ -0,0 +1,21 @@ +import mapboxgl from 'mapbox-gl'; +import { useEffect } from 'react'; +import { map } from './Map'; + +const CurrentLocationMap = () => { + useEffect(() => { + const control = new mapboxgl.GeolocateControl({ + positionOptions: { + enableHighAccuracy: true, + timeout: 5000, + }, + trackUserLocation: true, + }); + map.addControl(control); + return () => map.removeControl(control); + }, []); + + return null; +} + +export default CurrentLocationMap; diff --git a/modern/src/reports/ReplayPage.js b/modern/src/reports/ReplayPage.js index 2afa1a2..6b84d4d 100644 --- a/modern/src/reports/ReplayPage.js +++ b/modern/src/reports/ReplayPage.js @@ -76,19 +76,20 @@ const ReplayPage = () => { } - - ({ value: index }))} - value={index} - onChange={(_, index) => setIndex(index)} - valueLabelDisplay="auto" - valueLabelFormat={i => i < positions.length ? formatPosition(positions[i], 'fixTime') : ''} - ValueLabelComponent={TimeLabel} - /> - + {!!positions.length && + + ({ value: index }))} + value={index} + onChange={(_, index) => setIndex(index)} + valueLabelDisplay="auto" + valueLabelFormat={i => i < positions.length ? formatPosition(positions[i], 'fixTime') : ''} + ValueLabelComponent={TimeLabel} + /> + + }
setExpanded(!expanded)}> }> -- cgit v1.2.3