frontend: Replace openlayers track map with gl version
This commit is contained in:
parent
f54fe701e7
commit
12686abe14
|
@ -15,8 +15,6 @@
|
||||||
"luxon": "^1.27.0",
|
"luxon": "^1.27.0",
|
||||||
"maplibre-gl": "^2.0.0-pre.1",
|
"maplibre-gl": "^2.0.0-pre.1",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"ol": "^6.5.0",
|
|
||||||
"ol-mapbox-style": "^6.5.1",
|
|
||||||
"pkce": "^1.0.0-beta2",
|
"pkce": "^1.0.0-beta2",
|
||||||
"proj4": "^2.7.2",
|
"proj4": "^2.7.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
@ -64,7 +62,6 @@
|
||||||
"@craco/craco": "^6.1.2",
|
"@craco/craco": "^6.1.2",
|
||||||
"@semantic-ui-react/craco-less": "^1.2.1",
|
"@semantic-ui-react/craco-less": "^1.2.1",
|
||||||
"@types/lodash": "^4.14.169",
|
"@types/lodash": "^4.14.169",
|
||||||
"@types/ol": "^6.5.0",
|
|
||||||
"@types/react-redux": "^7.1.16",
|
"@types/react-redux": "^7.1.16",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"semantic-ui-less": "^2.4.1"
|
"semantic-ui-less": "^2.4.1"
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
import OlMap from 'ol/Map'
|
|
||||||
import OlView from 'ol/View'
|
|
||||||
import OlTileLayer from 'ol/layer/Tile'
|
|
||||||
import OlVectorLayer from 'ol/layer/Vector'
|
|
||||||
import OlGroupLayer from 'ol/layer/Group'
|
|
||||||
import OSM from 'ol/source/OSM'
|
|
||||||
import proj4 from 'proj4'
|
|
||||||
import {register} from 'ol/proj/proj4'
|
|
||||||
import {fromLonLat} from 'ol/proj'
|
|
||||||
|
|
||||||
// Import styles for open layers + addons
|
|
||||||
import 'ol/ol.css'
|
|
||||||
|
|
||||||
import {useConfig} from 'config'
|
|
||||||
|
|
||||||
// Prepare projection
|
|
||||||
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)
|
|
||||||
|
|
||||||
export const MapContext = React.createContext()
|
|
||||||
const MapLayerContext = React.createContext()
|
|
||||||
|
|
||||||
export function Map({children, ...props}) {
|
|
||||||
const ref = React.useRef()
|
|
||||||
|
|
||||||
const [map, setMap] = React.useState(null)
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
|
||||||
const map = new OlMap({target: ref.current})
|
|
||||||
|
|
||||||
setMap(map)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
map.setTarget(null)
|
|
||||||
setMap(null)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div ref={ref} {...props}>
|
|
||||||
{map && (
|
|
||||||
<MapContext.Provider value={map}>
|
|
||||||
<MapLayerContext.Provider value={map.getLayers()}>{children}</MapLayerContext.Provider>
|
|
||||||
</MapContext.Provider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Layer({layerClass, getDefaultOptions, children, ...props}) {
|
|
||||||
const context = React.useContext(MapContext)
|
|
||||||
|
|
||||||
const layer = React.useMemo(
|
|
||||||
() =>
|
|
||||||
new layerClass({
|
|
||||||
...(getDefaultOptions ? getDefaultOptions() : {}),
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
layer.setProperties(props)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
context?.addLayer(layer)
|
|
||||||
return () => context?.removeLayer(layer)
|
|
||||||
}, [layer, context])
|
|
||||||
|
|
||||||
if (typeof layer.getLayers === 'function') {
|
|
||||||
return <MapLayerContext.Provider value={layer.getLayers()}>{children}</MapLayerContext.Provider>
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TileLayer({osm, ...props}) {
|
|
||||||
return <Layer layerClass={OlTileLayer} getDefaultOptions={() => ({source: new OSM(osm)})} {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BaseLayer(props) {
|
|
||||||
const config = useConfig()
|
|
||||||
if (!config) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TileLayer
|
|
||||||
osm={{
|
|
||||||
url: config.mapTileset?.url ?? 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png',
|
|
||||||
crossOrigin: null,
|
|
||||||
}}
|
|
||||||
{...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 config = useConfig()
|
|
||||||
|
|
||||||
const view = React.useMemo(
|
|
||||||
() => {
|
|
||||||
if (!config) return null
|
|
||||||
|
|
||||||
const minZoom = config.mapTileset?.minZoom ?? 0
|
|
||||||
const maxZoom = config.mapTileset?.maxZoom ?? 18
|
|
||||||
const mapHomeZoom = config.mapHome?.zoom ?? 15
|
|
||||||
const mapHomeLongitude = config.mapHome?.longitude ?? 9.1797
|
|
||||||
const mapHomeLatitude = config.mapHome?.latitude ?? 48.7784
|
|
||||||
|
|
||||||
return new OlView({
|
|
||||||
minZoom,
|
|
||||||
maxZoom,
|
|
||||||
zoom: Math.max(Math.min(mapHomeZoom, maxZoom), minZoom),
|
|
||||||
center: fromLonLat([mapHomeLongitude, mapHomeLatitude]),
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[config]
|
|
||||||
)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (view && map) {
|
|
||||||
map.setView(view)
|
|
||||||
}
|
|
||||||
}, [view, map])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
Map.FitView = FitView
|
|
||||||
Map.GroupLayer = GroupLayer
|
|
||||||
Map.TileLayer = TileLayer
|
|
||||||
Map.VectorLayer = VectorLayer
|
|
||||||
Map.View = View
|
|
||||||
Map.Layer = Layer
|
|
||||||
Map.BaseLayer = BaseLayer
|
|
||||||
|
|
||||||
export default Map
|
|
|
@ -14,3 +14,7 @@
|
||||||
.fullScreen {
|
.fullScreen {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hasStage {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -4,9 +4,10 @@ import {Container} from 'semantic-ui-react'
|
||||||
|
|
||||||
import styles from './Page.module.scss'
|
import styles from './Page.module.scss'
|
||||||
|
|
||||||
export default function Page({small, children, fullScreen}: {small?: boolean, children: ReactNode, fullScreen?: boolean}) {
|
export default function Page({small, children, fullScreen, stage}: {small?: boolean, children: ReactNode, fullScreen?: boolean,stage?: ReactNode}) {
|
||||||
return (
|
return (
|
||||||
<main className={classnames(styles.page, small && styles.small, fullScreen && styles.fullScreen)}>
|
<main className={classnames(styles.page, small && styles.small, fullScreen && styles.fullScreen,stage && styles.hasStage)}>
|
||||||
|
{stage}
|
||||||
{fullScreen ? children : <Container>{children}</Container>}
|
{fullScreen ? children : <Container>{children}</Container>}
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,383 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import VectorSource from 'ol/source/Vector'
|
|
||||||
import GeoJSON from 'ol/format/GeoJSON'
|
|
||||||
import {Stroke, Style} from 'ol/style'
|
|
||||||
|
|
||||||
import Map from './Map'
|
|
||||||
|
|
||||||
import {paletteUrban, paletteRural, palettePercentage, palettePercentageInverted} from 'palettes'
|
|
||||||
|
|
||||||
// var criterion = "d_mean";
|
|
||||||
// var criterion = "p_above";
|
|
||||||
var criterion = 'p_below'
|
|
||||||
|
|
||||||
// var hist_xa = 0.0
|
|
||||||
// var hist_xb = 2.55
|
|
||||||
// var hist_dx = 0.25
|
|
||||||
// var hist_n = Math.ceil((hist_xb - hist_xa) / hist_dx)
|
|
||||||
|
|
||||||
// function histogramLabels() {
|
|
||||||
// var labels = Array(hist_n)
|
|
||||||
// for (var i = 0; i < hist_n; i++) {
|
|
||||||
// var xa = hist_xa + hist_dx * i
|
|
||||||
// var xb = xa + hist_dx
|
|
||||||
// var xc = xa + 0.5 * hist_dx
|
|
||||||
// labels[i] = (xa * 100).toFixed(0) + '-' + (xb * 100).toFixed(0)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return labels
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// function histogramColors(palette) {
|
|
||||||
// var colors = Array(hist_n)
|
|
||||||
// for (var i = 0; i < hist_n; i++) {
|
|
||||||
// var xc = hist_xa + hist_dx * i
|
|
||||||
// colors[i] = palette.rgb_hex(xc)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return colors
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// function histogram(samples) {
|
|
||||||
// var binCounts = new Array(hist_n).fill(0)
|
|
||||||
//
|
|
||||||
// for (var i = 0; i < samples.length; i++) {
|
|
||||||
// var v = samples[i]
|
|
||||||
// var j = Math.floor((v - hist_xa) / hist_dx)
|
|
||||||
// if (j >= 0 && j < hist_n) {
|
|
||||||
// binCounts[j]++
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return binCounts
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// function annotation_verbose(feature) {
|
|
||||||
// var s = ''
|
|
||||||
//
|
|
||||||
// s += 'name: ' + feature.get('name') + '\n'
|
|
||||||
// s += 'way_id: ' + feature.get('way_id') + '\n'
|
|
||||||
// s += 'direction: ' + feature.get('direction') + '\n'
|
|
||||||
// s += 'zone: ' + feature.get('zone') + '\n'
|
|
||||||
// s += 'valid: ' + feature.get('valid') + '\n'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_limit')
|
|
||||||
// s += 'distance_overtaker_limit: ' + (d == null ? 'n/a' : d.toFixed(2)) + ' m \n'
|
|
||||||
//
|
|
||||||
// s += '<hr></hr>statistics\n'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_mean')
|
|
||||||
// s += 'distance_overtaker_mean: ' + (d == null ? 'n/a' : d.toFixed(2)) + ' m \n'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_median')
|
|
||||||
// s += 'distance_overtaker_median: ' + (d == null ? 'n/a' : d.toFixed(2)) + ' m \n'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_minimum')
|
|
||||||
// s += 'distance_overtaker_minimum: ' + (d == null ? 'n/a' : d.toFixed(2)) + ' m \n'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_n')
|
|
||||||
// s += 'distance_overtaker_n: ' + (d == null ? 'n/a' : d.toFixed(0)) + '\n'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_n_above_limit')
|
|
||||||
// s += 'distance_overtaker_n_above_limit: ' + (d == null ? 'n/a' : d.toFixed(0)) + '\n'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_n_below_limit')
|
|
||||||
// s += 'distance_overtaker_n_below_limit: ' + (d == null ? 'n/a' : d.toFixed(0)) + '\n'
|
|
||||||
//
|
|
||||||
// var n_below = feature.get('distance_overtaker_n_below_limit')
|
|
||||||
// var n = feature.get('distance_overtaker_n')
|
|
||||||
// var p = (n_below / n) * 100.0
|
|
||||||
// s += 'overtakers below limit: ' + (p == null ? 'n/a' : p.toFixed(1)) + ' %\n'
|
|
||||||
//
|
|
||||||
// return s
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// function annotation(feature) {
|
|
||||||
// var s = '<table>'
|
|
||||||
//
|
|
||||||
// s +=
|
|
||||||
// '<tr><td>Straßenname:</td><td><a href="https://www.openstreetmap.org/way/' +
|
|
||||||
// feature.get('way_id') +
|
|
||||||
// '" target="_blank">' +
|
|
||||||
// feature.get('name') +
|
|
||||||
// '</a></td></tr>'
|
|
||||||
// d = feature.get('distance_overtaker_limit')
|
|
||||||
// s += '<tr><td>Mindestüberholabstand:</td><td>' + (d == null ? 'n/a' : d.toFixed(2)) + ' m </td>'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_n')
|
|
||||||
// s += '<tr><td>Anzahl Messungen:</td><td>' + (d == null ? 'n/a' : d.toFixed(0)) + '</td></tr>'
|
|
||||||
//
|
|
||||||
// var n_below = feature.get('distance_overtaker_n_below_limit')
|
|
||||||
// var n = feature.get('distance_overtaker_n')
|
|
||||||
// var p = (n_below / n) * 100.0
|
|
||||||
// s +=
|
|
||||||
// '<tr><td>Unterschreitung Mindestabstand:</td><td>' +
|
|
||||||
// (p == null ? 'n/a' : p.toFixed(1)) +
|
|
||||||
// '% der Überholenden</td></tr>'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_mean')
|
|
||||||
// s += '<tr><td>Durchschnitt Überholabstand:</td><td>' + (d == null ? 'n/a' : d.toFixed(2)) + ' m </td></tr>'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_median')
|
|
||||||
// s += '<tr><td>Median Überholabstand:</td><td>' + (d == null ? 'n/a' : d.toFixed(2)) + ' m </td></tr>'
|
|
||||||
//
|
|
||||||
// d = feature.get('distance_overtaker_minimum')
|
|
||||||
// s += '<tr><td>Minimum Überholabstand:</td><td>' + (d == null ? 'n/a' : d.toFixed(2)) + ' m </td></tr>'
|
|
||||||
//
|
|
||||||
// s += '</table>'
|
|
||||||
//
|
|
||||||
// return s
|
|
||||||
// }
|
|
||||||
|
|
||||||
function styleFunction(feature, resolution, active = false) {
|
|
||||||
const {
|
|
||||||
distance_overtaker_n: n,
|
|
||||||
distance_overtaker_n_above_limit: n_above_limit,
|
|
||||||
distance_overtaker_n_below_limit: n_below_limit,
|
|
||||||
distance_overtaker_mean: mean,
|
|
||||||
distance_overtaker_median: median,
|
|
||||||
distance_overtaker_minimum: minimum,
|
|
||||||
zone,
|
|
||||||
valid,
|
|
||||||
} = feature.getProperties()
|
|
||||||
|
|
||||||
let palette
|
|
||||||
if (zone === 'urban') {
|
|
||||||
palette = paletteUrban
|
|
||||||
} else if (zone === 'rural') {
|
|
||||||
palette = paletteRural
|
|
||||||
} else {
|
|
||||||
palette = paletteUrban
|
|
||||||
}
|
|
||||||
|
|
||||||
var color = [0, 0, 0, 255]
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
switch (criterion) {
|
|
||||||
case 'd_mean':
|
|
||||||
color = palette.rgba_css(mean)
|
|
||||||
break
|
|
||||||
case 'd_median':
|
|
||||||
color = palette.rgba_css(median)
|
|
||||||
break
|
|
||||||
case 'd_min':
|
|
||||||
color = palette.rgba_css(minimum)
|
|
||||||
break
|
|
||||||
case 'p_above':
|
|
||||||
color = palettePercentage.rgba_css(n > 0 ? (n_above_limit / n * 100) : undefined)
|
|
||||||
break
|
|
||||||
case 'p_below':
|
|
||||||
color = palettePercentageInverted.rgba_css(n > 0 ? (n_below_limit / n * 100) : undefined)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
color = [128, 128, 128, 255]
|
|
||||||
}
|
|
||||||
|
|
||||||
// var width = 2 + 1*Math.log10(n);
|
|
||||||
var width = active ? 6 : 3
|
|
||||||
// width =Math.max(2.0, width*1/resolution);
|
|
||||||
|
|
||||||
var style = new Style({
|
|
||||||
stroke: new Stroke({
|
|
||||||
color: color,
|
|
||||||
width: width,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
|
|
||||||
// var map = new ol.Map({
|
|
||||||
// target: 'map',
|
|
||||||
// layers: [
|
|
||||||
// new ol.layer.Tile({
|
|
||||||
// source: new ol.source.OSM({
|
|
||||||
// url: 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png',
|
|
||||||
// crossOrigin: null,
|
|
||||||
// // url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
|
||||||
// }),
|
|
||||||
// }),
|
|
||||||
// ],
|
|
||||||
// view: new ol.View({
|
|
||||||
// center: ol.proj.fromLonLat([9.1798, 48.7759]),
|
|
||||||
// zoom: 13,
|
|
||||||
// }),
|
|
||||||
// })
|
|
||||||
|
|
||||||
export default function RoadsLayer() {
|
|
||||||
const dataSource = React.useMemo(
|
|
||||||
() =>
|
|
||||||
new VectorSource({
|
|
||||||
format: new GeoJSON(),
|
|
||||||
url: 'https://dev.openbikesensor.org/public/json/roads.json',
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return <Map.VectorLayer source={dataSource} style={styleFunction} zIndex={1000} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// var histogramColorsRural = histogramColors(paletteRural).reverse()
|
|
||||||
// var histogramColorsUrban = histogramColors(paletteUrban).reverse()
|
|
||||||
//
|
|
||||||
// var chartOptions = {
|
|
||||||
// series: [
|
|
||||||
// {
|
|
||||||
// name: 'Überholende',
|
|
||||||
// data: Array(hist_n).fill(0),
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// chart: {
|
|
||||||
// type: 'bar',
|
|
||||||
// height: 350,
|
|
||||||
// animations: {
|
|
||||||
// animateGradually: {
|
|
||||||
// enabled: false,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// plotOptions: {
|
|
||||||
// bar: {
|
|
||||||
// horizontal: false,
|
|
||||||
// columnWidth: '95%',
|
|
||||||
// endingShape: 'flat',
|
|
||||||
// distributed: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// dataLabels: {
|
|
||||||
// enabled: true,
|
|
||||||
// },
|
|
||||||
// stroke: {
|
|
||||||
// show: false,
|
|
||||||
// },
|
|
||||||
// xaxis: {
|
|
||||||
// title: {
|
|
||||||
// text: 'Überholabstand in Zentimeter',
|
|
||||||
// },
|
|
||||||
// categories: histogramLabels().reverse(),
|
|
||||||
// },
|
|
||||||
// yaxis: {
|
|
||||||
// title: {
|
|
||||||
// text: 'Anzahl Überholende',
|
|
||||||
// },
|
|
||||||
// labels: {
|
|
||||||
// show: false,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// fill: {
|
|
||||||
// opacity: 1,
|
|
||||||
// },
|
|
||||||
// legend: {
|
|
||||||
// show: false,
|
|
||||||
// },
|
|
||||||
// tooltip: {
|
|
||||||
// y: {
|
|
||||||
// formatter: function (val) {
|
|
||||||
// return val
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var chart = new ApexCharts(document.querySelector('#chart'), chartOptions)
|
|
||||||
// chart.render()
|
|
||||||
|
|
||||||
// var noFeatureActive = true
|
|
||||||
|
|
||||||
// map.on('singleclick', function (evt) {
|
|
||||||
// var feature = map.forEachFeatureAtPixel(evt.pixel, function (feature, layer) {
|
|
||||||
// return feature
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// var resolution = map.getView().getResolution()
|
|
||||||
//
|
|
||||||
// if (!noFeatureActive) {
|
|
||||||
// vectorLayer
|
|
||||||
// .getSource()
|
|
||||||
// .getFeatures()
|
|
||||||
// .forEach((f) => {
|
|
||||||
// f.setStyle(styleFunction(f, resolution, false))
|
|
||||||
// })
|
|
||||||
// noFeatureActive = true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (feature && dataSource.hasFeature(feature)) {
|
|
||||||
// console.log(annotation_verbose(feature))
|
|
||||||
// caption.innerHTML = annotation(feature)
|
|
||||||
// caption.style.alignItems = 'flex-start'
|
|
||||||
//
|
|
||||||
// var zone = feature.get('zone')
|
|
||||||
// var colors = undefined
|
|
||||||
// switch (zone) {
|
|
||||||
// case 'urban':
|
|
||||||
// colors = histogramColorsUrban
|
|
||||||
// break
|
|
||||||
// case 'rural':
|
|
||||||
// colors = histogramColorsRural
|
|
||||||
// break
|
|
||||||
// default:
|
|
||||||
// colors = histogramColorsUrban
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// chart.updateOptions({
|
|
||||||
// colors: colors,
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// var hist = histogram(feature.get('distance_overtaker_measurements')).reverse()
|
|
||||||
//
|
|
||||||
// chart.updateSeries([
|
|
||||||
// {
|
|
||||||
// name: 'Überholende',
|
|
||||||
// data: hist,
|
|
||||||
// },
|
|
||||||
// ])
|
|
||||||
//
|
|
||||||
// feature.setStyle(styleFunction(feature, resolution, true))
|
|
||||||
// noFeatureActive = false
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// function writeLegend(palette, target, ticks, postfix) {
|
|
||||||
// const div = document.getElementById(target)
|
|
||||||
// const canvas = document.createElement('canvas')
|
|
||||||
// const context = canvas.getContext('2d')
|
|
||||||
//
|
|
||||||
// const barWidth = palette.n
|
|
||||||
// const barLeft = 25
|
|
||||||
// const barHeight = 25
|
|
||||||
//
|
|
||||||
// canvas.width = 300
|
|
||||||
// canvas.height = 50
|
|
||||||
//
|
|
||||||
// const imgData = context.getImageData(0, 0, barWidth, barHeight)
|
|
||||||
// const data = imgData.data
|
|
||||||
//
|
|
||||||
// let k = 0
|
|
||||||
// for (let y = 0; y < barHeight; y++) {
|
|
||||||
// for (let x = 0; x < barWidth; x++) {
|
|
||||||
// for (let c = 0; c < 4; c++) {
|
|
||||||
// data[k] = palette.rgba_sampled[x][c]
|
|
||||||
// k += 1
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// context.putImageData(imgData, barLeft, 0)
|
|
||||||
//
|
|
||||||
// context.font = '12px Arial'
|
|
||||||
// context.textAlign = 'center'
|
|
||||||
// context.textBaseline = 'top'
|
|
||||||
// for (let i = 0; i < ticks.length; i++) {
|
|
||||||
// const v = ticks[i]
|
|
||||||
// const x = barLeft + ((v - palette.a) / (palette.b - palette.a)) * (palette.n - 1)
|
|
||||||
// const y = 25
|
|
||||||
// context.fillText(v.toFixed(2) + postfix, x, y)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const image = new Image()
|
|
||||||
// image.src = canvas.toDataURL()
|
|
||||||
// image.height = canvas.height
|
|
||||||
// image.width = canvas.width
|
|
||||||
// div.appendChild(image)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// writeLegend(palettePercentageInverted, 'colorbar', [0, 100.0], '%')
|
|
|
@ -3,8 +3,6 @@ export {default as FileDrop} from './FileDrop'
|
||||||
export {default as FileUploadField} from './FileUploadField'
|
export {default as FileUploadField} from './FileUploadField'
|
||||||
export {default as FormattedDate} from './FormattedDate'
|
export {default as FormattedDate} from './FormattedDate'
|
||||||
export {default as LoginButton} from './LoginButton'
|
export {default as LoginButton} from './LoginButton'
|
||||||
export {default as Map} from './Map'
|
|
||||||
export {default as Page} from './Page'
|
export {default as Page} from './Page'
|
||||||
export {default as RoadsLayer} from './RoadsLayer'
|
|
||||||
export {default as Stats} from './Stats'
|
export {default as Stats} from './Stats'
|
||||||
export {default as StripMarkdown} from './StripMarkdown'
|
export {default as StripMarkdown} from './StripMarkdown'
|
||||||
|
|
|
@ -3,138 +3,83 @@ import _ from 'lodash'
|
||||||
import bright from './bright.json'
|
import bright from './bright.json'
|
||||||
import positron from './positron.json'
|
import positron from './positron.json'
|
||||||
|
|
||||||
|
export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC') {
|
||||||
|
return [
|
||||||
|
'case',
|
||||||
|
['!', ['to-boolean', ['get', attribute]]],
|
||||||
|
fallback,
|
||||||
|
[
|
||||||
|
'interpolate-hcl',
|
||||||
|
['linear'],
|
||||||
|
['get', attribute],
|
||||||
|
1,
|
||||||
|
'rgba(255, 0, 0, 1)',
|
||||||
|
1.3,
|
||||||
|
'rgba(255, 200, 0, 1)',
|
||||||
|
1.5,
|
||||||
|
'rgba(67, 200, 0, 1)',
|
||||||
|
1.7,
|
||||||
|
'rgba(67, 150, 0, 1)',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
function addRoadsStyle(style, mapSource) {
|
function addRoadsStyle(style, mapSource) {
|
||||||
style.sources.obs = mapSource
|
style.sources.obs = mapSource
|
||||||
|
|
||||||
// insert before "road_oneway" layer
|
// insert before "road_oneway" layer
|
||||||
let idx = style.layers.findIndex(l => l.id === 'road_oneway')
|
let idx = style.layers.findIndex((l) => l.id === 'road_oneway')
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
idx = style.layers.length
|
idx = style.layers.length
|
||||||
}
|
}
|
||||||
style.layers.splice(idx, 0, {
|
style.layers.splice(idx, 0, {
|
||||||
"id": "obs",
|
id: 'obs',
|
||||||
"type": "line",
|
type: 'line',
|
||||||
"source": "obs",
|
source: 'obs',
|
||||||
"source-layer": "obs_roads",
|
'source-layer': 'obs_roads',
|
||||||
"layout": {
|
layout: {
|
||||||
"line-cap": "round",
|
'line-cap': 'round',
|
||||||
"line-join": "round"
|
'line-join': 'round',
|
||||||
},
|
},
|
||||||
"paint": {
|
paint: {
|
||||||
"line-width": [
|
'line-width': [
|
||||||
"interpolate",
|
'interpolate',
|
||||||
["exponential", 1.5],
|
['exponential', 1.5],
|
||||||
["zoom"],
|
['zoom'],
|
||||||
12,
|
12,
|
||||||
2,
|
2,
|
||||||
17,
|
17,
|
||||||
[
|
['case', ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], 2, 6],
|
||||||
"case",
|
|
||||||
[
|
|
||||||
"!",
|
|
||||||
[
|
|
||||||
"to-boolean",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"distance_overtaker_mean"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
2,
|
|
||||||
6
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
"line-color": [
|
'line-color': colorByDistance(),
|
||||||
"case",
|
'line-opacity': [
|
||||||
[
|
'interpolate',
|
||||||
"!",
|
['linear'],
|
||||||
[
|
['zoom'],
|
||||||
"to-boolean",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"distance_overtaker_mean"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"#ABC",
|
|
||||||
[
|
|
||||||
"interpolate-hcl",
|
|
||||||
["linear"],
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"distance_overtaker_mean"
|
|
||||||
],
|
|
||||||
1,
|
|
||||||
"rgba(255, 0, 0, 1)",
|
|
||||||
1.3,
|
|
||||||
"rgba(255, 200, 0, 1)",
|
|
||||||
1.5,
|
|
||||||
"rgba(67, 200, 0, 1)",
|
|
||||||
1.7,
|
|
||||||
"rgba(67, 150, 0, 1)"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"line-opacity": [
|
|
||||||
"interpolate",
|
|
||||||
["linear"],
|
|
||||||
["zoom"],
|
|
||||||
12,
|
12,
|
||||||
0,
|
0,
|
||||||
13,
|
13,
|
||||||
[
|
['case', ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], 0, 1],
|
||||||
"case",
|
|
||||||
[
|
|
||||||
"!",
|
|
||||||
[
|
|
||||||
"to-boolean",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"distance_overtaker_mean"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
],
|
|
||||||
14,
|
14,
|
||||||
[
|
['case', ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], 0, 1],
|
||||||
"case",
|
|
||||||
[
|
|
||||||
"!",
|
|
||||||
[
|
|
||||||
"to-boolean",
|
|
||||||
[
|
|
||||||
"get",
|
|
||||||
"distance_overtaker_mean"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
],
|
|
||||||
15,
|
15,
|
||||||
1
|
1,
|
||||||
],
|
],
|
||||||
"line-offset": [
|
'line-offset': [
|
||||||
"interpolate",
|
'interpolate',
|
||||||
["exponential", 1.5],
|
['exponential', 1.5],
|
||||||
["zoom"],
|
['zoom'],
|
||||||
12,
|
12,
|
||||||
["get", "offset_direction"],
|
['get', 'offset_direction'],
|
||||||
19,
|
19,
|
||||||
[
|
['*', ['get', 'offset_direction'], 8],
|
||||||
"*",
|
],
|
||||||
["get", "offset_direction"],
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"minzoom": 12
|
minzoom: 12,
|
||||||
})
|
})
|
||||||
|
|
||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const basemap = positron
|
||||||
export const basemap = bright
|
export const obsRoads = (sourceUrl) => addRoadsStyle(_.cloneDeep(basemap), sourceUrl)
|
||||||
export const obsRoads = (sourceUrl) => addRoadsStyle(_.cloneDeep(positron), sourceUrl)
|
|
||||||
|
|
|
@ -9,11 +9,9 @@ import api from 'api'
|
||||||
import {Stats, Page} from 'components'
|
import {Stats, Page} from 'components'
|
||||||
|
|
||||||
import {TrackListItem} from './TracksPage'
|
import {TrackListItem} from './TracksPage'
|
||||||
import {RoadsMap} from './MapPage'
|
import {CustomMap} from './MapPage'
|
||||||
import styles from './HomePage.module.scss'
|
import styles from './HomePage.module.scss'
|
||||||
|
|
||||||
import 'ol/ol.css'
|
|
||||||
|
|
||||||
function MostRecentTrack() {
|
function MostRecentTrack() {
|
||||||
const track: Track | null = useObservable(
|
const track: Track | null = useObservable(
|
||||||
() =>
|
() =>
|
||||||
|
@ -49,7 +47,7 @@ export default function HomePage() {
|
||||||
<Grid.Row>
|
<Grid.Row>
|
||||||
<Grid.Column width={10}>
|
<Grid.Column width={10}>
|
||||||
<div className={styles.welcomeMap}>
|
<div className={styles.welcomeMap}>
|
||||||
<RoadsMap />
|
<CustomMap mode='roads' />
|
||||||
</div>
|
</div>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column width={6}>
|
<Grid.Column width={6}>
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
// import {Grid, Loader, Header, Item} from 'semantic-ui-react'
|
|
||||||
|
|
||||||
// import api from 'api'
|
|
||||||
import {Page} from 'components'
|
import {Page} from 'components'
|
||||||
import {useConfig, Config} from 'config'
|
import {useConfig, Config} from 'config'
|
||||||
|
import ReactMapGl, {AttributionControl } from 'react-map-gl'
|
||||||
|
|
||||||
import styles from './MapPage.module.scss'
|
import styles from './MapPage.module.scss'
|
||||||
|
|
||||||
import 'ol/ol.css'
|
import {obsRoads, basemap } from '../mapstyles'
|
||||||
import {obsRoads} from '../mapstyles'
|
|
||||||
import ReactMapGl, {AttributionControl } from 'react-map-gl'
|
function CustomMapInner({mapSource, config, mode, children}: {mapSource: string, config: Config, mode?: 'roads'}) {
|
||||||
|
const mapStyle = React.useMemo(() => {
|
||||||
|
if (mode === 'roads') {
|
||||||
|
return mapSource && obsRoads(mapSource)
|
||||||
|
} else {
|
||||||
|
return basemap
|
||||||
|
}
|
||||||
|
}, [mapSource, mode])
|
||||||
|
|
||||||
function RoadsMapInner({mapSource, config}: {mapSource: string ,config: Config}) {
|
|
||||||
const mapStyle = React.useMemo(() => mapSource && obsRoads(mapSource), [mapSource])
|
|
||||||
const [viewport, setViewport] = React.useState({
|
const [viewport, setViewport] = React.useState({
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
latitude: 0,
|
latitude: 0,
|
||||||
|
@ -32,15 +36,17 @@ function RoadsMapInner({mapSource, config}: {mapSource: string ,config: Config})
|
||||||
return (
|
return (
|
||||||
<ReactMapGl mapStyle={mapStyle} width="100%" height="100%" onViewportChange={setViewport} {...viewport}>
|
<ReactMapGl mapStyle={mapStyle} width="100%" height="100%" onViewportChange={setViewport} {...viewport}>
|
||||||
<AttributionControl style={{right: 0, bottom: 0}} customAttribution={[
|
<AttributionControl style={{right: 0, bottom: 0}} customAttribution={[
|
||||||
'<a href="https://openstreetmap.org/copyright" target="_blank" rel="nofollow noopener">© OpenStreetMap contributors</a>',
|
'<a href="https://openstreetmap.org/copyright" target="_blank" rel="nofollow noopener noreferrer">© OpenStreetMap contributors</a>',
|
||||||
'<a href="https://openmaptiles.org/" target="_blank" rel="nofollow noopener">© OpenMapTiles</a>',
|
'<a href="https://openmaptiles.org/" target="_blank" rel="nofollow noopener noreferrer">© OpenMapTiles</a>',
|
||||||
'<a href="https://openbikesensor.org/" target="_blank" rel="nofollow noopener">© OpenBikeSensor</a>',
|
'<a href="https://openbikesensor.org/" target="_blank" rel="nofollow noopener noreferrer">© OpenBikeSensor</a>',
|
||||||
]} />
|
]} />
|
||||||
|
|
||||||
|
{children}
|
||||||
</ReactMapGl>
|
</ReactMapGl>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoadsMap(props) {
|
export function CustomMap(props) {
|
||||||
const config = useConfig() || {}
|
const config = useConfig() || {}
|
||||||
if (!config) return null;
|
if (!config) return null;
|
||||||
const {obsMapSource: mapSource} = config
|
const {obsMapSource: mapSource} = config
|
||||||
|
@ -48,7 +54,7 @@ export function RoadsMap(props) {
|
||||||
if (!mapSource) return null;
|
if (!mapSource) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoadsMapInner {...{mapSource, config}} {...props} />
|
<CustomMapInner {...{mapSource, config}} {...props} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +62,7 @@ export default function MapPage() {
|
||||||
return (
|
return (
|
||||||
<Page fullScreen>
|
<Page fullScreen>
|
||||||
<div className={styles.mapContainer}>
|
<div className={styles.mapContainer}>
|
||||||
<RoadsMap />
|
<CustomMap mode='roads' />
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default function TrackDetails({track, isAuthor}) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{track?.processingStatus != null && (
|
{track?.processingStatus != null && track?.processingStatus != 'error' && (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header>Processing</List.Header>
|
<List.Header>Processing</List.Header>
|
||||||
{track.processingStatus}
|
{track.processingStatus}
|
||||||
|
|
|
@ -1,239 +1,85 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Vector as VectorSource} from 'ol/source'
|
import {Source, Layer} from 'react-map-gl'
|
||||||
import {LineString, Point} from 'ol/geom'
|
|
||||||
import Feature from 'ol/Feature'
|
|
||||||
import {fromLonLat} from 'ol/proj'
|
|
||||||
import {Fill, Stroke, Style, Text, Circle} from 'ol/style'
|
|
||||||
|
|
||||||
import {Map} from 'components'
|
import type {TrackData} from 'types'
|
||||||
import type {TrackData, TrackPoint} from 'types'
|
import {CustomMap} from '../MapPage'
|
||||||
|
|
||||||
const isValidTrackPoint = (point: TrackPoint): boolean => {
|
import {colorByDistance} from '../../mapstyles'
|
||||||
const longitude = point.geometry?.coordinates?.[0]
|
|
||||||
const latitude = point.geometry?.coordinates?.[1]
|
|
||||||
|
|
||||||
return latitude != null && longitude != null && (latitude !== 0 || longitude !== 0)
|
export default function TrackMap({
|
||||||
}
|
trackData,
|
||||||
|
showTrack,
|
||||||
const WARN_DISTANCE = 2
|
pointsMode = 'overtakingEvents',
|
||||||
const MIN_DISTANCE = 1.5
|
side = 'overtaker',
|
||||||
|
...props
|
||||||
const evaluateDistanceColor = function (distance: number) {
|
}: {
|
||||||
if (distance < MIN_DISTANCE) {
|
trackData: TrackData
|
||||||
return 'red'
|
showTrack: boolean
|
||||||
} else if (distance < WARN_DISTANCE) {
|
pointsMode: 'none' | 'overtakingEvents' | 'measurements'
|
||||||
return 'orange'
|
side: 'overtaker' | 'stationary'
|
||||||
} else {
|
}) {
|
||||||
return 'green'
|
if (!trackData) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const evaluateDistanceForFillColor = function (distance: number) {
|
|
||||||
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: number) {
|
|
||||||
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 createTextStyle = function (distance: number, resolution: number) {
|
|
||||||
return new Text({
|
|
||||||
textAlign: 'center',
|
|
||||||
textBaseline: 'middle',
|
|
||||||
font: 'normal 18px/1 Arial',
|
|
||||||
text: resolution < 6 ? '' + Number(distance).toFixed(2) : '',
|
|
||||||
fill: new Fill({color: evaluateDistanceColor(distance)}),
|
|
||||||
stroke: new Stroke({color: 'white', width: 2}),
|
|
||||||
offsetX: 0,
|
|
||||||
offsetY: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function PointLayer({features, title, visible, zIndex}) {
|
|
||||||
return (
|
|
||||||
<Map.VectorLayer {...{title, visible, zIndex}} style={pointStyleFunction} source={new VectorSource({features})} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackStroke = new Stroke({width: 4, color: 'rgb(30,144,255)'})
|
|
||||||
const trackLayerStyle = new Style({stroke: trackStroke})
|
|
||||||
|
|
||||||
function trackLayerStyleWithArrows(feature, resolution) {
|
|
||||||
const geometry = feature.getGeometry()
|
|
||||||
|
|
||||||
let styles = [trackLayerStyle]
|
|
||||||
|
|
||||||
// Numbers are in pixels
|
|
||||||
const arrowLength = 10 * resolution
|
|
||||||
const arrowSpacing = 200 * resolution
|
|
||||||
|
|
||||||
const a = arrowLength / Math.sqrt(2)
|
|
||||||
let spaceSinceLast = 0
|
|
||||||
|
|
||||||
geometry.forEachSegment(function (start, end) {
|
|
||||||
const dx = end[0] - start[0]
|
|
||||||
const dy = end[1] - start[1]
|
|
||||||
const d = Math.sqrt(dx * dx + dy * dy)
|
|
||||||
const rotation = Math.atan2(dy, dx)
|
|
||||||
spaceSinceLast += d
|
|
||||||
|
|
||||||
while (spaceSinceLast > arrowSpacing) {
|
|
||||||
spaceSinceLast -= arrowSpacing
|
|
||||||
|
|
||||||
let offsetAlongLine = (d - spaceSinceLast) / d
|
|
||||||
let pos = [start[0] + dx * offsetAlongLine, start[1] + dy * offsetAlongLine]
|
|
||||||
|
|
||||||
const lineStr1 = new LineString([pos, [pos[0] - a, pos[1] + a]])
|
|
||||||
lineStr1.rotate(rotation, pos)
|
|
||||||
const lineStr2 = new LineString([pos, [pos[0] - a, pos[1] - a]])
|
|
||||||
lineStr2.rotate(rotation, pos)
|
|
||||||
|
|
||||||
styles.push(
|
|
||||||
new Style({
|
|
||||||
geometry: lineStr1,
|
|
||||||
stroke: trackStroke,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
styles.push(
|
|
||||||
new Style({
|
|
||||||
geometry: lineStr2,
|
|
||||||
stroke: trackStroke,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return styles
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TrackMap({trackData, show, ...props}: {trackData: TrackData}) {
|
|
||||||
const {
|
|
||||||
trackVectorSource,
|
|
||||||
trackPointsD1,
|
|
||||||
trackPointsD2,
|
|
||||||
trackPointsUntaggedD1,
|
|
||||||
trackPointsUntaggedD2,
|
|
||||||
viewExtent,
|
|
||||||
} = React.useMemo(() => {
|
|
||||||
const trackPointsD1: Feature<Point>[] = []
|
|
||||||
const trackPointsD2: Feature<Point>[] = []
|
|
||||||
const trackPointsUntaggedD1: Feature<Point>[] = []
|
|
||||||
const trackPointsUntaggedD2: Feature<Point>[] = []
|
|
||||||
const filteredPoints: TrackPoint[] = trackData?.measurements?.features.filter(isValidTrackPoint) ?? []
|
|
||||||
|
|
||||||
for (const feature of filteredPoints) {
|
|
||||||
const {
|
|
||||||
geometry: {
|
|
||||||
coordinates: [latitude, longitude],
|
|
||||||
},
|
|
||||||
properties: {confirmed: flag, distanceOvertaker: d1, distanceStationary: d2},
|
|
||||||
} = feature
|
|
||||||
|
|
||||||
const p = fromLonLat([longitude, latitude])
|
|
||||||
|
|
||||||
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}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const points: Coordinate[] =
|
|
||||||
trackData?.track.geometry.coordinates.map(([latitude, longitude]) => {
|
|
||||||
return fromLonLat([longitude, latitude])
|
|
||||||
}) ?? []
|
|
||||||
|
|
||||||
//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?.measurements?.features])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Map {...props}>
|
<div style={props.style}>
|
||||||
<Map.BaseLayer zIndex={10} />
|
<CustomMap>
|
||||||
<Map.VectorLayer
|
{showTrack && (
|
||||||
visible
|
<Source id="route" type="geojson" data={trackData.track}>
|
||||||
updateWhileAnimating={false}
|
<Layer
|
||||||
updateWhileInteracting={false}
|
id="route"
|
||||||
source={trackVectorSource}
|
type="line"
|
||||||
style={trackLayerStyleWithArrows}
|
paint={{
|
||||||
zIndex={100}
|
'line-width': ['interpolate', ['linear'], ['zoom'], 14, 2, 17, 5],
|
||||||
/>
|
'line-color': '#F06292',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
)}
|
||||||
|
|
||||||
<Map.GroupLayer title="Tagged Points" visible>
|
{pointsMode !== 'none' && (
|
||||||
<PointLayer features={trackPointsD1} title="Left" visible={show.left} zIndex={101} />
|
<Source id="overtakingEvents" type="geojson" data={trackData[pointsMode]}>
|
||||||
<PointLayer features={trackPointsD2} title="Right" visible={show.right} zIndex={101} />
|
<Layer
|
||||||
</Map.GroupLayer>
|
id="overtakingEvents"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 5, 17, 10],
|
||||||
|
'circle-color': colorByDistance('distance_' + side),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Map.GroupLayer title="Untagged Points" fold="close" visible>
|
{[
|
||||||
<PointLayer
|
['distance_overtaker', 'right'],
|
||||||
features={trackPointsUntaggedD1}
|
['distance_stationary', 'left'],
|
||||||
title="Left Untagged"
|
].map(([p, a]) => (
|
||||||
visible={show.leftUnconfirmed}
|
<Layer
|
||||||
zIndex={101}
|
key={p}
|
||||||
/>
|
{...{
|
||||||
<PointLayer
|
id: p,
|
||||||
features={trackPointsUntaggedD2}
|
type: 'symbol',
|
||||||
title="Right Untagged"
|
minzoom: 15,
|
||||||
visible={show.rightUnconfirmed}
|
layout: {
|
||||||
zIndex={101}
|
'text-field': ['number-format', ['get', p], {'min-fraction-digits': 2, 'max-fraction-digits': 2}],
|
||||||
/>
|
'text-allow-overlap': true,
|
||||||
</Map.GroupLayer>
|
'text-font': ['Open Sans Bold', 'Arial Unicode MS Regular'],
|
||||||
|
'text-size': 12,
|
||||||
<Map.View />
|
'text-keep-upright': false,
|
||||||
<Map.FitView extent={viewExtent} />
|
'text-anchor': a,
|
||||||
</Map>
|
'text-radial-offset': 1,
|
||||||
|
'text-rotate': ['get', 'course'],
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-halo-color': 'rgba(255, 255, 255, 1)',
|
||||||
|
'text-halo-width': 1,
|
||||||
|
'text-opacity': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.3, 1],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Source>
|
||||||
|
)}
|
||||||
|
</CustomMap>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
13
frontend/src/pages/TrackPage/TrackPage.module.scss
Normal file
13
frontend/src/pages/TrackPage/TrackPage.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.stage {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details.details {
|
||||||
|
position: absolute;
|
||||||
|
width: 320px;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
max-height: calc(100% - 32px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {Table, Checkbox, Segment, Dimmer, Grid, Loader, Header, Message} from 'semantic-ui-react'
|
import {List, Dropdown, Checkbox, Segment, Dimmer, Grid, Loader, Header, Message, Container} from 'semantic-ui-react'
|
||||||
import {useParams, useHistory} from 'react-router-dom'
|
import {useParams, useHistory} from 'react-router-dom'
|
||||||
import {concat, combineLatest, of, from, Subject} from 'rxjs'
|
import {concat, combineLatest, of, from, Subject} from 'rxjs'
|
||||||
import {pluck, distinctUntilChanged, map, switchMap, startWith, catchError} from 'rxjs/operators'
|
import {pluck, distinctUntilChanged, map, switchMap, startWith, catchError} from 'rxjs/operators'
|
||||||
|
@ -16,12 +16,52 @@ import TrackComments from './TrackComments'
|
||||||
import TrackDetails from './TrackDetails'
|
import TrackDetails from './TrackDetails'
|
||||||
import TrackMap from './TrackMap'
|
import TrackMap from './TrackMap'
|
||||||
|
|
||||||
|
import styles from './TrackPage.module.scss'
|
||||||
|
|
||||||
function useTriggerSubject() {
|
function useTriggerSubject() {
|
||||||
const subject$ = React.useMemo(() => new Subject(), [])
|
const subject$ = React.useMemo(() => new Subject(), [])
|
||||||
const trigger = React.useCallback(() => subject$.next(null), [subject$])
|
const trigger = React.useCallback(() => subject$.next(null), [subject$])
|
||||||
return [trigger, subject$]
|
return [trigger, subject$]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TrackMapSettings({showTrack, setShowTrack, pointsMode, setPointsMode, side, setSide}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header as="h4">Map settings</Header>
|
||||||
|
<List>
|
||||||
|
<List.Item>
|
||||||
|
<Checkbox checked={showTrack} onChange={(e, d) => setShowTrack(d.checked)} /> Show track
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<List.Header>Points</List.Header>
|
||||||
|
<Dropdown
|
||||||
|
selection
|
||||||
|
value={pointsMode}
|
||||||
|
onChange={(e, d) => setPointsMode(d.value)}
|
||||||
|
options={[
|
||||||
|
{key: 'none', value: 'none', text: 'None'},
|
||||||
|
{key: 'overtakingEvents', value: 'overtakingEvents', text: 'Confirmed'},
|
||||||
|
{key: 'measurements', value: 'measurements', text: 'All measurements'},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<List.Header>Side (for color)</List.Header>
|
||||||
|
<Dropdown
|
||||||
|
selection
|
||||||
|
value={side}
|
||||||
|
onChange={(e, d) => setSide(d.value)}
|
||||||
|
options={[
|
||||||
|
{key: 'overtaker', value: 'overtaker', text: 'Overtaker (Left)'},
|
||||||
|
{key: 'stationary', value: 'stationary', text: 'Stationary (Right)'},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
@ -105,60 +145,47 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
||||||
[slug, reloadComments]
|
[slug, reloadComments]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onDownloadOriginal = React.useCallback(
|
const onDownloadOriginal = React.useCallback(() => {
|
||||||
() => {
|
api.downloadFile(`/tracks/${slug}/download/original.csv`)
|
||||||
api.downloadFile(`/tracks/${slug}/download/original.csv`)
|
}, [slug])
|
||||||
},
|
|
||||||
[slug]
|
|
||||||
)
|
|
||||||
|
|
||||||
const isAuthor = login?.username === data?.track?.author?.username
|
const isAuthor = login?.username === data?.track?.author?.username
|
||||||
|
|
||||||
const {track, trackData, comments} = data || {}
|
const {track, trackData, comments} = data || {}
|
||||||
|
|
||||||
console.log({track, trackData})
|
|
||||||
const loading = track == null || trackData === undefined
|
const loading = track == null || trackData === undefined
|
||||||
const processing = ['processing', 'queued', 'created'].includes(track?.processingStatus)
|
const processing = ['processing', 'queued', 'created'].includes(track?.processingStatus)
|
||||||
const error = track?.processingStatus === 'error'
|
const error = track?.processingStatus === 'error'
|
||||||
|
|
||||||
const [left, setLeft] = React.useState(true)
|
const [showTrack, setShowTrack] = React.useState(true)
|
||||||
const [right, setRight] = React.useState(false)
|
const [pointsMode, setPointsMode] = React.useState('overtakingEvents') // none|overtakingEvents|measurements
|
||||||
const [leftUnconfirmed, setLeftUnconfirmed] = React.useState(false)
|
const [side, setSide] = React.useState('overtaker') // overtaker|stationary
|
||||||
const [rightUnconfirmed, setRightUnconfirmed] = React.useState(false)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page
|
||||||
{processing && (
|
stage={
|
||||||
<Message warning>
|
<div className={styles.stage}>
|
||||||
<Message.Content>
|
<Loader active={loading} />
|
||||||
Track data is still being processed, please reload page in a while.
|
<Dimmer.Dimmable blurring dimmed={loading}>
|
||||||
</Message.Content>
|
<TrackMap {...{track, trackData, pointsMode, side, showTrack}} style={{height: '80vh'}} />
|
||||||
</Message>
|
</Dimmer.Dimmable>
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
<div className={styles.details}>
|
||||||
<Message error>
|
{processing && (
|
||||||
<Message.Content>
|
<Message warning>
|
||||||
The processing of this track failed, please ask your site
|
<Message.Content>Track data is still being processed, please reload page in a while.</Message.Content>
|
||||||
administrator for help in debugging the issue.
|
</Message>
|
||||||
</Message.Content>
|
)}
|
||||||
</Message>
|
|
||||||
)}
|
{error && (
|
||||||
|
<Message error>
|
||||||
|
<Message.Content>
|
||||||
|
The processing of this track failed, please ask your site administrator for help in debugging the
|
||||||
|
issue.
|
||||||
|
</Message.Content>
|
||||||
|
</Message>
|
||||||
|
)}
|
||||||
|
|
||||||
<Grid stackable>
|
|
||||||
<Grid.Row>
|
|
||||||
<Grid.Column width={12}>
|
|
||||||
<div style={{position: 'relative'}}>
|
|
||||||
<Loader active={loading} />
|
|
||||||
<Dimmer.Dimmable blurring dimmed={loading}>
|
|
||||||
<TrackMap
|
|
||||||
{...{track, trackData, show: {left, right, leftUnconfirmed, rightUnconfirmed}}}
|
|
||||||
style={{height: '60vh', minHeight: 400}}
|
|
||||||
/>
|
|
||||||
</Dimmer.Dimmable>
|
|
||||||
</div>
|
|
||||||
</Grid.Column>
|
|
||||||
<Grid.Column width={4}>
|
|
||||||
<Segment>
|
<Segment>
|
||||||
{track && (
|
{track && (
|
||||||
<>
|
<>
|
||||||
|
@ -168,58 +195,34 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Segment>
|
</Segment>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Grid stackable>
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column width={12}>
|
||||||
|
{track?.description && (
|
||||||
|
<Segment basic>
|
||||||
|
<Header as="h2" dividing>
|
||||||
|
Description
|
||||||
|
</Header>
|
||||||
|
<Markdown>{track.description}</Markdown>
|
||||||
|
</Segment>
|
||||||
|
)}
|
||||||
|
|
||||||
<Header as="h4">Map settings</Header>
|
<TrackComments
|
||||||
|
{...{hideLoader: loading, comments, login}}
|
||||||
<Table compact>
|
onSubmit={onSubmitComment}
|
||||||
<Table.Header>
|
onDelete={onDeleteComment}
|
||||||
<Table.Row>
|
/>
|
||||||
<Table.HeaderCell>Left</Table.HeaderCell>
|
</Grid.Column>
|
||||||
<Table.HeaderCell textAlign="center">Show distance of</Table.HeaderCell>
|
<Grid.Column width={4}>
|
||||||
<Table.HeaderCell textAlign="right">Right</Table.HeaderCell>
|
<TrackMapSettings {...{showTrack, setShowTrack, pointsMode, setPointsMode, side, setSide}} />
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
|
|
||||||
<Table.Body>
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell>
|
|
||||||
<Checkbox checked={left} onChange={(e, d) => setLeft(d.checked)} />{' '}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell textAlign="center">Events</Table.Cell>
|
|
||||||
<Table.Cell textAlign="right">
|
|
||||||
<Checkbox checked={right} onChange={(e, d) => setRight(d.checked)} />{' '}
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell>
|
|
||||||
<Checkbox checked={leftUnconfirmed} onChange={(e, d) => setLeftUnconfirmed(d.checked)} />{' '}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell textAlign="center">Other points</Table.Cell>
|
|
||||||
<Table.Cell textAlign="right">
|
|
||||||
<Checkbox checked={rightUnconfirmed} onChange={(e, d) => setRightUnconfirmed(d.checked)} />{' '}
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid.Row>
|
</Grid.Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{track?.description && (
|
|
||||||
<Segment basic>
|
|
||||||
<Header as="h2" dividing>
|
|
||||||
Description
|
|
||||||
</Header>
|
|
||||||
<Markdown>{track.description}</Markdown>
|
|
||||||
</Segment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TrackComments
|
|
||||||
{...{hideLoader: loading, comments, login}}
|
|
||||||
onSubmit={onSubmitComment}
|
|
||||||
onDelete={onDeleteComment}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* <pre>{JSON.stringify(data, null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(data, null, 2)}</pre> */}
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue