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 _ from 'lodash'
import {connect} from 'react-redux'
import {List, Select, Input, Divider, Label, Checkbox, Header} from 'semantic-ui-react'
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import {
List,
Select,
Input,
Divider,
Label,
Checkbox,
Header,
} from "semantic-ui-react";
import { useTranslation } from "react-i18next";
import {
MapConfig,
setMapConfigFlag as setMapConfigFlagAction,
initialState as defaultMapConfig,
} from 'reducers/mapConfig'
import {colorByDistance, colorByCount, viridisSimpleHtml} from 'mapstyles'
import {ColorMapLegend, DiscreteColorMapLegend} from 'components'
} from "reducers/mapConfig";
import { colorByDistance, colorByCount, viridisSimpleHtml } from "mapstyles";
import { ColorMapLegend, DiscreteColorMapLegend } from "components";
const BASEMAP_STYLE_OPTIONS = [
{value: 'positron', key: 'positron', text: 'Positron'},
{value: 'bright', key: 'bright', text: 'OSM Bright'},
]
const BASEMAP_STYLE_OPTIONS = ["positron", "bright"];
const ROAD_ATTRIBUTE_OPTIONS = [
{value: 'distance_overtaker_mean', key: 'distance_overtaker_mean', text: 'Overtaker distance mean'},
{value: 'distance_overtaker_min', key: 'distance_overtaker_min', text: 'Overtaker distance minimum'},
{value: 'distance_overtaker_max', key: 'distance_overtaker_max', text: 'Overtaker distance maximum'},
{value: 'distance_overtaker_median', key: 'distance_overtaker_median', text: 'Overtaker distance median'},
{value: 'overtaking_event_count', key: 'overtaking_event_count', text: 'Event count'},
{value: 'usage_count', key: 'usage_count', text: 'Usage count'},
{value: 'zone', key: 'zone', text: 'Overtaking distance zone'}
]
"distance_overtaker_mean",
"distance_overtaker_min",
"distance_overtaker_max",
"distance_overtaker_median",
"overtaking_event_count",
"usage_count",
"zone",
];
function LayerSidebar({
mapConfig,
setMapConfigFlag,
}: {
mapConfig: MapConfig
setMapConfigFlag: (flag: string, value: unknown) => void
mapConfig: MapConfig;
setMapConfigFlag: (flag: string, value: unknown) => void;
}) {
const { t } = useTranslation();
const {
baseMap: {style},
obsRoads: {show: showRoads, showUntagged, attribute, maxCount},
obsEvents: {show: showEvents},
} = mapConfig
baseMap: { style },
obsRoads: { show: showRoads, showUntagged, attribute, maxCount },
obsEvents: { show: showEvents },
} = mapConfig;
return (
<div>
<List relaxed>
<List.Item>
<List.Header>Basemap Style</List.Header>
<List.Header>{t("MapPage.sidebar.baseMap.style.label")}</List.Header>
<Select
options={BASEMAP_STYLE_OPTIONS}
options={BASEMAP_STYLE_OPTIONS.map((value) => ({
value,
key: value,
text: t(`MapPage.sidebar.baseMap.style.${value}`),
}))}
value={style}
onChange={(_e, {value}) => setMapConfigFlag('baseMap.style', value)}
onChange={(_e, { value }) =>
setMapConfigFlag("baseMap.style", value)
}
/>
</List.Item>
<Divider />
@ -56,12 +69,12 @@ function LayerSidebar({
toggle
size="small"
id="obsRoads.show"
style={{float: 'right'}}
style={{ float: "right" }}
checked={showRoads}
onChange={() => setMapConfigFlag('obsRoads.show', !showRoads)}
onChange={() => setMapConfigFlag("obsRoads.show", !showRoads)}
/>
<label htmlFor="obsRoads.show">
<Header as="h4">Road segments</Header>
<Header as="h4">{t("MapPage.sidebar.obsRoads.title")}</Header>
</label>
</List.Item>
{showRoads && (
@ -69,49 +82,80 @@ function LayerSidebar({
<List.Item>
<Checkbox
checked={showUntagged}
onChange={() => setMapConfigFlag('obsRoads.showUntagged', !showUntagged)}
label="Include roads without data"
onChange={() =>
setMapConfigFlag("obsRoads.showUntagged", !showUntagged)
}
label={t("MapPage.sidebar.obsRoads.showUntagged.label")}
/>
</List.Item>
<List.Item>
<List.Header>Color based on</List.Header>
<List.Header>
{t("MapPage.sidebar.obsRoads.attribute.label")}
</List.Header>
<Select
fluid
options={ROAD_ATTRIBUTE_OPTIONS}
options={ROAD_ATTRIBUTE_OPTIONS.map((value) => ({
value,
key: value,
text: t(`MapPage.sidebar.obsRoads.attribute.${value}`),
}))}
value={attribute}
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.attribute', value)}
onChange={(_e, { value }) =>
setMapConfigFlag("obsRoads.attribute", value)
}
/>
</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.Header>Maximum value</List.Header>
<Input
fluid
type="number"
value={maxCount}
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.maxCount', value)}
<DiscreteColorMapLegend
map={colorByDistance("distance_overtaker")[3].slice(2)}
/>
</List.Item>
<List.Item>
<ColorMapLegend map={_.chunk(colorByCount('obsRoads.maxCount', mapConfig.obsRoads.maxCount, viridisSimpleHtml ).slice(3), 2)} twoTicks />
</List.Item></>
) :
attribute.endsWith('zone') ? (
<>
<List.Item>
<Label size="small" style={{background: "blue",color:"white"}}>urban (1.5&nbsp;m)</Label>
<Label size="small" style={{background: "cyan", color:"black"}}>rural (2&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"}}>{t("general.rural")}(2&nbsp;m)</Label>
</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)} />
</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)} />
</List.Item>
</>
@ -124,29 +168,29 @@ function LayerSidebar({
toggle
size="small"
id="obsEvents.show"
style={{float: 'right'}}
style={{ float: "right" }}
checked={showEvents}
onChange={() => setMapConfigFlag('obsEvents.show', !showEvents)}
onChange={() => setMapConfigFlag("obsEvents.show", !showEvents)}
/>
<label htmlFor="obsEvents.show">
<Header as="h4">Event points</Header>
<Header as="h4">{t("MapPage.sidebar.obsEvents.title")}</Header>
</label>
</List.Item>
{showEvents && (
<>
<List.Item>
<List.Header>Urban</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
</List.Item>
<List.Header>{_.startCase(t('general.urban'))}</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][5].slice(2)} />
</List.Item>
<List.Item>
<List.Header>Rural</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
<List.Header>{_.startCase(t('general.rural'))}</List.Header>
<DiscreteColorMapLegend map={colorByDistance('distance_overtaker')[3][3].slice(2)} />
</List.Item>
</>
)}
</List>
</div>
)
);
}
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 _ from 'lodash'
import {Segment, Menu, Header, Label, Icon, Table} from 'semantic-ui-react'
import {Layer, Source} from 'react-map-gl'
import {of, from, concat} from 'rxjs'
import {useObservable} from 'rxjs-hooks'
import {switchMap, distinctUntilChanged} from 'rxjs/operators'
import {Chart} from 'components'
import {pairwise} from 'utils'
import React, { useState, useCallback } from "react";
import _ from "lodash";
import { Segment, Menu, Header, Label, Icon, Table } from "semantic-ui-react";
import { Layer, Source } from "react-map-gl";
import { of, from, concat } from "rxjs";
import { useObservable } from "rxjs-hooks";
import { switchMap, distinctUntilChanged } from "rxjs/operators";
import { Chart } from "components";
import { pairwise } from "utils";
import { useTranslation } from "react-i18next";
import api from 'api'
import {colorByDistance, borderByZone} from 'mapstyles'
import styles from './styles.module.less'
import styles from "./styles.module.less";
function selectFromColorMap(colormap, value) {
let last = null
let last = null;
for (let i = 0; i < colormap.length; i += 2) {
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 LABELS = {
distanceOvertaker: 'Left',
distanceStationary: 'Right',
speed: 'Speed',
count: 'No. of Measurements',
min: 'Minimum',
median: 'Median',
max: 'Maximum',
mean: 'Average',
}
const ZONE_COLORS = {urban: 'blue', rural: 'cyan', motorway: 'purple'}
const CARDINAL_DIRECTIONS = ['north', 'north-east', 'east', 'south-east', 'south', 'south-west', 'west', 'north-west']
const getCardinalDirection = (bearing) =>
bearing == null
? 'unknown'
: CARDINAL_DIRECTIONS[
Math.floor(((bearing / 360.0) * CARDINAL_DIRECTIONS.length + 0.5) % CARDINAL_DIRECTIONS.length)
] + ' bound'
const UNITS = {
distanceOvertaker: "m",
distanceStationary: "m",
speed: "km/h",
};
const ZONE_COLORS = { urban: "blue", rural: "cyan", motorway: "purple" };
const CARDINAL_DIRECTIONS = [
"north",
"northEast",
"east",
"southEast",
"south",
"southWest",
"west",
"northWest",
];
const getCardinalDirection = (t, bearing) => {
if (bearing == null) {
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 (
<Table size="small" compact>
<Table.Header>
<Table.Row>
<Table.HeaderCell textAlign="right"></Table.HeaderCell>
{['distanceOvertaker', 'distanceStationary', 'speed'].map((prop) => (
{["distanceOvertaker", "distanceStationary", "speed"].map((prop) => (
<Table.HeaderCell key={prop} textAlign="right">
{LABELS[prop]}
{t(`MapPage.roadInfo.${prop}`)}
</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{['count', 'min', 'median', 'max', 'mean'].map((stat) => (
{["count", "min", "median", "max", "mean"].map((stat) => (
<Table.Row key={stat}>
<Table.Cell>{LABELS[stat]}</Table.Cell>
{['distanceOvertaker', 'distanceStationary', 'speed'].map((prop) => (
<Table.Cell key={prop} textAlign="right">
{(data[prop]?.statistics?.[stat] * (prop === `speed` && stat != 'count' ? 3.6 : 1)).toFixed(
stat === 'count' ? 0 : 2
)}
{stat !== 'count' && ` ${UNITS[prop]}`}
</Table.Cell>
))}
<Table.Cell> {t(`MapPage.roadInfo.${stat}`)}</Table.Cell>
{["distanceOvertaker", "distanceStationary", "speed"].map(
(prop) => (
<Table.Cell key={prop} textAlign="right">
{(
data[prop]?.statistics?.[stat] *
(prop === `speed` && stat != "count" ? 3.6 : 1)
).toFixed(stat === "count" ? 0 : 2)}
{stat !== "count" && ` ${UNITS[prop]}`}
</Table.Cell>
)
)}
</Table.Row>
))}
</Table.Body>
</Table>
)
);
}
function HistogramChart({bins, counts, zone}) {
function HistogramChart({ bins, counts, zone }) {
const diff = bins[1] - bins[0]
const colortype = zone=="rural" ? 3:5;
const colortype = zone === "rural" ? 3 : 5;
const data = _.zip(
bins.slice(0, bins.length - 1).map((v) => v + diff / 2),
counts
@ -88,19 +100,19 @@ function HistogramChart({bins, counts, zone}) {
return (
<Chart
style={{height: 240}}
style={{ height: 240 }}
option={{
grid: {top: 30, bottom: 30, right: 30, left: 30},
grid: { top: 30, bottom: 30, right: 30, left: 30 },
xAxis: {
type: 'value',
axisLabel: {formatter: (v) => `${Math.round(v * 100)} cm`},
type: "value",
axisLabel: { formatter: (v) => `${Math.round(v * 100)} cm` },
min: 0,
max: 2.5,
},
yAxis: {},
series: [
{
type: 'bar',
type: "bar",
data,
barMaxWidth: 20,
@ -108,20 +120,21 @@ function HistogramChart({bins, counts, zone}) {
],
}}
/>
)
);
}
export default function RoadInfo({clickLocation}) {
const [direction, setDirection] = useState('forwards')
export default function RoadInfo({ clickLocation }) {
const { t } = useTranslation();
const [direction, setDirection] = useState("forwards");
const onClickDirection = useCallback(
(e, {name}) => {
e.preventDefault()
e.stopPropagation()
setDirection(name)
(e, { name }) => {
e.preventDefault();
e.stopPropagation();
setDirection(name);
},
[setDirection]
)
);
const info = useObservable(
(_$, inputs$) =>
@ -132,7 +145,7 @@ export default function RoadInfo({clickLocation}) {
? concat(
of(null),
from(
api.get('/mapdetails/road', {
api.get("/mapdetails/road", {
query: {
...location,
radius: 100,
@ -145,43 +158,60 @@ export default function RoadInfo({clickLocation}) {
),
null,
[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 =
!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 && (
<Label size="small" color={ZONE_COLORS[info?.road.zone]}>
{info?.road.zone}
{t(`MapPage.roadInfo.zone.${info.road.zone}`)}
</Label>
)}
{info?.road.oneway && (
<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>
)}
{info?.road.oneway ? null : (
<Menu size="tiny" fluid secondary>
<Menu.Item header>Direction</Menu.Item>
<Menu.Item name="forwards" active={direction === 'forwards'} onClick={onClickDirection}>
{getCardinalDirection(info?.forwards?.bearing)}
<Menu.Item header>{t("MapPage.roadInfo.direction")}</Menu.Item>
<Menu.Item
name="forwards"
active={direction === "forwards"}
onClick={onClickDirection}
>
{getCardinalDirection(t, info?.forwards?.bearing)}
</Menu.Item>
<Menu.Item name="backwards" active={direction === 'backwards'} onClick={onClickDirection}>
{getCardinalDirection(info?.backwards?.bearing)}
<Menu.Item
name="backwards"
active={direction === "backwards"}
onClick={onClickDirection}
>
{getCardinalDirection(t, info?.backwards?.bearing)}
</Menu.Item>
</Menu>
)}
@ -190,12 +220,16 @@ export default function RoadInfo({clickLocation}) {
{info?.[direction]?.distanceOvertaker?.histogram && (
<>
<Header as="h5">Overtaker distance distribution</Header>
<HistogramChart {...info[direction]?.distanceOvertaker?.histogram} />
<Header as="h5">
{t("MapPage.roadInfo.overtakerDistanceDistribution")}
</Header>
<HistogramChart
{...info[direction]?.distanceOvertaker?.histogram}
/>
</>
)}
</>
)
);
return (
<>
@ -205,14 +239,22 @@ export default function RoadInfo({clickLocation}) {
id="route"
type="line"
paint={{
'line-width': ['interpolate', ['linear'], ['zoom'], 14, 6, 17, 12],
'line-color': '#18FFFF',
'line-opacity': 0.5,
"line-width": [
"interpolate",
["linear"],
["zoom"],
14,
6,
17,
12,
],
"line-color": "#18FFFF",
"line-opacity": 0.5,
...{
'line-offset': [
'interpolate',
['exponential', 1.5],
['zoom'],
"line-offset": [
"interpolate",
["exponential", 1.5],
["zoom"],
12,
offsetDirection,
19,
@ -230,5 +272,5 @@ export default function RoadInfo({clickLocation}) {
</div>
)}
</>
)
);
}

View file

@ -1,6 +1,8 @@
general:
loading: Lädt
unnamedTrack: Unbenannte Fahrt
urban: innerorts
rural: außerorts
public: Öffentlich
private: Privat
show: Anzeigen
@ -116,3 +118,64 @@ NotFoundPage:
title: Seite nicht gefunden
description: Den Fehler kennst du sicher...
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:
loading: Loading
unnamedTrack: Unnamed track
urban: urban
rural: rural
public: Public
private: Private
show: Show
@ -121,3 +123,63 @@ NotFoundPage:
title: Page not found
description: You know what that means...
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