diff --git a/frontend/src/components/Map/index.js b/frontend/src/components/Map/index.js index ac4822f..f6b7ee8 100644 --- a/frontend/src/components/Map/index.js +++ b/frontend/src/components/Map/index.js @@ -52,7 +52,7 @@ export function Map({children, ...props}) { } export function Layer({layerClass, getDefaultOptions, children, ...props}) { - const context = React.useContext(MapLayerContext) + const context = React.useContext(MapContext) const layer = React.useMemo( () => @@ -64,13 +64,11 @@ export function Layer({layerClass, getDefaultOptions, children, ...props}) { [] ) - for (const [k, v] of Object.entries(props)) { - layer.set(k, v) - } + layer.setProperties(props) React.useEffect(() => { - context?.push(layer) - return () => context?.remove(layer) + context?.addLayer(layer) + return () => context?.removeLayer(layer) }, [layer, context]) if (typeof layer.getLayers === 'function') { @@ -80,9 +78,8 @@ export function Layer({layerClass, getDefaultOptions, children, ...props}) { } } -export function TileLayer(props) { - return ({source: new OSM()})} {...props} /> -} +export function TileLayer({osm, ...props}) { + return ({source: new OSM(osm)})} {...props} /> } export function VectorLayer(props) { return diff --git a/frontend/src/components/RoadsLayer.tsx b/frontend/src/components/RoadsLayer.tsx new file mode 100644 index 0000000..9e23dc5 --- /dev/null +++ b/frontend/src/components/RoadsLayer.tsx @@ -0,0 +1,386 @@ +import React from 'react' +import {useObservable} from 'rxjs-hooks' +import {of} from 'rxjs' +import {switchMap} from 'rxjs/operators' +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 += +// '' +// d = feature.get('distance_overtaker_limit') +// s += '' +// +// d = feature.get('distance_overtaker_n') +// s += '' +// +// 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 += +// '' +// +// d = feature.get('distance_overtaker_mean') +// s += '' +// +// d = feature.get('distance_overtaker_median') +// s += '' +// +// d = feature.get('distance_overtaker_minimum') +// s += '' +// +// s += '
Straßenname:' + +// feature.get('name') + +// '
Mindestüberholabstand:' + (d == null ? 'n/a' : d.toFixed(2)) + ' m
Anzahl Messungen:' + (d == null ? 'n/a' : d.toFixed(0)) + '
Unterschreitung Mindestabstand:' + +// (p == null ? 'n/a' : p.toFixed(1)) + +// '% der Überholenden
Durchschnitt Überholabstand:' + (d == null ? 'n/a' : d.toFixed(2)) + ' m
Median Überholabstand:' + (d == null ? 'n/a' : d.toFixed(2)) + ' m
Minimum Überholabstand:' + (d == null ? 'n/a' : d.toFixed(2)) + ' m
' +// +// 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 638194f..3beaec7 100644 --- a/frontend/src/components/index.js +++ b/frontend/src/components/index.js @@ -4,4 +4,5 @@ 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 StripMarkdown} from './StripMarkdown' diff --git a/frontend/src/pages/HomePage.js b/frontend/src/pages/HomePage.js index 1d7a41e..7a03cf6 100644 --- a/frontend/src/pages/HomePage.js +++ b/frontend/src/pages/HomePage.js @@ -8,7 +8,7 @@ import {fromLonLat} from 'ol/proj' import {Duration} from 'luxon' import api from '../api' -import {Map, Page} from '../components' +import {Map, Page, RoadsLayer} from '../components' import {TrackListItem} from './TracksPage' import styles from './HomePage.module.scss' @@ -22,18 +22,21 @@ function formatDuration(seconds) { function WelcomeMap() { return ( - - + + + {/* */} + ) } function Stats() { - const stats = useObservable( - () => of(null).pipe( - switchMap(() => api.fetch('/stats')) - ), - ) + const stats = useObservable(() => of(null).pipe(switchMap(() => api.fetch('/stats')))) return ( <> @@ -42,7 +45,7 @@ function Stats() { - + {Number(stats?.publicTrackLength / 1000).toFixed(1)} km track length @@ -106,7 +109,6 @@ export default function HomePage() { - ) diff --git a/frontend/src/pages/TrackPage/TrackMap.tsx b/frontend/src/pages/TrackPage/TrackMap.tsx index 00b8175..1c8bf93 100644 --- a/frontend/src/pages/TrackPage/TrackMap.tsx +++ b/frontend/src/pages/TrackPage/TrackMap.tsx @@ -157,7 +157,12 @@ export default function TrackMap({trackData, show, ...props}: {trackData: TrackD return ( - + = x[n - 1]) { + y = palette[x[n - 1]] + } else { + let ia = 0 + let ib = n - 1 + + while (ib - ia > 1) { + const ic = Math.round(0.5 * (ia + ib)) + if (d < x[ic]) { + ib = ic + } else { + ia = ic + } + } + + const xa = x[ia] + const xb = x[ib] + const w = (d - xa) / (xb - xa) + y = Array(4) + const ya = palette[xa] + const yb = palette[xb] + for (let i = 0; i < 4; i++) { + y[i] = Math.round(ya[i] * (1 - w) + yb[i] * w) + } + } + return y + } + + resamplePalette(palette, n) { + const x = Object.keys(palette) + + for (let i = 0; i < x.length; i++) { + x[i] = parseFloat(x[i]) + } + + const a = Math.min(...x) + const b = Math.max(...x) + + const p = new Array(n) + + for (let i = 0; i < n; i++) { + const xi = a + (parseFloat(i) / (n - 1)) * (b - a) + p[i] = this.samplePalette(palette, xi) + } + + this.a = a + this.b = b + this.rgba_sampled = p + this.n = n + } +} + +export const paletteUrban = new Palette( + { + 0.0: [64, 0, 0, 255], + 1.4999: [196, 0, 0, 255], + 1.5: [196, 196, 0, 255], + 2.0: [0, 196, 0, 255], + 2.55: [0, 255, 0, 255], + }, + [0, 0, 196, 255] +) + +export const paletteRural = new Palette( + { + 0.0: [64, 0, 0, 255], + 1.9999: [196, 0, 0, 255], + 2.0: [196, 196, 0, 255], + 2.5: [0, 196, 0, 255], + 2.55: [0, 255, 0, 255], + }, + [0, 0, 196, 255] +) + +export const paletteRural_ryg = new Palette( + { + 0.0: [196, 0, 0, 255], + 1.5: [196, 196, 0, 255], + 2.0: [0, 196, 0, 255], + }, + [0, 0, 196, 255] +) + +export const paletteUrban_ryg = new Palette( + { + 0.0: [196, 0, 0, 255], + 2.0: [196, 196, 0, 255], + 2.5: [0, 196, 0, 255], + }, + [0, 0, 196, 255] +) + +export const colorUndefinedDistance = [0, 0, 0, 0] + +export const palettePercentage = new Palette( + { + 0.0: [64, 0, 0, 255], + 25.0: [196, 0, 0, 255], + 90.0: [196, 196, 0, 255], + 100.0: [0, 255, 0, 255], + }, + [0, 0, 196, 255] +) + +export const palettePercentageInverted = new Palette( + { + 0.0: [0, 255, 0, 255], + 10.0: [196, 196, 0, 255], + 75.0: [196, 0, 0, 255], + 100.0: [64, 0, 0, 255], + }, + [0, 0, 196, 255] +)