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", "classnames": "^2.3.1",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"fomantic-ui-less": "^2.8.8", "fomantic-ui-less": "^2.8.8",
"immer": "^9.0.7",
"luxon": "^1.28.0", "luxon": "^1.28.0",
"maplibre-gl": "^1.15.2", "maplibre-gl": "^1.15.2",
"mini-css-extract-plugin": "^2.4.5", "mini-css-extract-plugin": "^2.4.5",
@ -4873,6 +4874,15 @@
"node": ">=0.10.0" "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": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -12790,6 +12800,11 @@
"optional": true, "optional": true,
"peer": 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": { "import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import bright from './bright.json'
import positron from './positron.json' import positron from './positron.json'
export {bright, positron} export {bright, positron}
export const baseMapStyles = {bright, positron}
export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC') { export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC') {
return [ 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 api from 'api'
import styles from './styles.module.less'
const UNITS = {distanceOvertaker: 'm', distanceStationary: 'm', speed: 'm/s'} const UNITS = {distanceOvertaker: 'm', distanceStationary: 'm', speed: 'm/s'}
const LABELS = {distanceOvertaker: 'Overtaker', distanceStationary: 'Stationary', speed: 'Speed'} const LABELS = {distanceOvertaker: 'Overtaker', distanceStationary: 'Stationary', speed: 'Speed'}
const ZONE_COLORS = {urban: 'olive', rural: 'brown', motorway: 'purple'} const ZONE_COLORS = {urban: 'olive', rural: 'brown', motorway: 'purple'}
@ -90,7 +92,7 @@ export default function RoadInfo({clickLocation}) {
const loading = info == null 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 = const content =
!loading && !info.road ? ( !loading && !info.road ? (

View file

@ -1,5 +1,6 @@
import React, {useState, useCallback} from 'react' import React, {useState, useCallback} from 'react'
import _ from 'lodash' import _ from 'lodash'
import {Sidebar, Button} from 'semantic-ui-react'
import {Layer, Source} from 'react-map-gl' import {Layer, Source} from 'react-map-gl'
import {Page, Map} from 'components' import {Page, Map} from 'components'
@ -8,9 +9,9 @@ import {useConfig} from 'config'
import {roadsLayer} from '../../mapstyles' import {roadsLayer} from '../../mapstyles'
import RoadInfo from './RoadInfo' import RoadInfo from './RoadInfo'
import LayerSidebar from './LayerSidebar'
import styles from './styles.module.less' import styles from './styles.module.less'
export default function MapPage() { 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)
@ -30,6 +31,8 @@ export default function MapPage() {
[setClickLocation] [setClickLocation]
) )
const [layerSidebar, setLayerSidebar] = useState(true)
if (!obsMapSource) { if (!obsMapSource) {
return null return null
} }
@ -37,7 +40,20 @@ export default function MapPage() {
return ( return (
<Page fullScreen> <Page fullScreen>
<div className={styles.mapContainer}> <div className={styles.mapContainer}>
{layerSidebar && <div className={styles.mapSidebar}><LayerSidebar /></div>}
<div className={styles.map}>
<Map viewportFromUrl onClick={onClick}> <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}> <Source id="obs" {...obsMapSource}>
<Layer {...roadsLayer} /> <Layer {...roadsLayer} />
</Source> </Source>
@ -45,6 +61,7 @@ export default function MapPage() {
<RoadInfo {...{clickLocation}} /> <RoadInfo {...{clickLocation}} />
</Map> </Map>
</div> </div>
</div>
</Page> </Page>
) )
} }

View file

@ -2,8 +2,21 @@
.mapContainer { .mapContainer {
height: calc(100vh - @menuHeight); height: calc(100vh - @menuHeight);
background: red; background: #F0F0F3;
position: relative; 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 { .mapInfoBox {
@ -15,3 +28,4 @@
overflow: auto; overflow: auto;
margin: 20px; margin: 20px;
} }

View file

@ -1,5 +1,6 @@
import {combineReducers} from 'redux' import {combineReducers} from 'redux'
import login from './login' 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' import rootReducer from './reducers'
const enhancer = compose(persistState(['login'])) const enhancer = compose(persistState(['login', 'mapConfig']))
const store = createStore(rootReducer, undefined, enhancer) const store = createStore(rootReducer, undefined, enhancer)

View file

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