Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getQueryStore,
getRouteStore,
getSettingsStore,
getCurrentLocationStore,
} from '@/stores/Stores'
import MapComponent from '@/map/MapComponent'
import MapOptions from '@/map/MapOptions'
Expand All @@ -22,6 +23,7 @@ import { QueryStoreState, RequestState } from '@/stores/QueryStore'
import { RouteStoreState } from '@/stores/RouteStore'
import { MapOptionsStoreState } from '@/stores/MapOptionsStore'
import { ErrorStoreState } from '@/stores/ErrorStore'
import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore'
import Search from '@/sidebar/search/Search'
import ErrorMessage from '@/sidebar/ErrorMessage'
import useBackgroundLayer from '@/layers/UseBackgroundLayer'
Expand All @@ -45,6 +47,7 @@ import useExternalMVTLayer from '@/layers/UseExternalMVTLayer'
import LocationButton from '@/map/LocationButton'
import { SettingsContext } from '@/contexts/SettingsContext'
import usePOIsLayer from '@/layers/UsePOIsLayer'
import useCurrentLocationLayer from '@/layers/UseCurrentLocationLayer'

export const POPUP_CONTAINER_ID = 'popup-container'
export const SIDEBAR_CONTENT_ID = 'sidebar-content'
Expand All @@ -59,6 +62,7 @@ export default function App() {
const [pathDetails, setPathDetails] = useState(getPathDetailsStore().state)
const [mapFeatures, setMapFeatures] = useState(getMapFeatureStore().state)
const [pois, setPOIs] = useState(getPOIsStore().state)
const [currentLocation, setCurrentLocation] = useState(getCurrentLocationStore().state)

const map = getMap()

Expand All @@ -72,6 +76,7 @@ export default function App() {
const onPathDetailsChanged = () => setPathDetails(getPathDetailsStore().state)
const onMapFeaturesChanged = () => setMapFeatures(getMapFeatureStore().state)
const onPOIsChanged = () => setPOIs(getPOIsStore().state)
const onCurrentLocationChanged = () => setCurrentLocation(getCurrentLocationStore().state)

getSettingsStore().register(onSettingsChanged)
getQueryStore().register(onQueryChanged)
Expand All @@ -82,6 +87,7 @@ export default function App() {
getPathDetailsStore().register(onPathDetailsChanged)
getMapFeatureStore().register(onMapFeaturesChanged)
getPOIsStore().register(onPOIsChanged)
getCurrentLocationStore().register(onCurrentLocationChanged)

onQueryChanged()
onInfoChanged()
Expand All @@ -91,6 +97,7 @@ export default function App() {
onPathDetailsChanged()
onMapFeaturesChanged()
onPOIsChanged()
onCurrentLocationChanged()

return () => {
getSettingsStore().deregister(onSettingsChanged)
Expand All @@ -102,6 +109,7 @@ export default function App() {
getPathDetailsStore().deregister(onPathDetailsChanged)
getMapFeatureStore().deregister(onMapFeaturesChanged)
getPOIsStore().deregister(onPOIsChanged)
getCurrentLocationStore().deregister(onCurrentLocationChanged)
}
}, [])

Expand All @@ -116,6 +124,7 @@ export default function App() {
useQueryPointsLayer(map, query.queryPoints)
usePathDetailsLayer(map, pathDetails)
usePOIsLayer(map, pois)
useCurrentLocationLayer(map, currentLocation)

const isSmallScreen = useMediaQuery({ query: '(max-width: 44rem)' })
return (
Expand All @@ -138,6 +147,7 @@ export default function App() {
error={error}
encodedValues={info.encoded_values}
drawAreas={settings.drawAreasEnabled}
currentLocation={currentLocation}
/>
) : (
<LargeScreenLayout
Expand All @@ -148,6 +158,7 @@ export default function App() {
error={error}
encodedValues={info.encoded_values}
drawAreas={settings.drawAreasEnabled}
currentLocation={currentLocation}
/>
)}
</div>
Expand All @@ -160,12 +171,22 @@ interface LayoutProps {
route: RouteStoreState
map: Map
mapOptions: MapOptionsStoreState
currentLocation: CurrentLocationStoreState
error: ErrorStoreState
encodedValues: object[]
drawAreas: boolean
}

function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues, drawAreas }: LayoutProps) {
function LargeScreenLayout({
query,
route,
map,
error,
mapOptions,
encodedValues,
drawAreas,
currentLocation,
}: LayoutProps) {
const [showSidebar, setShowSidebar] = useState(true)
const [showCustomModelBox, setShowCustomModelBox] = useState(false)
return (
Expand Down Expand Up @@ -216,7 +237,7 @@ function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues
<div className={styles.popupContainer} id={POPUP_CONTAINER_ID} />
<div className={styles.onMapRightSide}>
<MapOptions {...mapOptions} />
<LocationButton queryPoints={query.queryPoints} />
<LocationButton currentLocation={currentLocation} />
</div>
<div className={styles.map}>
<MapComponent map={map} />
Expand All @@ -229,7 +250,16 @@ function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues
)
}

function SmallScreenLayout({ query, route, map, error, mapOptions, encodedValues, drawAreas }: LayoutProps) {
function SmallScreenLayout({
query,
route,
map,
error,
mapOptions,
encodedValues,
drawAreas,
currentLocation,
}: LayoutProps) {
return (
<>
<div className={styles.smallScreenSidebar}>
Expand All @@ -248,7 +278,7 @@ function SmallScreenLayout({ query, route, map, error, mapOptions, encodedValues
<div className={styles.smallScreenMapOptions}>
<div className={styles.onMapRightSide}>
<MapOptions {...mapOptions} />
<LocationButton queryPoints={query.queryPoints} />
<LocationButton currentLocation={currentLocation} />
</div>
</div>

Expand Down
34 changes: 33 additions & 1 deletion src/actions/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export class ToggleExternalMVTLayer implements Action {

export class MapIsLoaded implements Action {}

export class ZoomMapToPoint implements Action {
export class MoveMapToPoint implements Action {
readonly coordinate: Coordinate

constructor(coordinate: Coordinate) {
Expand Down Expand Up @@ -272,3 +272,35 @@ export class SetPOIs implements Action {
this.pois = pois
}
}

/**
* Start watching the location and synchronizing the view.
*/
export class StartWatchCurrentLocation implements Action {}
export class StopWatchCurrentLocation implements Action {}

/**
* Start synchronizing the view again.
*/
export class StartSyncCurrentLocation implements Action {}
export class StopSyncCurrentLocation implements Action {}

export class CurrentLocationError implements Action {
readonly error: string

constructor(error: string) {
this.error = error
}
}

export class CurrentLocation implements Action {
readonly coordinate: Coordinate
readonly accuracy: number
readonly heading: number | null

constructor(coordinate: Coordinate, accuracy: number, heading: number | null) {
this.coordinate = coordinate
this.accuracy = accuracy
this.heading = heading
}
}
4 changes: 4 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getQueryStore,
getRouteStore,
getSettingsStore,
getCurrentLocationStore,
setStores,
} from '@/stores/Stores'
import Dispatcher from '@/stores/Dispatcher'
Expand All @@ -31,6 +32,7 @@ import MapFeatureStore from '@/stores/MapFeatureStore'
import SettingsStore from '@/stores/SettingsStore'
import { ErrorAction, InfoReceived } from '@/actions/Actions'
import POIsStore from '@/stores/POIsStore'
import CurrentLocationStore from '@/stores/CurrentLocationStore'
import { setDistanceFormat } from '@/Converters'
import { AddressParseResult } from '@/pois/AddressParseResult'

Expand Down Expand Up @@ -61,6 +63,7 @@ setStores({
pathDetailsStore: new PathDetailsStore(),
mapFeatureStore: new MapFeatureStore(),
poisStore: new POIsStore(),
currentLocationStore: new CurrentLocationStore(),
})

setMap(createMap())
Expand All @@ -75,6 +78,7 @@ Dispatcher.register(getMapOptionsStore())
Dispatcher.register(getPathDetailsStore())
Dispatcher.register(getMapFeatureStore())
Dispatcher.register(getPOIsStore())
Dispatcher.register(getCurrentLocationStore())

// register map action receiver
const smallScreenMediaQuery = window.matchMedia('(max-width: 44rem)')
Expand Down
129 changes: 129 additions & 0 deletions src/layers/UseCurrentLocationLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Feature, Map } from 'ol'
import { useEffect, useRef } from 'react'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import { Circle, Circle as CircleGeom, Point } from 'ol/geom'
import { Circle as CircleStyle, Fill, RegularShape, Stroke, Style } from 'ol/style'
import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore'
import { fromLonLat } from 'ol/proj'

export default function useCurrentLocationLayer(map: Map, locationState: CurrentLocationStoreState) {
const layerRef = useRef<VectorLayer<VectorSource> | null>(null)
const positionFeatureRef = useRef<Feature | null>(null)
const accuracyFeatureRef = useRef<Feature | null>(null)
const headingFeatureRef = useRef<Feature | null>(null)

useEffect(() => {
if (!locationState.enabled) {
if (layerRef.current) {
map.removeLayer(layerRef.current)
layerRef.current = null
}
return
} else if (!layerRef.current) {
const layer = createLocationLayer()
layer.getSource()?.addFeature((positionFeatureRef.current = new Feature()))
layer.getSource()?.addFeature((accuracyFeatureRef.current = new Feature()))
layer.getSource()?.addFeature((headingFeatureRef.current = new Feature()))
map.addLayer(layer)

layerRef.current = layer
}

return () => {
if (layerRef.current) {
map.removeLayer(layerRef.current)
layerRef.current = null
}
}
}, [locationState.enabled])

useEffect(() => {
if (
!locationState.enabled ||
!locationState.coordinate ||
!layerRef.current ||
// typescript complaints without the following
!positionFeatureRef.current ||
!accuracyFeatureRef.current ||
!headingFeatureRef.current
)
return

const coord = fromLonLat([locationState.coordinate.lng, locationState.coordinate.lat])
positionFeatureRef.current.setGeometry(new Point(coord))
accuracyFeatureRef.current.setGeometry(new Circle(coord, locationState.accuracy))

// set heading feature position (style will handle the triangle and rotation)
if (locationState.heading != null) {
headingFeatureRef.current.setGeometry(new Point(coord))
headingFeatureRef.current.set('heading', locationState.heading)
} else {
headingFeatureRef.current.setGeometry(undefined)
headingFeatureRef.current.unset('heading') // not strictly necessary
}

if (locationState.syncView) {
const currentZoom = map.getView().getZoom()
const targetZoom = currentZoom == undefined || currentZoom < 16 ? 16 : currentZoom
const zoomDifference = Math.abs(targetZoom - (currentZoom || 0))
if (zoomDifference > 0.1) {
map.getView().animate({ zoom: targetZoom, center: coord, duration: 400 })
} else {
// for smaller zoom changes set center without animation to avoid pulsing of map
map.getView().setCenter(coord)
}
}
}, [
locationState.coordinate,
locationState.accuracy,
locationState.heading,
locationState.syncView,
locationState.enabled,
])
}

function createLocationLayer(): VectorLayer<VectorSource> {
return new VectorLayer({
source: new VectorSource(),
style: feature => {
const geometry = feature.getGeometry()
if (geometry instanceof Point) {
const heading = feature.get('heading')
if (heading !== undefined) {
// triangle style for heading direction
return new Style({
image: new RegularShape({
points: 3,
radius: 8,
displacement: [0, 9],
rotation: (heading * Math.PI) / 180, // convert degrees to radians
fill: new Fill({ color: '#368fe8' }),
stroke: new Stroke({ color: '#FFFFFF', width: 1 }),
}),
zIndex: 1,
})
} else {
// blue dot style for position
return new Style({
image: new CircleStyle({
radius: 8,
fill: new Fill({ color: '#368fe8' }),
stroke: new Stroke({ color: '#FFFFFF', width: 2 }),
}),
zIndex: 2, // above the others
})
}
} else if (geometry instanceof CircleGeom) {
// accuracy circle style
return new Style({
fill: new Fill({ color: 'rgba(66, 133, 244, 0.1)' }),
stroke: new Stroke({ color: 'rgba(66, 133, 244, 0.3)', width: 1 }),
zIndex: 0, // behind the others
})
}
return []
},
zIndex: 4, // layer itself should be above paths and query points
})
}
2 changes: 1 addition & 1 deletion src/layers/UseQueryPointsLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function removeDragInteractions(map: Map) {
.forEach(i => map.removeInteraction(i))
}

function addDragInteractions(map: Map, queryPointsLayer: VectorLayer<VectorSource<Feature<Geometry>>>) {
function addDragInteractions(map: Map, queryPointsLayer: VectorLayer<VectorSource>) {
let tmp = queryPointsLayer.getSource()
if (tmp == null) throw new Error('source must not be null') // typescript requires this
const modify = new Modify({
Expand Down
4 changes: 2 additions & 2 deletions src/map/ContextMenuContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { coordinateToText } from '@/Converters'
import styles from './ContextMenuContent.module.css'
import QueryStore, { QueryPoint, QueryPointType } from '@/stores/QueryStore'
import Dispatcher from '@/stores/Dispatcher'
import { AddPoint, SetPoint, ZoomMapToPoint } from '@/actions/Actions'
import { AddPoint, SetPoint, MoveMapToPoint } from '@/actions/Actions'
import { RouteStoreState } from '@/stores/RouteStore'
import { findNextWayPoint } from '@/map/findNextWayPoint'
import { tr } from '@/translation/Translation'
Expand Down Expand Up @@ -143,7 +143,7 @@ export function ContextMenuContent({
className={styles.entry}
onClick={() => {
onSelect()
Dispatcher.dispatch(new ZoomMapToPoint(coordinate))
Dispatcher.dispatch(new MoveMapToPoint(coordinate))
}}
>
{tr('center_map')}
Expand Down
Loading