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",
|
||||
"@turf/bbox": "^6.5.0",
|
||||
"classnames": "^2.3.1",
|
||||
"colormap": "^2.3.2",
|
||||
"downloadjs": "^1.4.7",
|
||||
"fomantic-ui-less": "^2.8.8",
|
||||
"immer": "^9.0.7",
|
||||
|
@ -3262,6 +3263,14 @@
|
|||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||
"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": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
|
@ -5389,6 +5398,11 @@
|
|||
"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": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",
|
||||
|
@ -11551,6 +11565,14 @@
|
|||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||
"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": {
|
||||
"version": "2.20.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz",
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"@babel/runtime": "^7.16.3",
|
||||
"@turf/bbox": "^6.5.0",
|
||||
"classnames": "^2.3.1",
|
||||
"colormap": "^2.3.2",
|
||||
"downloadjs": "^1.4.7",
|
||||
"fomantic-ui-less": "^2.8.8",
|
||||
"immer": "^9.0.7",
|
||||
|
|
|
@ -3,9 +3,50 @@ import _ from 'lodash'
|
|||
import bright from './bright.json'
|
||||
import positron from './positron.json'
|
||||
|
||||
import viridisBase from 'colormap/res/res/viridis'
|
||||
|
||||
export {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') {
|
||||
return [
|
||||
'case',
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
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 = [
|
||||
{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'},
|
||||
]
|
||||
|
||||
function LayerSidebar({mapConfig, setMapConfigFlag}) {
|
||||
const showUntagged = mapConfig?.obsRoads?.showUntagged ?? true
|
||||
function LayerSidebar({
|
||||
mapConfig,
|
||||
setMapConfigFlag,
|
||||
}: {
|
||||
mapConfig: MapConfig
|
||||
setMapConfigFlag: (flag: string, value: unknown) => void
|
||||
}) {
|
||||
const {baseMap: {style}, obsRoads: {show, showUntagged, attribute, maxCount}} = mapConfig
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -27,28 +38,60 @@ function LayerSidebar({mapConfig, setMapConfigFlag}) {
|
|||
<List.Header>Basemap Style</List.Header>
|
||||
<Select
|
||||
options={BASEMAP_STYLE_OPTIONS}
|
||||
value={mapConfig?.baseMap?.style ?? 'positron'}
|
||||
value={style}
|
||||
onChange={(_e, {value}) => setMapConfigFlag('baseMap.style', value)}
|
||||
/>
|
||||
</List.Item>
|
||||
<Header as='h4' dividing>OBS Roads</Header>
|
||||
<Divider />
|
||||
<List.Item>
|
||||
<Checkbox label='Show untagged roads' checked={showUntagged}
|
||||
<Checkbox
|
||||
checked={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.Header style={{marginBlock: 8}}>Color based on</List.Header>
|
||||
<List.Header>Color based on</List.Header>
|
||||
<Select
|
||||
fluid
|
||||
options={ROAD_ATTRIBUTE_OPTIONS}
|
||||
value={mapConfig?.obsRoads?.attribute ?? 'distance_overtaker_mean'}
|
||||
value={attribute}
|
||||
onChange={(_e, {value}) => setMapConfigFlag('obsRoads.attribute', value)}
|
||||
/>
|
||||
</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>
|
||||
</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 {useConfig} from 'config'
|
||||
import {colorByDistance} from 'mapstyles'
|
||||
import {colorByDistance, colorByCount, reds} from 'mapstyles'
|
||||
|
||||
import RoadInfo from './RoadInfo'
|
||||
import LayerSidebar from './LayerSidebar'
|
||||
import styles from './styles.module.less'
|
||||
|
||||
|
||||
const untaggedRoadsLayer = {
|
||||
id: 'obs_roads_untagged',
|
||||
type: 'line',
|
||||
|
@ -25,25 +24,9 @@ const untaggedRoadsLayer = {
|
|||
'line-join': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-width': [
|
||||
'interpolate',
|
||||
['exponential', 1.5],
|
||||
['zoom'],
|
||||
12,
|
||||
2,
|
||||
17,
|
||||
2,
|
||||
],
|
||||
'line-color': "#ABC",
|
||||
'line-opacity': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
14,
|
||||
0,
|
||||
15,
|
||||
1,
|
||||
],
|
||||
'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2],
|
||||
'line-color': '#ABC',
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1],
|
||||
'line-offset': [
|
||||
'interpolate',
|
||||
['exponential', 1.5],
|
||||
|
@ -57,14 +40,23 @@ const untaggedRoadsLayer = {
|
|||
minzoom: 12,
|
||||
}
|
||||
|
||||
const getRoadsLayer = attribute => produce(untaggedRoadsLayer, draft => {
|
||||
draft.id = 'obs_roads_normal'
|
||||
draft.filter = draft.filter[1] // remove '!'
|
||||
draft.paint['line-width'][6] = 6
|
||||
draft.paint['line-color'] = colorByDistance(attribute)
|
||||
draft.paint['line-opacity'][3] = 12
|
||||
draft.paint['line-opacity'][5] = 13
|
||||
})
|
||||
const getRoadsLayer = (colorAttribute, maxCount) =>
|
||||
produce(untaggedRoadsLayer, (draft) => {
|
||||
draft.id = 'obs_roads_normal'
|
||||
if (colorAttribute.endsWith('_count')) {
|
||||
delete draft.filter
|
||||
} else {
|
||||
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}) {
|
||||
const {obsMapSource} = useConfig() || {}
|
||||
|
@ -89,7 +81,11 @@ function MapPage({mapConfig}) {
|
|||
|
||||
const showUntagged = mapConfig?.obsRoads?.showUntagged ?? true
|
||||
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) {
|
||||
return null
|
||||
|
@ -98,7 +94,11 @@ function MapPage({mapConfig}) {
|
|||
return (
|
||||
<Page fullScreen>
|
||||
<div className={styles.mapContainer}>
|
||||
{layerSidebar && <div className={styles.mapSidebar}><LayerSidebar /></div>}
|
||||
{layerSidebar && (
|
||||
<div className={styles.mapSidebar}>
|
||||
<LayerSidebar />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.map}>
|
||||
<Map viewportFromUrl onClick={onClick}>
|
||||
<Button
|
||||
|
|
|
@ -2,25 +2,48 @@ import produce from 'immer'
|
|||
import _ from 'lodash'
|
||||
|
||||
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: {
|
||||
style: BaseMapStyle
|
||||
}
|
||||
obsRoads:{
|
||||
show: boolean
|
||||
showUntagged: boolean
|
||||
attribute: RoadAttribute
|
||||
maxCount: number
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: MapConfigState = {
|
||||
export const initialState: MapConfig = {
|
||||
baseMap: {
|
||||
style: 'positron',
|
||||
},
|
||||
obsRoads: {
|
||||
show: true,
|
||||
showUntagged: true,
|
||||
attribute: 'distance_overtaker_median',
|
||||
maxCount: 20,
|
||||
},
|
||||
}
|
||||
|
||||
export function setMapConfigFlag(flag: string, value: unknown) {
|
||||
return {type: 'MAPCONFIG.SET_FLAG', payload: {flag, value}}
|
||||
type MapConfigAction =
|
||||
{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) {
|
||||
case 'MAPCONFIG.SET_FLAG':
|
||||
case 'MAP_CONFIG.SET_FLAG':
|
||||
return produce(state, draft => {
|
||||
_.set(draft, action.payload.flag, action.payload.value)
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue