-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathlocation-picker.ts
More file actions
188 lines (166 loc) · 5.73 KB
/
location-picker.ts
File metadata and controls
188 lines (166 loc) · 5.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import './location-picker.css';
export interface LatLng {
lat: number;
lng: number;
}
export interface LocationPickerOptions {
/** Attempt to set the map center to the user's current position via Geolocation. Defaults to true. */
setCurrentPosition?: boolean;
/** Initial latitude. If provided along with `lng`, geolocation is skipped. */
lat?: number;
/** Initial longitude. If provided along with `lat`, geolocation is skipped. */
lng?: number;
/**
* Use `google.maps.marker.AdvancedMarkerElement` instead of the CSS pin overlay when
* available. Requires the `marker` library to be loaded.
*/
useAdvancedMarker?: boolean;
/** Fired on every `idle` event with the new marker (map center) position. */
onLocationChange?: (pos: LatLng) => void;
}
type MaybeAdvancedMarker = google.maps.marker.AdvancedMarkerElement | null;
/**
* LocationPicker - wraps a Google Map so the user can drag the map and the centered
* marker reports the chosen position.
*/
export class LocationPicker {
public element: HTMLElement | null;
public map: google.maps.Map;
private readonly options: Required<
Pick<LocationPickerOptions, 'setCurrentPosition' | 'useAdvancedMarker'>
> &
LocationPickerOptions;
private markerNode: HTMLDivElement | null = null;
private advancedMarker: MaybeAdvancedMarker = null;
private idleListener: google.maps.MapsEventListener | null = null;
constructor(
element: string | HTMLElement,
options: LocationPickerOptions = {},
mapOptions: google.maps.MapOptions = {},
) {
this.options = {
setCurrentPosition: true,
useAdvancedMarker: false,
...options,
};
// Allow both a string id or a direct reference to the element
if (element instanceof HTMLElement) {
this.element = element;
} else {
this.element = document.getElementById(element);
}
if (!this.element) {
throw new Error(`LocationPicker: element "${String(element)}" was not found.`);
}
const center: google.maps.LatLngLiteral = {
lat: this.options.lat ?? 34.4346,
lng: this.options.lng ?? 35.8362,
};
const mergedMapOptions: google.maps.MapOptions = {
center,
zoom: 15,
...mapOptions,
};
this.map = new google.maps.Map(this.element, mergedMapOptions);
this.element.classList.add('location-picker');
this.initMarker();
// idle listener for onLocationChange
this.idleListener = this.map.addListener('idle', () => {
const pos = this.getMarkerPosition();
this.options.onLocationChange?.(pos);
});
if (this.options.setCurrentPosition && this.options.lat == null && this.options.lng == null) {
// fire and forget; caller can also call setCurrentPosition() themselves
void this.setCurrentPosition().catch(() => {
/* swallow - geolocation is best-effort at construction time */
});
}
}
/** Current marker position (map center). */
public getMarkerPosition(): LatLng {
const latLng = this.map.getCenter();
if (!latLng) {
return { lat: 0, lng: 0 };
}
return { lat: latLng.lat(), lng: latLng.lng() };
}
/** Center the map (and marker) on the given coordinates. */
public setLocation(lat: number, lng: number): void {
this.map.setCenter({ lat, lng });
}
/**
* Request the user's current position via the Geolocation API and center the map on it.
* Resolves with the resolved coordinates, rejects if geolocation is unavailable or denied.
*/
public setCurrentPosition(): Promise<LatLng> {
return new Promise<LatLng>((resolve, reject) => {
if (typeof navigator === 'undefined' || !navigator.geolocation) {
reject(new Error('Geolocation is not supported by this browser.'));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const pos: LatLng = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
this.map.setCenter(pos);
resolve(pos);
},
(err) => {
reject(err instanceof Error ? err : new Error('Could not determine your location.'));
},
);
});
}
/** Remove listeners, marker DOM nodes, and related classes. The map instance is released. */
public destroy(): void {
if (this.idleListener) {
this.idleListener.remove();
this.idleListener = null;
}
if (this.markerNode && this.markerNode.parentNode) {
this.markerNode.parentNode.removeChild(this.markerNode);
}
this.markerNode = null;
if (this.advancedMarker) {
this.advancedMarker.map = null;
this.advancedMarker = null;
}
if (this.element) {
this.element.classList.remove('location-picker');
}
}
private initMarker(): void {
const advancedAvailable =
this.options.useAdvancedMarker &&
typeof google !== 'undefined' &&
!!google.maps?.marker?.AdvancedMarkerElement;
if (advancedAvailable) {
const AdvancedMarker = google.maps.marker.AdvancedMarkerElement;
this.advancedMarker = new AdvancedMarker({
map: this.map,
position: this.map.getCenter() ?? undefined,
});
// Keep the advanced marker glued to the center on idle.
this.map.addListener('center_changed', () => {
const c = this.map.getCenter();
if (this.advancedMarker && c) {
this.advancedMarker.position = c;
}
});
return;
}
// CSS-pin overlay fallback
const node = document.createElement('div');
node.classList.add('centerMarker');
this.markerNode = node;
const firstChild = this.element?.children[0];
if (firstChild) {
firstChild.appendChild(node);
} else if (this.element) {
this.element.appendChild(node);
}
}
}
export default LocationPicker;