From 3084c957094b673314cc0e233a6f6140eedbdc79 Mon Sep 17 00:00:00 2001 From: Justin Fagnani Date: Thu, 27 Dec 2018 11:09:42 -0800 Subject: [PATCH 1/2] Start upgrade to LitElement... --- .gitignore | 7 +- demo/index.html | 117 ++++----- package-lock.json | 32 +++ package.json | 29 +++ src/google-map-marker.ts | 458 ++++++++++++++++++++++++++++++++ src/google-map.ts | 546 +++++++++++++++++++++++++++++++++++++++ src/maps-api.ts | 37 +++ tsconfig.json | 62 +++++ 8 files changed, 1226 insertions(+), 62 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/google-map-marker.ts create mode 100644 src/google-map.ts create mode 100644 src/maps-api.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 73102b8..216432f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ -bower_components +/bower_components +/node_modules + +/google-map.* +/google-map-marker.* +/maps-api.* .idea diff --git a/demo/index.html b/demo/index.html index 3e2c613..a7e10c9 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,34 +1,37 @@ - - - Google Map demo - - - - - - - - + + + Google Map demo + + + + + + - - - + language="en" api-key="Z7ekrT3tbhl_dy8DCXuIuDDRc"> + - + - + marker.addEventListener('google-map-marker-dragend', function(e) { + var latLng = e.detail.latLng; + console.log('pin dropped', latLng.lat(), latLng.lng()); + point.latitude = latLng.lat(); + point.longitude = latLng.lng(); + poly._buildPathFromPoints(); + }); + }); + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cb142c2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "google-map", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@polymer/lit-element": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@polymer/lit-element/-/lit-element-0.6.5.tgz", + "integrity": "sha512-KVjuU/5Ugp6PFob6YEe1/B4GCKjqhEy9Tj954shL6d3DohT2sNAmbX9QfbXvcZ8RhbVELK6dzbN3i2BRA3mOKg==", + "requires": { + "lit-html": "^1.0.0-rc.1" + } + }, + "@types/googlemaps": { + "version": "3.30.16", + "resolved": "https://registry.npmjs.org/@types/googlemaps/-/googlemaps-3.30.16.tgz", + "integrity": "sha512-6OZ64ahLzYfzuSr71y4jAHZXiwxjvvEM2bfF2tzjIc9KUZVbO30SkYa8WewTsR8aM5BFz/uBiNlZ14eRLXYS0g==" + }, + "@webcomponents/webcomponentsjs": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.2.1.tgz", + "integrity": "sha512-lZZ+Lkke6JhsJcQQqSVk1Pny6/8y4qhJ98LO7a/MwBSRO8WqHqK1X2vscfeL8vOnYGFnmBUyVG95lwYv/AXyLQ==", + "dev": true + }, + "lit-html": { + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.0.0-rc.1.tgz", + "integrity": "sha512-Qyu+lHpRtJS3Addw56rK68KgZ5cdxTfRDmYNwd8gRQBImdhkE0uyqIT5usLE9YDezFeYVqgAc2WujOOi8taF1A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d50ce7b --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "google-map", + "version": "1.0.0", + "description": "google-map ==========", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/GoogleWebComponents/google-map.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/GoogleWebComponents/google-map/issues" + }, + "homepage": "https://github.com/GoogleWebComponents/google-map#readme", + "dependencies": { + "@polymer/lit-element": "^0.6.5", + "@types/googlemaps": "^3.30.16" + }, + "devDependencies": { + "@webcomponents/webcomponentsjs": "^2.2.1" + } +} diff --git a/src/google-map-marker.ts b/src/google-map-marker.ts new file mode 100644 index 0000000..7b9b3e1 --- /dev/null +++ b/src/google-map-marker.ts @@ -0,0 +1,458 @@ +// + +import {LitElement, html} from '@polymer/lit-element'; +import {customElement, property, query} from '@polymer/lit-element/lib/decorators.js'; +import {loadGoogleMapsAPI} from './maps-api.js'; + +const markerEvents = [ + 'animation_changed', + 'click', + 'clickable_changed', + 'cursor_changed', + 'dblclick', + 'drag', + 'dragend', + 'draggable_changed', + 'dragstart', + 'flat_changed', + 'icon_changed', + 'mousedown', + 'mouseout', + 'mouseover', + 'mouseup', + 'position_changed', + 'rightclick', + 'shape_changed', + 'title_changed', + 'visible_changed', + 'zindex_changed', +]; + +/** + * The `google-map-marker` element represents a map marker. It is used as a + * child of `google-map`. + * + * Example: + * + * + * + * + * + * Example - marker with info window (children create the window content): + * + * + * + * + * + * Example - a draggable marker: + * + * + * + * Example - hide a marker: + * + * + * + */ +@customElement('google-map-marker') +export class GoogleMapMarker extends LitElement { + + /** + * Fired when the marker icon was clicked. Requires the clickEvents attribute to be true. + * + * @param {google.maps.MouseEvent} event The mouse event. + * @event google-map-marker-click + */ + + /** + * Fired when the marker icon was double clicked. Requires the clickEvents attribute to be true. + * + * @param {google.maps.MouseEvent} event The mouse event. + * @event google-map-marker-dblclick + */ + + /** + * Fired repeatedly while the user drags the marker. Requires the dragEvents attribute to be true. + * + * @event google-map-marker-drag + */ + + /** + * Fired when the user stops dragging the marker. Requires the dragEvents attribute to be true. + * + * @event google-map-marker-dragend + */ + + /** + * Fired when the user starts dragging the marker. Requires the dragEvents attribute to be true. + * + * @event google-map-marker-dragstart + */ + + /** + * Fired for a mousedown on the marker. Requires the mouseEvents attribute to be true. + * + * @event google-map-marker-mousedown + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when the DOM `mousemove` event is fired on the marker. Requires the mouseEvents + * attribute to be true. + * + * @event google-map-marker-mousemove + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when the mouse leaves the area of the marker icon. Requires the mouseEvents attribute to be + * true. + * + * @event google-map-marker-mouseout + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when the mouse enters the area of the marker icon. Requires the mouseEvents attribute to be + * true. + * + * @event google-map-marker-mouseover + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired for a mouseup on the marker. Requires the mouseEvents attribute to be true. + * + * @event google-map-marker-mouseup + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired for a rightclick on the marker. Requires the clickEvents attribute to be true. + * + * @event google-map-marker-rightclick + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when an infowindow is opened. + * + * @event google-map-marker-open + */ + + /** + * Fired when the close button of the infowindow is pressed. + * + * @event google-map-marker-close + */ + + /** + * A Google Maps marker object. + * + * @type google.maps.Marker + */ + marker?: google.maps.Marker; + + /** + * The Google map object. + * + * @type google.maps.Map + */ + @property() + map?: google.maps.Map; + + /** + * A Google Map Infowindow object. + * + * @type {?Object} + */ + infoWindow?: google.maps.InfoWindow; + + /** + * When true, marker *click events are automatically registered. + */ + @property({type: Boolean}) + clickEvents = false; + + /** + * When true, marker drag* events are automatically registered. + */ + @property({type: Boolean}) + dragEvents = false; + + /** + * When true, marker mouse* events are automatically registered. + */ + @property({type: Boolean}) + mouseEvents = false; + + /** + * Image URL for the marker icon. + * + * @type string|google.maps.Icon|google.maps.Symbol + */ + @property() + icon?: string|google.maps.Icon|google.maps.Symbol; + + /** + * Z-index for the marker icon. + */ + @property({type: Number}) + zIndex: number = 0; + + /** + * The marker's latitude coordinate. + */ + @property({type: Number, reflect: true}) + latitude?: number; + + /** + * The marker's longitude coordinate. + */ + @property({type: Number, reflect: true}) + longitude?: number; + + /** + * The marker's label. + */ + @property({type: String}) + label?: string; + + /** + * A animation for the marker. "DROP" or "BOUNCE". See + * https://developers.google.com/maps/documentation/javascript/examples/marker-animations. + */ + @property({type: String}) + animation?: google.maps.Animation; + + /** + * Specifies whether the InfoWindow is open or not + */ + @property({type: Boolean}) + open = false; + + private _dragHandler?: google.maps.MapsEventListener; + private _openInfoHandler?: google.maps.MapsEventListener; + private _closeInfoHandler?: google.maps.MapsEventListener; + + private _listeners?: any; + + // observers: [ + // '_updatePosition(latitude, longitude)' + // ], + + constructor() { + super(); + console.log('google-map-marker'); + } + + render() { + return html` + + + `; + } + + update(changedProperties: Map) { + if (changedProperties.has('map')) { + this._mapChanged(); + } + if (changedProperties.has('open')) { + this._openChanged(); + } + super.update(changedProperties); + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this.marker) { + google.maps.event.clearInstanceListeners(this.marker); + this._listeners = {}; + this.marker.setMap(null); + } + // if (this._contentObserver) { + // this._contentObserver.disconnect(); + // } + } + + connectedCallback() { + super.connectedCallback(); + // If element is added back to DOM, put it back on the map. + if (this.marker) { + this.marker.setMap(this.map!); + } + } + + _updatePosition() { + if (this.marker && this.latitude !== undefined && this.longitude !== undefined) { + this.marker.setPosition(new google.maps.LatLng(this.latitude, this.longitude)); + } + } + + _animationChanged() { + if (this.marker) { + this.marker.setAnimation(this.animation === undefined ? null : this.animation); + } + } + + _labelChanged() { + if (this.marker && this.label !== undefined) { + this.marker.setLabel(this.label); + } + } + + _iconChanged() { + if (this.marker && this.icon !== undefined) { + this.marker.setIcon(this.icon); + } + } + + _zIndexChanged() { + if (this.marker) { + this.marker.setZIndex(this.zIndex); + } + } + + _mapChanged() { + console.log('_mapChanged'); + // Marker will be rebuilt, so disconnect existing one from old map and listeners. + if (this.marker) { + this.marker.setMap(null); + google.maps.event.clearInstanceListeners(this.marker); + } + + if (this.map && this.map instanceof google.maps.Map) { + this._mapReady(); + } + } + + _contentChanged() { + // if (this._contentObserver) + // this._contentObserver.disconnect(); + // // Watch for future updates. + // this._contentObserver = new MutationObserver( this._contentChanged.bind(this)); + // this._contentObserver.observe( this, { + // childList: true, + // subtree: true + // }); + + const content = this.innerHTML.trim(); + console.log('_contentChanged', content, this.infoWindow); + if (content) { + if (!this.infoWindow) { + // Create a new infowindow + this.infoWindow = new google.maps.InfoWindow(); + this._openInfoHandler = google.maps.event.addListener(this.marker!, 'click', () => { + this.open = true; + }); + + this._closeInfoHandler = google.maps.event.addListener(this.infoWindow, 'closeclick', () => { + this.open = false; + }); + } + this.infoWindow.setContent(content); + } else { + if (this.infoWindow) { + // Destroy the existing infowindow. It doesn't make sense to have an empty one. + google.maps.event.removeListener(this._openInfoHandler!); + google.maps.event.removeListener(this._closeInfoHandler!); + this.infoWindow = undefined; + } + } + } + + _openChanged() { + if (this.infoWindow) { + if (this.open) { + this.infoWindow.open(this.map, this.marker); + this.dispatchEvent(new CustomEvent('google-map-marker-open')); + } else { + this.infoWindow.close(); + this.dispatchEvent(new CustomEvent('google-map-marker-close')); + } + } + } + + private _mapReady() { + console.log('_mapReady'); + this._listeners = {}; + this.marker = new google.maps.Marker({ + map: this.map, + position: { + lat: this.latitude!, + lng: this.longitude!, + }, + title: this.title, + animation: this.animation, + draggable: this.draggable, + visible: !this.hidden, + icon: this.icon, + label: this.label, + zIndex: this.zIndex + }); + this._contentChanged(); + markerEvents.forEach((e) => this._forwardEvent(e)); + this._openChanged(); + this._setupDragHandler(); + } + + private _forwardEvent(name: string) { + this._listeners[name] = google.maps.event.addListener(this.marker!, name, (event: Event) => { + this.dispatchEvent(new CustomEvent(`google-map-marker-${name}`, { + detail: { + mapsEvent: event, + } + })); + }); + } + + attributeChangedCallback(name: string, oldValue: string, newValue: string) { + super.attributeChangedCallback(name, oldValue, newValue); + if (!this.marker) { + return; + } + + // Cannot use *Changed watchers for native properties. + switch (name) { + case 'hidden': + this.marker.setVisible(!this.hidden); + break; + case 'draggable': + this.marker.setDraggable(this.draggable); + this._setupDragHandler(); + break; + case 'title': + this.marker.setTitle(this.title); + break; + } + } + + /** + * @this {GoogleMapMarkerElement} This function is called with .bind(this) in the map + * marker element below. + */ + private _setupDragHandler() { + if (this.draggable) { + this._dragHandler = google.maps.event.addListener( + this.marker!, 'dragend', this._onDragEnd); + } else { + google.maps.event.removeListener(this._dragHandler!); + this._dragHandler = undefined; + } + } + + /** + * @this {GoogleMapMarkerElement} This function is called with .bind(this) in setupDragHandler + *_above. + */ + private _onDragEnd = (e: google.maps.MouseEvent, _details: unknown, _sender: unknown) => { + this.latitude = e.latLng.lat(); + this.longitude = e.latLng.lng(); + } +} diff --git a/src/google-map.ts b/src/google-map.ts new file mode 100644 index 0000000..191d829 --- /dev/null +++ b/src/google-map.ts @@ -0,0 +1,546 @@ +import {LitElement, html, PropertyValues} from '@polymer/lit-element'; +import {customElement, property, query} from '@polymer/lit-element/lib/decorators.js'; +import {loadGoogleMapsAPI} from './maps-api.js'; +import { GoogleMapMarker } from './google-map-marker.js'; + +// +// +// +// + +const mapEvents = [ + 'bounds_changed', + 'center_changed', + 'click', + 'dblclick', + 'drag', + 'dragend', + 'dragstart', + 'heading_changed', + 'idle', + 'maptypeid_changed', + 'mousemove', + 'mouseout', + 'mouseover', + 'projection_changed', + 'rightclick', + 'tilesloaded', + 'tilt_changed', + 'zoom_changed' +]; + +/** + * The `google-map` element renders a Google Map. + * + * Example: + * + * + * + * + * Example - add markers to the map and ensure they're in view: + * + * + * + * + * + * + * Example: + * + * + * + * + * Example - with Google directions, using data-binding inside another + * Polymer element + * + * + * + * + * + * Disable dragging by adding `draggable="false"` on the `google-map` element. + * + * Example - loading the Maps API from another origin (China) + * + * + * + * ### Tips + * + * If you're seeing the message "You have included the Google Maps API multiple + * times on this page. This may cause unexpected errors." it probably means + * you're loading other maps elements on the page (``). + * Each maps element must include the same set of configuration options + * (`apiKey`, `clientId`, `language`, `version`, etc.) so the Maps API is loaded + * from the same URL. + * + * @demo demo/index.html + * @demo demo/polys.html + * @demo demo/kml.html + */ +@customElement('google-map') +export class GoogleMap extends LitElement { + + /** + * Fired when the Maps API has fully loaded. + * + * @event google-map-ready + */ + + /** + * Fired when the user clicks on the map (but not when they click on a marker, infowindow, or + * other object). Requires the clickEvents attribute to be true. + * + * @event google-map-click + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when the user double-clicks on the map. Note that the google-map-click event will also fire, + * right before this one. Requires the clickEvents attribute to be true. + * + * @event google-map-dblclick + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired repeatedly while the user drags the map. Requires the dragEvents attribute to be true. + * + * @event google-map-drag + */ + + /** + * Fired when the user stops dragging the map. Requires the dragEvents attribute to be true. + * + * @event google-map-dragend + */ + + /** + * Fired when the user starts dragging the map. Requires the dragEvents attribute to be true. + * + * @event google-map-dragstart + */ + + /** + * Fired whenever the user's mouse moves over the map container. Requires the mouseEvents attribute to + * be true. + * + * @event google-map-mousemove + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when the user's mouse exits the map container. Requires the mouseEvents attribute to be true. + * + * @event google-map-mouseout + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when the user's mouse enters the map container. Requires the mouseEvents attribute to be true. + * + * @event google-map-mouseover + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when the DOM `contextmenu` event is fired on the map container. Requires the clickEvents + * attribute to be true. + * + * @event google-map-rightclick + * @param {google.maps.MouseEvent} event The mouse event. + */ + + /** + * Fired when the map becomes idle after panning or zooming. + * + * @event google-map-idle + */ + + /** + * A Maps API key. To obtain an API key, see https://developers.google.com/maps/documentation/javascript/tutorial#api_key. + */ + @property({attribute: 'api-key'}) + apiKey?: string; + + /** + * Version of the Google Maps API to use. + */ + @property({attribute: 'api-version'}) + apiVersion = '3.33'; + + /** + * Overrides the origin the Maps API is loaded from. Defaults to `https://maps.googleapis.com`. + */ + @property() + mapsUrl?: string; + + /** + * A Maps API for Business Client ID. To obtain a Maps API for Business Client ID, see https://developers.google.com/maps/documentation/business/. + * If set, a Client ID will take precedence over an API Key. + */ + @property({attribute: 'client-id'}) + clientId?: string; + + /** + * A latitude to center the map on. + */ + @property({type: Number}) + latitude: number = 37.77493; + + /** + * A longitude to center the map on. + */ + @property({type: Number}) + longitude: number = -122.41942; + + /** + * A zoom level to set the map to. + */ + @property({type: Number}) + zoom: number = 10; + + /** + * A Maps API object. + */ + map?: google.maps.Map; + + /** + * A kml file to load. + */ + // kml: { + // type: String, + // value: null, + // observer: '_loadKml' + // }, + + @property({type: Number}) + tilt?: number; + + /** + * Map type to display. One of 'roadmap', 'satellite', 'hybrid', 'terrain'. + */ + @property({type: String, reflect: true}) + mapTypeId: google.maps.MapTypeId|'roadmap' | 'satellite' | 'hybrid' | 'terrain' = 'roadmap'; + + /** + * If set, removes the map's default UI controls. + */ + @property({type: Boolean, attribute: 'disable-default-ui'}) + disableDefaultUI?: boolean; + + @property({type: Boolean, attribute: 'map-type-control'}) + mapTypeControl?: boolean; + + @property({type: Boolean, attribute: 'street-view-control'}) + streetViewControl?: boolean; + + /** + * If set, the zoom level is set such that all markers (google-map-marker children) are brought into view. + */ + @property({type: Boolean, attribute: 'fit-to-markers'}) + fitToMarkers = false; + + /** + * If true, prevent the user from zooming the map interactively. + */ + @property({type: Boolean, attribute: 'disable-zoom'}) + disableZoom = false; + + /** + * If set, custom styles can be applied to the map. + * For style documentation see https://developers.google.com/maps/documentation/javascript/reference#MapTypeStyle + */ + @property({type: Object}) + styles?: google.maps.MapTypeStyle[]; + + /** + * A maximum zoom level which will be displayed on the map. + */ + @property({type: Number, attribute: 'max-zoom'}) + maxZoom?: number; + + /** + * A minimum zoom level which will be displayed on the map. + */ + @property({type: Number, attribute: 'min-zoom'}) + minZoom?: number; + + /** + * If true, sign-in is enabled. + * See https://developers.google.com/maps/documentation/javascript/signedin#enable_sign_in + */ + // signedIn: { + // type: Boolean, + // value: false + // }, + + /** + * The localized language to load the Maps API with. For more information + * see https://developers.google.com/maps/documentation/javascript/basics#Language + * + * Note: the Maps API defaults to the preffered language setting of the browser. + * Use this parameter to override that behavior. + */ + @property() + language?: string; + + /** + * Additional map options for google.maps.Map constructor. + * Use to specify additional options we do not expose as + * properties. + * Ex: `` + * + * Note, you can't use API enums like `google.maps.ControlPosition.TOP_RIGHT` + * when using this property as an HTML attribute. Instead, use the actual + * value (e.g. `3`) or set `.options` in JS rather than using + * the attribute. + */ + @property({type: Object}) + options: any; + + /** + * The markers on the map. + */ + markers: Array = []; + + /** + * The non-marker objects on the map. + */ + readonly objects!: Array; + + /** + * If set, all other info windows on markers are closed when opening a new one. + */ + @property({type: Boolean, attribute: 'single-info-window'}) + singleInfoWindow = false; + + @query('#map') + private _mapDiv!: HTMLDivElement; + + @query('slot') + private _slot!: HTMLSlotElement; + + private _markersChildrenListener?: EventListener; + + render() { + return html` + +
+ + `; + } + + protected update(changedProperties: PropertyValues) { + if (changedProperties.has('apiKey')) { + this._initGMap(); + } + // Re-set options every update. + // TODO(justinfagnani): Check to see if this hurts perf + if (this.map !== undefined) { + this.map.setOptions(this._getMapOptions()); + } + super.update(changedProperties); + } + + private async _initGMap() { + if (this.map) { + return; + } + await loadGoogleMapsAPI(this.apiKey); + + this.map = new google.maps.Map(this._mapDiv, this._getMapOptions()); + this._updateCenter(); + // this._loadKml(); + this._updateMarkers(); + mapEvents.forEach((event) => this._forwardEvent(event)); + this.dispatchEvent(new CustomEvent('google-map-ready')); + } + + private _getMapOptions(): google.maps.MapOptions { + return { + zoom: this.zoom, + tilt: this.tilt, + mapTypeId: this.mapTypeId as google.maps.MapTypeId, + disableDefaultUI: this.disableDefaultUI, + mapTypeControl: this.mapTypeControl, + streetViewControl: this.streetViewControl, + disableDoubleClickZoom: this.disableZoom, + // scrollwheel: this.scrollWheel, + styles: this.styles, + maxZoom: this.maxZoom, + minZoom: this.minZoom, + draggable: this.draggable, + ...this.options, + }; + } + + private _attachChildrenToMap(children: HTMLElement[]) { + if (this.map) { + console.log('_attachChildrenToMap'); + for (const child of children) { + (child as any).map = this.map; + } + } + } + + /** + * Watch for future updates to marker objects + */ + private _observeMarkers() { + // Watch for future updates. + if (this._markersChildrenListener) { + return; + } + this._markersChildrenListener = () => this._updateMarkers; + this._slot.addEventListener('slotchange', this._markersChildrenListener); + } + + private _updateMarkers() { + const newMarkers = this._slot.assignedNodes() + .filter((n) => n instanceof GoogleMapMarker) as GoogleMapMarker[]; + + console.log('_updateMarkers', newMarkers); + + // do not recompute if markers have not been added or removed + if (newMarkers.length === this.markers.length) { + const added = newMarkers.filter((m) => this.markers && this.markers.indexOf(m) === -1); + if (added.length === 0) { + // set up observer first time around + if (!this._markersChildrenListener) { + this._observeMarkers(); + } + return; + } + } + + this._observeMarkers(); + + this.markers = newMarkers; + + // Set the map on each marker and zoom viewport to ensure they're in view. + this._attachChildrenToMap(this.markers); + if (this.fitToMarkers) { + this._fitToMarkersChanged(); + } + } + + private _onMarkerOpen(e: Event) { + console.log('_onMarkerOpen', e); + } + + /** + * Explicitly resizes the map, updating its center. This is useful if the + * map does not show after you have unhidden it. + * + * @method resize + */ + resize() { + if (this.map) { + // saves and restores latitude/longitude because resize can move the center + const oldLatitude = this.latitude; + const oldLongitude = this.longitude; + google.maps.event.trigger(this.map, 'resize'); + this.latitude = oldLatitude; // restore because resize can move our center + this.longitude = oldLongitude; + + if (this.fitToMarkers) { // we might not have a center if we are doing fit-to-markers + this._fitToMarkersChanged(); + } + } + } + + // private _loadKml() { + // if (this.map && this.kml) { + // var kmlfile = new google.maps.KmlLayer({ + // url: this.kml, + // map: this.map + // }); + // } + // } + + private _updateCenter() { + console.log('_updateCenter'); + if (this.map !== undefined && this.latitude !== undefined && this.longitude !== undefined) { + const newCenter = new google.maps.LatLng(this.latitude, this.longitude); + let oldCenter = this.map.getCenter(); + + if (oldCenter === undefined) { + // If the map does not have a center, set it right away. + this.map.setCenter(newCenter); + } else { + // Using google.maps.LatLng returns corrected lat/lngs. + oldCenter = new google.maps.LatLng(oldCenter.lat(), oldCenter.lng()); + + // If the map currently has a center, slowly pan to the new one. + if (!oldCenter.equals(newCenter)) { + this.map.panTo(newCenter); + } + } + } + } + + private _fitToMarkersChanged() { + // TODO(ericbidelman): respect user's zoom level. + + if (this.map && this.fitToMarkers && this.markers.length > 0) { + const latLngBounds = new google.maps.LatLngBounds(); + for (const m of this.markers) { + latLngBounds.extend( + new google.maps.LatLng(m.latitude!, m.longitude!)); + } + + // For one marker, don't alter zoom, just center it. + if (this.markers.length > 1) { + this.map.fitBounds(latLngBounds); + } + + this.map.setCenter(latLngBounds.getCenter()); + } + } + + private _forwardEvent(name: string) { + google.maps.event.addListener(this.map!, name, (event: Event) => { + this.dispatchEvent(new CustomEvent(`google-map-${name}`, { + detail: { + mapsEvent: event, + } + })); + }); + } + + // private _deselectMarker(e: Event, _detail: unknown) { + // // If singleInfoWindow is set, update iron-selector's selected attribute to be null. + // // Else remove the marker from iron-selector's selected array. + // var markerIndex = this.$.selector.indexOf(e.target); + + // if (this.singleInfoWindow) { + // this.$.selector.selected = null; + // } else if (this.$.selector.selectedValues) { + // this.$.selector.selectedValues = this.$.selector.selectedValues.filter((i: number) => i !== markerIndex); + // } + // } +} diff --git a/src/maps-api.ts b/src/maps-api.ts new file mode 100644 index 0000000..20d61c3 --- /dev/null +++ b/src/maps-api.ts @@ -0,0 +1,37 @@ + +declare global { + interface Window { + _resolveGoogleMapsAPI: Function; + _rejectGoogleMapsAPI: Function; + } +} + +let initCalled = false; +const callbackPromise = new Promise((res, rej) => { + window._resolveGoogleMapsAPI = res; + window._rejectGoogleMapsAPI = rej; +}); + +export const loadGoogleMapsAPI = async (apiKey?: string): Promise => { + if (!initCalled) { + const script = document.createElement('script'); + script.addEventListener('error', (e) => { + window._rejectGoogleMapsAPI(e); + }); + script.src = `https://maps.googleapis.com/maps/api/js?${apiKey ? `key=${apiKey}&` : ''}callback=_resolveGoogleMapsAPI`; + document.head.appendChild(script); + initCalled = true; + } + await callbackPromise; + return google.maps; +}; + +// export const forwardEvent = (instance: object, name: string, target: EventTarget) => { +// google.maps.event.addListener(instance, name, (event: Event) => { +// target.dispatchEvent(new CustomEvent(`google-map-marker-${name}`, { +// detail: { +// mapsEvent: event, +// } +// })); +// }); +// } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..23fa920 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,62 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "lib": ["dom", "es2017"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": ["./src/**/*.ts"], + "exclude": [] +} From 8570781d718f02bc2abb0d77235406b5287db42b Mon Sep 17 00:00:00 2001 From: Justin Fagnani Date: Wed, 13 Feb 2019 19:39:03 -0800 Subject: [PATCH 2/2] WIP: convert to LitElement --- .gitignore | 14 ++- demo/kml.html | 60 ++++++----- package-lock.json | 28 +++--- package.json | 7 +- src/google-map-kml-layer.ts | 9 ++ src/google-map-marker.ts | 30 ++---- src/google-map.ts | 148 +++++++++++----------------- src/lib/deferred.ts | 8 ++ src/lib/google-map-child-element.ts | 64 ++++++++++++ 9 files changed, 198 insertions(+), 170 deletions(-) create mode 100644 src/google-map-kml-layer.ts create mode 100644 src/lib/deferred.ts create mode 100644 src/lib/google-map-child-element.ts diff --git a/.gitignore b/.gitignore index 216432f..5bd87ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ -/bower_components /node_modules -/google-map.* -/google-map-marker.* -/maps-api.* -.idea +/google-map*.d.ts +/google-map*.d.ts.map +/google-map*.js +/google-map*.js.map +/maps-api.d.ts +/maps-api.d.ts.map +/maps-api.js +/maps-api.js.map +/lib diff --git a/demo/kml.html b/demo/kml.html index 0524401..48222be 100644 --- a/demo/kml.html +++ b/demo/kml.html @@ -1,35 +1,33 @@ - - - Google Map Poly demo - - - - - - - - - - - + + + Google Maps KML demo + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index cb142c2..a785bcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,29 +4,29 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@polymer/lit-element": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@polymer/lit-element/-/lit-element-0.6.5.tgz", - "integrity": "sha512-KVjuU/5Ugp6PFob6YEe1/B4GCKjqhEy9Tj954shL6d3DohT2sNAmbX9QfbXvcZ8RhbVELK6dzbN3i2BRA3mOKg==", - "requires": { - "lit-html": "^1.0.0-rc.1" - } - }, "@types/googlemaps": { "version": "3.30.16", "resolved": "https://registry.npmjs.org/@types/googlemaps/-/googlemaps-3.30.16.tgz", "integrity": "sha512-6OZ64ahLzYfzuSr71y4jAHZXiwxjvvEM2bfF2tzjIc9KUZVbO30SkYa8WewTsR8aM5BFz/uBiNlZ14eRLXYS0g==" }, "@webcomponents/webcomponentsjs": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.2.1.tgz", - "integrity": "sha512-lZZ+Lkke6JhsJcQQqSVk1Pny6/8y4qhJ98LO7a/MwBSRO8WqHqK1X2vscfeL8vOnYGFnmBUyVG95lwYv/AXyLQ==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.2.7.tgz", + "integrity": "sha512-kPPjzV+5kpoWpTniyvBSPcXS33f3j/C6HvNOJ3YecF3pvz3XwVeU4ammbxtVy/osF3z7hr1DYNptIf4oPEvXZA==", "dev": true }, + "lit-element": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.0.1.tgz", + "integrity": "sha512-2bu3B2ZYUZgntvOgu3i5mRK8geo45CLUwxwJEYK54hyednoJasjiTZPB13NBg1D+hNM2JfmWTWJnh1QEUQv7zw==", + "requires": { + "lit-html": "^1.0.0" + } + }, "lit-html": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.0.0-rc.1.tgz", - "integrity": "sha512-Qyu+lHpRtJS3Addw56rK68KgZ5cdxTfRDmYNwd8gRQBImdhkE0uyqIT5usLE9YDezFeYVqgAc2WujOOi8taF1A==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.0.0.tgz", + "integrity": "sha512-oeWlpLmBW3gFl7979Wol2LKITpmKTUFNn7PnFbh6YNynF61W74l6x5WhwItAwPRSATpexaX1egNnRzlN4GOtfQ==" } } } diff --git a/package.json b/package.json index d50ce7b..3d3ce37 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "test": "test" }, "scripts": { + "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -20,10 +21,10 @@ }, "homepage": "https://github.com/GoogleWebComponents/google-map#readme", "dependencies": { - "@polymer/lit-element": "^0.6.5", - "@types/googlemaps": "^3.30.16" + "@types/googlemaps": "^3.30.16", + "lit-element": "^2.0.1" }, "devDependencies": { - "@webcomponents/webcomponentsjs": "^2.2.1" + "@webcomponents/webcomponentsjs": "^2.2.7" } } diff --git a/src/google-map-kml-layer.ts b/src/google-map-kml-layer.ts new file mode 100644 index 0000000..6bf6b97 --- /dev/null +++ b/src/google-map-kml-layer.ts @@ -0,0 +1,9 @@ +// + +import { GoogleMapChildElement, customElement, property } from './lib/google-map-child-element.js'; + +@customElement('google-map-kml-layer') +export class GoogleMapKmlLayer extends GoogleMapChildElement { + @property() + url?: string; +} diff --git a/src/google-map-marker.ts b/src/google-map-marker.ts index 7b9b3e1..851fb57 100644 --- a/src/google-map-marker.ts +++ b/src/google-map-marker.ts @@ -1,8 +1,6 @@ // -import {LitElement, html} from '@polymer/lit-element'; -import {customElement, property, query} from '@polymer/lit-element/lib/decorators.js'; -import {loadGoogleMapsAPI} from './maps-api.js'; +import { GoogleMapChildElement, html, customElement, property } from './lib/google-map-child-element.js'; const markerEvents = [ 'animation_changed', @@ -57,7 +55,7 @@ const markerEvents = [ * */ @customElement('google-map-marker') -export class GoogleMapMarker extends LitElement { +export class GoogleMapMarker extends GoogleMapChildElement { /** * Fired when the marker icon was clicked. Requires the clickEvents attribute to be true. @@ -155,14 +153,6 @@ export class GoogleMapMarker extends LitElement { */ marker?: google.maps.Marker; - /** - * The Google map object. - * - * @type google.maps.Map - */ - @property() - map?: google.maps.Map; - /** * A Google Map Infowindow object. * @@ -248,17 +238,6 @@ export class GoogleMapMarker extends LitElement { console.log('google-map-marker'); } - render() { - return html` - - - `; - } - update(changedProperties: Map) { if (changedProperties.has('map')) { this._mapChanged(); @@ -327,7 +306,7 @@ export class GoogleMapMarker extends LitElement { google.maps.event.clearInstanceListeners(this.marker); } - if (this.map && this.map instanceof google.maps.Map) { + if (this.map instanceof google.maps.Map) { this._mapReady(); } } @@ -342,6 +321,7 @@ export class GoogleMapMarker extends LitElement { // subtree: true // }); + // TODO(justinfagnani): no, no, no... Use Nodes, not innerHTML. const content = this.innerHTML.trim(); console.log('_contentChanged', content, this.infoWindow); if (content) { @@ -379,6 +359,7 @@ export class GoogleMapMarker extends LitElement { } } + // TODO(justinfagnani): call from GoogleMapChildElement private _mapReady() { console.log('_mapReady'); this._listeners = {}; @@ -402,6 +383,7 @@ export class GoogleMapMarker extends LitElement { this._setupDragHandler(); } + // TODO(justinfagnani): move to utils / base class private _forwardEvent(name: string) { this._listeners[name] = google.maps.event.addListener(this.marker!, name, (event: Event) => { this.dispatchEvent(new CustomEvent(`google-map-marker-${name}`, { diff --git a/src/google-map.ts b/src/google-map.ts index 191d829..267a9ac 100644 --- a/src/google-map.ts +++ b/src/google-map.ts @@ -1,12 +1,22 @@ -import {LitElement, html, PropertyValues} from '@polymer/lit-element'; -import {customElement, property, query} from '@polymer/lit-element/lib/decorators.js'; +/** + * @license + * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ + +import {LitElement, html, PropertyValues, css} from 'lit-element'; +import {customElement, property, query} from 'lit-element/lib/decorators.js'; import {loadGoogleMapsAPI} from './maps-api.js'; import { GoogleMapMarker } from './google-map-marker.js'; - -// -// -// -// +import { Deferred } from './lib/deferred.js'; const mapEvents = [ 'bounds_changed', @@ -215,15 +225,6 @@ export class GoogleMap extends LitElement { */ map?: google.maps.Map; - /** - * A kml file to load. - */ - // kml: { - // type: String, - // value: null, - // observer: '_loadKml' - // }, - @property({type: Number}) tilt?: number; @@ -333,22 +334,25 @@ export class GoogleMap extends LitElement { private _markersChildrenListener?: EventListener; + private _mapReadyDeferred = new Deferred(); + + static styles = css` + :host { + position: relative; + display: block; + height: 100%; + } + #map { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + `; + render() { return html` -
`; @@ -366,18 +370,32 @@ export class GoogleMap extends LitElement { super.update(changedProperties); } + constructor() { + super(); + // Respond to child elements requesting a Map instance + this.addEventListener('google-map-get-map-instance', (e: Event) => { + console.log('google-map google-map-get-map-instance'); + const detail = (e as CustomEvent).detail; + detail.mapReady = this._mapReadyDeferred.promise; + }); + // TODO(justinfagnani): Now that children register thmselves, figure out + // when to call this._fitToMarkersChanged(), or remove the feature + } + private async _initGMap() { - if (this.map) { + if (this.map !== undefined) { return; } + // TODO(justinfagnani): support a global API as well - a singleton API + // instance shared for the whole window, where each element doesn't need + // its own API key. await loadGoogleMapsAPI(this.apiKey); this.map = new google.maps.Map(this._mapDiv, this._getMapOptions()); this._updateCenter(); - // this._loadKml(); - this._updateMarkers(); mapEvents.forEach((event) => this._forwardEvent(event)); this.dispatchEvent(new CustomEvent('google-map-ready')); + this._mapReadyDeferred.resolve(this.map); } private _getMapOptions(): google.maps.MapOptions { @@ -398,56 +416,6 @@ export class GoogleMap extends LitElement { }; } - private _attachChildrenToMap(children: HTMLElement[]) { - if (this.map) { - console.log('_attachChildrenToMap'); - for (const child of children) { - (child as any).map = this.map; - } - } - } - - /** - * Watch for future updates to marker objects - */ - private _observeMarkers() { - // Watch for future updates. - if (this._markersChildrenListener) { - return; - } - this._markersChildrenListener = () => this._updateMarkers; - this._slot.addEventListener('slotchange', this._markersChildrenListener); - } - - private _updateMarkers() { - const newMarkers = this._slot.assignedNodes() - .filter((n) => n instanceof GoogleMapMarker) as GoogleMapMarker[]; - - console.log('_updateMarkers', newMarkers); - - // do not recompute if markers have not been added or removed - if (newMarkers.length === this.markers.length) { - const added = newMarkers.filter((m) => this.markers && this.markers.indexOf(m) === -1); - if (added.length === 0) { - // set up observer first time around - if (!this._markersChildrenListener) { - this._observeMarkers(); - } - return; - } - } - - this._observeMarkers(); - - this.markers = newMarkers; - - // Set the map on each marker and zoom viewport to ensure they're in view. - this._attachChildrenToMap(this.markers); - if (this.fitToMarkers) { - this._fitToMarkersChanged(); - } - } - private _onMarkerOpen(e: Event) { console.log('_onMarkerOpen', e); } @@ -459,7 +427,7 @@ export class GoogleMap extends LitElement { * @method resize */ resize() { - if (this.map) { + if (this.map !== undefined) { // saves and restores latitude/longitude because resize can move the center const oldLatitude = this.latitude; const oldLongitude = this.longitude; @@ -473,15 +441,6 @@ export class GoogleMap extends LitElement { } } - // private _loadKml() { - // if (this.map && this.kml) { - // var kmlfile = new google.maps.KmlLayer({ - // url: this.kml, - // map: this.map - // }); - // } - // } - private _updateCenter() { console.log('_updateCenter'); if (this.map !== undefined && this.latitude !== undefined && this.longitude !== undefined) { @@ -522,6 +481,9 @@ export class GoogleMap extends LitElement { } } + /** + * Forwards Maps API events as DOM CustomEvents + */ private _forwardEvent(name: string) { google.maps.event.addListener(this.map!, name, (event: Event) => { this.dispatchEvent(new CustomEvent(`google-map-${name}`, { diff --git a/src/lib/deferred.ts b/src/lib/deferred.ts new file mode 100644 index 0000000..1c35042 --- /dev/null +++ b/src/lib/deferred.ts @@ -0,0 +1,8 @@ +export class Deferred { + readonly promise: Promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + resolve!: (v: T) => void; + reject!: (e: Error) => void; +} diff --git a/src/lib/google-map-child-element.ts b/src/lib/google-map-child-element.ts new file mode 100644 index 0000000..afee3e7 --- /dev/null +++ b/src/lib/google-map-child-element.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at + * http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at + * http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at + * http://polymer.github.io/PATENTS.txt + */ + +import {LitElement, html, css} from 'lit-element'; +import {property} from 'lit-element/lib/decorators.js'; +import { loadGoogleMapsAPI } from '../maps-api'; + +export {html, svg, css} from 'lit-element'; +export {customElement, property, query} from 'lit-element/lib/decorators.js'; + +/** + * Base class that helps manage references to the containing google.maps.Map + * instance. + */ +export abstract class GoogleMapChildElement extends LitElement { + + static styles = css` + :host { + display: none; + } + `; + + @property() + map?: google.maps.Map; + + mapReady?: Promise; + + render() { + return html``; + } + + /** + * Gets an instance of google.maps.Map by firing a google-map-get-map-instance + * event to request the instance from an ancestor element. GoogleMap responds + * to this event. + */ + protected async _getMapInstance(): Promise { + const detail: {mapReady?: Promise} = {}; + this.dispatchEvent(new CustomEvent('google-map-get-map-instance', { + bubbles: true, + detail, + })); + return detail.mapReady; + } + + connectedCallback() { + super.connectedCallback(); + this.mapReady = this._getMapInstance(); + this.mapReady.then((map) => { + this.map = map; + }); + } +}