diff --git a/frontend/package.json b/frontend/package.json
index 2c52da7..575510a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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"
diff --git a/frontend/src/components/Map/index.js b/frontend/src/components/Map/index.js
deleted file mode 100644
index 1255642..0000000
--- a/frontend/src/components/Map/index.js
+++ /dev/null
@@ -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 (
- <>
-
- {map && (
-
- {children}
-
- )}
-
- >
- )
-}
-
-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 {children}
- } else {
- return null
- }
-}
-
-export function TileLayer({osm, ...props}) {
- return ({source: new OSM(osm)})} {...props} />
-}
-
-export function BaseLayer(props) {
- const config = useConfig()
- if (!config) {
- return null
- }
-
- return (
-
- )
-}
-
-export function VectorLayer(props) {
- return
-}
-
-export function GroupLayer(props) {
- return
-}
-
-function FitView({extent}) {
- const map = React.useContext(MapContext)
-
- React.useEffect(() => {
- if (extent && map) {
- map.getView().fit(extent)
- }
- }, [extent, map])
-
- return null
-}
-
-function View({...options}) {
- const map = React.useContext(MapContext)
- const 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
diff --git a/frontend/src/components/Page/Page.module.scss b/frontend/src/components/Page/Page.module.scss
index ceb337d..06304b0 100644
--- a/frontend/src/components/Page/Page.module.scss
+++ b/frontend/src/components/Page/Page.module.scss
@@ -14,3 +14,7 @@
.fullScreen {
margin: 0;
}
+
+.hasStage {
+ margin-top: 0;
+}
diff --git a/frontend/src/components/Page/index.tsx b/frontend/src/components/Page/index.tsx
index 407c81c..e6c4c4a 100644
--- a/frontend/src/components/Page/index.tsx
+++ b/frontend/src/components/Page/index.tsx
@@ -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 (
-
+
+ {stage}
{fullScreen ? children : {children} }
)
diff --git a/frontend/src/components/RoadsLayer.tsx b/frontend/src/components/RoadsLayer.tsx
deleted file mode 100644
index f7a757e..0000000
--- a/frontend/src/components/RoadsLayer.tsx
+++ /dev/null
@@ -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 += ' 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 = ''
-//
-// s +=
-// 'Straßenname: ' +
-// feature.get('name') +
-// ' '
-// d = feature.get('distance_overtaker_limit')
-// s += 'Mindestüberholabstand: ' + (d == null ? 'n/a' : d.toFixed(2)) + ' m '
-//
-// d = feature.get('distance_overtaker_n')
-// s += 'Anzahl Messungen: ' + (d == null ? 'n/a' : d.toFixed(0)) + ' '
-//
-// 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 +=
-// 'Unterschreitung Mindestabstand: ' +
-// (p == null ? 'n/a' : p.toFixed(1)) +
-// '% der Überholenden '
-//
-// d = feature.get('distance_overtaker_mean')
-// s += 'Durchschnitt Überholabstand: ' + (d == null ? 'n/a' : d.toFixed(2)) + ' m '
-//
-// d = feature.get('distance_overtaker_median')
-// s += 'Median Überholabstand: ' + (d == null ? 'n/a' : d.toFixed(2)) + ' m '
-//
-// d = feature.get('distance_overtaker_minimum')
-// s += 'Minimum Überholabstand: ' + (d == null ? 'n/a' : d.toFixed(2)) + ' m '
-//
-// s += '
'
-//
-// 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
-}
-
-// 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], '%')
diff --git a/frontend/src/components/index.js b/frontend/src/components/index.js
index 741b859..16bdb66 100644
--- a/frontend/src/components/index.js
+++ b/frontend/src/components/index.js
@@ -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'
diff --git a/frontend/src/mapstyles/index.js b/frontend/src/mapstyles/index.js
index 648851d..f1347b5 100644
--- a/frontend/src/mapstyles/index.js
+++ b/frontend/src/mapstyles/index.js
@@ -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)
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx
index f17239c..65262e2 100644
--- a/frontend/src/pages/HomePage.tsx
+++ b/frontend/src/pages/HomePage.tsx
@@ -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() {
-
+
diff --git a/frontend/src/pages/MapPage.tsx b/frontend/src/pages/MapPage.tsx
index 2459ed0..5fe1b20 100644
--- a/frontend/src/pages/MapPage.tsx
+++ b/frontend/src/pages/MapPage.tsx
@@ -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 (
© OpenStreetMap contributors',
- '© OpenMapTiles ',
- '© OpenBikeSensor ',
+ '© OpenStreetMap contributors ',
+ '© OpenMapTiles ',
+ '© OpenBikeSensor ',
]} />
+
+ {children}
)
}
-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 (
-
+
)
}
@@ -56,7 +62,7 @@ export default function MapPage() {
return (
-
+
)
diff --git a/frontend/src/pages/TrackPage/TrackDetails.tsx b/frontend/src/pages/TrackPage/TrackDetails.tsx
index 04e1455..778f07c 100644
--- a/frontend/src/pages/TrackPage/TrackDetails.tsx
+++ b/frontend/src/pages/TrackPage/TrackDetails.tsx
@@ -73,7 +73,7 @@ export default function TrackDetails({track, isAuthor}) {
)}
- {track?.processingStatus != null && (
+ {track?.processingStatus != null && track?.processingStatus != 'error' && (
Processing
{track.processingStatus}
diff --git a/frontend/src/pages/TrackPage/TrackMap.tsx b/frontend/src/pages/TrackPage/TrackMap.tsx
index 42ecca0..d06020f 100644
--- a/frontend/src/pages/TrackPage/TrackMap.tsx
+++ b/frontend/src/pages/TrackPage/TrackMap.tsx
@@ -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 (
-
- )
-}
-
-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[] = []
- const trackPointsD2: Feature[] = []
- const trackPointsUntaggedD1: Feature[] = []
- const trackPointsUntaggedD2: Feature[] = []
- 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 (
-
-
-
+
+
+ {showTrack && (
+
+
+
+ )}
-
-
-
-
+ {pointsMode !== 'none' && (
+
+
-
-
-
-
-
-
-
-
+ {[
+ ['distance_overtaker', 'right'],
+ ['distance_stationary', 'left'],
+ ].map(([p, a]) => (
+
+ ))}
+
+ )}
+
+
)
}
diff --git a/frontend/src/pages/TrackPage/TrackPage.module.scss b/frontend/src/pages/TrackPage/TrackPage.module.scss
new file mode 100644
index 0000000..927871a
--- /dev/null
+++ b/frontend/src/pages/TrackPage/TrackPage.module.scss
@@ -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;
+}
diff --git a/frontend/src/pages/TrackPage/index.tsx b/frontend/src/pages/TrackPage/index.tsx
index 6a8fcc9..99f2313 100644
--- a/frontend/src/pages/TrackPage/index.tsx
+++ b/frontend/src/pages/TrackPage/index.tsx
@@ -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 (
+ <>
+
+
+
+ setShowTrack(d.checked)} /> Show track
+
+
+ Points
+ setPointsMode(d.value)}
+ options={[
+ {key: 'none', value: 'none', text: 'None'},
+ {key: 'overtakingEvents', value: 'overtakingEvents', text: 'Confirmed'},
+ {key: 'measurements', value: 'measurements', text: 'All measurements'},
+ ]}
+ />
+
+
+ Side (for color)
+ setSide(d.value)}
+ options={[
+ {key: 'overtaker', value: 'overtaker', text: 'Overtaker (Left)'},
+ {key: 'stationary', value: 'stationary', text: 'Stationary (Right)'},
+ ]}
+ />
+
+
+ >
+ )
+}
+
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 (
-
- {processing && (
-
-
- Track data is still being processed, please reload page in a while.
-
-
- )}
+
+
+
+
+
- {error && (
-
-
- The processing of this track failed, please ask your site
- administrator for help in debugging the issue.
-
-
- )}
+
+ {processing && (
+
+ Track data is still being processed, please reload page in a while.
+
+ )}
+
+ {error && (
+
+
+ The processing of this track failed, please ask your site administrator for help in debugging the
+ issue.
+
+
+ )}
-
-
-
-
-
-
-
-
-
-
-
{track && (
<>
@@ -168,58 +195,34 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
>
)}
+
+
+ }
+ >
+
+
+
+ {track?.description && (
+
+
+ {track.description}
+
+ )}
-
-
-
-
-
- Left
- Show distance of
- Right
-
-
-
-
-
-
- setLeft(d.checked)} />{' '}
-
- Events
-
- setRight(d.checked)} />{' '}
-
-
-
-
- setLeftUnconfirmed(d.checked)} />{' '}
-
- Other points
-
- setRightUnconfirmed(d.checked)} />{' '}
-
-
-
-
+
+
+
+
- {track?.description && (
-
-
- {track.description}
-
- )}
-
-
-
{/* {JSON.stringify(data, null, 2)} */}
)