diff --git a/.gitignore b/.gitignore index 73102b8..5bd87ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ -bower_components -.idea +/node_modules + +/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/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/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 new file mode 100644 index 0000000..a785bcc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "google-map", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@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.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", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.0.0.tgz", + "integrity": "sha512-oeWlpLmBW3gFl7979Wol2LKITpmKTUFNn7PnFbh6YNynF61W74l6x5WhwItAwPRSATpexaX1egNnRzlN4GOtfQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3d3ce37 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "google-map", + "version": "1.0.0", + "description": "google-map ==========", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "build": "tsc", + "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": { + "@types/googlemaps": "^3.30.16", + "lit-element": "^2.0.1" + }, + "devDependencies": { + "@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 new file mode 100644 index 0000000..851fb57 --- /dev/null +++ b/src/google-map-marker.ts @@ -0,0 +1,440 @@ +// + +import { GoogleMapChildElement, html, customElement, property } from './lib/google-map-child-element.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 GoogleMapChildElement { + + /** + * 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; + + /** + * 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'); + } + + 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 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 + // }); + + // TODO(justinfagnani): no, no, no... Use Nodes, not innerHTML. + 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')); + } + } + } + + // TODO(justinfagnani): call from GoogleMapChildElement + 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(); + } + + // 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}`, { + 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..267a9ac --- /dev/null +++ b/src/google-map.ts @@ -0,0 +1,508 @@ +/** + * @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', + '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; + + @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; + + 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` +
+ + `; + } + + 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); + } + + 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 !== 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(); + mapEvents.forEach((event) => this._forwardEvent(event)); + this.dispatchEvent(new CustomEvent('google-map-ready')); + this._mapReadyDeferred.resolve(this.map); + } + + 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 _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 !== undefined) { + // 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 _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()); + } + } + + /** + * 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}`, { + 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/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; + }); + } +} 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": [] +}