diff --git a/package-lock.json b/package-lock.json index fc2b68f..a0ac8e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9282,6 +9282,11 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha1-+5FYjnjJACVnI5XLQLJffNatGCk=" + }, "microevent.ts": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", @@ -10064,6 +10069,11 @@ "rbush": "^3.0.1" } }, + "ol-layerswitcher": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/ol-layerswitcher/-/ol-layerswitcher-3.8.3.tgz", + "integrity": "sha512-UwUhalf/sGXjz3rvr0EjwsaUVlJAhyJCfcIPciKk1QdNbMKq/2ZXNKGafOjwP2eDxiqhkvnhpIrDGD8+gQ19Cg==" + }, "ol-mapbox-style": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-6.3.1.tgz", @@ -11632,6 +11642,15 @@ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" }, + "proj4": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.7.0.tgz", + "integrity": "sha512-UVhulf8m70/dREOBrJagWq8cDYUgjQUWILRqys/gqo/+ZLeNB/04zbtPhJbz8+cCPzZNQMychfBaWUCP60U9mQ==", + "requires": { + "mgrs": "1.0.0", + "wkt-parser": "^1.2.4" + } + }, "promise": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", @@ -16200,6 +16219,11 @@ } } }, + "wkt-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.2.4.tgz", + "integrity": "sha512-ZzKnc7ml/91fOPh5bANBL4vUlWPIYYv11waCtWTkl2TRN+LEmBg60Q1MA8gqV4hEp4MGfSj9JiHz91zw/gTDXg==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index b1d03fc..a9b351f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "luxon": "^1.25.0", "node-sass": "^4.14.1", "ol": "^6.5.0", + "ol-layerswitcher": "^3.8.3", + "proj4": "^2.7.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-redux": "^7.2.2", diff --git a/src/components/Map/index.js b/src/components/Map/index.js index 49bcd3b..85bfc28 100644 --- a/src/components/Map/index.js +++ b/src/components/Map/index.js @@ -1,14 +1,20 @@ import React from 'react' import OlMap from 'ol/Map' -import View from 'ol/View' +import OlView from 'ol/View' import OlTileLayer from 'ol/layer/Tile' +import OlVectorLayer from 'ol/layer/Vector' +import OlGroupLayer from 'ol/layer/Group' import {fromLonLat} from 'ol/proj' import OSM from 'ol/source/OSM' +import OlLayerSwitcher from 'ol-layerswitcher' + import 'ol/ol.css' +import "ol-layerswitcher/dist/ol-layerswitcher.css"; const MapContext = React.createContext() +const MapLayerContext = React.createContext() export function Map({children, ...props}) { const ref = React.useRef() @@ -18,11 +24,11 @@ export function Map({children, ...props}) { React.useLayoutEffect(() => { const map = new OlMap({ target: ref.current, - view: new View({ - maxZoom: 22, - center: fromLonLat([10, 51]), - zoom: 5, - }), + // view: new View({ + // maxZoom: 22, + // center: fromLonLat([10, 51]), + // zoom: 5, + // }), }) setMap(map) @@ -36,29 +42,106 @@ export function Map({children, ...props}) { return ( <>
- {children} + {map && ( + + {children} + + )}
) } -export function TileLayer() { - const map = React.useContext(MapContext) +export function Layer({layerClass, getDefaultOptions, children, ...props}) { + const context = React.useContext(MapLayerContext) const layer = React.useMemo( () => - new OlTileLayer({ - source: new OSM(), + new layerClass({ + ...(getDefaultOptions ? getDefaultOptions() : {}), + ...props, + }), + [] + ) + + for (const [k, v] of Object.entries(props)) { + layer.set(k, v) + } + + React.useEffect(() => { + context?.push(layer) + return () => context?.remove(layer) + }, [layer, context]) + + if (typeof layer.getLayers === 'function') { + return {children} + } else { + return null + } +} + +export function TileLayer(props) { + return ({source: new OSM()})} {...props} /> +} + +export function VectorLayer(props) { + return +} + +export function GroupLayer(props) { + return +} + +function FitView({extent}) { + const map = React.useContext(MapContext) + + React.useEffect(() => { + if (extent && map) { + map.getView().fit(extent) + } + }, [extent, map]) + + return null +} + +function View({...options}) { + const map = React.useContext(MapContext) + + const view = React.useMemo( + () => + new OlView({ + ...options, }), [] ) React.useEffect(() => { - map?.addLayer(layer) - return () => map?.removeLayer(layer) - }) + if (view && map) { + map.setView(view) + } + }, [view, map]) + return null } +function LayerSwitcher({...options}) { + const map = React.useContext(MapContext) + + const control = React.useMemo(() => new OlLayerSwitcher(options), []) + + React.useEffect(() => { + map?.addControl(control) + return () => map?.removeControl(control) + }, [control, map]) + + return null +} + +Map.FitView = FitView +Map.GroupLayer = GroupLayer +Map.LayerSwitcher = LayerSwitcher Map.TileLayer = TileLayer +Map.VectorLayer = VectorLayer +Map.View = View + export default Map diff --git a/src/pages/TrackPage.tsx b/src/pages/TrackPage.tsx index d93d6cf..3d612dd 100644 --- a/src/pages/TrackPage.tsx +++ b/src/pages/TrackPage.tsx @@ -8,10 +8,21 @@ import {pluck, distinctUntilChanged, map, switchMap, startWith} from 'rxjs/opera import {useObservable} from 'rxjs-hooks' import {Settings, DateTime, Duration} from 'luxon' +import {Vector as VectorSource} from 'ol/source'; +import {Geometry, LineString, Point} from 'ol/geom'; +import Feature from 'ol/Feature'; +import {fromLonLat} from 'ol/proj'; +import proj4 from 'proj4'; +import {register} from 'ol/proj/proj4'; +import {Fill, Stroke, Style, Text, Circle} from 'ol/style'; + import api from '../api' import {Map, Page} from '../components' import type {Track, TrackData, TrackComment} from '../types' +proj4.defs('projLayer1', '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs'); +register(proj4); + // TODO: remove Settings.defaultLocale = 'de-DE' @@ -148,6 +159,174 @@ function TrackComments({comments, login, hideLoader}) { ) } +const isValidTrackPoint = (point: TrackPoint): boolean => + point.latitude != null && point.longitude != null && (point.latitude !== 0 || point.longitude !== 0) + +function TrackMap({track, trackData, ...props}) { + const { + trackVectorSource, + trackPointsD1, + trackPointsD2, + trackPointsUntaggedD1, + trackPointsUntaggedD2, + viewExtent, + } = React.useMemo(() => { + const trackPointsD1: Feature[] = [] + const trackPointsD2: Feature[] = [] + const trackPointsUntaggedD1: Feature[] = [] + const trackPointsUntaggedD2: Feature[] = [] + const points: Coordinate[] = [] + const filteredPoints: TrackPoint[] = trackData?.points.filter(isValidTrackPoint) ?? [] + + for (const dataPoint of filteredPoints) { + const {longitude, latitude, flag, d1, d2} = dataPoint + + const p = fromLonLat([longitude, latitude]) + points.push(p) + + const geometry = new Point(p) + + if (flag && d1) { + trackPointsD1.push(new Feature({distance: d1, geometry})) + } + + if (flag && d2) { + trackPointsD2.push(new Feature({distance: d2, geometry})) + } + + if (!flag && d1) { + trackPointsUntaggedD1.push(new Feature({distance: d1, geometry})) + } + + if (!flag && d2) { + trackPointsUntaggedD2.push(new Feature({distance: d2, geometry})) + } + } + + //Simplify to 1 point per 2 meter + const trackVectorSource = new VectorSource({ + features: [new Feature(new LineString(points).simplify(2))], + }) + + const viewExtent = points.length ? trackVectorSource.getExtent() : null + return {trackVectorSource, trackPointsD1, trackPointsD2, trackPointsUntaggedD1, trackPointsUntaggedD2, viewExtent} + }, [trackData?.points]) + + + const trackLayerStyle = React.useMemo( + () => + new Style({ + stroke: new Stroke({ + width: 3, + color: 'rgb(30,144,255)', + }), + }), + [] + ) + + return ( + + + + + + + + + + + + + + + + + + + ) +} + +function pointStyleFunction(feature, resolution) { + let distance = feature.get('distance') + let radius = 200 / resolution + + return new Style({ + image: new Circle({ + radius: radius < 20 ? radius : 20, + fill: evaluateDistanceForFillColor(distance), + stroke: evaluateDistanceForStrokeColor(distance), + }), + text: createTextStyle(distance, resolution), + }) +} + +const evaluateDistanceForFillColor = function (distance) { + const redFill = new Fill({color: 'rgba(255, 0, 0, 0.2)'}) + const orangeFill = new Fill({color: 'rgba(245,134,0,0.2)'}) + const greenFill = new Fill({color: 'rgba(50, 205, 50, 0.2)'}) + + switch (evaluateDistanceColor(distance)) { + case 'red': + return redFill + case 'orange': + return orangeFill + case 'green': + return greenFill + } +} + +const evaluateDistanceForStrokeColor = function (distance) { + const redStroke = new Stroke({color: 'rgb(255, 0, 0)'}) + const orangeStroke = new Stroke({color: 'rgb(245,134,0)'}) + const greenStroke = new Stroke({color: 'rgb(50, 205, 50)'}) + + switch (evaluateDistanceColor(distance)) { + case 'red': + return redStroke + case 'orange': + return orangeStroke + case 'green': + return greenStroke + } +} + + const WARN_DISTANCE= 200 + const MIN_DISTANCE= 150 + + +const evaluateDistanceColor = function (distance) { + if (distance < MIN_DISTANCE) { + return 'red' + } else if (distance < WARN_DISTANCE) { + return 'orange' + } else { + return 'green' + } +} + +const createTextStyle = function (distance, resolution) { + return new Text({ + textAlign: 'center', + textBaseline: 'middle', + font: 'normal 18px/1 Arial', + text: resolution < 6 ? '' + distance : '', + fill: new Fill({color: evaluateDistanceColor(distance)}), + stroke: new Stroke({color: 'white', width: 2}), + offsetX: 0, + offsetY: 0, + }) +} + +function PointLayer({features, title, visible}) { + return +} + const TrackPage = connect((state) => ({login: state.login}))(function TrackPage({login}) { const {slug} = useParams() @@ -200,9 +379,7 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
- - - +