|
1 | 1 | import { Feature, Map } from 'ol' |
2 | | -import {useEffect, useRef} from 'react' |
| 2 | +import { useEffect, useRef } from 'react' |
3 | 3 | import VectorLayer from 'ol/layer/Vector' |
4 | 4 | import VectorSource from 'ol/source/Vector' |
5 | 5 | 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' |
7 | 7 | import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore' |
8 | 8 | import { fromLonLat } from 'ol/proj' |
9 | 9 |
|
10 | 10 | export default function useCurrentLocationLayer(map: Map, locationState: CurrentLocationStoreState) { |
11 | 11 | 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) |
14 | 15 |
|
15 | 16 | // Create layer once when enabled |
16 | 17 | useEffect(() => { |
17 | 18 | if (!locationState.enabled) { |
18 | 19 | if (layerRef.current) { |
19 | 20 | map.removeLayer(layerRef.current) |
20 | 21 | layerRef.current = null |
21 | | - positionFeatureRef.current = null |
22 | | - accuracyFeatureRef.current = null |
| 22 | + posFeatureRef.current = null |
| 23 | + accFeatureRef.current = null |
| 24 | + headingFeatureRef.current = null |
23 | 25 | } |
24 | 26 | return |
25 | 27 | } else if (!layerRef.current) { |
26 | 28 | const layer = createLocationLayer() |
27 | 29 | const positionFeature = new Feature() |
28 | 30 | const accuracyFeature = new Feature() |
| 31 | + const headingFeature = new Feature() |
29 | 32 | layer.getSource()?.addFeature(positionFeature) |
30 | 33 | layer.getSource()?.addFeature(accuracyFeature) |
| 34 | + layer.getSource()?.addFeature(headingFeature) |
31 | 35 | map.addLayer(layer) |
32 | 36 |
|
33 | 37 | layerRef.current = layer |
34 | | - positionFeatureRef.current = positionFeature |
35 | | - accuracyFeatureRef.current = accuracyFeature |
| 38 | + posFeatureRef.current = positionFeature |
| 39 | + accFeatureRef.current = accuracyFeature |
| 40 | + headingFeatureRef.current = headingFeature |
36 | 41 | } |
37 | 42 |
|
38 | 43 | return () => { |
39 | 44 | if (layerRef.current) { |
40 | 45 | map.removeLayer(layerRef.current) |
41 | 46 | layerRef.current = null |
42 | | - positionFeatureRef.current = null |
43 | | - accuracyFeatureRef.current = null |
| 47 | + posFeatureRef.current = null |
| 48 | + accFeatureRef.current = null |
| 49 | + headingFeatureRef.current = null |
44 | 50 | } |
45 | 51 | } |
46 | 52 | }, [locationState.enabled]) |
47 | 53 |
|
48 | 54 | 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 | + ) |
50 | 62 | return |
51 | | - } |
52 | 63 |
|
53 | 64 | 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 | + } |
56 | 75 |
|
57 | 76 | if (locationState.syncView) { |
58 | 77 | // TODO same code as for MoveMapToPoint action, but calling Dispatcher here is ugly |
59 | 78 | let zoom = map.getView().getZoom() |
60 | 79 | if (zoom == undefined || zoom < 8) zoom = 8 |
61 | 80 | map.getView().animate({ zoom: zoom, center: coord, duration: 400 }) |
62 | 81 | } |
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 | + ]) |
64 | 89 | } |
65 | 90 |
|
66 | 91 | function createLocationLayer(): VectorLayer<VectorSource> { |
67 | | - const layer = new VectorLayer({ |
| 92 | + return new VectorLayer({ |
68 | 93 | source: new VectorSource(), |
69 | 94 | style: feature => { |
70 | 95 | const geometry = feature.getGeometry() |
71 | 96 | 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({ |
75 | 115 | image: new CircleStyle({ |
76 | 116 | 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 }), |
84 | 119 | }), |
85 | | - }), |
86 | | - ] |
| 120 | + zIndex: 2, // above the others |
| 121 | + }) |
| 122 | + } |
87 | 123 | } else if (geometry instanceof CircleGeom) { |
88 | 124 | // Accuracy circle style |
89 | 125 | 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 |
97 | 129 | }) |
98 | 130 | } |
99 | 131 | return [] |
100 | 132 | }, |
| 133 | + zIndex: 4, // Above paths and query points |
101 | 134 | }) |
102 | | - |
103 | | - layer.setZIndex(4) // Above paths and query points |
104 | | - return layer |
105 | 135 | } |
0 commit comments