|
| 1 | +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; |
| 2 | +import {BehaviorSubject, combineLatest, Observable, ReplaySubject, Subject} from 'rxjs'; |
| 3 | +import {map, takeUntil} from 'rxjs/operators'; |
| 4 | + |
| 5 | +export const DEFAULT_OPTIONS = { |
| 6 | + position: { lat: 37.421995, lng: -122.084092 }, |
| 7 | +}; |
| 8 | + |
| 9 | +/** |
| 10 | + * Angular component that renders a Google Maps marker via the Google Maps JavaScript API. |
| 11 | + * @see https://developers.google.com/maps/documentation/javascript/reference/marker |
| 12 | + */ |
| 13 | +@Component({ |
| 14 | + selector: 'google-map-marker', |
| 15 | + template: '<ng-content></ng-content>', |
| 16 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 17 | +}) |
| 18 | +export class GoogleMapMarker implements OnInit, OnDestroy { |
| 19 | + @Input() set options(options: google.maps.MarkerOptions) { |
| 20 | + this._options.next(options || DEFAULT_OPTIONS); |
| 21 | + } |
| 22 | + |
| 23 | + @Input() set title(title: string) { |
| 24 | + this._title.next(title); |
| 25 | + } |
| 26 | + |
| 27 | + @Input() set position(position: google.maps.LatLngLiteral) { |
| 28 | + this._position.next(position); |
| 29 | + } |
| 30 | + |
| 31 | + @Input() set label(label: string|google.maps.MarkerLabel) { |
| 32 | + this._label.next(label); |
| 33 | + } |
| 34 | + |
| 35 | + @Input() set clickable(clickable: boolean) { |
| 36 | + this._clickable.next(clickable); |
| 37 | + } |
| 38 | + |
| 39 | + /** */ |
| 40 | + @Output() animationChanged = new EventEmitter<void>(); |
| 41 | + |
| 42 | + /** */ |
| 43 | + @Output() mapClick = new EventEmitter<google.maps.MouseEvent>(); |
| 44 | + |
| 45 | + /** */ |
| 46 | + @Output() clickableChanged = new EventEmitter<void>(); |
| 47 | + |
| 48 | + /** */ |
| 49 | + @Output() cursorChanged = new EventEmitter<void>(); |
| 50 | + |
| 51 | + /** */ |
| 52 | + @Output() mapDblclick = new EventEmitter<google.maps.MouseEvent>(); |
| 53 | + |
| 54 | + /** */ |
| 55 | + @Output() mapDrag = new EventEmitter<google.maps.MouseEvent>(); |
| 56 | + |
| 57 | + /** */ |
| 58 | + @Output() mapDragend = new EventEmitter<google.maps.MouseEvent>(); |
| 59 | + |
| 60 | + /** */ |
| 61 | + @Output() draggableChanged = new EventEmitter<void>(); |
| 62 | + |
| 63 | + /** */ |
| 64 | + @Output() mapDragstart = new EventEmitter<google.maps.MouseEvent>(); |
| 65 | + |
| 66 | + /** */ |
| 67 | + @Output() flatChanged = new EventEmitter<void>(); |
| 68 | + |
| 69 | + /** */ |
| 70 | + @Output() iconChanged = new EventEmitter<void>(); |
| 71 | + |
| 72 | + /** */ |
| 73 | + @Output() mapMousedown = new EventEmitter<google.maps.MouseEvent>(); |
| 74 | + |
| 75 | + /** */ |
| 76 | + @Output() mapMouseout = new EventEmitter<google.maps.MouseEvent>(); |
| 77 | + |
| 78 | + /** */ |
| 79 | + @Output() mapMouseover = new EventEmitter<google.maps.MouseEvent>(); |
| 80 | + |
| 81 | + /** */ |
| 82 | + @Output() mapMouseup = new EventEmitter<google.maps.MouseEvent>(); |
| 83 | + |
| 84 | + /** */ |
| 85 | + @Output() positionChanged = new EventEmitter<void>(); |
| 86 | + |
| 87 | + /** */ |
| 88 | + @Output() mapRightclick = new EventEmitter<google.maps.MouseEvent>(); |
| 89 | + |
| 90 | + /** */ |
| 91 | + @Output() shapeChanged = new EventEmitter<void>(); |
| 92 | + |
| 93 | + /** */ |
| 94 | + @Output() titleChanged = new EventEmitter<void>(); |
| 95 | + |
| 96 | + /** */ |
| 97 | + @Output() visibleChanged = new EventEmitter<void>(); |
| 98 | + |
| 99 | + /** */ |
| 100 | + @Output() zindexChanged = new EventEmitter<void>(); |
| 101 | + |
| 102 | + private readonly _options = new BehaviorSubject<google.maps.MarkerOptions>(DEFAULT_OPTIONS); |
| 103 | + private readonly _title = new BehaviorSubject<string|undefined>(undefined); |
| 104 | + private readonly _position = new BehaviorSubject<google.maps.LatLngLiteral|undefined>(undefined); |
| 105 | + private readonly _label = new BehaviorSubject<string|google.maps.MarkerLabel|undefined>(undefined); |
| 106 | + private readonly _clickable = new BehaviorSubject<boolean>(true); |
| 107 | + |
| 108 | + private readonly _map = new ReplaySubject<google.maps.Map>(1); |
| 109 | + |
| 110 | + private readonly _destroy = new Subject<void>(); |
| 111 | + |
| 112 | + private readonly _listeners: google.maps.MapsEventListener[] = []; |
| 113 | + |
| 114 | + private _marker?: google.maps.Marker; |
| 115 | + private _hasMap = false; |
| 116 | + |
| 117 | + ngOnInit() { |
| 118 | + const combinedOptionsChanges = this._combineOptions(); |
| 119 | + |
| 120 | + combineLatest(this._map, combinedOptionsChanges).pipe(takeUntil(this._destroy)).subscribe(([map, options]) => { |
| 121 | + if (this._marker) { |
| 122 | + this._marker.setOptions(options); |
| 123 | + } else { |
| 124 | + this._marker = new google.maps.Marker(options); |
| 125 | + this._marker.setMap(map); |
| 126 | + this._initializeEventHandlers(); |
| 127 | + } |
| 128 | + }); |
| 129 | + } |
| 130 | + |
| 131 | + ngOnDestroy() { |
| 132 | + this._destroy.next(); |
| 133 | + this._destroy.complete(); |
| 134 | + for (let listener of this._listeners) { |
| 135 | + listener.remove(); |
| 136 | + } |
| 137 | + if (this._marker) { |
| 138 | + this._marker.setMap(null); |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + setMap(map: google.maps.Map) { |
| 143 | + if (!this._hasMap) { |
| 144 | + this._map.next(map); |
| 145 | + this._hasMap = true; |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + getAnimation(): google.maps.Animation|null { |
| 150 | + return this._marker!.getAnimation() || null; |
| 151 | + } |
| 152 | + |
| 153 | + getClickable(): boolean { |
| 154 | + return this._marker!.getClickable(); |
| 155 | + } |
| 156 | + |
| 157 | + getCursor(): string|null { |
| 158 | + return this._marker!.getCursor() || null; |
| 159 | + } |
| 160 | + |
| 161 | + getDraggable(): boolean { |
| 162 | + return !!this._marker!.getDraggable(); |
| 163 | + } |
| 164 | + |
| 165 | + getIcon(): string|google.maps.Icon|google.maps.Symbol|null { |
| 166 | + return this._marker!.getIcon() || null; |
| 167 | + } |
| 168 | + |
| 169 | + getLabel(): google.maps.MarkerLabel|null { |
| 170 | + return this._marker!.getLabel() || null; |
| 171 | + } |
| 172 | + |
| 173 | + getOpacity(): number|null { |
| 174 | + return this._marker!.getOpacity() || null; |
| 175 | + } |
| 176 | + |
| 177 | + getPosition(): google.maps.LatLng|null { |
| 178 | + return this._marker!.getPosition() || null; |
| 179 | + } |
| 180 | + |
| 181 | + getShape(): google.maps.MarkerShape|null { |
| 182 | + return this._marker!.getShape() || null; |
| 183 | + } |
| 184 | + |
| 185 | + getTitle(): string|null { |
| 186 | + return this._marker!.getTitle() || null; |
| 187 | + } |
| 188 | + |
| 189 | + getVisible(): boolean { |
| 190 | + return this._marker!.getVisible(); |
| 191 | + } |
| 192 | + |
| 193 | + getZIndex(): number|null { |
| 194 | + return this._marker!.getZIndex() || null; |
| 195 | + } |
| 196 | + |
| 197 | + private _combineOptions(): Observable<google.maps.MarkerOptions> { |
| 198 | + return combineLatest(this._options, this._title, this._position, this._label, this._clickable, this._map) |
| 199 | + .pipe(map(([options, title, position, label, clickable, map]) => { |
| 200 | + const combinedOptions: google.maps.MarkerOptions = { |
| 201 | + ...options, |
| 202 | + title: title || options.title, |
| 203 | + position: position || options.position, |
| 204 | + label: label || options.label, |
| 205 | + clickable: clickable || options.clickable, |
| 206 | + map: map || null, |
| 207 | + }; |
| 208 | + return combinedOptions; |
| 209 | + })); |
| 210 | + } |
| 211 | + |
| 212 | + private _initializeEventHandlers() { |
| 213 | + const eventHandlers = new Map<string, EventEmitter<void>>([]); |
| 214 | + const mouseEventHandlers = new Map<string, EventEmitter<google.maps.MouseEvent>>([]); |
| 215 | + |
| 216 | + eventHandlers.forEach((eventHandler: EventEmitter<void>, name: string) => { |
| 217 | + if (eventHandler.observers.length > 0) { |
| 218 | + this._listeners.push(this._marker!.addListener(name, () => { |
| 219 | + eventHandler.emit(); |
| 220 | + })); |
| 221 | + } |
| 222 | + }); |
| 223 | + mouseEventHandlers.forEach((eventHandler: EventEmitter<google.maps.MouseEvent>, name: string) => { |
| 224 | + if (eventHandler.observers.length > 0) { |
| 225 | + this._listeners.push(this._marker!.addListener(name, (event: google.maps.MouseEvent) => { |
| 226 | + eventHandler.emit(event); |
| 227 | + })); |
| 228 | + } |
| 229 | + }); |
| 230 | + } |
| 231 | +} |
0 commit comments