Add sidebar for configuring map layers - make basemap style choosable

This commit is contained in:
Paul Bienkowski 2021-12-03 22:20:41 +01:00
parent 83e945c7ff
commit f0c715bcbc
12 changed files with 138 additions and 16 deletions

View file

@ -13,6 +13,7 @@
"classnames": "^2.3.1",
"downloadjs": "^1.4.7",
"fomantic-ui-less": "^2.8.8",
"immer": "^9.0.7",
"luxon": "^1.28.0",
"maplibre-gl": "^1.15.2",
"mini-css-extract-plugin": "^2.4.5",
@ -4873,6 +4874,15 @@
"node": ">=0.10.0"
}
},
"node_modules/immer": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz",
"integrity": "sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -12790,6 +12800,11 @@
"optional": true,
"peer": true
},
"immer": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz",
"integrity": "sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA=="
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",

View file

@ -12,6 +12,7 @@
"classnames": "^2.3.1",
"downloadjs": "^1.4.7",
"fomantic-ui-less": "^2.8.8",
"immer": "^9.0.7",
"luxon": "^1.28.0",
"maplibre-gl": "^1.15.2",
"mini-css-extract-plugin": "^2.4.5",

View file

@ -1,4 +1,5 @@
import React, {useState, useCallback, useMemo, useEffect} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import ReactMapGl, {WebMercatorViewport, AttributionControl, NavigationControl} from 'react-map-gl'
import turfBbox from '@turf/bbox'
@ -6,10 +7,13 @@ import {useHistory, useLocation} from 'react-router-dom'
import {useConfig} from 'config'
import {basemap} from '../../mapstyles'
import {baseMapStyles} from '../../mapstyles'
const EMPTY_VIEWPORT = {longitude: 0, latitude: 0, zoom: 0}
export const withBaseMapStyle = connect((state) => ({baseMapStyle: state.mapConfig?.baseMap?.style ?? 'positron'}))
function parseHash(v) {
if (!v) return null
const m = v.match(/^#([0-9\.]+)\/([0-9\.]+)\/([0-9\.]+)$/)
@ -41,15 +45,17 @@ function useViewportFromUrl() {
}
export default function Map({
function Map({
viewportFromUrl,
children,
boundsFromJson,
baseMapStyle,
...props
}: {
viewportFromUrl?: boolean
children: React.ReactNode
boundsFromJson: GeoJSON.Geometry
children: React.ReactNode
boundsFromJson: GeoJSON.Geometry
baseMapStyle: string
}) {
const [viewportState, setViewportState] = useState(EMPTY_VIEWPORT)
const [viewportUrl, setViewportUrl] = useViewportFromUrl()
@ -81,7 +87,7 @@ export default function Map({
}, [boundsFromJson])
return (
<ReactMapGl mapStyle={basemap} width="100%" height="100%" onViewportChange={setViewport} {...viewport} {...props}>
<ReactMapGl mapStyle={baseMapStyles[baseMapStyle]} width="100%" height="100%" onViewportChange={setViewport} {...viewport} {...props}>
<AttributionControl
style={{right: 0, bottom: 0}}
customAttribution={[
@ -96,3 +102,5 @@ export default function Map({
</ReactMapGl>
)
}
export default withBaseMapStyle(Map)

View file

@ -4,6 +4,7 @@ import bright from './bright.json'
import positron from './positron.json'
export {bright, positron}
export const baseMapStyles = {bright, positron}
export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC') {
return [

View file

@ -0,0 +1,29 @@
import React from 'react'
import {connect} from 'react-redux'
import {List, Select} from 'semantic-ui-react'
import * as mapConfigActions from 'reducers/mapConfig'
const BASEMAP_STYLE_OPTIONS = [
{key: 'positron', value: 'positron', text: 'Positron'},
{key: 'bright', value: 'bright', text: 'OSM Bright'},
]
function LayerSidebar({mapConfig, setBasemapStyle}) {
return (
<div>
<List>
<List.Item>
<List.Header>Basemap Style</List.Header>
<Select
options={BASEMAP_STYLE_OPTIONS}
value={mapConfig?.baseMap?.style ?? 'positron'}
onChange={(_e, {value}) => setBasemapStyle(value)}
/>
</List.Item>
</List>
</div>
)
}
export default connect((state) => ({mapConfig: state.mapConfig}), mapConfigActions)(LayerSidebar)

View file

@ -8,6 +8,8 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators'
import api from 'api'
import styles from './styles.module.less'
const UNITS = {distanceOvertaker: 'm', distanceStationary: 'm', speed: 'm/s'}
const LABELS = {distanceOvertaker: 'Overtaker', distanceStationary: 'Stationary', speed: 'Speed'}
const ZONE_COLORS = {urban: 'olive', rural: 'brown', motorway: 'purple'}
@ -90,7 +92,7 @@ export default function RoadInfo({clickLocation}) {
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 ? (

View file

@ -1,5 +1,6 @@
import React, {useState, useCallback} from 'react'
import _ from 'lodash'
import {Sidebar, Button} from 'semantic-ui-react'
import {Layer, Source} from 'react-map-gl'
import {Page, Map} from 'components'
@ -8,9 +9,9 @@ import {useConfig} from 'config'
import {roadsLayer} from '../../mapstyles'
import RoadInfo from './RoadInfo'
import LayerSidebar from './LayerSidebar'
import styles from './styles.module.less'
export default function MapPage() {
const {obsMapSource} = useConfig() || {}
const [clickLocation, setClickLocation] = useState<{longitude: number; latitude: number} | null>(null)
@ -30,6 +31,8 @@ export default function MapPage() {
[setClickLocation]
)
const [layerSidebar, setLayerSidebar] = useState(true)
if (!obsMapSource) {
return null
}
@ -37,13 +40,27 @@ export default function MapPage() {
return (
<Page fullScreen>
<div className={styles.mapContainer}>
<Map viewportFromUrl onClick={onClick}>
<Source id="obs" {...obsMapSource}>
<Layer {...roadsLayer} />
</Source>
{layerSidebar && <div className={styles.mapSidebar}><LayerSidebar /></div>}
<div className={styles.map}>
<Map viewportFromUrl onClick={onClick}>
<Button
style={{
position: 'absolute',
left: 44,
top: 9,
}}
primary
icon="bars"
active={layerSidebar}
onClick={() => setLayerSidebar(layerSidebar ? false : true)}
/>
<Source id="obs" {...obsMapSource}>
<Layer {...roadsLayer} />
</Source>
<RoadInfo {...{clickLocation}} />
</Map>
<RoadInfo {...{clickLocation}} />
</Map>
</div>
</div>
</Page>
)

View file

@ -2,8 +2,21 @@
.mapContainer {
height: calc(100vh - @menuHeight);
background: red;
background: #F0F0F3;
position: relative;
display: flex;
align-items: stretch;
}
.mapSidebar {
width: 20rem;
background: white;
border-right: 1px solid @borderColor;
padding: 1rem;
}
.map {
flex: 1 1 0;
}
.mapInfoBox {
@ -15,3 +28,4 @@
overflow: auto;
margin: 20px;
}

View file

@ -1,5 +1,6 @@
import {combineReducers} from 'redux'
import login from './login'
import mapConfig from './mapConfig'
export default combineReducers({login})
export default combineReducers({login, mapConfig})

View file

@ -0,0 +1,33 @@
import produce from 'immer'
type BaseMapStyle = 'positron' | 'bright'
type MapConfigState = {
baseMap: {
style: BaseMapStyle
}
}
const initialState: MapConfigState = {
baseMap: {
style: 'positron',
},
}
export function setBasemapStyle(style: BaseMapStyle) {
return {type: 'MAPCONFIG.SET_BASEMAP_STYLE', payload: {style}}
}
export default function mapConfigReducer(state = initialState, action) {
switch (action.type) {
case 'MAPCONFIG.SET_BASEMAP_STYLE':
return produce(state, draft => {
if (!draft.baseMap) {
draft.baseMap = {}
}
draft.baseMap.style = action.payload.style
})
default:
return state
}
}

View file

@ -3,7 +3,7 @@ import persistState from 'redux-localstorage'
import rootReducer from './reducers'
const enhancer = compose(persistState(['login']))
const enhancer = compose(persistState(['login', 'mapConfig']))
const store = createStore(rootReducer, undefined, enhancer)

View file

@ -7,6 +7,7 @@
@primaryColor: @obsColorB4;
@secondaryColor: @obsColorG1;
@borderColor: #E0E0E0;
@menuHeight: 50px;