Add sidebar for configuring map layers - make basemap style choosable
This commit is contained in:
parent
83e945c7ff
commit
f0c715bcbc
12 changed files with 138 additions and 16 deletions
15
frontend/package-lock.json
generated
15
frontend/package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 [
|
||||||
|
|
29
frontend/src/pages/MapPage/LayerSidebar.tsx
Normal file
29
frontend/src/pages/MapPage/LayerSidebar.tsx
Normal 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)
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
|
|
33
frontend/src/reducers/mapConfig.ts
Normal file
33
frontend/src/reducers/mapConfig.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
@primaryColor: @obsColorB4;
|
@primaryColor: @obsColorB4;
|
||||||
@secondaryColor: @obsColorG1;
|
@secondaryColor: @obsColorG1;
|
||||||
|
@borderColor: #E0E0E0;
|
||||||
|
|
||||||
@menuHeight: 50px;
|
@menuHeight: 50px;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue