Skip to content

Commit 22b34ea

Browse files
committed
simplify code; add heading; use 'our' blue
1 parent 8edd353 commit 22b34ea

File tree

5 files changed

+94
-67
lines changed

5 files changed

+94
-67
lines changed

src/actions/Actions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,11 @@ export class CurrentLocationError implements Action {
296296
export class CurrentLocation implements Action {
297297
readonly coordinate: Coordinate
298298
readonly accuracy: number
299+
readonly heading: number | null
299300

300-
constructor(coordinate: Coordinate, accuracy: number) {
301+
constructor(coordinate: Coordinate, accuracy: number, heading: number | null) {
301302
this.coordinate = coordinate
302303
this.accuracy = accuracy
304+
this.heading = heading
303305
}
304306
}
Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,135 @@
11
import { Feature, Map } from 'ol'
2-
import {useEffect, useRef} from 'react'
2+
import { useEffect, useRef } from 'react'
33
import VectorLayer from 'ol/layer/Vector'
44
import VectorSource from 'ol/source/Vector'
55
import { Circle, Circle as CircleGeom, Point } from 'ol/geom'
6-
import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style'
6+
import { Circle as CircleStyle, Fill, RegularShape, Stroke, Style } from 'ol/style'
77
import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore'
88
import { fromLonLat } from 'ol/proj'
99

1010
export default function useCurrentLocationLayer(map: Map, locationState: CurrentLocationStoreState) {
1111
const layerRef = useRef<VectorLayer<VectorSource> | null>(null)
12-
const positionFeatureRef = useRef<Feature | null>(null)
13-
const accuracyFeatureRef = useRef<Feature | null>(null)
12+
const posFeatureRef = useRef<Feature | null>(null)
13+
const accFeatureRef = useRef<Feature | null>(null)
14+
const headingFeatureRef = useRef<Feature | null>(null)
1415

1516
// Create layer once when enabled
1617
useEffect(() => {
1718
if (!locationState.enabled) {
1819
if (layerRef.current) {
1920
map.removeLayer(layerRef.current)
2021
layerRef.current = null
21-
positionFeatureRef.current = null
22-
accuracyFeatureRef.current = null
22+
posFeatureRef.current = null
23+
accFeatureRef.current = null
24+
headingFeatureRef.current = null
2325
}
2426
return
2527
} else if (!layerRef.current) {
2628
const layer = createLocationLayer()
2729
const positionFeature = new Feature()
2830
const accuracyFeature = new Feature()
31+
const headingFeature = new Feature()
2932
layer.getSource()?.addFeature(positionFeature)
3033
layer.getSource()?.addFeature(accuracyFeature)
34+
layer.getSource()?.addFeature(headingFeature)
3135
map.addLayer(layer)
3236

3337
layerRef.current = layer
34-
positionFeatureRef.current = positionFeature
35-
accuracyFeatureRef.current = accuracyFeature
38+
posFeatureRef.current = positionFeature
39+
accFeatureRef.current = accuracyFeature
40+
headingFeatureRef.current = headingFeature
3641
}
3742

3843
return () => {
3944
if (layerRef.current) {
4045
map.removeLayer(layerRef.current)
4146
layerRef.current = null
42-
positionFeatureRef.current = null
43-
accuracyFeatureRef.current = null
47+
posFeatureRef.current = null
48+
accFeatureRef.current = null
49+
headingFeatureRef.current = null
4450
}
4551
}
4652
}, [locationState.enabled])
4753

4854
useEffect(() => {
49-
if (!locationState.enabled || !locationState.coordinate || !positionFeatureRef.current || !accuracyFeatureRef.current) {
55+
if (
56+
!locationState.enabled ||
57+
!locationState.coordinate ||
58+
!posFeatureRef.current ||
59+
!accFeatureRef.current ||
60+
!headingFeatureRef.current
61+
)
5062
return
51-
}
5263

5364
const coord = fromLonLat([locationState.coordinate.lng, locationState.coordinate.lat])
54-
positionFeatureRef.current.setGeometry(new Point(coord))
55-
accuracyFeatureRef.current.setGeometry(new Circle(coord, locationState.accuracy))
65+
posFeatureRef.current.setGeometry(new Point(coord))
66+
accFeatureRef.current.setGeometry(new Circle(coord, locationState.accuracy))
67+
68+
// Set heading feature position (style will handle the triangle and rotation)
69+
if (locationState.heading != null) {
70+
headingFeatureRef.current.setGeometry(new Point(coord))
71+
headingFeatureRef.current.set('heading', locationState.heading)
72+
} else {
73+
headingFeatureRef.current.setGeometry(undefined)
74+
}
5675

5776
if (locationState.syncView) {
5877
// TODO same code as for MoveMapToPoint action, but calling Dispatcher here is ugly
5978
let zoom = map.getView().getZoom()
6079
if (zoom == undefined || zoom < 8) zoom = 8
6180
map.getView().animate({ zoom: zoom, center: coord, duration: 400 })
6281
}
63-
}, [locationState.coordinate, locationState.accuracy, locationState.syncView, locationState.enabled])
82+
}, [
83+
locationState.coordinate,
84+
locationState.accuracy,
85+
locationState.heading,
86+
locationState.syncView,
87+
locationState.enabled,
88+
])
6489
}
6590

6691
function createLocationLayer(): VectorLayer<VectorSource> {
67-
const layer = new VectorLayer({
92+
return new VectorLayer({
6893
source: new VectorSource(),
6994
style: feature => {
7095
const geometry = feature.getGeometry()
7196
if (geometry instanceof Point) {
72-
// Blue dot style for position
73-
return [
74-
new Style({
97+
// Check if this is the heading feature
98+
const heading = feature.get('heading')
99+
if (heading !== undefined) {
100+
// Triangle style for heading direction
101+
return new Style({
102+
image: new RegularShape({
103+
points: 3,
104+
radius: 8,
105+
displacement: [0, 9],
106+
rotation: (heading * Math.PI) / 180, // Convert degrees to radians
107+
fill: new Fill({ color: '#368fe8' }),
108+
stroke: new Stroke({ color: '#FFFFFF', width: 1 }),
109+
}),
110+
zIndex: 1,
111+
})
112+
} else {
113+
// Blue dot style for position
114+
return new Style({
75115
image: new CircleStyle({
76116
radius: 8,
77-
fill: new Fill({
78-
color: '#4285F4',
79-
}),
80-
stroke: new Stroke({
81-
color: '#FFFFFF',
82-
width: 2,
83-
}),
117+
fill: new Fill({ color: '#368fe8' }),
118+
stroke: new Stroke({ color: '#FFFFFF', width: 2 }),
84119
}),
85-
}),
86-
]
120+
zIndex: 2, // above the others
121+
})
122+
}
87123
} else if (geometry instanceof CircleGeom) {
88124
// Accuracy circle style
89125
return new Style({
90-
fill: new Fill({
91-
color: 'rgba(66, 133, 244, 0.1)',
92-
}),
93-
stroke: new Stroke({
94-
color: 'rgba(66, 133, 244, 0.3)',
95-
width: 1,
96-
}),
126+
fill: new Fill({ color: 'rgba(66, 133, 244, 0.1)' }),
127+
stroke: new Stroke({ color: 'rgba(66, 133, 244, 0.3)', width: 1 }),
128+
zIndex: 0, // behind the others
97129
})
98130
}
99131
return []
100132
},
133+
zIndex: 4, // Above paths and query points
101134
})
102-
103-
layer.setZIndex(4) // Above paths and query points
104-
return layer
105135
}

src/map/LocationButton.tsx

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,14 @@
11
import styles from './LocationButton.module.css'
22
import Dispatcher from '@/stores/Dispatcher'
3-
import {StartSyncCurrentLocation, StartWatchCurrentLocation, StopWatchCurrentLocation} from '@/actions/Actions'
3+
import { StartSyncCurrentLocation, StartWatchCurrentLocation, StopWatchCurrentLocation } from '@/actions/Actions'
44
import LocationError from '@/map/location_error.svg'
55
import LocationSearching from '@/map/location_searching.svg'
66
import LocationOn from '@/map/location_on.svg'
77
import Location from '@/map/location.svg'
88
import LocationNotInSync from '@/map/location_not_in_sync.svg'
9-
import { useEffect, useState } from 'react'
109
import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore'
1110

1211
export default function LocationButton(props: { currentLocation: CurrentLocationStoreState }) {
13-
const [locationSearch, setLocationSearch] = useState('initial')
14-
15-
useEffect(() => {
16-
if (props.currentLocation.enabled) {
17-
if (!props.currentLocation.syncView) setLocationSearch('on_but_not_in_sync')
18-
else if (props.currentLocation.error) setLocationSearch('error')
19-
else if (props.currentLocation.coordinate != null) setLocationSearch('on')
20-
else setLocationSearch('search')
21-
} else {
22-
setLocationSearch('initial')
23-
}
24-
}, [
25-
props.currentLocation.syncView,
26-
props.currentLocation.error,
27-
props.currentLocation.enabled,
28-
props.currentLocation.coordinate,
29-
])
30-
3112
return (
3213
<div
3314
className={styles.locationOnOff}
@@ -43,11 +24,13 @@ export default function LocationButton(props: { currentLocation: CurrentLocation
4324
}
4425
}}
4526
>
46-
{locationSearch == 'initial' && <Location />}
47-
{locationSearch == 'error' && <LocationError />}
48-
{locationSearch == 'search' && <LocationSearching />}
49-
{locationSearch == 'on' && <LocationOn />}
50-
{locationSearch == 'on_but_not_in_sync' && <LocationNotInSync />}
27+
{(() => {
28+
if (props.currentLocation.error) return <LocationError />
29+
if (!props.currentLocation.enabled) return <Location />
30+
if (!props.currentLocation.syncView) return <LocationNotInSync />
31+
if (props.currentLocation.coordinate != null) return <LocationOn />
32+
return <LocationSearching />
33+
})()}
5134
</div>
5235
)
5336
}

src/map/location_on.svg

Lines changed: 2 additions & 2 deletions
Loading

src/stores/CurrentLocationStore.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface CurrentLocationStoreState {
1616
enabled: boolean
1717
syncView: boolean
1818
accuracy: number // meters
19+
heading: number | null
1920
coordinate: Coordinate | null
2021
}
2122

@@ -28,11 +29,15 @@ export default class CurrentLocationStore extends Store<CurrentLocationStoreStat
2829
enabled: false,
2930
syncView: false,
3031
accuracy: 0,
32+
heading: null,
3133
coordinate: null,
3234
})
3335
}
3436

3537
reduce(state: CurrentLocationStoreState, action: Action): CurrentLocationStoreState {
38+
// console.log('NOW ', action.constructor.name, action)
39+
// console.log('NOW state ', state)
40+
3641
if (action instanceof StartWatchCurrentLocation) {
3742
if (state.enabled) {
3843
console.log('NOW cannot start as already started. ID = ' + this.watchId)
@@ -45,6 +50,7 @@ export default class CurrentLocationStore extends Store<CurrentLocationStoreStat
4550
error: null,
4651
enabled: true,
4752
syncView: true,
53+
heading: null,
4854
coordinate: null,
4955
}
5056
} else if (action instanceof StopWatchCurrentLocation) {
@@ -53,18 +59,22 @@ export default class CurrentLocationStore extends Store<CurrentLocationStoreStat
5359
...state,
5460
error: null,
5561
enabled: false,
62+
heading: null,
5663
syncView: false,
5764
}
5865
} else if (action instanceof CurrentLocationError) {
5966
return {
6067
...state,
6168
enabled: false,
69+
syncView: false,
6270
error: action.error,
71+
heading: null,
6372
coordinate: null,
6473
}
6574
} else if (action instanceof CurrentLocation) {
6675
return {
6776
...state,
77+
heading: action.heading,
6878
accuracy: action.accuracy,
6979
coordinate: action.coordinate,
7080
}
@@ -104,7 +114,9 @@ export default class CurrentLocationStore extends Store<CurrentLocationStoreStat
104114
Dispatcher.dispatch(
105115
new CurrentLocation(
106116
{ lng: position.coords.longitude, lat: position.coords.latitude },
107-
position.coords.accuracy
117+
position.coords.accuracy,
118+
// heading is in degrees from north, clockwise
119+
position.coords.heading
108120
)
109121
)
110122
},

0 commit comments

Comments
 (0)