Add more options for roads layer
This commit is contained in:
parent
6b38540586
commit
1669713fc5
27
frontend/package-lock.json
generated
27
frontend/package-lock.json
generated
|
@ -11,6 +11,7 @@
|
||||||
"@babel/runtime": "^7.16.3",
|
"@babel/runtime": "^7.16.3",
|
||||||
"@turf/bbox": "^6.5.0",
|
"@turf/bbox": "^6.5.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"colormap": "^2.3.2",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"fomantic-ui-less": "^2.8.8",
|
"fomantic-ui-less": "^2.8.8",
|
||||||
"immer": "^9.0.7",
|
"immer": "^9.0.7",
|
||||||
|
@ -3262,6 +3263,14 @@
|
||||||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/colormap": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/colormap/-/colormap-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-jDOjaoEEmA9AgA11B/jCSAvYE95r3wRoAyTf3LEHGiUVlNHJaL1mRkf5AyLSpQBVGfTEPwGEqCIzL+kgr2WgNA==",
|
||||||
|
"dependencies": {
|
||||||
|
"lerp": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
@ -5389,6 +5398,11 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lerp": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lerp/-/lerp-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-oYyJaPkXiW3hXM/MKNVaa3Med24="
|
||||||
|
},
|
||||||
"node_modules/less": {
|
"node_modules/less": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",
|
||||||
|
@ -11551,6 +11565,14 @@
|
||||||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"colormap": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/colormap/-/colormap-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-jDOjaoEEmA9AgA11B/jCSAvYE95r3wRoAyTf3LEHGiUVlNHJaL1mRkf5AyLSpQBVGfTEPwGEqCIzL+kgr2WgNA==",
|
||||||
|
"requires": {
|
||||||
|
"lerp": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
@ -13150,6 +13172,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz",
|
||||||
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ=="
|
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ=="
|
||||||
},
|
},
|
||||||
|
"lerp": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lerp/-/lerp-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-oYyJaPkXiW3hXM/MKNVaa3Med24="
|
||||||
|
},
|
||||||
"less": {
|
"less": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"@babel/runtime": "^7.16.3",
|
"@babel/runtime": "^7.16.3",
|
||||||
"@turf/bbox": "^6.5.0",
|
"@turf/bbox": "^6.5.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"colormap": "^2.3.2",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"fomantic-ui-less": "^2.8.8",
|
"fomantic-ui-less": "^2.8.8",
|
||||||
"immer": "^9.0.7",
|
"immer": "^9.0.7",
|
||||||
|
|
|
@ -3,9 +3,50 @@ import _ from 'lodash'
|
||||||
import bright from './bright.json'
|
import bright from './bright.json'
|
||||||
import positron from './positron.json'
|
import positron from './positron.json'
|
||||||
|
|
||||||
|
import viridisBase from 'colormap/res/res/viridis'
|
||||||
|
|
||||||
export {bright, positron}
|
export {bright, positron}
|
||||||
export const baseMapStyles = {bright, positron}
|
export const baseMapStyles = {bright, positron}
|
||||||
|
|
||||||
|
|
||||||
|
function simplifyColormap(colormap, maxCount = 16) {
|
||||||
|
const result = []
|
||||||
|
const step = Math.ceil(colormap.length / maxCount)
|
||||||
|
for (let i = 0; i < colormap.length; i+= step) {
|
||||||
|
result.push(colormap[i])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbArrayToColor(arr) {
|
||||||
|
return ['rgb', ...arr.map(v => Math.round(v*255))]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colormapToScale(colormap, value, min, max) {
|
||||||
|
return [
|
||||||
|
'interpolate-hcl',
|
||||||
|
['linear'],
|
||||||
|
value,
|
||||||
|
...colormap.flatMap((v, i, a) => [
|
||||||
|
(i / (a.length - 1)) * (max - min) + min,
|
||||||
|
v,
|
||||||
|
])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const viridis = simplifyColormap(viridisBase.map(rgbArrayToColor), 20);
|
||||||
|
export const grayscale = ['#FFFFFF', '#000000']
|
||||||
|
export const reds = [['rgba', 255, 0, 0, 0], ['rgba', 255, 0, 0, 1]]
|
||||||
|
|
||||||
|
export function colorByCount(attribute = 'event_count', maxCount, colormap = viridis) {
|
||||||
|
return colormapToScale(
|
||||||
|
colormap,
|
||||||
|
['case', ['to-boolean', ['get', attribute]], ['get', attribute], 0],
|
||||||
|
0,
|
||||||
|
maxCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC') {
|
export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC') {
|
||||||
return [
|
return [
|
||||||
'case',
|
'case',
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {List, Select, Header, Checkbox} from 'semantic-ui-react'
|
import {List, Select, Input, Divider, Checkbox} from 'semantic-ui-react'
|
||||||
|
|
||||||
import * as mapConfigActions from 'reducers/mapConfig'
|
import {
|
||||||
|
MapConfig,
|
||||||
|
setMapConfigFlag as setMapConfigFlagAction,
|
||||||
|
initialState as defaultMapConfig,
|
||||||
|
} from 'reducers/mapConfig'
|
||||||
|
|
||||||
const BASEMAP_STYLE_OPTIONS = [
|
const BASEMAP_STYLE_OPTIONS = [
|
||||||
{value: 'positron', key: 'positron', text: 'Positron'},
|
{value: 'positron', key: 'positron', text: 'Positron'},
|
||||||
|
@ -17,8 +22,14 @@ const ROAD_ATTRIBUTE_OPTIONS = [
|
||||||
{value: 'overtaking_event_count', key: 'overtaking_event_count', text: 'Event count'},
|
{value: 'overtaking_event_count', key: 'overtaking_event_count', text: 'Event count'},
|
||||||
]
|
]
|
||||||
|
|
||||||
function LayerSidebar({mapConfig, setMapConfigFlag}) {
|
function LayerSidebar({
|
||||||
const showUntagged = mapConfig?.obsRoads?.showUntagged ?? true
|
mapConfig,
|
||||||
|
setMapConfigFlag,
|
||||||
|
}: {
|
||||||
|
mapConfig: MapConfig
|
||||||
|
setMapConfigFlag: (flag: string, value: unknown) => void
|
||||||
|
}) {
|
||||||
|
const {baseMap: {style}, obsRoads: {show, showUntagged, attribute, maxCount}} = mapConfig
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -27,28 +38,60 @@ function LayerSidebar({mapConfig, setMapConfigFlag}) {
|
||||||
<List.Header>Basemap Style</List.Header>
|
<List.Header>Basemap Style</List.Header>
|
||||||
<Select
|
<Select
|
||||||
options={BASEMAP_STYLE_OPTIONS}
|
options={BASEMAP_STYLE_OPTIONS}
|
||||||
value={mapConfig?.baseMap?.style ?? 'positron'}
|
value={style}
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('baseMap.style', value)}
|
onChange={(_e, {value}) => setMapConfigFlag('baseMap.style', value)}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<Header as='h4' dividing>OBS Roads</Header>
|
<Divider />
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Checkbox label='Show untagged roads' checked={showUntagged}
|
<Checkbox
|
||||||
|
checked={showUntagged}
|
||||||
onChange={() => setMapConfigFlag('obsRoads.showUntagged', !showUntagged)}
|
onChange={() => setMapConfigFlag('obsRoads.showUntagged', !showUntagged)}
|
||||||
|
label="Untagged roads"
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
<Divider />
|
||||||
|
<List.Item>
|
||||||
|
<Checkbox
|
||||||
|
checked={show}
|
||||||
|
onChange={() => setMapConfigFlag('obsRoads.show', !show)}
|
||||||
|
label="OBS Roads"
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header style={{marginBlock: 8}}>Color based on</List.Header>
|
<List.Header>Color based on</List.Header>
|
||||||
<Select
|
<Select
|
||||||
fluid
|
fluid
|
||||||
options={ROAD_ATTRIBUTE_OPTIONS}
|
options={ROAD_ATTRIBUTE_OPTIONS}
|
||||||
value={mapConfig?.obsRoads?.attribute ?? 'distance_overtaker_mean'}
|
value={attribute}
|
||||||
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.attribute', value)}
|
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.attribute', value)}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
{attribute.endsWith('_count') ? (
|
||||||
|
<List.Item>
|
||||||
|
<List.Header>Maximum value</List.Header>
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
type="number"
|
||||||
|
value={maxCount}
|
||||||
|
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.maxCount', value)}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
) : null}
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((state) => ({mapConfig: state.mapConfig}), mapConfigActions)(LayerSidebar)
|
export default connect(
|
||||||
|
(state) => ({
|
||||||
|
mapConfig: _.merge(
|
||||||
|
{},
|
||||||
|
defaultMapConfig,
|
||||||
|
(state as any).mapConfig as MapConfig,
|
||||||
|
//
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{setMapConfigFlag: setMapConfigFlagAction}
|
||||||
|
//
|
||||||
|
)(LayerSidebar)
|
||||||
|
|
|
@ -7,13 +7,12 @@ import {connect} from 'react-redux'
|
||||||
|
|
||||||
import {Page, Map} from 'components'
|
import {Page, Map} from 'components'
|
||||||
import {useConfig} from 'config'
|
import {useConfig} from 'config'
|
||||||
import {colorByDistance} from 'mapstyles'
|
import {colorByDistance, colorByCount, reds} from 'mapstyles'
|
||||||
|
|
||||||
import RoadInfo from './RoadInfo'
|
import RoadInfo from './RoadInfo'
|
||||||
import LayerSidebar from './LayerSidebar'
|
import LayerSidebar from './LayerSidebar'
|
||||||
import styles from './styles.module.less'
|
import styles from './styles.module.less'
|
||||||
|
|
||||||
|
|
||||||
const untaggedRoadsLayer = {
|
const untaggedRoadsLayer = {
|
||||||
id: 'obs_roads_untagged',
|
id: 'obs_roads_untagged',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
@ -25,25 +24,9 @@ const untaggedRoadsLayer = {
|
||||||
'line-join': 'round',
|
'line-join': 'round',
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'line-width': [
|
'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2],
|
||||||
'interpolate',
|
'line-color': '#ABC',
|
||||||
['exponential', 1.5],
|
'line-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1],
|
||||||
['zoom'],
|
|
||||||
12,
|
|
||||||
2,
|
|
||||||
17,
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
'line-color': "#ABC",
|
|
||||||
'line-opacity': [
|
|
||||||
'interpolate',
|
|
||||||
['linear'],
|
|
||||||
['zoom'],
|
|
||||||
14,
|
|
||||||
0,
|
|
||||||
15,
|
|
||||||
1,
|
|
||||||
],
|
|
||||||
'line-offset': [
|
'line-offset': [
|
||||||
'interpolate',
|
'interpolate',
|
||||||
['exponential', 1.5],
|
['exponential', 1.5],
|
||||||
|
@ -57,11 +40,20 @@ const untaggedRoadsLayer = {
|
||||||
minzoom: 12,
|
minzoom: 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoadsLayer = attribute => produce(untaggedRoadsLayer, draft => {
|
const getRoadsLayer = (colorAttribute, maxCount) =>
|
||||||
|
produce(untaggedRoadsLayer, (draft) => {
|
||||||
draft.id = 'obs_roads_normal'
|
draft.id = 'obs_roads_normal'
|
||||||
|
if (colorAttribute.endsWith('_count')) {
|
||||||
|
delete draft.filter
|
||||||
|
} else {
|
||||||
draft.filter = draft.filter[1] // remove '!'
|
draft.filter = draft.filter[1] // remove '!'
|
||||||
draft.paint['line-width'][6] = 6
|
}
|
||||||
draft.paint['line-color'] = colorByDistance(attribute)
|
draft.paint['line-width'][6] = 6 // scale bigger on zoom
|
||||||
|
draft.paint['line-color'] = colorAttribute.startsWith('distance_')
|
||||||
|
? colorByDistance(colorAttribute)
|
||||||
|
: colorAttribute.endsWith('_count')
|
||||||
|
? colorByCount(colorAttribute, maxCount, reds)
|
||||||
|
: '#DDD'
|
||||||
draft.paint['line-opacity'][3] = 12
|
draft.paint['line-opacity'][3] = 12
|
||||||
draft.paint['line-opacity'][5] = 13
|
draft.paint['line-opacity'][5] = 13
|
||||||
})
|
})
|
||||||
|
@ -89,7 +81,11 @@ function MapPage({mapConfig}) {
|
||||||
|
|
||||||
const showUntagged = mapConfig?.obsRoads?.showUntagged ?? true
|
const showUntagged = mapConfig?.obsRoads?.showUntagged ?? true
|
||||||
const roadsLayerColorAttribute = mapConfig?.obsRoads?.attribute ?? 'distance_overtaker_mean'
|
const roadsLayerColorAttribute = mapConfig?.obsRoads?.attribute ?? 'distance_overtaker_mean'
|
||||||
const roadsLayer = useMemo(() => getRoadsLayer(roadsLayerColorAttribute ), [roadsLayerColorAttribute ])
|
const roadsLayerMaxCount = mapConfig?.obsRoads?.maxCount ?? 20
|
||||||
|
const roadsLayer = useMemo(() => getRoadsLayer(roadsLayerColorAttribute, roadsLayerMaxCount), [
|
||||||
|
roadsLayerColorAttribute,
|
||||||
|
roadsLayerMaxCount,
|
||||||
|
])
|
||||||
|
|
||||||
if (!obsMapSource) {
|
if (!obsMapSource) {
|
||||||
return null
|
return null
|
||||||
|
@ -98,7 +94,11 @@ function MapPage({mapConfig}) {
|
||||||
return (
|
return (
|
||||||
<Page fullScreen>
|
<Page fullScreen>
|
||||||
<div className={styles.mapContainer}>
|
<div className={styles.mapContainer}>
|
||||||
{layerSidebar && <div className={styles.mapSidebar}><LayerSidebar /></div>}
|
{layerSidebar && (
|
||||||
|
<div className={styles.mapSidebar}>
|
||||||
|
<LayerSidebar />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={styles.map}>
|
<div className={styles.map}>
|
||||||
<Map viewportFromUrl onClick={onClick}>
|
<Map viewportFromUrl onClick={onClick}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -2,25 +2,48 @@ import produce from 'immer'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
type BaseMapStyle = 'positron' | 'bright'
|
type BaseMapStyle = 'positron' | 'bright'
|
||||||
type MapConfigState = {
|
|
||||||
|
type RoadAttribute =
|
||||||
|
| 'distance_overtaker_mean'
|
||||||
|
| 'distance_overtaker_min'
|
||||||
|
| 'distance_overtaker_max'
|
||||||
|
| 'distance_overtaker_median'
|
||||||
|
| 'overtaking_event_count'
|
||||||
|
|
||||||
|
export type MapConfig = {
|
||||||
baseMap: {
|
baseMap: {
|
||||||
style: BaseMapStyle
|
style: BaseMapStyle
|
||||||
}
|
}
|
||||||
|
obsRoads:{
|
||||||
|
show: boolean
|
||||||
|
showUntagged: boolean
|
||||||
|
attribute: RoadAttribute
|
||||||
|
maxCount: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: MapConfigState = {
|
export const initialState: MapConfig = {
|
||||||
baseMap: {
|
baseMap: {
|
||||||
style: 'positron',
|
style: 'positron',
|
||||||
},
|
},
|
||||||
|
obsRoads: {
|
||||||
|
show: true,
|
||||||
|
showUntagged: true,
|
||||||
|
attribute: 'distance_overtaker_median',
|
||||||
|
maxCount: 20,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setMapConfigFlag(flag: string, value: unknown) {
|
type MapConfigAction =
|
||||||
return {type: 'MAPCONFIG.SET_FLAG', payload: {flag, value}}
|
{type: 'MAP_CONFIG.SET_FLAG', payload: {flag: string, value: any}}
|
||||||
|
|
||||||
|
export function setMapConfigFlag(flag: string, value: unknown): MapConfigAction {
|
||||||
|
return {type: 'MAP_CONFIG.SET_FLAG', payload: {flag, value}}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function mapConfigReducer(state = initialState, action) {
|
export default function mapConfigReducer(state: MapConfig = initialState, action: MapConfigAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'MAPCONFIG.SET_FLAG':
|
case 'MAP_CONFIG.SET_FLAG':
|
||||||
return produce(state, draft => {
|
return produce(state, draft => {
|
||||||
_.set(draft, action.payload.flag, action.payload.value)
|
_.set(draft, action.payload.flag, action.payload.value)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue