Skip to content

Commit 615e82c

Browse files
committed
show current location as blue dot - fix #430
1 parent 8228f12 commit 615e82c

File tree

3 files changed

+230
-1
lines changed

3 files changed

+230
-1
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { Feature, Map } from 'ol'
2+
import { useEffect, useRef, useState } from 'react'
3+
import VectorLayer from 'ol/layer/Vector'
4+
import VectorSource from 'ol/source/Vector'
5+
import { Point, Circle as CircleGeom } from 'ol/geom'
6+
import { fromLonLat } from 'ol/proj'
7+
import { Style, Fill, Stroke, Circle as CircleStyle } from 'ol/style'
8+
import Geolocation from 'ol/Geolocation'
9+
10+
interface CurrentLocationState {
11+
enabled: boolean
12+
tracking: boolean
13+
}
14+
15+
const LOCATION_LAYER_KEY = 'gh:current_location'
16+
17+
export default function useCurrentLocationLayer(map: Map, locationState: CurrentLocationState) {
18+
const geolocationRef = useRef<Geolocation | null>(null)
19+
const [hasPermission, setHasPermission] = useState<boolean | null>(null)
20+
21+
useEffect(() => {
22+
if (!locationState.enabled) {
23+
removeCurrentLocationLayer(map)
24+
if (geolocationRef.current) {
25+
geolocationRef.current.setTracking(false)
26+
geolocationRef.current = null
27+
}
28+
return
29+
}
30+
31+
// Check for geolocation permission
32+
if ('permissions' in navigator) {
33+
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
34+
setHasPermission(result.state === 'granted')
35+
result.addEventListener('change', () => {
36+
setHasPermission(result.state === 'granted')
37+
})
38+
})
39+
}
40+
41+
// Create geolocation instance
42+
const geolocation = new Geolocation({
43+
trackingOptions: {
44+
enableHighAccuracy: true
45+
},
46+
projection: map.getView().getProjection()
47+
})
48+
49+
geolocationRef.current = geolocation
50+
51+
// Create the location layer
52+
const locationLayer = createLocationLayer()
53+
map.addLayer(locationLayer)
54+
55+
// Handle position updates
56+
const positionFeature = new Feature()
57+
const accuracyFeature = new Feature()
58+
59+
geolocation.on('change:position', () => {
60+
const coordinates = geolocation.getPosition()
61+
if (coordinates) {
62+
positionFeature.setGeometry(new Point(coordinates))
63+
64+
// Update view if tracking is enabled
65+
if (locationState.tracking) {
66+
map.getView().animate({
67+
center: coordinates,
68+
duration: 500
69+
})
70+
}
71+
}
72+
})
73+
74+
geolocation.on('change:accuracyGeometry', () => {
75+
const accuracy = geolocation.getAccuracyGeometry()
76+
if (accuracy) {
77+
accuracyFeature.setGeometry(accuracy)
78+
}
79+
})
80+
81+
geolocation.on('error', (error) => {
82+
console.error('Geolocation error:', error)
83+
setHasPermission(false)
84+
})
85+
86+
// Add features to the layer
87+
const source = locationLayer.getSource()
88+
if (source) {
89+
source.addFeature(accuracyFeature)
90+
source.addFeature(positionFeature)
91+
}
92+
93+
// Start tracking
94+
geolocation.setTracking(true)
95+
96+
return () => {
97+
geolocation.setTracking(false)
98+
removeCurrentLocationLayer(map)
99+
}
100+
}, [map, locationState.enabled, locationState.tracking])
101+
102+
return hasPermission
103+
}
104+
105+
function removeCurrentLocationLayer(map: Map) {
106+
map.getLayers()
107+
.getArray()
108+
.filter(l => l.get(LOCATION_LAYER_KEY))
109+
.forEach(l => map.removeLayer(l))
110+
}
111+
112+
function createLocationLayer(): VectorLayer<VectorSource> {
113+
const layer = new VectorLayer({
114+
source: new VectorSource(),
115+
style: (feature) => {
116+
const geometry = feature.getGeometry()
117+
if (geometry instanceof Point) {
118+
// Blue dot style for position
119+
return [
120+
new Style({
121+
image: new CircleStyle({
122+
radius: 8,
123+
fill: new Fill({
124+
color: '#4285F4'
125+
}),
126+
stroke: new Stroke({
127+
color: '#FFFFFF',
128+
width: 2
129+
})
130+
})
131+
}),
132+
// Pulsing effect outer ring
133+
new Style({
134+
image: new CircleStyle({
135+
radius: 16,
136+
fill: new Fill({
137+
color: 'rgba(66, 133, 244, 0.2)'
138+
})
139+
})
140+
})
141+
]
142+
} else if (geometry instanceof CircleGeom) {
143+
// Accuracy circle style
144+
return new Style({
145+
fill: new Fill({
146+
color: 'rgba(66, 133, 244, 0.1)'
147+
}),
148+
stroke: new Stroke({
149+
color: 'rgba(66, 133, 244, 0.3)',
150+
width: 1
151+
})
152+
})
153+
}
154+
return []
155+
}
156+
})
157+
158+
layer.set(LOCATION_LAYER_KEY, true)
159+
layer.setZIndex(4) // Above paths and query points
160+
161+
return layer
162+
}

src/layers/UseQueryPointsLayer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function removeDragInteractions(map: Map) {
8585
.forEach(i => map.removeInteraction(i))
8686
}
8787

88-
function addDragInteractions(map: Map, queryPointsLayer: VectorLayer<VectorSource<Feature<Geometry>>>) {
88+
function addDragInteractions(map: Map, queryPointsLayer: VectorLayer<VectorSource>) {
8989
let tmp = queryPointsLayer.getSource()
9090
if (tmp == null) throw new Error('source must not be null') // typescript requires this
9191
const modify = new Modify({

src/stores/CurrentLocationStore.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Store from '@/stores/Store'
2+
import { Action } from '@/stores/Dispatcher'
3+
4+
export interface CurrentLocationStoreState {
5+
enabled: boolean
6+
tracking: boolean
7+
hasPermission: boolean | null
8+
}
9+
10+
export class ToggleCurrentLocation implements Action {
11+
readonly enabled: boolean
12+
13+
constructor(enabled: boolean) {
14+
this.enabled = enabled
15+
}
16+
}
17+
18+
export class ToggleLocationTracking implements Action {
19+
readonly tracking: boolean
20+
21+
constructor(tracking: boolean) {
22+
this.tracking = tracking
23+
}
24+
}
25+
26+
export class SetLocationPermission implements Action {
27+
readonly hasPermission: boolean | null
28+
29+
constructor(hasPermission: boolean | null) {
30+
this.hasPermission = hasPermission
31+
}
32+
}
33+
34+
export default class CurrentLocationStore extends Store<CurrentLocationStoreState> {
35+
constructor() {
36+
super(CurrentLocationStore.getInitialState())
37+
}
38+
39+
private static getInitialState(): CurrentLocationStoreState {
40+
return {
41+
enabled: false,
42+
tracking: false,
43+
hasPermission: null
44+
}
45+
}
46+
47+
reduce(state: CurrentLocationStoreState, action: Action): CurrentLocationStoreState {
48+
if (action instanceof ToggleCurrentLocation) {
49+
return {
50+
...state,
51+
enabled: action.enabled,
52+
tracking: action.enabled ? state.tracking : false
53+
}
54+
} else if (action instanceof ToggleLocationTracking) {
55+
return {
56+
...state,
57+
tracking: action.tracking
58+
}
59+
} else if (action instanceof SetLocationPermission) {
60+
return {
61+
...state,
62+
hasPermission: action.hasPermission
63+
}
64+
}
65+
return state
66+
}
67+
}

0 commit comments

Comments
 (0)