From 2d056bad81127b74606a9875b0db87b008da3785 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2019 08:47:02 -0700 Subject: [PATCH] Allow picking location on a map (#3244) * Allow picking location on a map * Add some better defaults * Close connection before navigation --- build-scripts/gulp/service-worker.js | 4 + src/common/dom/setup-leaflet-map.ts | 4 +- src/components/map/ha-location-editor.ts | 125 ++++++++++++++++++ src/onboarding/ha-onboarding.ts | 15 ++- src/onboarding/onboarding-core-config.ts | 87 +++++------- src/panels/config/core/ha-config-core-form.ts | 51 +++---- 6 files changed, 195 insertions(+), 91 deletions(-) create mode 100644 src/components/map/ha-location-editor.ts diff --git a/build-scripts/gulp/service-worker.js b/build-scripts/gulp/service-worker.js index 1303983790..d8cd421852 100644 --- a/build-scripts/gulp/service-worker.js +++ b/build-scripts/gulp/service-worker.js @@ -15,6 +15,10 @@ gulp.task("gen-service-worker-dev", (done) => { writeSW( ` console.debug('Service worker disabled in development'); + +self.addEventListener('install', (event) => { + self.skipWaiting(); +}); ` ); done(); diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts index 1b3f300a8f..70d282d1b2 100644 --- a/src/common/dom/setup-leaflet-map.ts +++ b/src/common/dom/setup-leaflet-map.ts @@ -11,14 +11,14 @@ export const setupLeafletMap = async ( } // tslint:disable-next-line const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType; - Leaflet.Icon.Default.imagePath = "/static/images/leaflet"; + Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; const map = Leaflet.map(mapElement); const style = document.createElement("link"); style.setAttribute("href", "/static/images/leaflet/leaflet.css"); style.setAttribute("rel", "stylesheet"); mapElement.parentNode.appendChild(style); - map.setView([51.505, -0.09], 13); + map.setView([52.3731339, 4.8903147], 13); Leaflet.tileLayer( `https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}${ Leaflet.Browser.retina ? "@2x.png" : ".png" diff --git a/src/components/map/ha-location-editor.ts b/src/components/map/ha-location-editor.ts new file mode 100644 index 0000000000..9f3d45c01a --- /dev/null +++ b/src/components/map/ha-location-editor.ts @@ -0,0 +1,125 @@ +import { + LitElement, + property, + TemplateResult, + html, + CSSResult, + css, + customElement, + PropertyValues, +} from "lit-element"; +import { Marker, Map, LeafletMouseEvent, DragEndEvent, LatLng } from "leaflet"; +import { + setupLeafletMap, + LeafletModuleType, +} from "../../common/dom/setup-leaflet-map"; +import { fireEvent } from "../../common/dom/fire_event"; + +@customElement("ha-location-editor") +class LocationEditor extends LitElement { + @property() public location?: [number, number]; + public fitZoom = 16; + + private _ignoreFitToMap?: [number, number]; + + // tslint:disable-next-line + private Leaflet?: LeafletModuleType; + private _leafletMap?: Map; + private _locationMarker?: Marker; + + public fitMap(): void { + if (!this._leafletMap || !this.location) { + return; + } + this._leafletMap.setView(this.location, this.fitZoom); + } + + protected render(): TemplateResult | void { + return html` +
+ `; + } + + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + this._initMap(); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + // Still loading. + if (!this.Leaflet) { + return; + } + + this._updateMarker(); + if (!this._ignoreFitToMap || this._ignoreFitToMap !== this.location) { + this.fitMap(); + } + this._ignoreFitToMap = undefined; + } + + private get _mapEl(): HTMLDivElement { + return this.shadowRoot!.querySelector("div")!; + } + + private async _initMap(): Promise { + [this._leafletMap, this.Leaflet] = await setupLeafletMap(this._mapEl); + this._leafletMap.addEventListener( + "click", + // @ts-ignore + (ev: LeafletMouseEvent) => this._updateLocation(ev.latlng) + ); + this._updateMarker(); + this.fitMap(); + this._leafletMap.invalidateSize(); + } + + private _updateLocation(latlng: LatLng) { + this.location = this._ignoreFitToMap = [latlng.lat, latlng.lng]; + fireEvent(this, "change", undefined, { bubbles: false }); + } + + private _updateMarker(): void { + if (!this.location) { + if (this._locationMarker) { + this._locationMarker.remove(); + this._locationMarker = undefined; + } + return; + } + + if (this._locationMarker) { + this._locationMarker.setLatLng(this.location); + return; + } + this._locationMarker = this.Leaflet!.marker(this.location, { + draggable: true, + }); + this._locationMarker.addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._updateLocation(ev.target.getLatLng()) + ); + this._leafletMap!.addLayer(this._locationMarker); + } + + static get styles(): CSSResult { + return css` + :host { + display: block; + height: 300px; + } + #map { + height: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-location-editor": LocationEditor; + } +} diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index 48f6d266ef..6c932a6326 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -84,8 +84,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this._fetchOnboardingSteps(); - import("./onboarding-integrations"); - import("./onboarding-core-config"); + import(/* webpackChunkName: "onboarding-integrations" */ "./onboarding-integrations"); + import(/* webpackChunkName: "onboarding-core-config" */ "./onboarding-core-config"); registerServiceWorker(false); this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev)); } @@ -156,6 +156,15 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { const result = stepResult.result as OnboardingResponses["integration"]; this._loading = true; + // If we don't close the connection manually, the connection will be + // closed when we navigate away from the page. Firefox allows JS to + // continue to execute, and so HAWS will automatically reconnect once + // the connection is closed. However, since we revoke our token below, + // HAWS will reload the page, since that will trigger the auth flow. + // In Firefox, triggering a reload will overrule the navigation that + // was in progress. + this.hass!.connection.close(); + // Revoke current auth token. await this.hass!.auth.revoke(); @@ -184,6 +193,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { this.initializeHass(auth, conn); // Load config strings for integrations (this as any)._loadFragmentTranslations(this.hass!.language, "config"); + // Make sure hass is initialized + the config/user callbacks have called. + await new Promise((resolve) => setTimeout(resolve, 0)); } } diff --git a/src/onboarding/onboarding-core-config.ts b/src/onboarding/onboarding-core-config.ts index 65c4841e59..2e163e01db 100644 --- a/src/onboarding/onboarding-core-config.ts +++ b/src/onboarding/onboarding-core-config.ts @@ -19,12 +19,14 @@ import { detectCoreConfig, saveCoreConfig, } from "../data/core"; -import { UNIT_C } from "../common/const"; import { PolymerChangedEvent } from "../polymer-types"; import { onboardCoreConfigStep } from "../data/onboarding"; import { fireEvent } from "../common/dom/fire_event"; import { LocalizeFunc } from "../common/translations/localize"; import { createTimezoneListEl } from "../components/timezone-datalist"; +import "../components/map/ha-location-editor"; + +const amsterdam = [52.3731339, 4.8903147]; @customElement("onboarding-core-config") class OnboardingCoreConfig extends LitElement { @@ -34,8 +36,7 @@ class OnboardingCoreConfig extends LitElement { @property() private _working = false; @property() private _name!: ConfigUpdateValues["location_name"]; - @property() private _latitude!: string; - @property() private _longitude!: string; + @property() private _location!: [number, number]; @property() private _elevation!: string; @property() private _unitSystem!: ConfigUpdateValues["unit_system"]; @property() private _timeZone!: string; @@ -82,26 +83,12 @@ class OnboardingCoreConfig extends LitElement {
- - + .location=${this._locationValue} + .fitZoom=${14} + @change=${this._locationChanged} + >
@@ -205,36 +192,20 @@ class OnboardingCoreConfig extends LitElement { ); } - private get _latitudeValue() { - return this._latitude !== undefined - ? this._latitude - : this.hass.config.latitude; - } - - private get _longitudeValue() { - return this._longitude !== undefined - ? this._longitude - : this.hass.config.longitude; + private get _locationValue() { + return this._location || amsterdam; } private get _elevationValue() { - return this._elevation !== undefined - ? this._elevation - : this.hass.config.elevation; + return this._elevation !== undefined ? this._elevation : 0; } private get _timeZoneValue() { - return this._timeZone !== undefined - ? this._timeZone - : this.hass.config.time_zone; + return this._timeZone; } private get _unitSystemValue() { - return this._unitSystem !== undefined - ? this._unitSystem - : this.hass.config.unit_system.temperature === UNIT_C - ? "metric" - : "imperial"; + return this._unitSystem !== undefined ? this._unitSystem : "metric"; } private _handleChange(ev: PolymerChangedEvent) { @@ -242,6 +213,10 @@ class OnboardingCoreConfig extends LitElement { this[`_${target.name}`] = target.value; } + private _locationChanged(ev) { + this._location = ev.currentTarget.location; + } + private _unitSystemChanged( ev: PolymerChangedEvent ) { @@ -252,14 +227,17 @@ class OnboardingCoreConfig extends LitElement { this._working = true; try { const values = await detectCoreConfig(this.hass); - for (const key in values) { - if (key === "unit_system") { - this._unitSystem = values[key]!; - } else if (key === "time_zone") { - this._timeZone = values[key]!; - } else { - this[`_${key}`] = values[key]; - } + if (values.latitude && values.longitude) { + this._location = [Number(values.latitude), Number(values.longitude)]; + } + if (values.elevation) { + this._elevation = String(values.elevation); + } + if (values.unit_system) { + this._unitSystem = values.unit_system; + } + if (values.time_zone) { + this._timeZone = values.time_zone; } } catch (err) { alert(`Failed to detect location information: ${err.message}`); @@ -272,13 +250,14 @@ class OnboardingCoreConfig extends LitElement { ev.preventDefault(); this._working = true; try { + const location = this._locationValue; await saveCoreConfig(this.hass, { location_name: this._nameValue, - latitude: Number(this._latitudeValue), - longitude: Number(this._longitudeValue), + latitude: location[0], + longitude: location[1], elevation: Number(this._elevationValue), unit_system: this._unitSystemValue, - time_zone: this._timeZoneValue, + time_zone: this._timeZoneValue || "UTC", }); const result = await onboardCoreConfigStep(this.hass); fireEvent(this, "onboarding-step", { diff --git a/src/panels/config/core/ha-config-core-form.ts b/src/panels/config/core/ha-config-core-form.ts index 94d25fed73..9f10446924 100644 --- a/src/panels/config/core/ha-config-core-form.ts +++ b/src/panels/config/core/ha-config-core-form.ts @@ -19,6 +19,7 @@ import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { UNIT_C } from "../../../common/const"; import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core"; import { createTimezoneListEl } from "../../../components/timezone-datalist"; +import "../../../components/map/ha-location-editor"; @customElement("ha-config-core-form") class ConfigCoreForm extends LitElement { @@ -26,8 +27,8 @@ class ConfigCoreForm extends LitElement { @property() private _working = false; - @property() private _latitude!: string; - @property() private _longitude!: string; + @property() private _location!: [number, number]; + @property() private _elevation!: string; @property() private _unitSystem!: ConfigUpdateValues["unit_system"]; @property() private _timeZone!: string; @@ -56,26 +57,11 @@ class ConfigCoreForm extends LitElement { : ""}
- - + .location=${this._locationValue} + @change=${this._locationChanged} + >
@@ -176,16 +162,10 @@ class ConfigCoreForm extends LitElement { input.inputElement.appendChild(createTimezoneListEl()); } - private get _latitudeValue() { - return this._latitude !== undefined - ? this._latitude - : this.hass.config.latitude; - } - - private get _longitudeValue() { - return this._longitude !== undefined - ? this._longitude - : this.hass.config.longitude; + private get _locationValue() { + return this._location !== undefined + ? this._location + : [Number(this.hass.config.latitude), Number(this.hass.config.longitude)]; } private get _elevationValue() { @@ -213,6 +193,10 @@ class ConfigCoreForm extends LitElement { this[`_${target.name}`] = target.value; } + private _locationChanged(ev) { + this._location = ev.currentTarget.location; + } + private _unitSystemChanged( ev: PolymerChangedEvent ) { @@ -222,9 +206,10 @@ class ConfigCoreForm extends LitElement { private async _save() { this._working = true; try { + const location = this._locationValue; await saveCoreConfig(this.hass, { - latitude: Number(this._latitudeValue), - longitude: Number(this._longitudeValue), + latitude: location[0], + longitude: location[1], elevation: Number(this._elevationValue), unit_system: this._unitSystemValue, time_zone: this._timeZoneValue,