Add more options for roads layer

This commit is contained in:
Paul Bienkowski 2021-12-05 18:35:23 +01:00
parent 6b38540586
commit 1669713fc5
6 changed files with 182 additions and 47 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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',

View file

@ -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)

View file

@ -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,14 +40,23 @@ const untaggedRoadsLayer = {
minzoom: 12, minzoom: 12,
} }
const getRoadsLayer = attribute => produce(untaggedRoadsLayer, draft => { const getRoadsLayer = (colorAttribute, maxCount) =>
draft.id = 'obs_roads_normal' produce(untaggedRoadsLayer, (draft) => {
draft.filter = draft.filter[1] // remove '!' draft.id = 'obs_roads_normal'
draft.paint['line-width'][6] = 6 if (colorAttribute.endsWith('_count')) {
draft.paint['line-color'] = colorByDistance(attribute) delete draft.filter
draft.paint['line-opacity'][3] = 12 } else {
draft.paint['line-opacity'][5] = 13 draft.filter = draft.filter[1] // remove '!'
}) }
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'][5] = 13
})
function MapPage({mapConfig}) { function MapPage({mapConfig}) {
const {obsMapSource} = useConfig() || {} const {obsMapSource} = useConfig() || {}
@ -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

View file

@ -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)
}) })