Translate MapPage

This commit is contained in:
Paul Bienkowski 2022-07-24 18:43:26 +02:00
parent a977e2d1c3
commit 248f8b4a6f
4 changed files with 359 additions and 148 deletions

View file

@ -1,53 +1,66 @@
import React from 'react' import React from "react";
import _ from 'lodash' import _ from "lodash";
import {connect} from 'react-redux' import { connect } from "react-redux";
import {List, Select, Input, Divider, Label, Checkbox, Header} from 'semantic-ui-react' import {
List,
Select,
Input,
Divider,
Label,
Checkbox,
Header,
} from "semantic-ui-react";
import { useTranslation } from "react-i18next";
import { import {
MapConfig, MapConfig,
setMapConfigFlag as setMapConfigFlagAction, setMapConfigFlag as setMapConfigFlagAction,
initialState as defaultMapConfig, initialState as defaultMapConfig,
} from 'reducers/mapConfig' } from "reducers/mapConfig";
import {colorByDistance, colorByCount, viridisSimpleHtml} from 'mapstyles' import { colorByDistance, colorByCount, viridisSimpleHtml } from "mapstyles";
import {ColorMapLegend, DiscreteColorMapLegend} from 'components' import { ColorMapLegend, DiscreteColorMapLegend } from "components";
const BASEMAP_STYLE_OPTIONS = [ const BASEMAP_STYLE_OPTIONS = ["positron", "bright"];
{value: 'positron', key: 'positron', text: 'Positron'},
{value: 'bright', key: 'bright', text: 'OSM Bright'},
]
const ROAD_ATTRIBUTE_OPTIONS = [ const ROAD_ATTRIBUTE_OPTIONS = [
{value: 'distance_overtaker_mean', key: 'distance_overtaker_mean', text: 'Overtaker distance mean'}, "distance_overtaker_mean",
{value: 'distance_overtaker_min', key: 'distance_overtaker_min', text: 'Overtaker distance minimum'}, "distance_overtaker_min",
{value: 'distance_overtaker_max', key: 'distance_overtaker_max', text: 'Overtaker distance maximum'}, "distance_overtaker_max",
{value: 'distance_overtaker_median', key: 'distance_overtaker_median', text: 'Overtaker distance median'}, "distance_overtaker_median",
{value: 'overtaking_event_count', key: 'overtaking_event_count', text: 'Event count'}, "overtaking_event_count",
{value: 'usage_count', key: 'usage_count', text: 'Usage count'}, "usage_count",
{value: 'zone', key: 'zone', text: 'Overtaking distance zone'} "zone",
] ];
function LayerSidebar({ function LayerSidebar({
mapConfig, mapConfig,
setMapConfigFlag, setMapConfigFlag,
}: { }: {
mapConfig: MapConfig mapConfig: MapConfig;
setMapConfigFlag: (flag: string, value: unknown) => void setMapConfigFlag: (flag: string, value: unknown) => void;
}) { }) {
const { t } = useTranslation();
const { const {
baseMap: {style}, baseMap: { style },
obsRoads: {show: showRoads, showUntagged, attribute, maxCount}, obsRoads: { show: showRoads, showUntagged, attribute, maxCount },
obsEvents: {show: showEvents}, obsEvents: { show: showEvents },
} = mapConfig } = mapConfig;
return ( return (
<div> <div>
<List relaxed> <List relaxed>
<List.Item> <List.Item>
<List.Header>Basemap Style</List.Header> <List.Header>{t("MapPage.sidebar.baseMap.style.label")}</List.Header>
<Select <Select
options={BASEMAP_STYLE_OPTIONS} options={BASEMAP_STYLE_OPTIONS.map((value) => ({
value,
key: value,
text: t(`MapPage.sidebar.baseMap.style.${value}`),
}))}
value={style} value={style}
onChange={(_e, {value}) => setMapConfigFlag('baseMap.style', value)} onChange={(_e, { value }) =>
setMapConfigFlag("baseMap.style", value)
}
/> />
</List.Item> </List.Item>
<Divider /> <Divider />
@ -56,12 +69,12 @@ function LayerSidebar({
toggle toggle
size="small" size="small"
id="obsRoads.show" id="obsRoads.show"
style={{float: 'right'}} style={{ float: "right" }}
checked={showRoads} checked={showRoads}
onChange={() => setMapConfigFlag('obsRoads.show', !showRoads)} onChange={() => setMapConfigFlag("obsRoads.show", !showRoads)}
/> />
<label htmlFor="obsRoads.show"> <label htmlFor="obsRoads.show">
<Header as="h4">Road segments</Header> <Header as="h4">{t("MapPage.sidebar.obsRoads.title")}</Header>
</label> </label>
</List.Item> </List.Item>
{showRoads && ( {showRoads && (
@ -69,49 +82,80 @@ function LayerSidebar({
<List.Item> <List.Item>
<Checkbox <Checkbox
checked={showUntagged} checked={showUntagged}
onChange={() => setMapConfigFlag('obsRoads.showUntagged', !showUntagged)} onChange={() =>
label="Include roads without data" setMapConfigFlag("obsRoads.showUntagged", !showUntagged)
}
label={t("MapPage.sidebar.obsRoads.showUntagged.label")}
/> />
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Header>Color based on</List.Header> <List.Header>
{t("MapPage.sidebar.obsRoads.attribute.label")}
</List.Header>
<Select <Select
fluid fluid
options={ROAD_ATTRIBUTE_OPTIONS} options={ROAD_ATTRIBUTE_OPTIONS.map((value) => ({
value,
key: value,
text: t(`MapPage.sidebar.obsRoads.attribute.${value}`),
}))}
value={attribute} value={attribute}
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.attribute', value)} onChange={(_e, { value }) =>
setMapConfigFlag("obsRoads.attribute", value)
}
/> />
</List.Item> </List.Item>
{attribute.endsWith('_count') ? ( {attribute.endsWith("_count") ? (
<> <>
<List.Item>
<List.Header>
{t("MapPage.sidebar.obsRoads.maxCount.label")}
</List.Header>
<Input
fluid
type="number"
value={maxCount}
onChange={(_e, { value }) =>
setMapConfigFlag("obsRoads.maxCount", value)
}
/>
</List.Item>
<List.Item>
<ColorMapLegend
map={_.chunk(
colorByCount(
"obsRoads.maxCount",
mapConfig.obsRoads.maxCount,
viridisSimpleHtml
).slice(3),
2
)}
twoTicks
/>
</List.Item>
</>
) : (
<List.Item> <List.Item>
<List.Header>Maximum value</List.Header> <DiscreteColorMapLegend
<Input map={colorByDistance("distance_overtaker")[3].slice(2)}
fluid
type="number"
value={maxCount}
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.maxCount', value)}
/> />
</List.Item> </List.Item>
<List.Item>
<ColorMapLegend map={_.chunk(colorByCount('obsRoads.maxCount', mapConfig.obsRoads.maxCount, viridisSimpleHtml ).slice(3), 2)} twoTicks />
</List.Item></>
) : ) :
attribute.endsWith('zone') ? ( attribute.endsWith('zone') ? (
<> <>
<List.Item> <List.Item>
<Label size="small" style={{background: "blue",color:"white"}}>urban (1.5&nbsp;m)</Label> <Label size="small" style={{background: "blue",color:"white"}}>{t("general.urban")} (1.5&nbsp;m)</Label>
<Label size="small" style={{background: "cyan", color:"black"}}>rural (2&nbsp;m)</Label> <Label size="small" style={{background: "cyan", color:"black"}}>{t("general.rural")}(2&nbsp;m)</Label>
</List.Item></> </List.Item></>
) : ) :
( (
<> <>
<List.Item> <List.Item>
<List.Header>Urban</List.Header> <List.Header>{_.startCase(t("general.urban"))}</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} /> <DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Header>Rural</List.Header> <List.Header>{_.startCase(t("general.rural"))}</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} /> <DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
</List.Item> </List.Item>
</> </>
@ -124,29 +168,29 @@ function LayerSidebar({
toggle toggle
size="small" size="small"
id="obsEvents.show" id="obsEvents.show"
style={{float: 'right'}} style={{ float: "right" }}
checked={showEvents} checked={showEvents}
onChange={() => setMapConfigFlag('obsEvents.show', !showEvents)} onChange={() => setMapConfigFlag("obsEvents.show", !showEvents)}
/> />
<label htmlFor="obsEvents.show"> <label htmlFor="obsEvents.show">
<Header as="h4">Event points</Header> <Header as="h4">{t("MapPage.sidebar.obsEvents.title")}</Header>
</label> </label>
</List.Item> </List.Item>
{showEvents && ( {showEvents && (
<> <>
<List.Item> <List.Item>
<List.Header>Urban</List.Header> <List.Header>{_.startCase(t('general.urban'))}</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} /> <DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Header>Rural</List.Header> <List.Header>{_.startCase(t('general.rural'))}</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} /> <DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
</List.Item> </List.Item>
</> </>
)} )}
</List> </List>
</div> </div>
) );
} }
export default connect( export default connect(
@ -158,6 +202,6 @@ export default connect(
// //
), ),
}), }),
{setMapConfigFlag: setMapConfigFlagAction} { setMapConfigFlag: setMapConfigFlagAction }
// //
)(LayerSidebar) )(LayerSidebar);

View file

@ -1,83 +1,95 @@
import React, {useState, useCallback} from 'react' import React, { useState, useCallback } from "react";
import _ from 'lodash' import _ from "lodash";
import {Segment, Menu, Header, Label, Icon, Table} from 'semantic-ui-react' import { Segment, Menu, Header, Label, Icon, Table } from "semantic-ui-react";
import {Layer, Source} from 'react-map-gl' import { Layer, Source } from "react-map-gl";
import {of, from, concat} from 'rxjs' import { of, from, concat } from "rxjs";
import {useObservable} from 'rxjs-hooks' import { useObservable } from "rxjs-hooks";
import {switchMap, distinctUntilChanged} from 'rxjs/operators' import { switchMap, distinctUntilChanged } from "rxjs/operators";
import {Chart} from 'components' import { Chart } from "components";
import {pairwise} from 'utils' import { pairwise } from "utils";
import { useTranslation } from "react-i18next";
import api from 'api' import api from 'api'
import {colorByDistance, borderByZone} from 'mapstyles' import {colorByDistance, borderByZone} from 'mapstyles'
import styles from './styles.module.less' import styles from "./styles.module.less";
function selectFromColorMap(colormap, value) { function selectFromColorMap(colormap, value) {
let last = null let last = null;
for (let i = 0; i < colormap.length; i += 2) { for (let i = 0; i < colormap.length; i += 2) {
if (colormap[i + 1] > value) { if (colormap[i + 1] > value) {
return colormap[i] return colormap[i];
} }
} }
return colormap[colormap.length - 1] return colormap[colormap.length - 1];
} }
const UNITS = {distanceOvertaker: 'm', distanceStationary: 'm', speed: 'km/h'} const UNITS = {
const LABELS = { distanceOvertaker: "m",
distanceOvertaker: 'Left', distanceStationary: "m",
distanceStationary: 'Right', speed: "km/h",
speed: 'Speed', };
count: 'No. of Measurements', const ZONE_COLORS = { urban: "blue", rural: "cyan", motorway: "purple" };
min: 'Minimum', const CARDINAL_DIRECTIONS = [
median: 'Median', "north",
max: 'Maximum', "northEast",
mean: 'Average', "east",
} "southEast",
const ZONE_COLORS = {urban: 'blue', rural: 'cyan', motorway: 'purple'} "south",
const CARDINAL_DIRECTIONS = ['north', 'north-east', 'east', 'south-east', 'south', 'south-west', 'west', 'north-west'] "southWest",
const getCardinalDirection = (bearing) => "west",
bearing == null "northWest",
? 'unknown' ];
: CARDINAL_DIRECTIONS[ const getCardinalDirection = (t, bearing) => {
Math.floor(((bearing / 360.0) * CARDINAL_DIRECTIONS.length + 0.5) % CARDINAL_DIRECTIONS.length) if (bearing == null) {
] + ' bound' return t("MapPage.roadInfo.cardinalDirections.unknown");
} else {
const n = CARDINAL_DIRECTIONS.length;
const i = Math.floor(((bearing / 360.0) * n + 0.5) % n);
const name = CARDINAL_DIRECTIONS[i];
return t(`MapPage.roadInfo.cardinalDirections.${name}`);
}
};
function RoadStatsTable({data}) { function RoadStatsTable({ data }) {
const { t } = useTranslation();
return ( return (
<Table size="small" compact> <Table size="small" compact>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.HeaderCell textAlign="right"></Table.HeaderCell> <Table.HeaderCell textAlign="right"></Table.HeaderCell>
{['distanceOvertaker', 'distanceStationary', 'speed'].map((prop) => ( {["distanceOvertaker", "distanceStationary", "speed"].map((prop) => (
<Table.HeaderCell key={prop} textAlign="right"> <Table.HeaderCell key={prop} textAlign="right">
{LABELS[prop]} {t(`MapPage.roadInfo.${prop}`)}
</Table.HeaderCell> </Table.HeaderCell>
))} ))}
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{['count', 'min', 'median', 'max', 'mean'].map((stat) => ( {["count", "min", "median", "max", "mean"].map((stat) => (
<Table.Row key={stat}> <Table.Row key={stat}>
<Table.Cell>{LABELS[stat]}</Table.Cell> <Table.Cell> {t(`MapPage.roadInfo.${stat}`)}</Table.Cell>
{['distanceOvertaker', 'distanceStationary', 'speed'].map((prop) => ( {["distanceOvertaker", "distanceStationary", "speed"].map(
<Table.Cell key={prop} textAlign="right"> (prop) => (
{(data[prop]?.statistics?.[stat] * (prop === `speed` && stat != 'count' ? 3.6 : 1)).toFixed( <Table.Cell key={prop} textAlign="right">
stat === 'count' ? 0 : 2 {(
)} data[prop]?.statistics?.[stat] *
{stat !== 'count' && ` ${UNITS[prop]}`} (prop === `speed` && stat != "count" ? 3.6 : 1)
</Table.Cell> ).toFixed(stat === "count" ? 0 : 2)}
))} {stat !== "count" && ` ${UNITS[prop]}`}
</Table.Cell>
)
)}
</Table.Row> </Table.Row>
))} ))}
</Table.Body> </Table.Body>
</Table> </Table>
) );
} }
function HistogramChart({bins, counts, zone}) { function HistogramChart({ bins, counts, zone }) {
const diff = bins[1] - bins[0] const diff = bins[1] - bins[0]
const colortype = zone=="rural" ? 3:5; const colortype = zone === "rural" ? 3 : 5;
const data = _.zip( const data = _.zip(
bins.slice(0, bins.length - 1).map((v) => v + diff / 2), bins.slice(0, bins.length - 1).map((v) => v + diff / 2),
counts counts
@ -88,19 +100,19 @@ function HistogramChart({bins, counts, zone}) {
return ( return (
<Chart <Chart
style={{height: 240}} style={{ height: 240 }}
option={{ option={{
grid: {top: 30, bottom: 30, right: 30, left: 30}, grid: { top: 30, bottom: 30, right: 30, left: 30 },
xAxis: { xAxis: {
type: 'value', type: "value",
axisLabel: {formatter: (v) => `${Math.round(v * 100)} cm`}, axisLabel: { formatter: (v) => `${Math.round(v * 100)} cm` },
min: 0, min: 0,
max: 2.5, max: 2.5,
}, },
yAxis: {}, yAxis: {},
series: [ series: [
{ {
type: 'bar', type: "bar",
data, data,
barMaxWidth: 20, barMaxWidth: 20,
@ -108,20 +120,21 @@ function HistogramChart({bins, counts, zone}) {
], ],
}} }}
/> />
) );
} }
export default function RoadInfo({clickLocation}) { export default function RoadInfo({ clickLocation }) {
const [direction, setDirection] = useState('forwards') const { t } = useTranslation();
const [direction, setDirection] = useState("forwards");
const onClickDirection = useCallback( const onClickDirection = useCallback(
(e, {name}) => { (e, { name }) => {
e.preventDefault() e.preventDefault();
e.stopPropagation() e.stopPropagation();
setDirection(name) setDirection(name);
}, },
[setDirection] [setDirection]
) );
const info = useObservable( const info = useObservable(
(_$, inputs$) => (_$, inputs$) =>
@ -132,7 +145,7 @@ export default function RoadInfo({clickLocation}) {
? concat( ? concat(
of(null), of(null),
from( from(
api.get('/mapdetails/road', { api.get("/mapdetails/road", {
query: { query: {
...location, ...location,
radius: 100, radius: 100,
@ -145,43 +158,60 @@ export default function RoadInfo({clickLocation}) {
), ),
null, null,
[clickLocation] [clickLocation]
) );
if (!clickLocation) { if (!clickLocation) {
return null return null;
} }
const loading = info == null const loading = info == null;
const offsetDirection = info?.road?.oneway ? 0 : direction === 'forwards' ? 1 : -1 // TODO: change based on left-hand/right-hand traffic const offsetDirection = info?.road?.oneway
? 0
: direction === "forwards"
? 1
: -1; // TODO: change based on left-hand/right-hand traffic
const content = const content =
!loading && !info.road ? ( !loading && !info.road ? (
'No road found.' "No road found."
) : ( ) : (
<> <>
<Header as="h3">{loading ? '...' : info?.road.name || 'Unnamed way'}</Header> <Header as="h3">
{loading
? "..."
: info?.road.name || t("MapPage.roadInfo.unnamedWay")}
</Header>
{info?.road.zone && ( {info?.road.zone && (
<Label size="small" color={ZONE_COLORS[info?.road.zone]}> <Label size="small" color={ZONE_COLORS[info?.road.zone]}>
{info?.road.zone} {t(`MapPage.roadInfo.zone.${info.road.zone}`)}
</Label> </Label>
)} )}
{info?.road.oneway && ( {info?.road.oneway && (
<Label size="small" color="blue"> <Label size="small" color="blue">
<Icon name="long arrow alternate right" fitted /> oneway <Icon name="long arrow alternate right" fitted />{" "}
{t("MapPage.roadInfo.oneway")}
</Label> </Label>
)} )}
{info?.road.oneway ? null : ( {info?.road.oneway ? null : (
<Menu size="tiny" fluid secondary> <Menu size="tiny" fluid secondary>
<Menu.Item header>Direction</Menu.Item> <Menu.Item header>{t("MapPage.roadInfo.direction")}</Menu.Item>
<Menu.Item name="forwards" active={direction === 'forwards'} onClick={onClickDirection}> <Menu.Item
{getCardinalDirection(info?.forwards?.bearing)} name="forwards"
active={direction === "forwards"}
onClick={onClickDirection}
>
{getCardinalDirection(t, info?.forwards?.bearing)}
</Menu.Item> </Menu.Item>
<Menu.Item name="backwards" active={direction === 'backwards'} onClick={onClickDirection}> <Menu.Item
{getCardinalDirection(info?.backwards?.bearing)} name="backwards"
active={direction === "backwards"}
onClick={onClickDirection}
>
{getCardinalDirection(t, info?.backwards?.bearing)}
</Menu.Item> </Menu.Item>
</Menu> </Menu>
)} )}
@ -190,12 +220,16 @@ export default function RoadInfo({clickLocation}) {
{info?.[direction]?.distanceOvertaker?.histogram && ( {info?.[direction]?.distanceOvertaker?.histogram && (
<> <>
<Header as="h5">Overtaker distance distribution</Header> <Header as="h5">
<HistogramChart {...info[direction]?.distanceOvertaker?.histogram} /> {t("MapPage.roadInfo.overtakerDistanceDistribution")}
</Header>
<HistogramChart
{...info[direction]?.distanceOvertaker?.histogram}
/>
</> </>
)} )}
</> </>
) );
return ( return (
<> <>
@ -205,14 +239,22 @@ export default function RoadInfo({clickLocation}) {
id="route" id="route"
type="line" type="line"
paint={{ paint={{
'line-width': ['interpolate', ['linear'], ['zoom'], 14, 6, 17, 12], "line-width": [
'line-color': '#18FFFF', "interpolate",
'line-opacity': 0.5, ["linear"],
["zoom"],
14,
6,
17,
12,
],
"line-color": "#18FFFF",
"line-opacity": 0.5,
...{ ...{
'line-offset': [ "line-offset": [
'interpolate', "interpolate",
['exponential', 1.5], ["exponential", 1.5],
['zoom'], ["zoom"],
12, 12,
offsetDirection, offsetDirection,
19, 19,
@ -230,5 +272,5 @@ export default function RoadInfo({clickLocation}) {
</div> </div>
)} )}
</> </>
) );
} }

View file

@ -1,6 +1,8 @@
general: general:
loading: Lädt loading: Lädt
unnamedTrack: Unbenannte Fahrt unnamedTrack: Unbenannte Fahrt
urban: innerorts
rural: außerorts
public: Öffentlich public: Öffentlich
private: Privat private: Privat
show: Anzeigen show: Anzeigen
@ -116,3 +118,64 @@ NotFoundPage:
title: Seite nicht gefunden title: Seite nicht gefunden
description: Den Fehler kennst du sicher... description: Den Fehler kennst du sicher...
goBack: Zurückgehen goBack: Zurückgehen
MapPage:
sidebar:
baseMap:
style:
label: Stil der Basiskarte
positron: Positron
bright: OSM Bright
obsRoads:
title: Straßenabschnitte
showUntagged:
label: Abschnitte ohne Daten anzeigen
attribute:
label: Einfärben nach...
distance_overtaker_mean: Durchschnitt Überholabstand
distance_overtaker_min: Minimum Überholabstand
distance_overtaker_max: Maximum Überholabstand
distance_overtaker_median: Median Überholabstand
overtaking_event_count: Anzahl Überholvorgänge
usage_count: Anzahl Befahrungen
zone: Überholabstands-Zone
maxCount:
label: Maximalwert für Farbskala
obsEvents:
title: Überholvorgänge
roadInfo:
unnamedWay: Unbenannter Weg
oneway: Einbahnstraße
direction: Richtung
zone:
rural: außerorts
urban: innerorts
motorway: Autobahn
distanceOvertaker: Links
distanceStationary: Rechts
speed: Geschwindigkeit
count: Anzahl Messungen
min: Minimum
median: Median
max: Maximum
mean: Durchschnitt
overtakerDistanceDistribution: Verteilung der Überholabstände
cardinalDirections:
unknown: unbekannt
north: nordwärts
northEast: nordostwärts
east: ostwärts
southEast: südostwärts
south: südwärts
southWest: südwestwärts
west: westwärts
northWest: nordwestwärts

View file

@ -5,6 +5,8 @@ locales:
general: general:
loading: Loading loading: Loading
unnamedTrack: Unnamed track unnamedTrack: Unnamed track
urban: urban
rural: rural
public: Public public: Public
private: Private private: Private
show: Show show: Show
@ -121,3 +123,63 @@ NotFoundPage:
title: Page not found title: Page not found
description: You know what that means... description: You know what that means...
goBack: Go back goBack: Go back
MapPage:
sidebar:
baseMap:
style:
label: Basemap Style
positron: Positron
bright: OSM Bright
obsRoads:
title: Road segments
showUntagged:
label: Include roads without data
attribute:
label: Color based on...
distance_overtaker_mean: Overtaker distance mean
distance_overtaker_min: Overtaker distance minimum
distance_overtaker_max: Overtaker distance maximum
distance_overtaker_median: Overtaker distance median
overtaking_event_count: Event count
usage_count: Usage count
zone: Overtaking distance zone
maxCount:
label: Maximum value for color scale
obsEvents:
title: Event points
roadInfo:
unnamedWay: Unnamed way
oneway: oneway
direction: Direction
zone:
rural: Rural
urban: Urban
motorway: Motorway
distanceOvertaker: Left
distanceStationary: Right
speed: Speed
count: No. of Measurements
min: Minimum
median: Median
max: Maximum
mean: Average
overtakerDistanceDistribution: Overtaker distance distribution
cardinalDirections:
unknown: unknown
north: north bound
northEast: north-east bound
east: east bound
southEast: south-east bound
south: south bound
southWest: south-west bound
west: west bound
northWest: north-west bound