Reintroduce event view (fixes #111)
This commit is contained in:
parent
1669713fc5
commit
3db5132199
24
frontend/src/components/ColorMapLegend.module.less
Normal file
24
frontend/src/components/ColorMapLegend.module.less
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
.colorMapLegend {
|
||||||
|
position: relative;
|
||||||
|
margin: 1rem;
|
||||||
|
font-size: 10pt;
|
||||||
|
letter-spacing: -0.2pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 2px);
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: -4px;
|
||||||
|
height: 6px;
|
||||||
|
border-left: 1px solid currentcolor;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
}
|
30
frontend/src/components/ColorMapLegend.tsx
Normal file
30
frontend/src/components/ColorMapLegend.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
type ColorMap = [number, string][]
|
||||||
|
|
||||||
|
import styles from './ColorMapLegend.module.less'
|
||||||
|
|
||||||
|
export default function ColorMapLegend({map}: {map: ColorMap}) {
|
||||||
|
const min = map[0][0]
|
||||||
|
const max = map[map.length - 1][0]
|
||||||
|
const normalizeValue = (v) => (v - min) / (max - min)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.colorMapLegend}>
|
||||||
|
<svg width="100%" height="20" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="0" x2="1" y1="0" y2="0">
|
||||||
|
{map.map(([value, color]) => (
|
||||||
|
<stop key={value} offset={normalizeValue(value) * 100 + '%'} stop-color={color} />
|
||||||
|
))}
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect id="rect1" x="0" y="0" width="100%" height="100%" fill="url(#gradient)" />
|
||||||
|
</svg>
|
||||||
|
{map.map(([value]) => <span className={styles.tick} key={value}
|
||||||
|
style={{left: normalizeValue(value)*100 + '%'}}
|
||||||
|
>
|
||||||
|
{value.toFixed(2)}
|
||||||
|
</span>)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import React, {useState, useCallback, useMemo, useEffect} from 'react'
|
import React, {useState, useCallback, useMemo, useEffect} from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ReactMapGl, {WebMercatorViewport, AttributionControl, NavigationControl} from 'react-map-gl'
|
import ReactMapGl, {WebMercatorViewport, ScaleControl, NavigationControl} from 'react-map-gl'
|
||||||
import turfBbox from '@turf/bbox'
|
import turfBbox from '@turf/bbox'
|
||||||
import {useHistory, useLocation} from 'react-router-dom'
|
import {useHistory, useLocation} from 'react-router-dom'
|
||||||
|
|
||||||
|
@ -9,12 +10,14 @@ import {useConfig} from 'config'
|
||||||
|
|
||||||
import {baseMapStyles} from '../../mapstyles'
|
import {baseMapStyles} from '../../mapstyles'
|
||||||
|
|
||||||
|
import styles from './styles.module.less'
|
||||||
|
|
||||||
const EMPTY_VIEWPORT = {longitude: 0, latitude: 0, zoom: 0}
|
interface Viewport {longitude: number; latitude: number; zoom: number}
|
||||||
|
const EMPTY_VIEWPORT: Viewport = {longitude: 0, latitude: 0, zoom: 0}
|
||||||
|
|
||||||
export const withBaseMapStyle = connect((state) => ({baseMapStyle: state.mapConfig?.baseMap?.style ?? 'positron'}))
|
export const withBaseMapStyle = connect((state) => ({baseMapStyle: state.mapConfig?.baseMap?.style ?? 'positron'}))
|
||||||
|
|
||||||
function parseHash(v) {
|
function parseHash(v: string): Viewport | null {
|
||||||
if (!v) return null
|
if (!v) return null
|
||||||
const m = v.match(/^#([0-9\.]+)\/([0-9\.]+)\/([0-9\.]+)$/)
|
const m = v.match(/^#([0-9\.]+)\/([0-9\.]+)\/([0-9\.]+)$/)
|
||||||
if (!m) return null
|
if (!m) return null
|
||||||
|
@ -25,11 +28,11 @@ function parseHash(v) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHash(v) {
|
function buildHash(v: Viewport): string {
|
||||||
return `${v.zoom.toFixed(2)}/${v.latitude}/${v.longitude}`
|
return `${v.zoom.toFixed(2)}/${v.latitude}/${v.longitude}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function useViewportFromUrl() {
|
function useViewportFromUrl(): [Viewport|null, (v: Viewport) => void] {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const value = useMemo(() => parseHash(location.hash), [location.hash])
|
const value = useMemo(() => parseHash(location.hash), [location.hash])
|
||||||
|
@ -44,7 +47,6 @@ function useViewportFromUrl() {
|
||||||
return [value || EMPTY_VIEWPORT, setter]
|
return [value || EMPTY_VIEWPORT, setter]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function Map({
|
function Map({
|
||||||
viewportFromUrl,
|
viewportFromUrl,
|
||||||
children,
|
children,
|
||||||
|
@ -53,8 +55,8 @@ function Map({
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
viewportFromUrl?: boolean
|
viewportFromUrl?: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
boundsFromJson: GeoJSON.Geometry
|
boundsFromJson: GeoJSON.Geometry
|
||||||
baseMapStyle: string
|
baseMapStyle: string
|
||||||
}) {
|
}) {
|
||||||
const [viewportState, setViewportState] = useState(EMPTY_VIEWPORT)
|
const [viewportState, setViewportState] = useState(EMPTY_VIEWPORT)
|
||||||
|
@ -64,7 +66,7 @@ function Map({
|
||||||
|
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.mapHome && viewport.latitude === 0 && viewport.longitude === 0 && !boundsFromJson) {
|
if (config?.mapHome && viewport?.latitude === 0 && viewport?.longitude === 0 && !boundsFromJson) {
|
||||||
setViewport(config.mapHome)
|
setViewport(config.mapHome)
|
||||||
}
|
}
|
||||||
}, [config, boundsFromJson])
|
}, [config, boundsFromJson])
|
||||||
|
@ -87,16 +89,17 @@ function Map({
|
||||||
}, [boundsFromJson])
|
}, [boundsFromJson])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMapGl mapStyle={baseMapStyles[baseMapStyle]} width="100%" height="100%" onViewportChange={setViewport} {...viewport} {...props}>
|
<ReactMapGl
|
||||||
<AttributionControl
|
mapStyle={baseMapStyles[baseMapStyle]}
|
||||||
style={{right: 0, bottom: 0}}
|
width="100%"
|
||||||
customAttribution={[
|
height="100%"
|
||||||
'<a href="https://openstreetmap.org/copyright" target="_blank" rel="nofollow noopener noreferrer">© OpenStreetMap contributors</a>',
|
onViewportChange={setViewport}
|
||||||
'<a href="https://openmaptiles.org/" target="_blank" rel="nofollow noopener noreferrer">© OpenMapTiles</a>',
|
{...viewport}
|
||||||
'<a href="https://openbikesensor.org/" target="_blank" rel="nofollow noopener noreferrer">© OpenBikeSensor</a>',
|
{...props}
|
||||||
]}
|
className={classnames(styles.map, props.className)}
|
||||||
/>
|
>
|
||||||
<NavigationControl style={{left: 10, top: 10}} />
|
<NavigationControl style={{left: 10, top: 10}} />
|
||||||
|
<ScaleControl maxWidth={200} unit="metric" style={{left: 10, bottom: 10}} />
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</ReactMapGl>
|
</ReactMapGl>
|
||||||
|
|
5
frontend/src/components/Map/styles.module.less
Normal file
5
frontend/src/components/Map/styles.module.less
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
:global(.mapboxgl-ctrl-scale) {
|
||||||
|
height: 16px;
|
||||||
|
line-height: 14px;
|
||||||
|
background: none;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export {default as Avatar} from './Avatar'
|
export {default as Avatar} from './Avatar'
|
||||||
|
export {default as ColorMapLegend} from './ColorMapLegend'
|
||||||
export {default as FileDrop} from './FileDrop'
|
export {default as FileDrop} from './FileDrop'
|
||||||
export {default as FileUploadField} from './FileUploadField'
|
export {default as FileUploadField} from './FileUploadField'
|
||||||
export {default as FormattedDate} from './FormattedDate'
|
export {default as FormattedDate} from './FormattedDate'
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
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, Checkbox} from 'semantic-ui-react'
|
import {List, Select, Input, Divider, Checkbox, Header} from 'semantic-ui-react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MapConfig,
|
MapConfig,
|
||||||
setMapConfigFlag as setMapConfigFlagAction,
|
setMapConfigFlag as setMapConfigFlagAction,
|
||||||
initialState as defaultMapConfig,
|
initialState as defaultMapConfig,
|
||||||
} from 'reducers/mapConfig'
|
} from 'reducers/mapConfig'
|
||||||
|
import {colorByDistance} from 'mapstyles'
|
||||||
|
import {ColorMapLegend} from 'components'
|
||||||
|
|
||||||
const BASEMAP_STYLE_OPTIONS = [
|
const BASEMAP_STYLE_OPTIONS = [
|
||||||
{value: 'positron', key: 'positron', text: 'Positron'},
|
{value: 'positron', key: 'positron', text: 'Positron'},
|
||||||
|
@ -29,11 +31,15 @@ function LayerSidebar({
|
||||||
mapConfig: MapConfig
|
mapConfig: MapConfig
|
||||||
setMapConfigFlag: (flag: string, value: unknown) => void
|
setMapConfigFlag: (flag: string, value: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const {baseMap: {style}, obsRoads: {show, showUntagged, attribute, maxCount}} = mapConfig
|
const {
|
||||||
|
baseMap: {style},
|
||||||
|
obsRoads: {show: showRoads, showUntagged, attribute, maxCount},
|
||||||
|
obsEvents: {show: showEvents},
|
||||||
|
} = mapConfig
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<List>
|
<List relaxed>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<List.Header>Basemap Style</List.Header>
|
<List.Header>Basemap Style</List.Header>
|
||||||
<Select
|
<Select
|
||||||
|
@ -45,50 +51,78 @@ function LayerSidebar({
|
||||||
<Divider />
|
<Divider />
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={showUntagged}
|
toggle
|
||||||
onChange={() => setMapConfigFlag('obsRoads.showUntagged', !showUntagged)}
|
size="small"
|
||||||
label="Untagged roads"
|
id="obsRoads.show"
|
||||||
|
style={{float: 'right'}}
|
||||||
|
checked={showRoads}
|
||||||
|
onChange={() => setMapConfigFlag('obsRoads.show', !showRoads)}
|
||||||
/>
|
/>
|
||||||
|
<label htmlFor="obsRoads.show">
|
||||||
|
<Header as="h4">Road segments</Header>
|
||||||
|
</label>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
{showRoads && (
|
||||||
|
<>
|
||||||
|
<List.Item>
|
||||||
|
<Checkbox
|
||||||
|
checked={showUntagged}
|
||||||
|
onChange={() => setMapConfigFlag('obsRoads.showUntagged', !showUntagged)}
|
||||||
|
label="Include roads without data"
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<List.Header>Color based on</List.Header>
|
||||||
|
<Select
|
||||||
|
fluid
|
||||||
|
options={ROAD_ATTRIBUTE_OPTIONS}
|
||||||
|
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}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={show}
|
toggle
|
||||||
onChange={() => setMapConfigFlag('obsRoads.show', !show)}
|
size="small"
|
||||||
label="OBS Roads"
|
id="obsEvents.show"
|
||||||
|
style={{float: 'right'}}
|
||||||
|
checked={showEvents}
|
||||||
|
onChange={() => setMapConfigFlag('obsEvents.show', !showEvents)}
|
||||||
/>
|
/>
|
||||||
|
<label htmlFor="obsEvents.show">
|
||||||
|
<Header as="h4">Event points</Header>
|
||||||
|
</label>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
{showEvents && <><List.Item>
|
||||||
<List.Header>Color based on</List.Header>
|
<ColorMapLegend map={_.chunk(colorByDistance('distance_overtaker')[3].slice(3), 2)} />
|
||||||
<Select
|
|
||||||
fluid
|
|
||||||
options={ROAD_ATTRIBUTE_OPTIONS}
|
|
||||||
value={attribute}
|
|
||||||
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(
|
export default connect(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
mapConfig: _.merge(
|
mapConfig: _.merge(
|
||||||
{},
|
{},
|
||||||
defaultMapConfig,
|
defaultMapConfig,
|
||||||
(state as any).mapConfig as MapConfig,
|
(state as any).mapConfig as MapConfig
|
||||||
//
|
//
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -3,11 +3,11 @@ import _ from 'lodash'
|
||||||
import {Button} from 'semantic-ui-react'
|
import {Button} from 'semantic-ui-react'
|
||||||
import {Layer, Source} from 'react-map-gl'
|
import {Layer, Source} from 'react-map-gl'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
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, colorByCount, reds} from 'mapstyles'
|
import {colorByDistance, colorByCount, reds} from 'mapstyles'
|
||||||
|
import {useMapConfig} from 'reducers/mapConfig'
|
||||||
|
|
||||||
import RoadInfo from './RoadInfo'
|
import RoadInfo from './RoadInfo'
|
||||||
import LayerSidebar from './LayerSidebar'
|
import LayerSidebar from './LayerSidebar'
|
||||||
|
@ -48,7 +48,7 @@ const getRoadsLayer = (colorAttribute, maxCount) =>
|
||||||
} else {
|
} else {
|
||||||
draft.filter = draft.filter[1] // remove '!'
|
draft.filter = draft.filter[1] // remove '!'
|
||||||
}
|
}
|
||||||
draft.paint['line-width'][6] = 6 // scale bigger on zoom
|
draft.paint['line-width'][6] = 6 // scale bigger on zoom
|
||||||
draft.paint['line-color'] = colorAttribute.startsWith('distance_')
|
draft.paint['line-color'] = colorAttribute.startsWith('distance_')
|
||||||
? colorByDistance(colorAttribute)
|
? colorByDistance(colorAttribute)
|
||||||
: colorAttribute.endsWith('_count')
|
: colorAttribute.endsWith('_count')
|
||||||
|
@ -58,10 +58,52 @@ const getRoadsLayer = (colorAttribute, maxCount) =>
|
||||||
draft.paint['line-opacity'][5] = 13
|
draft.paint['line-opacity'][5] = 13
|
||||||
})
|
})
|
||||||
|
|
||||||
function MapPage({mapConfig}) {
|
const getEventsLayer = () => ({
|
||||||
|
id: 'obs_events',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'obs',
|
||||||
|
'source-layer': 'obs_events',
|
||||||
|
paint: {
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 3, 17, 8],
|
||||||
|
'circle-color': colorByDistance('distance_overtaker'),
|
||||||
|
},
|
||||||
|
minzoom: 11,
|
||||||
|
})
|
||||||
|
|
||||||
|
const getEventsTextLayer = () => ({
|
||||||
|
id: 'obs_events_text',
|
||||||
|
type: 'symbol',
|
||||||
|
minzoom: 18,
|
||||||
|
source: 'obs',
|
||||||
|
'source-layer': 'obs_events',
|
||||||
|
layout: {
|
||||||
|
'text-field': [
|
||||||
|
'number-format',
|
||||||
|
['get', 'distance_overtaker'],
|
||||||
|
{'min-fraction-digits': 2, 'max-fraction-digits': 2},
|
||||||
|
],
|
||||||
|
'text-allow-overlap': true,
|
||||||
|
'text-font': ['Open Sans Bold', 'Arial Unicode MS Regular'],
|
||||||
|
'text-size': 14,
|
||||||
|
'text-keep-upright': false,
|
||||||
|
'text-anchor': 'left',
|
||||||
|
'text-radial-offset': 1,
|
||||||
|
'text-rotate': ['-', 90, ['*', ['get', 'course'], 180/Math.PI]],
|
||||||
|
'text-rotation-alignment': 'map',
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-halo-color': 'rgba(255, 255, 255, 1)',
|
||||||
|
'text-halo-width': 1,
|
||||||
|
'text-opacity': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.3, 1],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function MapPage() {
|
||||||
const {obsMapSource} = useConfig() || {}
|
const {obsMapSource} = useConfig() || {}
|
||||||
const [clickLocation, setClickLocation] = useState<{longitude: number; latitude: number} | null>(null)
|
const [clickLocation, setClickLocation] = useState<{longitude: number; latitude: number} | null>(null)
|
||||||
|
|
||||||
|
const mapConfig = useMapConfig()
|
||||||
|
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
let node = e.target
|
let node = e.target
|
||||||
|
@ -79,13 +121,27 @@ function MapPage({mapConfig}) {
|
||||||
|
|
||||||
const [layerSidebar, setLayerSidebar] = useState(true)
|
const [layerSidebar, setLayerSidebar] = useState(true)
|
||||||
|
|
||||||
const showUntagged = mapConfig?.obsRoads?.showUntagged ?? true
|
const {
|
||||||
const roadsLayerColorAttribute = mapConfig?.obsRoads?.attribute ?? 'distance_overtaker_mean'
|
obsRoads: {attribute, maxCount},
|
||||||
const roadsLayerMaxCount = mapConfig?.obsRoads?.maxCount ?? 20
|
} = mapConfig
|
||||||
const roadsLayer = useMemo(() => getRoadsLayer(roadsLayerColorAttribute, roadsLayerMaxCount), [
|
|
||||||
roadsLayerColorAttribute,
|
const layers = []
|
||||||
roadsLayerMaxCount,
|
|
||||||
])
|
if (mapConfig.obsRoads.show && mapConfig.obsRoads.showUntagged) {
|
||||||
|
layers.push(untaggedRoadsLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const roadsLayer = useMemo(() => getRoadsLayer(attribute, maxCount), [attribute, maxCount])
|
||||||
|
if (mapConfig.obsRoads.show) {
|
||||||
|
layers.push(roadsLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsLayer = useMemo(() => getEventsLayer(), [])
|
||||||
|
const eventsTextLayer = useMemo(() => getEventsTextLayer(), [])
|
||||||
|
if (mapConfig.obsEvents.show) {
|
||||||
|
layers.push(eventsLayer)
|
||||||
|
layers.push(eventsTextLayer)
|
||||||
|
}
|
||||||
|
|
||||||
if (!obsMapSource) {
|
if (!obsMapSource) {
|
||||||
return null
|
return null
|
||||||
|
@ -113,8 +169,9 @@ function MapPage({mapConfig}) {
|
||||||
onClick={() => setLayerSidebar(layerSidebar ? false : true)}
|
onClick={() => setLayerSidebar(layerSidebar ? false : true)}
|
||||||
/>
|
/>
|
||||||
<Source id="obs" {...obsMapSource}>
|
<Source id="obs" {...obsMapSource}>
|
||||||
{showUntagged && <Layer key={untaggedRoadsLayer.id} {...untaggedRoadsLayer} />}
|
{layers.map((layer) => (
|
||||||
<Layer key={roadsLayer.id} {...roadsLayer} />
|
<Layer key={layer.id} {...layer} />
|
||||||
|
))}
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
<RoadInfo {...{clickLocation}} />
|
<RoadInfo {...{clickLocation}} />
|
||||||
|
@ -124,5 +181,3 @@ function MapPage({mapConfig}) {
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((state) => ({mapConfig: state.mapConfig}))(MapPage)
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {useMemo} from 'react'
|
||||||
|
import {useSelector} from 'react-redux'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
@ -14,12 +16,15 @@ export type MapConfig = {
|
||||||
baseMap: {
|
baseMap: {
|
||||||
style: BaseMapStyle
|
style: BaseMapStyle
|
||||||
}
|
}
|
||||||
obsRoads:{
|
obsRoads: {
|
||||||
show: boolean
|
show: boolean
|
||||||
showUntagged: boolean
|
showUntagged: boolean
|
||||||
attribute: RoadAttribute
|
attribute: RoadAttribute
|
||||||
maxCount: number
|
maxCount: number
|
||||||
}
|
}
|
||||||
|
obsEvents: {
|
||||||
|
show: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialState: MapConfig = {
|
export const initialState: MapConfig = {
|
||||||
|
@ -32,19 +37,27 @@ export const initialState: MapConfig = {
|
||||||
attribute: 'distance_overtaker_median',
|
attribute: 'distance_overtaker_median',
|
||||||
maxCount: 20,
|
maxCount: 20,
|
||||||
},
|
},
|
||||||
|
obsEvents: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type MapConfigAction =
|
type MapConfigAction = {type: 'MAP_CONFIG.SET_FLAG'; payload: {flag: string; value: any}}
|
||||||
{type: 'MAP_CONFIG.SET_FLAG', payload: {flag: string, value: any}}
|
|
||||||
|
|
||||||
export function setMapConfigFlag(flag: string, value: unknown): MapConfigAction {
|
export function setMapConfigFlag(flag: string, value: unknown): MapConfigAction {
|
||||||
return {type: 'MAP_CONFIG.SET_FLAG', payload: {flag, value}}
|
return {type: 'MAP_CONFIG.SET_FLAG', payload: {flag, value}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMapConfig() {
|
||||||
|
const mapConfig = useSelector((state) => state.mapConfig)
|
||||||
|
const result = useMemo(() => _.merge({}, initialState, mapConfig), [mapConfig])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export default function mapConfigReducer(state: MapConfig = initialState, action: MapConfigAction) {
|
export default function mapConfigReducer(state: MapConfig = initialState, action: MapConfigAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'MAP_CONFIG.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