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
+
+
+
+
+
+
-
-
-
-
+
+
@@ -39,43 +42,35 @@
-
-
+ 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": []
+}