Skip to content

Commit c52bd72

Browse files
committed
add orientation
1 parent 8edd353 commit c52bd72

File tree

1 file changed

+198
-57
lines changed

1 file changed

+198
-57
lines changed
Lines changed: 198 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,246 @@
11
import { Feature, Map } from 'ol'
2-
import {useEffect, useRef} from 'react'
2+
import { useEffect, useRef, useState } from 'react'
33
import VectorLayer from 'ol/layer/Vector'
44
import VectorSource from 'ol/source/Vector'
5-
import { Circle, Circle as CircleGeom, Point } from 'ol/geom'
6-
import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style'
7-
import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore'
5+
import { Point, Circle as CircleGeom } from 'ol/geom'
86
import { fromLonLat } from 'ol/proj'
7+
import { Style, Fill, Stroke, Circle as CircleStyle, RegularShape } from 'ol/style'
8+
import Geolocation from 'ol/Geolocation'
99

10-
export default function useCurrentLocationLayer(map: Map, locationState: CurrentLocationStoreState) {
11-
const layerRef = useRef<VectorLayer<VectorSource> | null>(null)
12-
const positionFeatureRef = useRef<Feature | null>(null)
13-
const accuracyFeatureRef = useRef<Feature | null>(null)
10+
interface CurrentLocationState {
11+
enabled: boolean
12+
tracking?: boolean // Make optional to match your existing state
13+
showDirection?: boolean // New option for showing direction
14+
}
1415

15-
// Create layer once when enabled
16-
useEffect(() => {
17-
if (!locationState.enabled) {
18-
if (layerRef.current) {
19-
map.removeLayer(layerRef.current)
20-
layerRef.current = null
21-
positionFeatureRef.current = null
22-
accuracyFeatureRef.current = null
16+
const LOCATION_LAYER_KEY = 'gh:current_location'
17+
18+
export default function useCurrentLocationLayer(map: Map, locationState: CurrentLocationState) {
19+
const geolocationRef = useRef<Geolocation | null>(null)
20+
const [hasPermission, setHasPermission] = useState<boolean | null>(null)
21+
const [deviceHeading, setDeviceHeading] = useState<number | null>(null)
22+
const orientationPermissionRef = useRef<boolean>(false)
23+
24+
// Request device orientation permission (iOS 13+)
25+
const requestOrientationPermission = async () => {
26+
if (typeof (DeviceOrientationEvent as any).requestPermission === 'function') {
27+
try {
28+
const permission = await (DeviceOrientationEvent as any).requestPermission()
29+
orientationPermissionRef.current = permission === 'granted'
30+
return permission === 'granted'
31+
} catch (error) {
32+
console.error('Error requesting device orientation permission:', error)
33+
return false
2334
}
35+
} else {
36+
// Non-iOS devices or older iOS versions
37+
orientationPermissionRef.current = true
38+
return true
39+
}
40+
}
41+
42+
// Handle device orientation
43+
useEffect(() => {
44+
if (!locationState.enabled || !locationState.showDirection) {
2445
return
25-
} else if (!layerRef.current) {
26-
const layer = createLocationLayer()
27-
const positionFeature = new Feature()
28-
const accuracyFeature = new Feature()
29-
layer.getSource()?.addFeature(positionFeature)
30-
layer.getSource()?.addFeature(accuracyFeature)
31-
map.addLayer(layer)
32-
33-
layerRef.current = layer
34-
positionFeatureRef.current = positionFeature
35-
accuracyFeatureRef.current = accuracyFeature
3646
}
3747

38-
return () => {
39-
if (layerRef.current) {
40-
map.removeLayer(layerRef.current)
41-
layerRef.current = null
42-
positionFeatureRef.current = null
43-
accuracyFeatureRef.current = null
48+
const handleOrientation = (event: DeviceOrientationEvent) => {
49+
if (event.alpha !== null) {
50+
// Convert to compass heading (0 = North, 90 = East, etc.)
51+
let heading = event.alpha
52+
53+
// Adjust for different browsers/devices if needed
54+
// Some devices may need calibration adjustments
55+
setDeviceHeading(heading)
4456
}
4557
}
46-
}, [locationState.enabled])
58+
59+
const setupOrientation = async () => {
60+
const hasPermission = await requestOrientationPermission()
61+
if (hasPermission) {
62+
window.addEventListener('deviceorientation', handleOrientation)
63+
}
64+
}
65+
66+
setupOrientation()
67+
68+
return () => {
69+
window.removeEventListener('deviceorientation', handleOrientation)
70+
}
71+
}, [locationState.enabled, locationState.showDirection])
4772

4873
useEffect(() => {
49-
if (!locationState.enabled || !locationState.coordinate || !positionFeatureRef.current || !accuracyFeatureRef.current) {
74+
if (!locationState.enabled) {
75+
removeCurrentLocationLayer(map)
76+
if (geolocationRef.current) {
77+
geolocationRef.current.setTracking(false)
78+
geolocationRef.current = null
79+
}
5080
return
5181
}
5282

53-
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))
83+
// Check for geolocation permission
84+
if ('permissions' in navigator) {
85+
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
86+
setHasPermission(result.state === 'granted')
87+
result.addEventListener('change', () => {
88+
setHasPermission(result.state === 'granted')
89+
})
90+
})
91+
}
92+
93+
// Create geolocation instance
94+
const geolocation = new Geolocation({
95+
trackingOptions: {
96+
enableHighAccuracy: true
97+
},
98+
projection: map.getView().getProjection()
99+
})
100+
101+
geolocationRef.current = geolocation
102+
103+
// Create the location layer
104+
const locationLayer = createLocationLayer(deviceHeading, locationState.showDirection)
105+
map.addLayer(locationLayer)
106+
107+
// Create features
108+
const positionFeature = new Feature()
109+
const accuracyFeature = new Feature()
110+
const directionFeature = new Feature() // New feature for direction indicator
111+
112+
// Set feature types for styling
113+
positionFeature.set('featureType', 'position')
114+
accuracyFeature.set('featureType', 'accuracy')
115+
directionFeature.set('featureType', 'direction')
116+
117+
geolocation.on('change:position', () => {
118+
const coordinates = geolocation.getPosition()
119+
if (coordinates) {
120+
positionFeature.setGeometry(new Point(coordinates))
121+
122+
// Update direction feature position
123+
if (locationState.showDirection) {
124+
directionFeature.setGeometry(new Point(coordinates))
125+
}
126+
127+
// Update view if tracking is enabled
128+
if (locationState.tracking) {
129+
map.getView().animate({
130+
center: coordinates,
131+
duration: 500
132+
})
133+
}
134+
}
135+
})
136+
137+
geolocation.on('change:accuracyGeometry', () => {
138+
const accuracy = geolocation.getAccuracyGeometry()
139+
if (accuracy) {
140+
accuracyFeature.setGeometry(accuracy)
141+
}
142+
})
143+
144+
geolocation.on('error', (error) => {
145+
console.error('Geolocation error:', error)
146+
setHasPermission(false)
147+
})
148+
149+
// Add features to the layer
150+
const source = locationLayer.getSource()
151+
if (source) {
152+
source.addFeature(accuracyFeature)
153+
source.addFeature(positionFeature)
154+
if (locationState.showDirection) {
155+
source.addFeature(directionFeature)
156+
}
157+
}
158+
159+
// Start tracking
160+
geolocation.setTracking(true)
56161

57-
if (locationState.syncView) {
58-
// TODO same code as for MoveMapToPoint action, but calling Dispatcher here is ugly
59-
let zoom = map.getView().getZoom()
60-
if (zoom == undefined || zoom < 8) zoom = 8
61-
map.getView().animate({ zoom: zoom, center: coord, duration: 400 })
162+
return () => {
163+
geolocation.setTracking(false)
164+
removeCurrentLocationLayer(map)
62165
}
63-
}, [locationState.coordinate, locationState.accuracy, locationState.syncView, locationState.enabled])
166+
}, [map, locationState.enabled, locationState.tracking, locationState.showDirection, deviceHeading])
167+
168+
return hasPermission
169+
}
170+
171+
function removeCurrentLocationLayer(map: Map) {
172+
map.getLayers()
173+
.getArray()
174+
.filter(l => l.get(LOCATION_LAYER_KEY))
175+
.forEach(l => map.removeLayer(l))
64176
}
65177

66-
function createLocationLayer(): VectorLayer<VectorSource> {
178+
function createLocationLayer(heading: number | null, showDirection?: boolean): VectorLayer<VectorSource> {
67179
const layer = new VectorLayer({
68180
source: new VectorSource(),
69-
style: feature => {
181+
style: (feature) => {
182+
const featureType = feature.get('featureType')
70183
const geometry = feature.getGeometry()
71-
if (geometry instanceof Point) {
184+
185+
if (featureType === 'position' && geometry instanceof Point) {
72186
// Blue dot style for position
73187
return [
74188
new Style({
75189
image: new CircleStyle({
76190
radius: 8,
77191
fill: new Fill({
78-
color: '#4285F4',
192+
color: '#4285F4'
79193
}),
80194
stroke: new Stroke({
81195
color: '#FFFFFF',
82-
width: 2,
83-
}),
84-
}),
196+
width: 2
197+
})
198+
})
85199
}),
200+
// Pulsing effect outer ring
201+
new Style({
202+
image: new CircleStyle({
203+
radius: 16,
204+
fill: new Fill({
205+
color: 'rgba(66, 133, 244, 0.2)'
206+
})
207+
})
208+
})
86209
]
87-
} else if (geometry instanceof CircleGeom) {
210+
} else if (featureType === 'direction' && geometry instanceof Point && showDirection && heading !== null) {
211+
// Direction indicator (triangle/arrow pointing in heading direction)
212+
return new Style({
213+
image: new RegularShape({
214+
points: 3,
215+
radius: 12,
216+
rotation: (heading * Math.PI) / 180, // Convert degrees to radians
217+
fill: new Fill({
218+
color: '#4285F4'
219+
}),
220+
stroke: new Stroke({
221+
color: '#FFFFFF',
222+
width: 2
223+
})
224+
})
225+
})
226+
} else if (featureType === 'accuracy' && geometry instanceof CircleGeom) {
88227
// Accuracy circle style
89228
return new Style({
90229
fill: new Fill({
91-
color: 'rgba(66, 133, 244, 0.1)',
230+
color: 'rgba(66, 133, 244, 0.1)'
92231
}),
93232
stroke: new Stroke({
94233
color: 'rgba(66, 133, 244, 0.3)',
95-
width: 1,
96-
}),
234+
width: 1
235+
})
97236
})
98237
}
99238
return []
100-
},
239+
}
101240
})
102241

242+
layer.set(LOCATION_LAYER_KEY, true)
103243
layer.setZIndex(4) // Above paths and query points
244+
104245
return layer
105-
}
246+
}

0 commit comments

Comments
 (0)