import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {BehaviorSubject, combineLatest, Observable, ReplaySubject, Subject} from 'rxjs'; import {map, takeUntil} from 'rxjs/operators'; /** * Default options for the Google Maps marker component. Displays a marker * at the Googleplex. */ export const DEFAULT_MARKER_OPTIONS = { position: {lat: 37.421995, lng: -122.084092}, }; /** * Angular component that renders a Google Maps marker via the Google Maps JavaScript API. * @see developers.google.com/maps/documentation/javascript/reference/marker */ @Component({ selector: 'map-marker', template: '', changeDetection: ChangeDetectionStrategy.OnPush, }) export class MapMarker implements OnInit, OnDestroy { @Input() set options(options: google.maps.MarkerOptions) { this._options.next(options || DEFAULT_MARKER_OPTIONS); } @Input() set title(title: string) { this._title.next(title); } @Input() set position(position: google.maps.LatLngLiteral) { this._position.next(position); } @Input() set label(label: string|google.maps.MarkerLabel) { this._label.next(label); } @Input() set clickable(clickable: boolean) { this._clickable.next(clickable); } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.animation_changed */ @Output() animationChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.click */ @Output() mapClick = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.clickable_changed */ @Output() clickableChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.cursor_changed */ @Output() cursorChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.dblclick */ @Output() mapDblclick = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.drag */ @Output() mapDrag = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.dragend */ @Output() mapDragend = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.draggable_changed */ @Output() draggableChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.dragstart */ @Output() mapDragstart = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.flat_changed */ @Output() flatChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.icon_changed */ @Output() iconChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.mousedown */ @Output() mapMousedown = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.mouseout */ @Output() mapMouseout = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.mouseover */ @Output() mapMouseover = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.mouseup */ @Output() mapMouseup = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.position_changed */ @Output() positionChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.rightclick */ @Output() mapRightclick = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.shape_changed */ @Output() shapeChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.title_changed */ @Output() titleChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.visible_changed */ @Output() visibleChanged = new EventEmitter(); /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.zindex_changed */ @Output() zindexChanged = new EventEmitter(); private readonly _options = new BehaviorSubject(DEFAULT_MARKER_OPTIONS); private readonly _title = new BehaviorSubject(undefined); private readonly _position = new BehaviorSubject(undefined); private readonly _label = new BehaviorSubject(undefined); private readonly _clickable = new BehaviorSubject(undefined); private readonly _map = new ReplaySubject(1); private readonly _destroy = new Subject(); private readonly _listeners: google.maps.MapsEventListener[] = []; private _marker?: google.maps.Marker; private _hasMap = false; ngOnInit() { const combinedOptionsChanges = this._combineOptions(); combineLatest(this._map, combinedOptionsChanges) .pipe(takeUntil(this._destroy)) .subscribe(([googleMap, options]) => { if (this._marker) { this._marker.setOptions(options); } else { this._marker = new google.maps.Marker(options); this._marker.setMap(googleMap); this._initializeEventHandlers(); } }); } ngOnDestroy() { this._destroy.next(); this._destroy.complete(); for (let listener of this._listeners) { listener.remove(); } if (this._marker) { this._marker.setMap(null); } } _setMap(googleMap: google.maps.Map) { if (!this._hasMap) { this._map.next(googleMap); this._hasMap = true; } } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getAnimation */ getAnimation(): google.maps.Animation|null { return this._marker!.getAnimation() || null; } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getClickable */ getClickable(): boolean { return this._marker!.getClickable(); } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getCursor */ getCursor(): string|null { return this._marker!.getCursor() || null; } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getDraggable */ getDraggable(): boolean { return !!this._marker!.getDraggable(); } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getIcon */ getIcon(): string|google.maps.Icon|google.maps.Symbol|null { return this._marker!.getIcon() || null; } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getLabel */ getLabel(): google.maps.MarkerLabel|null { return this._marker!.getLabel() || null; } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getOpacity */ getOpacity(): number|null { return this._marker!.getOpacity() || null; } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getPosition */ getPosition(): google.maps.LatLng|null { return this._marker!.getPosition() || null; } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getShape */ getShape(): google.maps.MarkerShape|null { return this._marker!.getShape() || null; } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getTitle */ getTitle(): string|null { return this._marker!.getTitle() || null; } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getVisible */ getVisible(): boolean { return this._marker!.getVisible(); } /** * See * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getZIndex */ getZIndex(): number|null { return this._marker!.getZIndex() || null; } private _combineOptions(): Observable { return combineLatest( this._options, this._title, this._position, this._label, this._clickable, this._map) .pipe(map(([options, title, position, label, clickable, googleMap]) => { const combinedOptions: google.maps.MarkerOptions = { ...options, title: title || options.title, position: position || options.position, label: label || options.label, clickable: clickable !== undefined ? clickable : options.clickable, map: googleMap || null, }; return combinedOptions; })); } private _initializeEventHandlers() { const eventHandlers = new Map>([ ['animation_changed', this.animationChanged], ['clickable_changed', this.clickableChanged], ['cursor_changed', this.cursorChanged], ['draggable_changed', this.draggableChanged], ['flat_changed', this.flatChanged], ['icon_changed', this.iconChanged], ['position_changed', this.positionChanged], ['shape_changed', this.shapeChanged], ['title_changed', this.titleChanged], ['visible_changed', this.visibleChanged], ['zindex_changed', this.zindexChanged], ]); const mouseEventHandlers = new Map>([ ['click', this.mapClick], ['dblclick', this.mapDblclick], ['drag', this.mapDrag], ['dragend', this.mapDragend], ['dragstart', this.mapDragstart], ['mousedown', this.mapMousedown], ['mouseout', this.mapMouseout], ['mouseover', this.mapMouseover], ['mouseup', this.mapMouseup], ['rightclick', this.mapRightclick], ]); eventHandlers.forEach((eventHandler: EventEmitter, name: string) => { if (eventHandler.observers.length > 0) { this._listeners.push(this._marker!.addListener(name, () => { eventHandler.emit(); })); } }); mouseEventHandlers.forEach( (eventHandler: EventEmitter, name: string) => { if (eventHandler.observers.length > 0) { this._listeners.push( this._marker!.addListener(name, (event: google.maps.MouseEvent) => { eventHandler.emit(event); })); } }); } }