Track details map
This commit is contained in:
parent
66e00359a9
commit
0e12898521
24
package-lock.json
generated
24
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue