From 33f202cc1c68c6788d31950642f36be7490ba07c Mon Sep 17 00:00:00 2001 From: Oskar Kumor Date: Thu, 1 Feb 2024 21:43:48 +0100 Subject: [PATCH 1/3] feat(google-maps): add advanced marker This commit introduces the advanced-marker feature to the map package, enabling users to add custom, interactive markers to their maps. Related #25897 --- src/dev-app/google-map/google-map-demo.html | 7 + src/dev-app/google-map/google-map-demo.ts | 10 + src/google-maps/google-map/google-map.spec.ts | 11 +- src/google-maps/google-map/google-map.ts | 10 + src/google-maps/google-maps-module.ts | 2 + .../map-advanced-marker.spec.ts | 182 +++++++++++++ .../map-advanced-marker.ts | 255 ++++++++++++++++++ .../map-info-window/map-info-window.ts | 19 ++ src/google-maps/public-api.ts | 1 + .../testing/fake-google-map-utils.ts | 42 +++ 10 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 src/google-maps/map-advanced-marker/map-advanced-marker.spec.ts create mode 100644 src/google-maps/map-advanced-marker/map-advanced-marker.ts diff --git a/src/dev-app/google-map/google-map-demo.html b/src/dev-app/google-map/google-map-demo.html index 610b1141132f..b5e37311ade8 100644 --- a/src/dev-app/google-map/google-map-demo.html +++ b/src/dev-app/google-map/google-map-demo.html @@ -20,6 +20,13 @@ (mapClick)="clickMarker(marker)"> } + Testing 1 2 3 @if (isPolylineDisplayed) { diff --git a/src/dev-app/google-map/google-map-demo.ts b/src/dev-app/google-map/google-map-demo.ts index 3803163aa723..d351b0f893dc 100644 --- a/src/dev-app/google-map/google-map-demo.ts +++ b/src/dev-app/google-map/google-map-demo.ts @@ -25,6 +25,7 @@ import { MapRectangle, MapTrafficLayer, MapTransitLayer, + MapAdvancedMarker, } from '@angular/google-maps'; const POLYLINE_PATH: google.maps.LatLngLiteral[] = [ @@ -72,6 +73,7 @@ let apiLoadingPromise: Promise | null = null; MapKmlLayer, MapMarker, MapMarkerClusterer, + MapAdvancedMarker, MapPolygon, MapPolyline, MapRectangle, @@ -87,6 +89,7 @@ export class GoogleMapDemo { @ViewChild(MapCircle) circle: MapCircle; center = {lat: 24, lng: 12}; + mapAdvancedMarkerPosition = {lat: 24, lng: 16}; markerOptions = {draggable: false}; markerPositions: google.maps.LatLngLiteral[] = []; zoom = 4; @@ -173,6 +176,13 @@ export class GoogleMapDemo { this.infoWindow.open(marker); } + clickAdvancedMarker(advancedMarker: MapAdvancedMarker) { + this.infoWindow.openAdvancedMarkerElement( + advancedMarker.advancedMarker, + advancedMarker.advancedMarker.title, + ); + } + handleRightclick() { this.markerPositions.pop(); } diff --git a/src/google-maps/google-map/google-map.spec.ts b/src/google-maps/google-map/google-map.spec.ts index 51203384f363..a1df53aa539c 100644 --- a/src/google-maps/google-map/google-map.spec.ts +++ b/src/google-maps/google-map/google-map.spec.ts @@ -114,7 +114,12 @@ describe('GoogleMap', () => { }); it('sets center and zoom of the map', () => { - const options = {center: {lat: 3, lng: 5}, zoom: 7, mapTypeId: DEFAULT_OPTIONS.mapTypeId}; + const options = { + center: {lat: 3, lng: 5}, + zoom: 7, + mapTypeId: DEFAULT_OPTIONS.mapTypeId, + mapId: DEFAULT_OPTIONS.mapId, + }; mapSpy = createMapSpy(options); mapConstructorSpy = createMapConstructorSpy(mapSpy); @@ -140,6 +145,7 @@ describe('GoogleMap', () => { zoom: 7, draggable: false, mapTypeId: DEFAULT_OPTIONS.mapTypeId, + mapId: '123', }; mapSpy = createMapSpy(options); mapConstructorSpy = createMapConstructorSpy(mapSpy); @@ -194,12 +200,13 @@ describe('GoogleMap', () => { }); it('gives precedence to center and zoom over options', () => { - const inputOptions = {center: {lat: 3, lng: 5}, zoom: 7, heading: 170}; + const inputOptions = {center: {lat: 3, lng: 5}, zoom: 7, heading: 170, mapId: '123'}; const correctedOptions = { center: {lat: 12, lng: 15}, zoom: 5, heading: 170, mapTypeId: DEFAULT_OPTIONS.mapTypeId, + mapId: '123', }; mapSpy = createMapSpy(correctedOptions); mapConstructorSpy = createMapConstructorSpy(mapSpy); diff --git a/src/google-maps/google-map/google-map.ts b/src/google-maps/google-map/google-map.ts index 848917c80cf8..2c9b9c61b2f2 100644 --- a/src/google-maps/google-map/google-map.ts +++ b/src/google-maps/google-map/google-map.ts @@ -42,6 +42,7 @@ export const DEFAULT_OPTIONS: google.maps.MapOptions = { zoom: 17, // Note: the type conversion here isn't necessary for our CI, but it resolves a g3 failure. mapTypeId: 'roadmap' as unknown as google.maps.MapTypeId, + mapId: '123', }; /** Arbitrary default height for the map element */ @@ -83,6 +84,12 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { /** Width of the map. Set this to `null` if you'd like to control the width through CSS. */ @Input() width: string | number | null = DEFAULT_WIDTH; + /** + * The Map ID of the map. This parameter cannot be set or changed after a map is instantiated. + * See: https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.mapId + */ + @Input() mapId: string | undefined; + /** * Type of map that should be rendered. E.g. hybrid map, terrain map etc. * See: https://developers.google.com/maps/documentation/javascript/reference/map#MapTypeId @@ -525,6 +532,9 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { // Passing in an undefined `mapTypeId` seems to break tile loading // so make sure that we have some kind of default (see #22082). mapTypeId: this.mapTypeId || options.mapTypeId || DEFAULT_OPTIONS.mapTypeId, + // Passing in an undefined `mapTypeId` seems to break tile loading + // so make sure that we have some kind of default (see #22082). + mapId: this.mapId || options.mapId || DEFAULT_OPTIONS.mapId, }; } diff --git a/src/google-maps/google-maps-module.ts b/src/google-maps/google-maps-module.ts index 266e583b1d26..621a13c00667 100644 --- a/src/google-maps/google-maps-module.ts +++ b/src/google-maps/google-maps-module.ts @@ -24,6 +24,7 @@ import {MapRectangle} from './map-rectangle/map-rectangle'; import {MapTrafficLayer} from './map-traffic-layer/map-traffic-layer'; import {MapTransitLayer} from './map-transit-layer/map-transit-layer'; import {MapHeatmapLayer} from './map-heatmap-layer/map-heatmap-layer'; +import {MapAdvancedMarker} from './map-advanced-marker/map-advanced-marker'; const COMPONENTS = [ GoogleMap, @@ -36,6 +37,7 @@ const COMPONENTS = [ MapInfoWindow, MapKmlLayer, MapMarker, + MapAdvancedMarker, MapMarkerClusterer, MapPolygon, MapPolyline, diff --git a/src/google-maps/map-advanced-marker/map-advanced-marker.spec.ts b/src/google-maps/map-advanced-marker/map-advanced-marker.spec.ts new file mode 100644 index 000000000000..c35ef1d92853 --- /dev/null +++ b/src/google-maps/map-advanced-marker/map-advanced-marker.spec.ts @@ -0,0 +1,182 @@ +import {Component, ViewChild} from '@angular/core'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; + +import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; +import { + createAdvancedMarkerConstructorSpy, + createAdvancedMarkerSpy, + createMapConstructorSpy, + createMapSpy, +} from '../testing/fake-google-map-utils'; +import {DEFAULT_MARKER_OPTIONS, MapAdvancedMarker} from './map-advanced-marker'; + +describe('MapAdvancedMarker', () => { + let mapSpy: jasmine.SpyObj; + + beforeEach(() => { + mapSpy = createMapSpy(DEFAULT_OPTIONS); + createMapConstructorSpy(mapSpy); + }); + + afterEach(() => { + (window.google as any) = undefined; + }); + + it('initializes a Google Map advanced marker', fakeAsync(() => { + const advancedMarkerSpy = createAdvancedMarkerSpy(DEFAULT_MARKER_OPTIONS); + const advancedMarkerConstructorSpy = createAdvancedMarkerConstructorSpy(advancedMarkerSpy); + + const fixture = TestBed.createComponent(TestApp); + fixture.detectChanges(); + flush(); + expect(advancedMarkerConstructorSpy).toHaveBeenCalledWith({ + ...DEFAULT_MARKER_OPTIONS, + title: undefined, + content: undefined, + gmpDraggable: undefined, + zIndex: undefined, + map: mapSpy, + }); + })); + + it('sets advanced marker inputs', fakeAsync(() => { + const options: google.maps.marker.AdvancedMarkerElementOptions = { + position: {lat: 3, lng: 5}, + title: 'marker title', + map: mapSpy, + content: undefined, + gmpDraggable: true, + zIndex: 1, + }; + const advancedMarkerSpy = createAdvancedMarkerSpy(options); + const advancedMarkerConstructorSpy = createAdvancedMarkerConstructorSpy(advancedMarkerSpy); + + const fixture = TestBed.createComponent(TestApp); + fixture.componentInstance.position = options.position; + fixture.componentInstance.title = options.title; + fixture.componentInstance.content = options.content; + fixture.componentInstance.gmpDraggable = options.gmpDraggable; + fixture.componentInstance.zIndex = options.zIndex; + + fixture.detectChanges(); + flush(); + + expect(advancedMarkerConstructorSpy).toHaveBeenCalledWith(options); + })); + + it('sets marker options, ignoring map', fakeAsync(() => { + const options: google.maps.marker.AdvancedMarkerElementOptions = { + position: {lat: 3, lng: 5}, + title: 'marker title', + content: undefined, + gmpDraggable: true, + zIndex: 1, + }; + const advancedMarkerSpy = createAdvancedMarkerSpy(options); + const advancedMarkerConstructorSpy = createAdvancedMarkerConstructorSpy(advancedMarkerSpy); + + const fixture = TestBed.createComponent(TestApp); + fixture.componentInstance.options = options; + fixture.detectChanges(); + flush(); + + expect(advancedMarkerConstructorSpy).toHaveBeenCalledWith({...options, map: mapSpy}); + })); + + it('gives precedence to specific inputs over options', fakeAsync(() => { + const options: google.maps.marker.AdvancedMarkerElementOptions = { + position: {lat: 3, lng: 5}, + title: 'marker title', + content: undefined, + gmpDraggable: true, + zIndex: 1, + }; + + const expectedOptions: google.maps.marker.AdvancedMarkerElementOptions = { + position: {lat: 4, lng: 6}, + title: 'marker title 2', + content: undefined, + gmpDraggable: false, + zIndex: 999, + map: mapSpy, + }; + const advancedMarkerSpy = createAdvancedMarkerSpy(options); + const advancedMarkerConstructorSpy = createAdvancedMarkerConstructorSpy(advancedMarkerSpy); + + const fixture = TestBed.createComponent(TestApp); + fixture.componentInstance.position = expectedOptions.position; + fixture.componentInstance.title = expectedOptions.title; + fixture.componentInstance.content = expectedOptions.content; + fixture.componentInstance.gmpDraggable = expectedOptions.gmpDraggable; + fixture.componentInstance.zIndex = expectedOptions.zIndex; + fixture.componentInstance.options = options; + + fixture.detectChanges(); + flush(); + + expect(advancedMarkerConstructorSpy).toHaveBeenCalledWith(expectedOptions); + })); + + it('initializes marker event handlers', fakeAsync(() => { + const advancedMarkerSpy = createAdvancedMarkerSpy(DEFAULT_MARKER_OPTIONS); + createAdvancedMarkerConstructorSpy(advancedMarkerSpy); + + const addSpy = advancedMarkerSpy.addListener; + const fixture = TestBed.createComponent(TestApp); + fixture.detectChanges(); + flush(); + + expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function)); + expect(addSpy).not.toHaveBeenCalledWith('drag', jasmine.any(Function)); + expect(addSpy).not.toHaveBeenCalledWith('dragend', jasmine.any(Function)); + expect(addSpy).not.toHaveBeenCalledWith('dragstart', jasmine.any(Function)); + })); + + it('should be able to add an event listener after init', fakeAsync(() => { + const advancedMarkerSpy = createAdvancedMarkerSpy(DEFAULT_MARKER_OPTIONS); + createAdvancedMarkerConstructorSpy(advancedMarkerSpy); + + const addSpy = advancedMarkerSpy.addListener; + const fixture = TestBed.createComponent(TestApp); + fixture.detectChanges(); + flush(); + + expect(addSpy).not.toHaveBeenCalledWith('drag', jasmine.any(Function)); + + // Pick an event that isn't bound in the template. + const subscription = fixture.componentInstance.advancedMarker.mapDrag.subscribe(); + fixture.detectChanges(); + + expect(addSpy).toHaveBeenCalledWith('drag', jasmine.any(Function)); + subscription.unsubscribe(); + })); +}); + +@Component({ + selector: 'test-app', + template: ` + + + + `, + standalone: true, + imports: [GoogleMap, MapAdvancedMarker], +}) +class TestApp { + @ViewChild(MapAdvancedMarker) advancedMarker: MapAdvancedMarker; + title?: string | null; + position?: google.maps.LatLng | google.maps.LatLngLiteral | null; + content?: Node | google.maps.marker.PinElement | null; + gmpDraggable?: boolean | null; + zIndex?: number | null; + options: google.maps.marker.AdvancedMarkerElementOptions; + + handleClick() {} +} diff --git a/src/google-maps/map-advanced-marker/map-advanced-marker.ts b/src/google-maps/map-advanced-marker/map-advanced-marker.ts new file mode 100644 index 000000000000..6bb820699ff4 --- /dev/null +++ b/src/google-maps/map-advanced-marker/map-advanced-marker.ts @@ -0,0 +1,255 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 +/// + +import { + Input, + OnDestroy, + OnInit, + Output, + NgZone, + Directive, + OnChanges, + SimpleChanges, + inject, + EventEmitter, +} from '@angular/core'; + +import {GoogleMap} from '../google-map/google-map'; +import {MapEventManager} from '../map-event-manager'; +import {Observable} from 'rxjs'; + +/** + * Default options for the Google Maps marker component. Displays a marker + * at the Googleplex. + */ +export const DEFAULT_MARKER_OPTIONS = { + position: {lat: 37.221995, lng: -122.184092}, +}; + +/** + * Angular component that renders a Google Maps marker via the Google Maps JavaScript API. + * + * See developers.google.com/maps/documentation/javascript/reference/marker + */ +@Directive({ + selector: 'map-advanced-marker', + exportAs: 'mapAdvancedMarker', + standalone: true, +}) +export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy { + private _eventManager = new MapEventManager(inject(NgZone)); + + /** + * Rollover text. If provided, an accessibility text (e.g. for use with screen readers) will be added to the AdvancedMarkerElement with the provided value. + * See: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.title + */ + @Input() + set title(title: string) { + this._title = title; + } + private _title: string; + + /** + * Sets the AdvancedMarkerElement's position. An AdvancedMarkerElement may be constructed without a position, but will not be displayed until its position is provided - for example, by a user's actions or choices. An AdvancedMarkerElement's position can be provided by setting AdvancedMarkerElement.position if not provided at the construction. + * Note: AdvancedMarkerElement with altitude is only supported on vector maps. + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.position + */ + @Input() + set position( + position: + | google.maps.LatLngLiteral + | google.maps.LatLng + | google.maps.LatLngAltitude + | google.maps.LatLngAltitudeLiteral, + ) { + this._position = position; + } + private _position: google.maps.LatLngLiteral | google.maps.LatLng; + + /** + * The DOM Element backing the visual of an AdvancedMarkerElement. + * Note: AdvancedMarkerElement does not clone the passed-in DOM element. Once the DOM element is passed to an AdvancedMarkerElement, passing the same DOM element to another AdvancedMarkerElement will move the DOM element and cause the previous AdvancedMarkerElement to look empty. + * See: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.content + */ + @Input() + set content(content: Node | google.maps.marker.PinElement) { + this._content = content; + } + private _content: Node; + + /** + * If true, the AdvancedMarkerElement can be dragged. + * Note: AdvancedMarkerElement with altitude is not draggable. + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.gmpDraggable + */ + @Input() + set gmpDraggable(draggable: boolean) { + this._draggable = draggable; + } + private _draggable: boolean; + + /** + * Options for constructing an AdvancedMarkerElement. + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions + */ + @Input() + set options(options: google.maps.marker.AdvancedMarkerElementOptions) { + this._options = options; + } + private _options: google.maps.marker.AdvancedMarkerElementOptions; + + /** + * All AdvancedMarkerElements are displayed on the map in order of their zIndex, with higher values displaying in front of AdvancedMarkerElements with lower values. By default, AdvancedMarkerElements are displayed according to their vertical position on screen, with lower AdvancedMarkerElements appearing in front of AdvancedMarkerElements farther up the screen. Note that zIndex is also used to help determine relative priority between CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY Advanced Markers. A higher zIndex value indicates higher priority. + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.zIndex + */ + @Input() + set zIndex(zIndex: number) { + this._zIndex = zIndex; + } + private _zIndex: number; + + /** + * This event is fired when the AdvancedMarkerElement element is clicked. + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.click + */ + @Output() readonly mapClick: Observable = + this._eventManager.getLazyEmitter('click'); + + /** + * This event is repeatedly fired while the user drags the AdvancedMarkerElement. + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.drag + */ + @Output() readonly mapDrag: Observable = + this._eventManager.getLazyEmitter('drag'); + + /** + * This event is fired when the user stops dragging the AdvancedMarkerElement. + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.dragend + */ + @Output() readonly mapDragend: Observable = + this._eventManager.getLazyEmitter('dragend'); + + /** + * This event is fired when the user starts dragging the AdvancedMarkerElement. + * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.dragstart + */ + @Output() readonly mapDragstart: Observable = + this._eventManager.getLazyEmitter('dragstart'); + + /** Event emitted when the marker is initialized. */ + @Output() readonly markerInitialized: EventEmitter = + new EventEmitter(); + + /** + * The underlying google.maps.marker.AdvancedMarkerElement object. + * + * See developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement + */ + advancedMarker: google.maps.marker.AdvancedMarkerElement; + + constructor( + private readonly _googleMap: GoogleMap, + private _ngZone: NgZone, + ) {} + + ngOnInit() { + if (!this._googleMap._isBrowser) { + return; + } + if (google.maps.marker?.AdvancedMarkerElement && this._googleMap.googleMap) { + this._initialize(this._googleMap.googleMap, google.maps.marker.AdvancedMarkerElement); + } else { + this._ngZone.runOutsideAngular(() => { + Promise.all([this._googleMap._resolveMap(), google.maps.importLibrary('marker')]).then( + ([map, lib]) => { + this._initialize(map, (lib as google.maps.MarkerLibrary).AdvancedMarkerElement); + }, + ); + }); + } + } + + private _initialize( + map: google.maps.Map, + advancedMarkerConstructor: typeof google.maps.marker.AdvancedMarkerElement, + ) { + // Create the object outside the zone so its events don't trigger change detection. + // We'll bring it back in inside the `MapEventManager` only for the events that the + // user has subscribed to. + this._ngZone.runOutsideAngular(() => { + this.advancedMarker = new advancedMarkerConstructor(this._combineOptions()); + this._assertInitialized(); + this.advancedMarker.map = map; + this._eventManager.setTarget(this.advancedMarker); + this.markerInitialized.next(this.advancedMarker); + }); + } + + ngOnChanges(changes: SimpleChanges) { + const {advancedMarker, _content, _position, _title, _draggable, _zIndex} = this; + if (advancedMarker) { + if (changes['title']) { + advancedMarker.title = _title; + } + + if (changes['content']) { + advancedMarker.content = _content; + } + + if (changes['gmpDraggable']) { + advancedMarker.gmpDraggable = _draggable; + } + + if (changes['content']) { + advancedMarker.content = _content; + } + + if (changes['position']) { + advancedMarker.position = _position; + } + + if (changes['zIndex']) { + advancedMarker.zIndex = _zIndex; + } + } + } + + ngOnDestroy() { + this.markerInitialized.complete(); + this._eventManager.destroy(); + } + + /** Creates a combined options object using the passed-in options and the individual inputs. */ + private _combineOptions(): google.maps.marker.AdvancedMarkerElementOptions { + const options = this._options || DEFAULT_MARKER_OPTIONS; + return { + ...options, + title: this._title || options.title, + position: this._position || options.position, + content: this._content || options.content, + zIndex: this._zIndex ?? options.zIndex, + gmpDraggable: this._draggable ?? options.gmpDraggable, + map: this._googleMap.googleMap, + }; + } + + /** Asserts that the map has been initialized. */ + private _assertInitialized(): asserts this is {marker: google.maps.marker.AdvancedMarkerElement} { + if (typeof ngDevMode === 'undefined' || ngDevMode) { + if (!this.advancedMarker) { + throw Error( + 'Cannot interact with a Google Map Marker before it has been ' + + 'initialized. Please wait for the Marker to load before trying to interact with it.', + ); + } + } + } +} diff --git a/src/google-maps/map-info-window/map-info-window.ts b/src/google-maps/map-info-window/map-info-window.ts index 7df31252414c..1a9f6981a79b 100644 --- a/src/google-maps/map-info-window/map-info-window.ts +++ b/src/google-maps/map-info-window/map-info-window.ts @@ -193,6 +193,25 @@ export class MapInfoWindow implements OnInit, OnDestroy { return this.infoWindow.getZIndex(); } + /** + * Opens the MapInfoWindow using the provided AdvancedMarkerElement. + */ + openAdvancedMarkerElement( + advancedMarkerElement: google.maps.marker.AdvancedMarkerElement, + content?: string | Element | Text, + ): void { + this._assertInitialized(); + if (!advancedMarkerElement) { + return; + } + + this.infoWindow.close(); + if (content) { + this.infoWindow.setContent(content); + } + this.infoWindow.open(this._googleMap.googleMap, advancedMarkerElement); + } + /** * Opens the MapInfoWindow using the provided anchor. If the anchor is not set, * then the position property of the options input is used instead. diff --git a/src/google-maps/public-api.ts b/src/google-maps/public-api.ts index 07266ec3ee9a..ed4a5a3251b3 100644 --- a/src/google-maps/public-api.ts +++ b/src/google-maps/public-api.ts @@ -21,6 +21,7 @@ export {MapGroundOverlay} from './map-ground-overlay/map-ground-overlay'; export {MapInfoWindow} from './map-info-window/map-info-window'; export {MapKmlLayer} from './map-kml-layer/map-kml-layer'; export {MapMarker} from './map-marker/map-marker'; +export {MapAdvancedMarker} from './map-advanced-marker/map-advanced-marker'; export {MapMarkerClusterer} from './map-marker-clusterer/map-marker-clusterer'; export {MapPolygon} from './map-polygon/map-polygon'; export {MapPolyline} from './map-polyline/map-polyline'; diff --git a/src/google-maps/testing/fake-google-map-utils.ts b/src/google-maps/testing/fake-google-map-utils.ts index 1c56ec6b320f..fd408581756a 100644 --- a/src/google-maps/testing/fake-google-map-utils.ts +++ b/src/google-maps/testing/fake-google-map-utils.ts @@ -36,6 +36,9 @@ export interface TestingWindow extends Window { visualization?: { HeatmapLayer?: jasmine.Spy; }; + marker?: { + AdvancedMarkerElement?: jasmine.Spy; + }; Geocoder?: jasmine.Spy; }; }; @@ -139,6 +142,45 @@ export function createMarkerConstructorSpy( return markerConstructorSpy; } +/** Creates a jasmine.SpyObj for a google.maps.marker.AdvancedMarkerElement */ +export function createAdvancedMarkerSpy( + options: google.maps.marker.AdvancedMarkerElementOptions, +): jasmine.SpyObj { + const advancedMarkerSpy = jasmine.createSpyObj('google.maps.marker.AdvancedMarkerElement', [ + 'addListener', + ]); + advancedMarkerSpy.addListener.and.returnValue({remove: () => {}}); + return advancedMarkerSpy; +} + +/** Creates a jasmine.Spy to watch for the constructor of a google.maps.marker.AdvancedMarkerElement */ +export function createAdvancedMarkerConstructorSpy( + advancedMarkerSpy: jasmine.SpyObj, +): jasmine.Spy { + // The spy target function cannot be an arrow-function as this breaks when created through `new`. + const advancedMarkerConstructorSpy = jasmine + .createSpy('Advanced Marker constructor', function () { + return advancedMarkerSpy; + }) + .and.callThrough(); + const testingWindow: TestingWindow = window; + if (testingWindow.google && testingWindow.google.maps) { + testingWindow.google.maps.marker = { + 'AdvancedMarkerElement': advancedMarkerConstructorSpy, + }; + } else { + testingWindow.google = { + maps: { + marker: { + 'AdvancedMarkerElement': advancedMarkerConstructorSpy, + }, + }, + }; + } + + return advancedMarkerConstructorSpy; +} + /** Creates a jasmine.SpyObj for a google.maps.InfoWindow */ export function createInfoWindowSpy( options: google.maps.InfoWindowOptions, From 2a798e382c3e9a5a7de86cb95887ec504f68a56d Mon Sep 17 00:00:00 2001 From: Oskar Kumor Date: Wed, 7 Feb 2024 14:29:13 +0100 Subject: [PATCH 2/3] feat(google-maps): remove default value for mapId and correct z-index description property for advanced markers --- src/google-maps/google-map/google-map.spec.ts | 1 - src/google-maps/google-map/google-map.ts | 4 ---- src/google-maps/map-advanced-marker/map-advanced-marker.ts | 2 +- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/google-maps/google-map/google-map.spec.ts b/src/google-maps/google-map/google-map.spec.ts index a1df53aa539c..270327ef522c 100644 --- a/src/google-maps/google-map/google-map.spec.ts +++ b/src/google-maps/google-map/google-map.spec.ts @@ -118,7 +118,6 @@ describe('GoogleMap', () => { center: {lat: 3, lng: 5}, zoom: 7, mapTypeId: DEFAULT_OPTIONS.mapTypeId, - mapId: DEFAULT_OPTIONS.mapId, }; mapSpy = createMapSpy(options); mapConstructorSpy = createMapConstructorSpy(mapSpy); diff --git a/src/google-maps/google-map/google-map.ts b/src/google-maps/google-map/google-map.ts index 2c9b9c61b2f2..387257b6d5f3 100644 --- a/src/google-maps/google-map/google-map.ts +++ b/src/google-maps/google-map/google-map.ts @@ -42,7 +42,6 @@ export const DEFAULT_OPTIONS: google.maps.MapOptions = { zoom: 17, // Note: the type conversion here isn't necessary for our CI, but it resolves a g3 failure. mapTypeId: 'roadmap' as unknown as google.maps.MapTypeId, - mapId: '123', }; /** Arbitrary default height for the map element */ @@ -532,9 +531,6 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { // Passing in an undefined `mapTypeId` seems to break tile loading // so make sure that we have some kind of default (see #22082). mapTypeId: this.mapTypeId || options.mapTypeId || DEFAULT_OPTIONS.mapTypeId, - // Passing in an undefined `mapTypeId` seems to break tile loading - // so make sure that we have some kind of default (see #22082). - mapId: this.mapId || options.mapId || DEFAULT_OPTIONS.mapId, }; } diff --git a/src/google-maps/map-advanced-marker/map-advanced-marker.ts b/src/google-maps/map-advanced-marker/map-advanced-marker.ts index 6bb820699ff4..b3d72bc03a39 100644 --- a/src/google-maps/map-advanced-marker/map-advanced-marker.ts +++ b/src/google-maps/map-advanced-marker/map-advanced-marker.ts @@ -107,7 +107,7 @@ export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy { private _options: google.maps.marker.AdvancedMarkerElementOptions; /** - * All AdvancedMarkerElements are displayed on the map in order of their zIndex, with higher values displaying in front of AdvancedMarkerElements with lower values. By default, AdvancedMarkerElements are displayed according to their vertical position on screen, with lower AdvancedMarkerElements appearing in front of AdvancedMarkerElements farther up the screen. Note that zIndex is also used to help determine relative priority between CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY Advanced Markers. A higher zIndex value indicates higher priority. + * AdvancedMarkerElements on the map are prioritized by zIndex, with higher values indicating higher display. * https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions.zIndex */ @Input() From 8ac5ea02a01627bae5fd1789025beca6e7957bca Mon Sep 17 00:00:00 2001 From: Oskar Kumor Date: Thu, 8 Feb 2024 21:35:31 +0100 Subject: [PATCH 3/3] feat(google-maps): generate api report file for google-maps --- .../google-maps/google-maps.md | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tools/public_api_guard/google-maps/google-maps.md b/tools/public_api_guard/google-maps/google-maps.md index 892c09c3ec6a..1f71a6f4a183 100644 --- a/tools/public_api_guard/google-maps/google-maps.md +++ b/tools/public_api_guard/google-maps/google-maps.md @@ -86,6 +86,7 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { readonly mapDrag: Observable; readonly mapDragend: Observable; readonly mapDragstart: Observable; + mapId: string | undefined; readonly mapInitialized: EventEmitter; readonly mapMousemove: Observable; readonly mapMouseout: Observable; @@ -115,7 +116,7 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { set zoom(zoom: number); readonly zoomChanged: Observable; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -127,12 +128,39 @@ export class GoogleMapsModule { // (undocumented) static ɵinj: i0.ɵɵInjectorDeclaration; // (undocumented) - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; } // @public export type HeatmapData = google.maps.MVCArray | (google.maps.LatLng | google.maps.visualization.WeightedLocation | google.maps.LatLngLiteral)[]; +// @public +export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy { + constructor(_googleMap: GoogleMap, _ngZone: NgZone); + advancedMarker: google.maps.marker.AdvancedMarkerElement; + set content(content: Node | google.maps.marker.PinElement); + set gmpDraggable(draggable: boolean); + readonly mapClick: Observable; + readonly mapDrag: Observable; + readonly mapDragend: Observable; + readonly mapDragstart: Observable; + readonly markerInitialized: EventEmitter; + // (undocumented) + ngOnChanges(changes: SimpleChanges): void; + // (undocumented) + ngOnDestroy(): void; + // (undocumented) + ngOnInit(): void; + set options(options: google.maps.marker.AdvancedMarkerElementOptions); + set position(position: google.maps.LatLngLiteral | google.maps.LatLng | google.maps.LatLngAltitude | google.maps.LatLngAltitudeLiteral); + set title(title: string); + set zIndex(zIndex: number); + // (undocumented) + static ɵdir: i0.ɵɵDirectiveDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + // @public export interface MapAnchorPoint { // (undocumented) @@ -364,6 +392,7 @@ export class MapInfoWindow implements OnInit, OnDestroy { // (undocumented) ngOnInit(): void; open(anchor?: MapAnchorPoint, shouldFocus?: boolean): void; + openAdvancedMarkerElement(advancedMarkerElement: google.maps.marker.AdvancedMarkerElement, content?: string | Element | Text): void; // (undocumented) set options(options: google.maps.InfoWindowOptions); // (undocumented)