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",
|
||||
"maplibre-gl": "^2.0.0-pre.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"ol": "^6.5.0",
|
||||
"ol-mapbox-style": "^6.5.1",
|
||||
"pkce": "^1.0.0-beta2",
|
||||
"proj4": "^2.7.2",
|
||||
"react": "^17.0.2",
|
||||
|
@ -64,7 +62,6 @@
|
|||
"@craco/craco": "^6.1.2",
|
||||
"@semantic-ui-react/craco-less": "^1.2.1",
|
||||
"@types/lodash": "^4.14.169",
|
||||
"@types/ol": "^6.5.0",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"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 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hasStage {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ import {Container} from 'semantic-ui-react'
|
|||
|
||||
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 (
|
||||
<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>}
|
||||
</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 FormattedDate} from './FormattedDate'
|
||||
export {default as LoginButton} from './LoginButton'
|
||||
export {default as Map} from './Map'
|
||||
export {default as Page} from './Page'
|
||||
export {default as RoadsLayer} from './RoadsLayer'
|
||||
export {default as Stats} from './Stats'
|
||||
export {default as StripMarkdown} from './StripMarkdown'
|
||||
|
|
|
@ -3,138 +3,83 @@ import _ from 'lodash'
|
|||
import bright from './bright.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) {
|
||||
style.sources.obs = mapSource
|
||||
|
||||
// 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) {
|
||||
idx = style.layers.length
|
||||
}
|
||||
style.layers.splice(idx, 0, {
|
||||
"id": "obs",
|
||||
"type": "line",
|
||||
"source": "obs",
|
||||
"source-layer": "obs_roads",
|
||||
"layout": {
|
||||
"line-cap": "round",
|
||||
"line-join": "round"
|
||||
id: 'obs',
|
||||
type: 'line',
|
||||
source: 'obs',
|
||||
'source-layer': 'obs_roads',
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
"paint": {
|
||||
"line-width": [
|
||||
"interpolate",
|
||||
["exponential", 1.5],
|
||||
["zoom"],
|
||||
paint: {
|
||||
'line-width': [
|
||||
'interpolate',
|
||||
['exponential', 1.5],
|
||||
['zoom'],
|
||||
12,
|
||||
2,
|
||||
17,
|
||||
[
|
||||
"case",
|
||||
[
|
||||
"!",
|
||||
[
|
||||
"to-boolean",
|
||||
[
|
||||
"get",
|
||||
"distance_overtaker_mean"
|
||||
]
|
||||
]
|
||||
],
|
||||
2,
|
||||
6
|
||||
]
|
||||
['case', ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], 2, 6],
|
||||
],
|
||||
"line-color": [
|
||||
"case",
|
||||
[
|
||||
"!",
|
||||
[
|
||||
"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"],
|
||||
'line-color': colorByDistance(),
|
||||
'line-opacity': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
12,
|
||||
0,
|
||||
13,
|
||||
[
|
||||
"case",
|
||||
[
|
||||
"!",
|
||||
[
|
||||
"to-boolean",
|
||||
[
|
||||
"get",
|
||||
"distance_overtaker_mean"
|
||||
]
|
||||
]
|
||||
],
|
||||
0,
|
||||
1
|
||||
],
|
||||
['case', ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], 0, 1],
|
||||
14,
|
||||
[
|
||||
"case",
|
||||
[
|
||||
"!",
|
||||
[
|
||||
"to-boolean",
|
||||
[
|
||||
"get",
|
||||
"distance_overtaker_mean"
|
||||
]
|
||||
]
|
||||
],
|
||||
0,
|
||||
1
|
||||
],
|
||||
['case', ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], 0, 1],
|
||||
15,
|
||||
1
|
||||
1,
|
||||
],
|
||||
"line-offset": [
|
||||
"interpolate",
|
||||
["exponential", 1.5],
|
||||
["zoom"],
|
||||
'line-offset': [
|
||||
'interpolate',
|
||||
['exponential', 1.5],
|
||||
['zoom'],
|
||||
12,
|
||||
["get", "offset_direction"],
|
||||
['get', 'offset_direction'],
|
||||
19,
|
||||
[
|
||||
"*",
|
||||
["get", "offset_direction"],
|
||||
8
|
||||
]
|
||||
]
|
||||
['*', ['get', 'offset_direction'], 8],
|
||||
],
|
||||
},
|
||||
"minzoom": 12
|
||||
minzoom: 12,
|
||||
})
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
|
||||
export const basemap = bright
|
||||
export const obsRoads = (sourceUrl) => addRoadsStyle(_.cloneDeep(positron), sourceUrl)
|
||||
export const basemap = positron
|
||||
export const obsRoads = (sourceUrl) => addRoadsStyle(_.cloneDeep(basemap), sourceUrl)
|
||||
|
|
|
@ -9,11 +9,9 @@ import api from 'api'
|
|||
import {Stats, Page} from 'components'
|
||||
|
||||
import {TrackListItem} from './TracksPage'
|
||||
import {RoadsMap} from './MapPage'
|
||||
import {CustomMap} from './MapPage'
|
||||
import styles from './HomePage.module.scss'
|
||||
|
||||
import 'ol/ol.css'
|
||||
|
||||
function MostRecentTrack() {
|
||||
const track: Track | null = useObservable(
|
||||
() =>
|
||||
|
@ -49,7 +47,7 @@ export default function HomePage() {
|
|||
<Grid.Row>
|
||||
<Grid.Column width={10}>
|
||||
<div className={styles.welcomeMap}>
|
||||
<RoadsMap />
|
||||
<CustomMap mode='roads' />
|
||||
</div>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={6}>
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import React from 'react'
|
||||
// import {Grid, Loader, Header, Item} from 'semantic-ui-react'
|
||||
|
||||
// import api from 'api'
|
||||
import {Page} from 'components'
|
||||
import {useConfig, Config} from 'config'
|
||||
import ReactMapGl, {AttributionControl } from 'react-map-gl'
|
||||
|
||||
import styles from './MapPage.module.scss'
|
||||
|
||||
import 'ol/ol.css'
|
||||
import {obsRoads} from '../mapstyles'
|
||||
import ReactMapGl, {AttributionControl } from 'react-map-gl'
|
||||
import {obsRoads, basemap } from '../mapstyles'
|
||||
|
||||
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({
|
||||
longitude: 0,
|
||||
latitude: 0,
|
||||
|
@ -32,15 +36,17 @@ function RoadsMapInner({mapSource, config}: {mapSource: string ,config: Config})
|
|||
return (
|
||||
<ReactMapGl mapStyle={mapStyle} width="100%" height="100%" onViewportChange={setViewport} {...viewport}>
|
||||
<AttributionControl style={{right: 0, bottom: 0}} customAttribution={[
|
||||
'<a href="https://openstreetmap.org/copyright" target="_blank" rel="nofollow noopener">© OpenStreetMap contributors</a>',
|
||||
'<a href="https://openmaptiles.org/" target="_blank" rel="nofollow noopener">© OpenMapTiles</a>',
|
||||
'<a href="https://openbikesensor.org/" target="_blank" rel="nofollow noopener">© OpenBikeSensor</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 noreferrer">© OpenMapTiles</a>',
|
||||
'<a href="https://openbikesensor.org/" target="_blank" rel="nofollow noopener noreferrer">© OpenBikeSensor</a>',
|
||||
]} />
|
||||
|
||||
{children}
|
||||
</ReactMapGl>
|
||||
)
|
||||
}
|
||||
|
||||
export function RoadsMap(props) {
|
||||
export function CustomMap(props) {
|
||||
const config = useConfig() || {}
|
||||
if (!config) return null;
|
||||
const {obsMapSource: mapSource} = config
|
||||
|
@ -48,7 +54,7 @@ export function RoadsMap(props) {
|
|||
if (!mapSource) return null;
|
||||
|
||||
return (
|
||||
<RoadsMapInner {...{mapSource, config}} {...props} />
|
||||
<CustomMapInner {...{mapSource, config}} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -56,7 +62,7 @@ export default function MapPage() {
|
|||
return (
|
||||
<Page fullScreen>
|
||||
<div className={styles.mapContainer}>
|
||||
<RoadsMap />
|
||||
<CustomMap mode='roads' />
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
|
|
|
@ -73,7 +73,7 @@ export default function TrackDetails({track, isAuthor}) {
|
|||
)}
|
||||
|
||||
|
||||
{track?.processingStatus != null && (
|
||||
{track?.processingStatus != null && track?.processingStatus != 'error' && (
|
||||
<List.Item>
|
||||
<List.Header>Processing</List.Header>
|
||||
{track.processingStatus}
|
||||
|
|
|
@ -1,239 +1,85 @@
|
|||
import React from 'react'
|
||||
import {Vector as VectorSource} from 'ol/source'
|
||||
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 {Source, Layer} from 'react-map-gl'
|
||||
|
||||
import {Map} from 'components'
|
||||
import type {TrackData, TrackPoint} from 'types'
|
||||
import type {TrackData} from 'types'
|
||||
import {CustomMap} from '../MapPage'
|
||||
|
||||
const isValidTrackPoint = (point: TrackPoint): boolean => {
|
||||
const longitude = point.geometry?.coordinates?.[0]
|
||||
const latitude = point.geometry?.coordinates?.[1]
|
||||
import {colorByDistance} from '../../mapstyles'
|
||||
|
||||
return latitude != null && longitude != null && (latitude !== 0 || longitude !== 0)
|
||||
}
|
||||
|
||||
const WARN_DISTANCE = 2
|
||||
const MIN_DISTANCE = 1.5
|
||||
|
||||
const evaluateDistanceColor = function (distance: number) {
|
||||
if (distance < MIN_DISTANCE) {
|
||||
return 'red'
|
||||
} else if (distance < WARN_DISTANCE) {
|
||||
return 'orange'
|
||||
} else {
|
||||
return 'green'
|
||||
export default function TrackMap({
|
||||
trackData,
|
||||
showTrack,
|
||||
pointsMode = 'overtakingEvents',
|
||||
side = 'overtaker',
|
||||
...props
|
||||
}: {
|
||||
trackData: TrackData
|
||||
showTrack: boolean
|
||||
pointsMode: 'none' | 'overtakingEvents' | 'measurements'
|
||||
side: 'overtaker' | 'stationary'
|
||||
}) {
|
||||
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 (
|
||||
<Map {...props}>
|
||||
<Map.BaseLayer zIndex={10} />
|
||||
<Map.VectorLayer
|
||||
visible
|
||||
updateWhileAnimating={false}
|
||||
updateWhileInteracting={false}
|
||||
source={trackVectorSource}
|
||||
style={trackLayerStyleWithArrows}
|
||||
zIndex={100}
|
||||
/>
|
||||
<div style={props.style}>
|
||||
<CustomMap>
|
||||
{showTrack && (
|
||||
<Source id="route" type="geojson" data={trackData.track}>
|
||||
<Layer
|
||||
id="route"
|
||||
type="line"
|
||||
paint={{
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 14, 2, 17, 5],
|
||||
'line-color': '#F06292',
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
)}
|
||||
|
||||
<Map.GroupLayer title="Tagged Points" visible>
|
||||
<PointLayer features={trackPointsD1} title="Left" visible={show.left} zIndex={101} />
|
||||
<PointLayer features={trackPointsD2} title="Right" visible={show.right} zIndex={101} />
|
||||
</Map.GroupLayer>
|
||||
{pointsMode !== 'none' && (
|
||||
<Source id="overtakingEvents" type="geojson" data={trackData[pointsMode]}>
|
||||
<Layer
|
||||
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
|
||||
features={trackPointsUntaggedD1}
|
||||
title="Left Untagged"
|
||||
visible={show.leftUnconfirmed}
|
||||
zIndex={101}
|
||||
/>
|
||||
<PointLayer
|
||||
features={trackPointsUntaggedD2}
|
||||
title="Right Untagged"
|
||||
visible={show.rightUnconfirmed}
|
||||
zIndex={101}
|
||||
/>
|
||||
</Map.GroupLayer>
|
||||
|
||||
<Map.View />
|
||||
<Map.FitView extent={viewExtent} />
|
||||
</Map>
|
||||
{[
|
||||
['distance_overtaker', 'right'],
|
||||
['distance_stationary', 'left'],
|
||||
].map(([p, a]) => (
|
||||
<Layer
|
||||
key={p}
|
||||
{...{
|
||||
id: p,
|
||||
type: 'symbol',
|
||||
minzoom: 15,
|
||||
layout: {
|
||||
'text-field': ['number-format', ['get', p], {'min-fraction-digits': 2, 'max-fraction-digits': 2}],
|
||||
'text-allow-overlap': true,
|
||||
'text-font': ['Open Sans Bold', 'Arial Unicode MS Regular'],
|
||||
'text-size': 12,
|
||||
'text-keep-upright': false,
|
||||
'text-anchor': a,
|
||||
'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 {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 {concat, combineLatest, of, from, Subject} from 'rxjs'
|
||||
import {pluck, distinctUntilChanged, map, switchMap, startWith, catchError} from 'rxjs/operators'
|
||||
|
@ -16,12 +16,52 @@ import TrackComments from './TrackComments'
|
|||
import TrackDetails from './TrackDetails'
|
||||
import TrackMap from './TrackMap'
|
||||
|
||||
import styles from './TrackPage.module.scss'
|
||||
|
||||
function useTriggerSubject() {
|
||||
const subject$ = React.useMemo(() => new Subject(), [])
|
||||
const trigger = React.useCallback(() => subject$.next(null), [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 {slug} = useParams()
|
||||
|
||||
|
@ -105,60 +145,47 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
|||
[slug, reloadComments]
|
||||
)
|
||||
|
||||
const onDownloadOriginal = React.useCallback(
|
||||
() => {
|
||||
api.downloadFile(`/tracks/${slug}/download/original.csv`)
|
||||
},
|
||||
[slug]
|
||||
)
|
||||
const onDownloadOriginal = React.useCallback(() => {
|
||||
api.downloadFile(`/tracks/${slug}/download/original.csv`)
|
||||
}, [slug])
|
||||
|
||||
const isAuthor = login?.username === data?.track?.author?.username
|
||||
|
||||
const {track, trackData, comments} = data || {}
|
||||
|
||||
console.log({track, trackData})
|
||||
const loading = track == null || trackData === undefined
|
||||
const processing = ['processing', 'queued', 'created'].includes(track?.processingStatus)
|
||||
const error = track?.processingStatus === 'error'
|
||||
|
||||
const [left, setLeft] = React.useState(true)
|
||||
const [right, setRight] = React.useState(false)
|
||||
const [leftUnconfirmed, setLeftUnconfirmed] = React.useState(false)
|
||||
const [rightUnconfirmed, setRightUnconfirmed] = React.useState(false)
|
||||
const [showTrack, setShowTrack] = React.useState(true)
|
||||
const [pointsMode, setPointsMode] = React.useState('overtakingEvents') // none|overtakingEvents|measurements
|
||||
const [side, setSide] = React.useState('overtaker') // overtaker|stationary
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{processing && (
|
||||
<Message warning>
|
||||
<Message.Content>
|
||||
Track data is still being processed, please reload page in a while.
|
||||
</Message.Content>
|
||||
</Message>
|
||||
)}
|
||||
<Page
|
||||
stage={
|
||||
<div className={styles.stage}>
|
||||
<Loader active={loading} />
|
||||
<Dimmer.Dimmable blurring dimmed={loading}>
|
||||
<TrackMap {...{track, trackData, pointsMode, side, showTrack}} style={{height: '80vh'}} />
|
||||
</Dimmer.Dimmable>
|
||||
|
||||
{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>
|
||||
)}
|
||||
<div className={styles.details}>
|
||||
{processing && (
|
||||
<Message warning>
|
||||
<Message.Content>Track data is still being processed, please reload page in a while.</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>
|
||||
{track && (
|
||||
<>
|
||||
|
@ -168,58 +195,34 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
|||
</>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<Table compact>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Left</Table.HeaderCell>
|
||||
<Table.HeaderCell textAlign="center">Show distance of</Table.HeaderCell>
|
||||
<Table.HeaderCell textAlign="right">Right</Table.HeaderCell>
|
||||
</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>
|
||||
<TrackComments
|
||||
{...{hideLoader: loading, comments, login}}
|
||||
onSubmit={onSubmitComment}
|
||||
onDelete={onDeleteComment}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={4}>
|
||||
<TrackMapSettings {...{showTrack, setShowTrack, pointsMode, setPointsMode, side, setSide}} />
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</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> */}
|
||||
</Page>
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue