11import { Feature , Map } from 'ol'
2- import { useEffect , useRef } from 'react'
2+ import { useEffect , useRef , useState } from 'react'
33import VectorLayer from 'ol/layer/Vector'
44import 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'
86import { 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