aboutsummaryrefslogtreecommitdiff
path: root/modern/src/map
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src/map')
-rw-r--r--modern/src/map/MainMap.js129
-rw-r--r--modern/src/map/mapManager.js218
2 files changed, 347 insertions, 0 deletions
diff --git a/modern/src/map/MainMap.js b/modern/src/map/MainMap.js
new file mode 100644
index 00000000..7332d93e
--- /dev/null
+++ b/modern/src/map/MainMap.js
@@ -0,0 +1,129 @@
+import React, { useRef, useLayoutEffect, useEffect, useState } from 'react';
+import ReactDOM from 'react-dom';
+import { Provider, useSelector } from 'react-redux';
+import mapboxgl from 'mapbox-gl';
+
+import mapManager from './mapManager';
+import store from '../store';
+import StatusView from '../StatusView';
+import { useHistory } from 'react-router-dom';
+
+const MainMap = () => {
+ const history = useHistory();
+
+ const containerEl = useRef(null);
+
+ const [mapReady, setMapReady] = useState(false);
+
+ const mapCenter = useSelector(state => {
+ if (state.devices.selectedId) {
+ const position = state.positions.items[state.devices.selectedId] || null;
+ if (position) {
+ return [position.longitude, position.latitude];
+ }
+ }
+ return null;
+ });
+
+ const createFeature = (state, position) => {
+ const device = state.devices.items[position.deviceId] || null;
+ return {
+ deviceId: position.deviceId,
+ name: device ? device.name : '',
+ }
+ };
+
+ 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),
+ })),
+ }));
+
+ useLayoutEffect(() => {
+ const currentEl = containerEl.current;
+ currentEl.appendChild(mapManager.element);
+ if (mapManager.map) {
+ mapManager.map.resize();
+ }
+ return () => {
+ currentEl.removeChild(mapManager.element);
+ };
+ }, [containerEl]);
+
+ useEffect(() => {
+ mapManager.registerListener(() => setMapReady(true));
+ }, []);
+
+ const markerClickHandler = (event) => {
+ const feature = event.features[0];
+ let coordinates = feature.geometry.coordinates.slice();
+ while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) {
+ coordinates[0] += event.lngLat.lng > coordinates[0] ? 360 : -360;
+ }
+
+ const placeholder = document.createElement('div');
+ ReactDOM.render(
+ <Provider store={store}>
+ <StatusView deviceId={feature.properties.deviceId} onShowDetails={positionId => history.push(`/position/${positionId}`)} />
+ </Provider>,
+ placeholder);
+
+ new mapboxgl.Popup({
+ offset: 25,
+ anchor: 'top'
+ })
+ .setDOMContent(placeholder)
+ .setLngLat(coordinates)
+ .addTo(mapManager.map);
+ };
+
+ useEffect(() => {
+ if (mapReady) {
+ mapManager.map.addSource('positions', {
+ 'type': 'geojson',
+ 'data': positions,
+ });
+ mapManager.addLayer('device-icon', 'positions', 'icon-marker', '{name}', markerClickHandler);
+
+ const bounds = mapManager.calculateBounds(positions.features);
+ if (bounds) {
+ mapManager.map.fitBounds(bounds, {
+ padding: 100,
+ maxZoom: 9
+ });
+ }
+
+ return () => {
+ mapManager.removeLayer('device-icon', 'positions');
+ };
+ }
+ }, [mapReady]);
+
+ useEffect(() => {
+ mapManager.map.easeTo({
+ center: mapCenter
+ });
+ }, [mapCenter]);
+
+ useEffect(() => {
+ const source = mapManager.map.getSource('positions');
+ if (source) {
+ source.setData(positions);
+ }
+ }, [positions]);
+
+ const style = {
+ width: '100%',
+ height: '100%',
+ };
+
+ return <div style={style} ref={containerEl} />;
+}
+
+export default MainMap;
diff --git a/modern/src/map/mapManager.js b/modern/src/map/mapManager.js
new file mode 100644
index 00000000..9e5d631d
--- /dev/null
+++ b/modern/src/map/mapManager.js
@@ -0,0 +1,218 @@
+import 'mapbox-gl/dist/mapbox-gl.css';
+import mapboxgl from 'mapbox-gl';
+
+let ready = false;
+let registeredListener = null;
+
+const registerListener = listener => {
+ if (ready) {
+ listener();
+ } else {
+ registeredListener = listener;
+ }
+};
+
+const loadImage = (url) => {
+ return new Promise(imageLoaded => {
+ const image = new Image();
+ image.onload = () => imageLoaded(image);
+ image.src = url;
+ });
+};
+
+const loadIcon = (key, background, url) => {
+ return loadImage(url).then((image) => {
+ const canvas = document.createElement('canvas');
+ canvas.width = background.width * window.devicePixelRatio;
+ canvas.height = background.height * window.devicePixelRatio;
+ canvas.style.width = `${background.width}px`;
+ canvas.style.height = `${background.height}px`;
+ const context = canvas.getContext('2d');
+ context.drawImage(background, 0, 0, canvas.width, canvas.height);
+
+ const imageWidth = image.width * window.devicePixelRatio;
+ const imageHeight = image.height * window.devicePixelRatio;
+ context.drawImage(image, (canvas.width - imageWidth) / 2, (canvas.height - imageHeight) / 2, imageWidth, imageHeight);
+
+ map.addImage(key, context.getImageData(0, 0, canvas.width, canvas.height), {
+ pixelRatio: window.devicePixelRatio
+ });
+ });
+};
+
+const layerClickCallbacks = {};
+const layerMouseEnterCallbacks = {};
+const layerMauseLeaveCallbacks = {};
+
+const addLayer = (id, source, icon, text, onClick) => {
+ const layer = {
+ 'id': id,
+ 'type': 'symbol',
+ 'source': source,
+ 'layout': {
+ 'icon-image': icon,
+ 'icon-allow-overlap': true,
+ },
+ };
+ if (text) {
+ layer.layout = {
+ ...layer.layout,
+ 'text-field': text,
+ 'text-allow-overlap': true,
+ 'text-anchor': 'bottom',
+ 'text-offset': [0, -2],
+ 'text-font': ['Roboto Regular'],
+ 'text-size': 12,
+ }
+ layer.paint = {
+ 'text-halo-color': 'white',
+ 'text-halo-width': 1,
+ }
+ }
+ map.addLayer(layer);
+
+ layerClickCallbacks[id] = onClick;
+ map.on('click', id, onClick);
+
+ layerMouseEnterCallbacks[id] = () => {
+ map.getCanvas().style.cursor = 'pointer';
+ };
+ map.on('mouseenter', id, layerMouseEnterCallbacks[id]);
+
+ layerMauseLeaveCallbacks[id] = () => {
+ map.getCanvas().style.cursor = '';
+ };
+ map.on('mouseleave', id, layerMauseLeaveCallbacks[id]);
+}
+
+const removeLayer = (id, source) => {
+ const popups = element.getElementsByClassName('mapboxgl-popup');
+ if (popups.length) {
+ popups[0].remove();
+ }
+
+ map.off('click', id, layerClickCallbacks[id]);
+ delete layerClickCallbacks[id];
+
+ map.off('mouseenter', id, layerMouseEnterCallbacks[id]);
+ delete layerMouseEnterCallbacks[id];
+
+ map.off('mouseleave', id, layerMauseLeaveCallbacks[id]);
+ delete layerMauseLeaveCallbacks[id];
+
+ map.removeLayer(id);
+ map.removeSource(source);
+}
+
+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;
+ }
+}
+
+const element = document.createElement('div');
+element.style.width = '100%';
+element.style.height = '100%';
+
+/*const map = new mapboxgl.Map({
+ container: this.mapContainer,
+ style: 'https://cdn.traccar.com/map/basic.json',
+ center: [0, 0],
+ zoom: 1
+});*/
+
+/*const map = new mapboxgl.Map({
+ container: element,
+ style: {
+ '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': '© <a target="_top" rel="noopener" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a target="_top" rel="noopener" href="https://carto.com/attribution">CARTO</a>'
+ }
+ },
+ 'glyphs': 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf',
+ 'layers': [
+ {
+ 'id': 'simple-tiles',
+ 'type': 'raster',
+ 'source': 'raster-tiles',
+ 'minzoom': 0,
+ 'maxzoom': 22
+ }
+ ]
+ },
+ center: [0, 0],
+ zoom: 1
+});*/
+
+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: '© <a target="_top" rel="noopener" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
+ },
+ },
+ glyphs: 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf',
+ layers: [{
+ id: 'osm',
+ type: 'raster',
+ source: 'osm',
+ }],
+ },
+});
+
+map.addControl(new mapboxgl.NavigationControl());
+
+map.on('load', () => {
+ loadImage('images/background.svg').then(background => {
+ Promise.all([
+ loadIcon('icon-marker', background, 'images/icon/marker.svg')
+ ]).then(() => {
+ ready = true;
+ if (registeredListener) {
+ registeredListener();
+ registeredListener = null;
+ }
+ });
+ });
+});
+
+export default {
+ element,
+ map,
+ registerListener,
+ addLayer,
+ removeLayer,
+ calculateBounds,
+};