Track details map

This commit is contained in:
Paul Bienkowski 2021-02-14 18:44:13 +01:00
parent 66e00359a9
commit 0e12898521
4 changed files with 303 additions and 17 deletions

24
package-lock.json generated
View file

@ -9282,6 +9282,11 @@
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" "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": { "microevent.ts": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz",
@ -10064,6 +10069,11 @@
"rbush": "^3.0.1" "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": { "ol-mapbox-style": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-6.3.1.tgz", "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", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" "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": { "promise": {
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", "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": { "word-wrap": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",

View file

@ -13,6 +13,8 @@
"luxon": "^1.25.0", "luxon": "^1.25.0",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"ol": "^6.5.0", "ol": "^6.5.0",
"ol-layerswitcher": "^3.8.3",
"proj4": "^2.7.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",

View file

@ -1,14 +1,20 @@
import React from 'react' import React from 'react'
import OlMap from 'ol/Map' import OlMap from 'ol/Map'
import View from 'ol/View' import OlView from 'ol/View'
import OlTileLayer from 'ol/layer/Tile' import OlTileLayer from 'ol/layer/Tile'
import OlVectorLayer from 'ol/layer/Vector'
import OlGroupLayer from 'ol/layer/Group'
import {fromLonLat} from 'ol/proj' import {fromLonLat} from 'ol/proj'
import OSM from 'ol/source/OSM' import OSM from 'ol/source/OSM'
import OlLayerSwitcher from 'ol-layerswitcher'
import 'ol/ol.css' import 'ol/ol.css'
import "ol-layerswitcher/dist/ol-layerswitcher.css";
const MapContext = React.createContext() const MapContext = React.createContext()
const MapLayerContext = React.createContext()
export function Map({children, ...props}) { export function Map({children, ...props}) {
const ref = React.useRef() const ref = React.useRef()
@ -18,11 +24,11 @@ export function Map({children, ...props}) {
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const map = new OlMap({ const map = new OlMap({
target: ref.current, target: ref.current,
view: new View({ // view: new View({
maxZoom: 22, // maxZoom: 22,
center: fromLonLat([10, 51]), // center: fromLonLat([10, 51]),
zoom: 5, // zoom: 5,
}), // }),
}) })
setMap(map) setMap(map)
@ -36,29 +42,106 @@ export function Map({children, ...props}) {
return ( return (
<> <>
<div ref={ref} {...props}> <div ref={ref} {...props}>
<MapContext.Provider value={map}>{children}</MapContext.Provider> {map && (
<MapContext.Provider value={map}>
<MapLayerContext.Provider value={map.getLayers()}>{children}</MapLayerContext.Provider>
</MapContext.Provider>
)}
</div> </div>
</> </>
) )
} }
export function TileLayer() { export function Layer({layerClass, getDefaultOptions, children, ...props}) {
const map = React.useContext(MapContext) const context = React.useContext(MapLayerContext)
const layer = React.useMemo( const layer = React.useMemo(
() => () =>
new OlTileLayer({ new layerClass({
source: new OSM(), ...(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 <MapLayerContext.Provider value={layer.getLayers()}>{children}</MapLayerContext.Provider>
} else {
return null
}
}
export function TileLayer(props) {
return <Layer layerClass={OlTileLayer} getDefaultOptions={() => ({source: new OSM()})} {...props} />
}
export function VectorLayer(props) {
return <Layer layerClass={OlVectorLayer} {...props} />
}
export function GroupLayer(props) {
return <Layer layerClass={OlGroupLayer} {...props} />
}
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(() => { React.useEffect(() => {
map?.addLayer(layer) if (view && map) {
return () => map?.removeLayer(layer) map.setView(view)
}) }
}, [view, map])
return null 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.TileLayer = TileLayer
Map.VectorLayer = VectorLayer
Map.View = View
export default Map export default Map

View file

@ -8,10 +8,21 @@ import {pluck, distinctUntilChanged, map, switchMap, startWith} from 'rxjs/opera
import {useObservable} from 'rxjs-hooks' import {useObservable} from 'rxjs-hooks'
import {Settings, DateTime, Duration} from 'luxon' 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 api from '../api'
import {Map, Page} from '../components' import {Map, Page} from '../components'
import type {Track, TrackData, TrackComment} from '../types' 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 // TODO: remove
Settings.defaultLocale = 'de-DE' 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<Geometry>[] = []
const trackPointsD2: Feature<Geometry>[] = []
const trackPointsUntaggedD1: Feature<Geometry>[] = []
const trackPointsUntaggedD2: Feature<Geometry>[] = []
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 (
<Map {...props}>
<Map.TileLayer />
<Map.VectorLayer
visible
updateWhileAnimating={false}
updateWhileInteracting={false}
source={trackVectorSource}
style={trackLayerStyle}
/>
<Map.GroupLayer title="Tagged Points">
<PointLayer features={trackPointsD1} title="Left" visible={true} />
<PointLayer features={trackPointsD2} title="Right" visible={false} />
</Map.GroupLayer>
<Map.GroupLayer title="Untagged Points" fold="close" visible={false}>
<PointLayer features={trackPointsUntaggedD1} title="Left Untagged" visible={false} />
<PointLayer features={trackPointsUntaggedD2} title="Right Untagged" visible={false} />
</Map.GroupLayer>
<Map.View maxZoom={22} zoom={15} center={fromLonLat([9.1797, 48.7784])} />
<Map.FitView extent={viewExtent} />
<Map.LayerSwitcher groupSelectStyle='children' startActive activationMode='click' reverse={false} />
</Map>
)
}
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 <Map.VectorLayer {...{title, visible}} style={pointStyleFunction} source={new VectorSource({features})} />
}
const TrackPage = connect((state) => ({login: state.login}))(function TrackPage({login}) { const TrackPage = connect((state) => ({login: state.login}))(function TrackPage({login}) {
const {slug} = useParams() const {slug} = useParams()
@ -200,9 +379,7 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
<div style={{position: 'relative'}}> <div style={{position: 'relative'}}>
<Loader active={loading} /> <Loader active={loading} />
<Dimmer.Dimmable blurring dimmed={loading}> <Dimmer.Dimmable blurring dimmed={loading}>
<Map style={{height: '60vh', minHeight: 400}}> <TrackMap {...{track, trackData}} style={{height: '60vh', minHeight: 400}} />
<Map.TileLayer />
</Map>
</Dimmer.Dimmable> </Dimmer.Dimmable>
</div> </div>
</Grid.Column> </Grid.Column>