From 194829f5b169be90a4d5c9ccafd6be46e8bff3cd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Jun 2021 14:22:44 +0200 Subject: [PATCH] Generalize map (#9331) * Generalize map * Fix path opacity * Add fitZones --- package.json | 2 - src/common/dom/setup-leaflet-map.ts | 7 +- src/components/map/ha-entity-marker.ts | 69 ++ src/components/map/ha-location-editor.ts | 299 -------- src/components/map/ha-locations-editor.ts | 236 +++---- src/components/map/ha-map.ts | 464 ++++++++----- src/data/zone.ts | 8 - .../more-info/controls/more-info-person.ts | 1 + src/onboarding/onboarding-core-config.ts | 27 +- src/panels/config/core/ha-config-core-form.ts | 47 +- src/panels/config/zone/dialog-zone-detail.ts | 69 +- src/panels/config/zone/ha-config-zone.ts | 24 +- src/panels/logbook/ha-panel-logbook.ts | 2 +- src/panels/lovelace/cards/hui-map-card.ts | 653 +++++------------- .../components/hui-input-list-editor.ts | 8 + .../config-elements/hui-map-card-editor.ts | 2 +- src/panels/map/ha-entity-marker.js | 88 --- src/panels/map/ha-panel-map.js | 263 ------- src/panels/map/ha-panel-map.ts | 103 +++ .../profile/ha-push-notifications-row.js | 1 - src/resources/ha-style.ts | 4 +- yarn.lock | 14 - 22 files changed, 878 insertions(+), 1513 deletions(-) create mode 100644 src/components/map/ha-entity-marker.ts delete mode 100644 src/components/map/ha-location-editor.ts delete mode 100644 src/panels/map/ha-entity-marker.js delete mode 100644 src/panels/map/ha-panel-map.js create mode 100644 src/panels/map/ha-panel-map.ts diff --git a/package.json b/package.json index e54c178377..5e99d75f81 100644 --- a/package.json +++ b/package.json @@ -66,9 +66,7 @@ "@polymer/iron-autogrow-textarea": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-icon": "^3.0.1", - "@polymer/iron-image": "^3.0.1", "@polymer/iron-input": "^3.0.1", - "@polymer/iron-label": "^3.0.1", "@polymer/iron-overlay-behavior": "^3.0.2", "@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/paper-checkbox": "^3.1.0", diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts index 50bc778402..8d0d658f5c 100644 --- a/src/common/dom/setup-leaflet-map.ts +++ b/src/common/dom/setup-leaflet-map.ts @@ -6,8 +6,7 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw"); export const setupLeafletMap = async ( mapElement: HTMLElement, - darkMode?: boolean, - draw = false + darkMode?: boolean ): Promise<[Map, LeafletModuleType, TileLayer]> => { if (!mapElement.parentNode) { throw new Error("Cannot setup Leaflet map on disconnected element"); @@ -17,10 +16,6 @@ export const setupLeafletMap = async ( .default as LeafletModuleType; Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; - if (draw) { - await import("leaflet-draw"); - } - const map = Leaflet.map(mapElement); const style = document.createElement("link"); style.setAttribute("href", "/static/images/leaflet/leaflet.css"); diff --git a/src/components/map/ha-entity-marker.ts b/src/components/map/ha-entity-marker.ts new file mode 100644 index 0000000000..946c9597c7 --- /dev/null +++ b/src/components/map/ha-entity-marker.ts @@ -0,0 +1,69 @@ +import { LitElement, html, css } from "lit"; +import { property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { fireEvent } from "../../common/dom/fire_event"; +import { HomeAssistant } from "../../types"; + +class HaEntityMarker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: "entity-id" }) public entityId?: string; + + @property({ attribute: "entity-name" }) public entityName?: string; + + @property({ attribute: "entity-picture" }) public entityPicture?: string; + + @property({ attribute: "entity-color" }) public entityColor?: string; + + protected render() { + return html` +
+ ${this.entityPicture + ? html`
` + : this.entityName} +
+ `; + } + + private _badgeTap(ev: Event) { + ev.stopPropagation(); + if (this.entityId) { + fireEvent(this, "hass-more-info", { entityId: this.entityId }); + } + } + + static get styles() { + return css` + .marker { + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + overflow: hidden; + width: 48px; + height: 48px; + font-size: var(--ha-marker-font-size, 1.5em); + border-radius: 50%; + border: 1px solid var(--ha-marker-color, var(--primary-color)); + color: var(--primary-text-color); + background-color: var(--card-background-color); + } + .entity-picture { + background-size: cover; + height: 100%; + width: 100%; + } + `; + } +} + +customElements.define("ha-entity-marker", HaEntityMarker); diff --git a/src/components/map/ha-location-editor.ts b/src/components/map/ha-location-editor.ts deleted file mode 100644 index 4ee130d52d..0000000000 --- a/src/components/map/ha-location-editor.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { - Circle, - DivIcon, - DragEndEvent, - LatLng, - LeafletMouseEvent, - Map, - Marker, - TileLayer, -} from "leaflet"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../common/dom/fire_event"; -import { - LeafletModuleType, - replaceTileLayer, - setupLeafletMap, -} from "../../common/dom/setup-leaflet-map"; -import { nextRender } from "../../common/util/render-status"; -import { defaultRadiusColor } from "../../data/zone"; -import { HomeAssistant } from "../../types"; - -@customElement("ha-location-editor") -class LocationEditor extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ type: Array }) public location?: [number, number]; - - @property({ type: Number }) public radius?: number; - - @property() public radiusColor?: string; - - @property() public icon?: string; - - @property({ type: Boolean }) public darkMode?: boolean; - - public fitZoom = 16; - - private _iconEl?: DivIcon; - - private _ignoreFitToMap?: [number, number]; - - // eslint-disable-next-line - private Leaflet?: LeafletModuleType; - - private _leafletMap?: Map; - - private _tileLayer?: TileLayer; - - private _locationMarker?: Marker | Circle; - - public fitMap(): void { - if (!this._leafletMap || !this.location) { - return; - } - if (this._locationMarker && "getBounds" in this._locationMarker) { - this._leafletMap.fitBounds(this._locationMarker.getBounds()); - } else { - this._leafletMap.setView(this.location, this.fitZoom); - } - this._ignoreFitToMap = this.location; - } - - protected render(): TemplateResult { - 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; - } - - if (changedProps.has("location")) { - this._updateMarker(); - if ( - this.location && - (!this._ignoreFitToMap || - this._ignoreFitToMap[0] !== this.location[0] || - this._ignoreFitToMap[1] !== this.location[1]) - ) { - this.fitMap(); - } - } - if (changedProps.has("radius")) { - this._updateRadius(); - } - if (changedProps.has("radiusColor")) { - this._updateRadiusColor(); - } - if (changedProps.has("icon")) { - this._updateIcon(); - } - - if (changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) { - return; - } - if (!this._leafletMap || !this._tileLayer) { - return; - } - this._tileLayer = replaceTileLayer( - this.Leaflet, - this._leafletMap, - this._tileLayer, - this.hass.themes.darkMode - ); - } - } - - private get _mapEl(): HTMLDivElement { - return this.shadowRoot!.querySelector("div")!; - } - - private async _initMap(): Promise { - [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( - this._mapEl, - this.darkMode ?? this.hass.themes.darkMode, - Boolean(this.radius) - ); - this._leafletMap.addEventListener( - "click", - // @ts-ignore - (ev: LeafletMouseEvent) => this._locationUpdated(ev.latlng) - ); - this._updateIcon(); - this._updateMarker(); - this.fitMap(); - this._leafletMap.invalidateSize(); - } - - private _locationUpdated(latlng: LatLng) { - let longitude = latlng.lng; - if (Math.abs(longitude) > 180.0) { - // Normalize longitude if map provides values beyond -180 to +180 degrees. - longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0; - } - this.location = this._ignoreFitToMap = [latlng.lat, longitude]; - fireEvent(this, "change", undefined, { bubbles: false }); - } - - private _radiusUpdated() { - this._ignoreFitToMap = this.location; - this.radius = (this._locationMarker as Circle).getRadius(); - fireEvent(this, "change", undefined, { bubbles: false }); - } - - private _updateIcon() { - if (!this.icon) { - this._iconEl = undefined; - return; - } - - // create icon - let iconHTML = ""; - const el = document.createElement("ha-icon"); - el.setAttribute("icon", this.icon); - iconHTML = el.outerHTML; - - this._iconEl = this.Leaflet!.divIcon({ - html: iconHTML, - iconSize: [24, 24], - className: "light leaflet-edit-move", - }); - this._setIcon(); - } - - private _setIcon() { - if (!this._locationMarker || !this._iconEl) { - return; - } - - if (!this.radius) { - (this._locationMarker as Marker).setIcon(this._iconEl); - return; - } - - // @ts-ignore - const moveMarker = this._locationMarker.editing._moveMarker; - moveMarker.setIcon(this._iconEl); - } - - private _setupEdit() { - // @ts-ignore - this._locationMarker.editing.enable(); - // @ts-ignore - const moveMarker = this._locationMarker.editing._moveMarker; - // @ts-ignore - const resizeMarker = this._locationMarker.editing._resizeMarkers[0]; - this._setIcon(); - moveMarker.addEventListener( - "dragend", - // @ts-ignore - (ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng()) - ); - resizeMarker.addEventListener( - "dragend", - // @ts-ignore - (ev: DragEndEvent) => this._radiusUpdated(ev) - ); - } - - private async _updateMarker(): Promise { - if (!this.location) { - if (this._locationMarker) { - this._locationMarker.remove(); - this._locationMarker = undefined; - } - return; - } - - if (this._locationMarker) { - this._locationMarker.setLatLng(this.location); - if (this.radius) { - // @ts-ignore - this._locationMarker.editing.disable(); - await nextRender(); - this._setupEdit(); - } - return; - } - - if (!this.radius) { - this._locationMarker = this.Leaflet!.marker(this.location, { - draggable: true, - }); - this._setIcon(); - this._locationMarker.addEventListener( - "dragend", - // @ts-ignore - (ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng()) - ); - this._leafletMap!.addLayer(this._locationMarker); - } else { - this._locationMarker = this.Leaflet!.circle(this.location, { - color: this.radiusColor || defaultRadiusColor, - radius: this.radius, - }); - this._leafletMap!.addLayer(this._locationMarker); - this._setupEdit(); - } - } - - private _updateRadius(): void { - if (!this._locationMarker || !this.radius) { - return; - } - (this._locationMarker as Circle).setRadius(this.radius); - } - - private _updateRadiusColor(): void { - if (!this._locationMarker || !this.radius) { - return; - } - (this._locationMarker as Circle).setStyle({ color: this.radiusColor }); - } - - static get styles(): CSSResultGroup { - return css` - :host { - display: block; - height: 300px; - } - #map { - height: 100%; - background: inherit; - } - .leaflet-edit-move { - border-radius: 50%; - cursor: move !important; - } - .leaflet-edit-resize { - border-radius: 50%; - cursor: nesw-resize !important; - } - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-location-editor": LocationEditor; - } -} diff --git a/src/components/map/ha-locations-editor.ts b/src/components/map/ha-locations-editor.ts index b0b8008906..06959e9506 100644 --- a/src/components/map/ha-locations-editor.ts +++ b/src/components/map/ha-locations-editor.ts @@ -3,10 +3,8 @@ import { DivIcon, DragEndEvent, LatLng, - Map, Marker, MarkerOptions, - TileLayer, } from "leaflet"; import { css, @@ -16,15 +14,13 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; -import { - LeafletModuleType, - replaceTileLayer, - setupLeafletMap, -} from "../../common/dom/setup-leaflet-map"; -import { defaultRadiusColor } from "../../data/zone"; -import { HomeAssistant } from "../../types"; +import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map"; +import type { HomeAssistant } from "../../types"; +import "./ha-map"; +import type { HaMap } from "./ha-map"; declare global { // for fire event @@ -51,38 +47,40 @@ export interface MarkerLocation { export class HaLocationsEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public locations?: MarkerLocation[]; + @property({ attribute: false }) public locations?: MarkerLocation[]; - public fitZoom = 16; + @property({ type: Boolean }) public autoFit = false; + + @property({ type: Number }) public zoom = 16; + + @property({ type: Boolean }) public darkMode?: boolean; + + @state() private _locationMarkers?: Record; + + @state() private _circles: Record = {}; + + @query("ha-map", true) private map!: HaMap; - // eslint-disable-next-line private Leaflet?: LeafletModuleType; - // eslint-disable-next-line - private _leafletMap?: Map; + constructor() { + super(); - private _tileLayer?: TileLayer; - - private _locationMarkers?: { [key: string]: Marker | Circle }; - - private _circles: Record = {}; + import("leaflet").then((module) => { + import("leaflet-draw").then(() => { + this.Leaflet = module.default as LeafletModuleType; + this._updateMarkers(); + this.updateComplete.then(() => this.fitMap()); + }); + }); + } public fitMap(): void { - if ( - !this._leafletMap || - !this._locationMarkers || - !Object.keys(this._locationMarkers).length - ) { - return; - } - const bounds = this.Leaflet!.latLngBounds( - Object.values(this._locationMarkers).map((item) => item.getLatLng()) - ); - this._leafletMap.fitBounds(bounds.pad(0.5)); + this.map.fitMap(); } public fitMarker(id: string): void { - if (!this._leafletMap || !this._locationMarkers) { + if (!this.map.leafletMap || !this._locationMarkers) { return; } const marker = this._locationMarkers[id]; @@ -90,29 +88,44 @@ export class HaLocationsEditor extends LitElement { return; } if ("getBounds" in marker) { - this._leafletMap.fitBounds(marker.getBounds()); + this.map.leafletMap.fitBounds(marker.getBounds()); (marker as Circle).bringToFront(); } else { const circle = this._circles[id]; if (circle) { - this._leafletMap.fitBounds(circle.getBounds()); + this.map.leafletMap.fitBounds(circle.getBounds()); } else { - this._leafletMap.setView(marker.getLatLng(), this.fitZoom); + this.map.leafletMap.setView(marker.getLatLng(), this.zoom); } } } protected render(): TemplateResult { - return html`
`; + return html``; } - protected firstUpdated(changedProps: PropertyValues): void { - super.firstUpdated(changedProps); - this._initMap(); - } + private _getLayers = memoizeOne( + ( + circles: Record, + markers?: Record + ): Array => { + const layers: Array = []; + Array.prototype.push.apply(layers, Object.values(circles)); + if (markers) { + Array.prototype.push.apply(layers, Object.values(markers)); + } + return layers; + } + ); - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); + public willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); // Still loading. if (!this.Leaflet) { @@ -122,37 +135,6 @@ export class HaLocationsEditor extends LitElement { if (changedProps.has("locations")) { this._updateMarkers(); } - - if (changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) { - return; - } - if (!this._leafletMap || !this._tileLayer) { - return; - } - this._tileLayer = replaceTileLayer( - this.Leaflet, - this._leafletMap, - this._tileLayer, - this.hass.themes.darkMode - ); - } - } - - private get _mapEl(): HTMLDivElement { - return this.shadowRoot!.querySelector("div")!; - } - - private async _initMap(): Promise { - [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( - this._mapEl, - this.hass.themes.darkMode, - true - ); - this._updateMarkers(); - this.fitMap(); - this._leafletMap.invalidateSize(); } private _updateLocation(ev: DragEndEvent) { @@ -189,21 +171,18 @@ export class HaLocationsEditor extends LitElement { } private _updateMarkers(): void { - if (this._locationMarkers) { - Object.values(this._locationMarkers).forEach((marker) => { - marker.remove(); - }); - this._locationMarkers = undefined; - - Object.values(this._circles).forEach((circle) => circle.remove()); - this._circles = {}; - } - if (!this.locations || !this.locations.length) { + this._circles = {}; + this._locationMarkers = undefined; return; } - this._locationMarkers = {}; + const locationMarkers = {}; + const circles = {}; + + const defaultZoneRadiusColor = getComputedStyle(this).getPropertyValue( + "--accent-color" + ); this.locations.forEach((location: MarkerLocation) => { let icon: DivIcon | undefined; @@ -228,45 +207,46 @@ export class HaLocationsEditor extends LitElement { const circle = this.Leaflet!.circle( [location.latitude, location.longitude], { - color: location.radius_color || defaultRadiusColor, + color: location.radius_color || defaultZoneRadiusColor, radius: location.radius, } ); - circle.addTo(this._leafletMap!); if (location.radius_editable || location.location_editable) { // @ts-ignore circle.editing.enable(); - // @ts-ignore - const moveMarker = circle.editing._moveMarker; - // @ts-ignore - const resizeMarker = circle.editing._resizeMarkers[0]; - if (icon) { - moveMarker.setIcon(icon); - } - resizeMarker.id = moveMarker.id = location.id; - moveMarker - .addEventListener( - "dragend", - // @ts-ignore - (ev: DragEndEvent) => this._updateLocation(ev) - ) - .addEventListener( - "click", - // @ts-ignore - (ev: MouseEvent) => this._markerClicked(ev) - ); - if (location.radius_editable) { - resizeMarker.addEventListener( - "dragend", - // @ts-ignore - (ev: DragEndEvent) => this._updateRadius(ev) - ); - } else { - resizeMarker.remove(); - } - this._locationMarkers![location.id] = circle; + circle.addEventListener("add", () => { + // @ts-ignore + const moveMarker = circle.editing._moveMarker; + // @ts-ignore + const resizeMarker = circle.editing._resizeMarkers[0]; + if (icon) { + moveMarker.setIcon(icon); + } + resizeMarker.id = moveMarker.id = location.id; + moveMarker + .addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._updateLocation(ev) + ) + .addEventListener( + "click", + // @ts-ignore + (ev: MouseEvent) => this._markerClicked(ev) + ); + if (location.radius_editable) { + resizeMarker.addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._updateRadius(ev) + ); + } else { + resizeMarker.remove(); + } + }); + locationMarkers[location.id] = circle; } else { - this._circles[location.id] = circle; + circles[location.id] = circle; } } if ( @@ -275,6 +255,7 @@ export class HaLocationsEditor extends LitElement { ) { const options: MarkerOptions = { title: location.name, + draggable: location.location_editable, }; if (icon) { @@ -293,13 +274,14 @@ export class HaLocationsEditor extends LitElement { "click", // @ts-ignore (ev: MouseEvent) => this._markerClicked(ev) - ) - .addTo(this._leafletMap!); + ); (marker as any).id = location.id; - this._locationMarkers![location.id] = marker; + locationMarkers[location.id] = marker; } }); + this._circles = circles; + this._locationMarkers = locationMarkers; } static get styles(): CSSResultGroup { @@ -308,23 +290,9 @@ export class HaLocationsEditor extends LitElement { display: block; height: 300px; } - #map { + ha-map { height: 100%; } - .leaflet-marker-draggable { - cursor: move !important; - } - .leaflet-edit-resize { - border-radius: 50%; - cursor: nesw-resize !important; - } - .named-icon { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - text-align: center; - } `; } } diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index fd29d2a268..c34a819d51 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -1,13 +1,15 @@ -import { Circle, Layer, Map, Marker, TileLayer } from "leaflet"; import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property } from "lit/decorators"; + Circle, + CircleMarker, + LatLngTuple, + Layer, + Map, + Marker, + Polyline, + TileLayer, +} from "leaflet"; +import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; import { LeafletModuleType, replaceTileLayer, @@ -15,194 +17,324 @@ import { } from "../../common/dom/setup-leaflet-map"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; -import { debounce } from "../../common/util/debounce"; -import "../../panels/map/ha-entity-marker"; +import "./ha-entity-marker"; import { HomeAssistant } from "../../types"; import "../ha-icon-button"; +import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; + +const getEntityId = (entity: string | HaMapEntity): string => + typeof entity === "string" ? entity : entity.entity_id; + +export interface HaMapPaths { + points: LatLngTuple[]; + color?: string; + gradualOpacity?: number; +} + +export interface HaMapEntity { + entity_id: string; + color: string; +} @customElement("ha-map") -class HaMap extends LitElement { +export class HaMap extends ReactiveElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public entities?: string[]; + @property({ attribute: false }) public entities?: string[] | HaMapEntity[]; - @property() public darkMode?: boolean; + @property({ attribute: false }) public paths?: HaMapPaths[]; - @property() public zoom?: number; + @property({ attribute: false }) public layers?: Layer[]; + + @property({ type: Boolean }) public autoFit = false; + + @property({ type: Boolean }) public fitZones?: boolean; + + @property({ type: Boolean }) public darkMode?: boolean; + + @property({ type: Number }) public zoom = 14; + + @state() private _loaded = false; + + public leafletMap?: Map; - // eslint-disable-next-line private Leaflet?: LeafletModuleType; - private _leafletMap?: Map; - private _tileLayer?: TileLayer; - // @ts-ignore private _resizeObserver?: ResizeObserver; - private _debouncedResizeListener = debounce( - () => { - if (!this._leafletMap) { - return; - } - this._leafletMap.invalidateSize(); - }, - 100, - false - ); - private _mapItems: Array = []; private _mapZones: Array = []; - private _connected = false; + private _mapPaths: Array = []; public connectedCallback(): void { super.connectedCallback(); - this._connected = true; - if (this.hasUpdated) { - this.loadMap(); - this._attachObserver(); - } + this._loadMap(); + this._attachObserver(); } public disconnectedCallback(): void { super.disconnectedCallback(); - this._connected = false; - - if (this._leafletMap) { - this._leafletMap.remove(); - this._leafletMap = undefined; + if (this.leafletMap) { + this.leafletMap.remove(); + this.leafletMap = undefined; this.Leaflet = undefined; } + this._loaded = false; + if (this._resizeObserver) { - this._resizeObserver.unobserve(this._mapEl); - } else { - window.removeEventListener("resize", this._debouncedResizeListener); + this._resizeObserver.unobserve(this); } } - protected render(): TemplateResult { - if (!this.entities) { - return html``; - } - return html`
`; - } + protected update(changedProps: PropertyValues) { + super.update(changedProps); - protected firstUpdated(changedProps: PropertyValues): void { - super.firstUpdated(changedProps); - this.loadMap(); - - if (this._connected) { - this._attachObserver(); - } - } - - protected shouldUpdate(changedProps) { - if (!changedProps.has("hass") || changedProps.size > 1) { - return true; - } - - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - - if (!oldHass || !this.entities) { - return true; - } - - // Check if any state has changed - for (const entity of this.entities) { - if (oldHass.states[entity] !== this.hass!.states[entity]) { - return true; - } - } - - return false; - } - - protected updated(changedProps: PropertyValues): void { - if (changedProps.has("hass")) { - this._drawEntities(); - this._fitMap(); - - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) { - return; - } - if (!this.Leaflet || !this._leafletMap || !this._tileLayer) { - return; - } - this._tileLayer = replaceTileLayer( - this.Leaflet, - this._leafletMap, - this._tileLayer, - this.hass.themes.darkMode - ); - } - } - - private get _mapEl(): HTMLDivElement { - return this.shadowRoot!.getElementById("map") as HTMLDivElement; - } - - private async loadMap(): Promise { - [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( - this._mapEl, - this.darkMode ?? this.hass.themes.darkMode - ); - this._drawEntities(); - this._leafletMap.invalidateSize(); - this._fitMap(); - } - - private _fitMap(): void { - if (!this._leafletMap || !this.Leaflet || !this.hass) { + if (!this._loaded) { return; } - if (this._mapItems.length === 0) { - this._leafletMap.setView( + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + if (changedProps.has("_loaded") || changedProps.has("entities")) { + this._drawEntities(); + } else if (this._loaded && oldHass && this.entities) { + // Check if any state has changed + for (const entity of this.entities) { + if ( + oldHass.states[getEntityId(entity)] !== + this.hass!.states[getEntityId(entity)] + ) { + this._drawEntities(); + break; + } + } + } + + if (changedProps.has("_loaded") || changedProps.has("paths")) { + this._drawPaths(); + } + + if (changedProps.has("_loaded") || changedProps.has("layers")) { + this._drawLayers(changedProps.get("layers") as Layer[] | undefined); + } + + if ( + changedProps.has("_loaded") || + ((changedProps.has("entities") || changedProps.has("layers")) && + this.autoFit) + ) { + this.fitMap(); + } + + if (changedProps.has("zoom")) { + this.leafletMap!.setZoom(this.zoom); + } + + if ( + !changedProps.has("darkMode") && + (!changedProps.has("hass") || + (oldHass && oldHass.themes.darkMode === this.hass.themes.darkMode)) + ) { + return; + } + const darkMode = this.darkMode ?? this.hass.themes.darkMode; + this._tileLayer = replaceTileLayer( + this.Leaflet!, + this.leafletMap!, + this._tileLayer!, + darkMode + ); + this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode); + } + + private async _loadMap(): Promise { + let map = this.shadowRoot!.getElementById("map"); + if (!map) { + map = document.createElement("div"); + map.id = "map"; + this.shadowRoot!.append(map); + } + const darkMode = this.darkMode ?? this.hass.themes.darkMode; + [this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( + map, + darkMode + ); + this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode); + this._loaded = true; + } + + public fitMap(): void { + if (!this.leafletMap || !this.Leaflet || !this.hass) { + return; + } + + if (!this._mapItems.length && !this.layers?.length) { + this.leafletMap.setView( new this.Leaflet.LatLng( this.hass.config.latitude, this.hass.config.longitude ), - this.zoom || 14 + this.zoom ); return; } - const bounds = this.Leaflet.latLngBounds( + let bounds = this.Leaflet.latLngBounds( this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : [] ); - this._leafletMap.fitBounds(bounds.pad(0.5)); - if (this.zoom && this._leafletMap.getZoom() > this.zoom) { - this._leafletMap.setZoom(this.zoom); + if (this.fitZones) { + this._mapZones?.forEach((zone) => { + bounds.extend( + "getBounds" in zone ? zone.getBounds() : zone.getLatLng() + ); + }); } + + this.layers?.forEach((layer: any) => { + bounds.extend( + "getBounds" in layer ? layer.getBounds() : layer.getLatLng() + ); + }); + + if (!this.layers) { + bounds = bounds.pad(0.5); + } + + this.leafletMap.fitBounds(bounds, { maxZoom: this.zoom }); + } + + private _drawLayers(prevLayers: Layer[] | undefined): void { + if (prevLayers) { + prevLayers.forEach((layer) => layer.remove()); + } + if (!this.layers) { + return; + } + const map = this.leafletMap!; + this.layers.forEach((layer) => { + map.addLayer(layer); + }); + } + + private _drawPaths(): void { + const hass = this.hass; + const map = this.leafletMap; + const Leaflet = this.Leaflet; + + if (!hass || !map || !Leaflet) { + return; + } + if (this._mapPaths.length) { + this._mapPaths.forEach((marker) => marker.remove()); + this._mapPaths = []; + } + if (!this.paths) { + return; + } + + const darkPrimaryColor = getComputedStyle(this).getPropertyValue( + "--dark-primary-color" + ); + + this.paths.forEach((path) => { + let opacityStep: number; + let baseOpacity: number; + if (path.gradualOpacity) { + opacityStep = path.gradualOpacity / (path.points.length - 2); + baseOpacity = 1 - path.gradualOpacity; + } + + for ( + let pointIndex = 0; + pointIndex < path.points.length - 1; + pointIndex++ + ) { + const opacity = path.gradualOpacity + ? baseOpacity! + pointIndex * opacityStep! + : undefined; + + // DRAW point + this._mapPaths.push( + Leaflet!.circleMarker(path.points[pointIndex], { + radius: 3, + color: path.color || darkPrimaryColor, + opacity, + fillOpacity: opacity, + interactive: false, + }) + ); + + // DRAW line between this and next point + this._mapPaths.push( + Leaflet!.polyline( + [path.points[pointIndex], path.points[pointIndex + 1]], + { + color: path.color || darkPrimaryColor, + opacity, + interactive: false, + } + ) + ); + } + const pointIndex = path.points.length - 1; + if (pointIndex >= 0) { + const opacity = path.gradualOpacity + ? baseOpacity! + pointIndex * opacityStep! + : undefined; + // DRAW end path point + this._mapPaths.push( + Leaflet!.circleMarker(path.points[pointIndex], { + radius: 3, + color: path.color || darkPrimaryColor, + opacity, + fillOpacity: opacity, + interactive: false, + }) + ); + } + this._mapPaths.forEach((marker) => map.addLayer(marker)); + }); } private _drawEntities(): void { const hass = this.hass; - const map = this._leafletMap; + const map = this.leafletMap; const Leaflet = this.Leaflet; + if (!hass || !map || !Leaflet) { return; } - if (this._mapItems) { + if (this._mapItems.length) { this._mapItems.forEach((marker) => marker.remove()); + this._mapItems = []; } - const mapItems: Layer[] = (this._mapItems = []); - if (this._mapZones) { + if (this._mapZones.length) { this._mapZones.forEach((marker) => marker.remove()); + this._mapZones = []; } - const mapZones: Layer[] = (this._mapZones = []); - const allEntities = this.entities!.concat(); + if (!this.entities) { + return; + } - for (const entity of allEntities) { - const entityId = entity; - const stateObj = hass.states[entityId]; + const computedStyles = getComputedStyle(this); + const zoneColor = computedStyles.getPropertyValue("--accent-color"); + const darkPrimaryColor = computedStyles.getPropertyValue( + "--dark-primary-color" + ); + + const className = + this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light"; + + for (const entity of this.entities) { + const stateObj = hass.states[getEntityId(entity)]; if (!stateObj) { continue; } @@ -240,13 +372,12 @@ class HaMap extends LitElement { } // create marker with the icon - mapZones.push( + this._mapZones.push( Leaflet.marker([latitude, longitude], { icon: Leaflet.divIcon({ html: iconHTML, iconSize: [24, 24], - className: - this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light", + className, }), interactive: false, title, @@ -254,10 +385,10 @@ class HaMap extends LitElement { ); // create circle around it - mapZones.push( + this._mapZones.push( Leaflet.circle([latitude, longitude], { interactive: false, - color: "#FF9800", + color: zoneColor, radius, }) ); @@ -273,17 +404,20 @@ class HaMap extends LitElement { .join("") .substr(0, 3); - // create market with the icon - mapItems.push( + // create marker with the icon + this._mapItems.push( Leaflet.marker([latitude, longitude], { icon: Leaflet.divIcon({ - // Leaflet clones this element before adding it to the map. This messes up - // our Polymer object and we can't pass data through. Thus we hack like this. html: ` `, iconSize: [48, 48], @@ -295,10 +429,10 @@ class HaMap extends LitElement { // create circle around if entity has accuracy if (gpsAccuracy) { - mapItems.push( + this._mapItems.push( Leaflet.circle([latitude, longitude], { interactive: false, - color: "#0288D1", + color: darkPrimaryColor, radius: gpsAccuracy, }) ); @@ -309,20 +443,14 @@ class HaMap extends LitElement { this._mapZones.forEach((marker) => map.addLayer(marker)); } - private _attachObserver(): void { - // Observe changes to map size and invalidate to prevent broken rendering - // Uses ResizeObserver in Chrome, otherwise window resize event - - // @ts-ignore - if (typeof ResizeObserver === "function") { - // @ts-ignore - this._resizeObserver = new ResizeObserver(() => - this._debouncedResizeListener() - ); - this._resizeObserver.observe(this._mapEl); - } else { - window.addEventListener("resize", this._debouncedResizeListener); + private async _attachObserver(): Promise { + if (!this._resizeObserver) { + await installResizeObserver(); + this._resizeObserver = new ResizeObserver(() => { + this.leafletMap?.invalidateSize({ debounceMoveend: true }); + }); } + this._resizeObserver.observe(this); } static get styles(): CSSResultGroup { @@ -337,13 +465,25 @@ class HaMap extends LitElement { #map.dark { background: #090909; } - + .light { + color: #000000; + } .dark { color: #ffffff; } - - .light { - color: #000000; + .leaflet-marker-draggable { + cursor: move !important; + } + .leaflet-edit-resize { + border-radius: 50%; + cursor: nesw-resize !important; + } + .named-icon { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; } `; } diff --git a/src/data/zone.ts b/src/data/zone.ts index 737ae07dd8..e1c630f020 100644 --- a/src/data/zone.ts +++ b/src/data/zone.ts @@ -1,14 +1,6 @@ import { navigate } from "../common/navigate"; -import { - DEFAULT_ACCENT_COLOR, - DEFAULT_PRIMARY_COLOR, -} from "../resources/ha-style"; import { HomeAssistant } from "../types"; -export const defaultRadiusColor = DEFAULT_ACCENT_COLOR; -export const homeRadiusColor = DEFAULT_PRIMARY_COLOR; -export const passiveRadiusColor = "#9b9b9b"; - export interface Zone { id: string; name: string; diff --git a/src/dialogs/more-info/controls/more-info-person.ts b/src/dialogs/more-info/controls/more-info-person.ts index ea7858de78..1b98b6501b 100644 --- a/src/dialogs/more-info/controls/more-info-person.ts +++ b/src/dialogs/more-info/controls/more-info-person.ts @@ -28,6 +28,7 @@ class MoreInfoPerson extends LitElement { ` : ""} diff --git a/src/onboarding/onboarding-core-config.ts b/src/onboarding/onboarding-core-config.ts index f1bc58a83d..cf330ea87d 100644 --- a/src/onboarding/onboarding-core-config.ts +++ b/src/onboarding/onboarding-core-config.ts @@ -5,9 +5,11 @@ import "@polymer/paper-radio-button/paper-radio-button"; import "@polymer/paper-radio-group/paper-radio-group"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import type { LocalizeFunc } from "../common/translations/localize"; -import "../components/map/ha-location-editor"; +import "../components/map/ha-locations-editor"; +import type { MarkerLocation } from "../components/map/ha-locations-editor"; import { createTimezoneListEl } from "../components/timezone-datalist"; import { ConfigUpdateValues, @@ -81,14 +83,14 @@ class OnboardingCoreConfig extends LitElement {
- + @location-updated=${this._locationChanged} + >
@@ -208,13 +210,24 @@ class OnboardingCoreConfig extends LitElement { return this._unitSystem !== undefined ? this._unitSystem : "metric"; } + private _markerLocation = memoizeOne( + (location: [number, number]): MarkerLocation[] => [ + { + id: "location", + latitude: location[0], + longitude: location[1], + location_editable: true, + }, + ] + ); + private _handleChange(ev: PolymerChangedEvent) { const target = ev.currentTarget as PaperInputElement; this[`_${target.name}`] = target.value; } private _locationChanged(ev) { - this._location = ev.currentTarget.location; + this._location = ev.detail.location; } private _unitSystemChanged( diff --git a/src/panels/config/core/ha-config-core-form.ts b/src/panels/config/core/ha-config-core-form.ts index 322c2697c1..2d26def4ce 100644 --- a/src/panels/config/core/ha-config-core-form.ts +++ b/src/panels/config/core/ha-config-core-form.ts @@ -8,7 +8,8 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { UNIT_C } from "../../../common/const"; import "../../../components/ha-card"; -import "../../../components/map/ha-location-editor"; +import "../../../components/map/ha-locations-editor"; +import type { MarkerLocation } from "../../../components/map/ha-locations-editor"; import { createTimezoneListEl } from "../../../components/timezone-datalist"; import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core"; import type { PolymerChangedEvent } from "../../../polymer-types"; @@ -20,13 +21,13 @@ class ConfigCoreForm extends LitElement { @state() private _working = false; - @state() private _location!: [number, number]; + @state() private _location?: [number, number]; - @state() private _elevation!: string; + @state() private _elevation?: string; - @state() private _unitSystem!: ConfigUpdateValues["unit_system"]; + @state() private _unitSystem?: ConfigUpdateValues["unit_system"]; - @state() private _timeZone!: string; + @state() private _timeZone?: string; protected render(): TemplateResult { const canEdit = ["storage", "default"].includes( @@ -52,16 +53,16 @@ class ConfigCoreForm extends LitElement { : ""}
- + @location-updated=${this._locationChanged} + >
@@ -162,8 +163,19 @@ class ConfigCoreForm extends LitElement { input.inputElement.appendChild(createTimezoneListEl()); } - private _locationValue = memoizeOne( - (location, lat, lng) => location || [Number(lat), Number(lng)] + private _markerLocation = memoizeOne( + ( + lat: number, + lng: number, + location?: [number, number] + ): MarkerLocation[] => [ + { + id: "location", + latitude: location ? location[0] : lat, + longitude: location ? location[1] : lng, + location_editable: true, + }, + ] ); private get _elevationValue() { @@ -192,7 +204,7 @@ class ConfigCoreForm extends LitElement { } private _locationChanged(ev) { - this._location = ev.currentTarget.location; + this._location = ev.detail.location; } private _unitSystemChanged( @@ -204,11 +216,10 @@ class ConfigCoreForm extends LitElement { private async _save() { this._working = true; try { - const location = this._locationValue( - this._location, + const location = this._location || [ this.hass.config.latitude, - this.hass.config.longitude - ); + this.hass.config.longitude, + ]; await saveCoreConfig(this.hass, { latitude: location[0], longitude: location[1], diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts index ac4cedc32a..f49ab613e3 100644 --- a/src/panels/config/zone/dialog-zone-detail.ts +++ b/src/panels/config/zone/dialog-zone-detail.ts @@ -9,13 +9,9 @@ import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-formfield"; import "../../../components/ha-switch"; -import "../../../components/map/ha-location-editor"; -import { - defaultRadiusColor, - getZoneEditorInitData, - passiveRadiusColor, - ZoneMutableParams, -} from "../../../data/zone"; +import "../../../components/map/ha-locations-editor"; +import type { MarkerLocation } from "../../../components/map/ha-locations-editor"; +import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { ZoneDetailDialogParams } from "./show-dialog-zone-detail"; @@ -132,17 +128,19 @@ class DialogZoneDetail extends LitElement { )}" .invalid=${iconValid} > - + .locations=${this._location( + this._latitude, + this._longitude, + this._radius, + this._passive, + this._icon + )} + @location-updated=${this._locationChanged} + @radius-updated=${this._radiusChanged} + >
[Number(lat), Number(lng)]); + private _location = memoizeOne( + ( + lat: number, + lng: number, + radius: number, + passive: boolean, + icon: string + ): MarkerLocation[] => { + const computedStyles = getComputedStyle(this); + const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color"); + const passiveRadiusColor = computedStyles.getPropertyValue( + "--secondary-text-color" + ); + return [ + { + id: "location", + latitude: Number(lat), + longitude: Number(lng), + radius, + radius_color: passive ? passiveRadiusColor : zoneRadiusColor, + icon, + location_editable: true, + radius_editable: true, + }, + ]; + } + ); - private _locationChanged(ev) { - [this._latitude, this._longitude] = ev.currentTarget.location; - this._radius = ev.currentTarget.radius; + private _locationChanged(ev: CustomEvent) { + [this._latitude, this._longitude] = ev.detail.location; + } + + private _radiusChanged(ev: CustomEvent) { + this._radius = ev.detail.radius; } private _passiveChanged(ev) { @@ -292,7 +319,7 @@ class DialogZoneDetail extends LitElement { .location > *:last-child { margin-left: 4px; } - ha-location-editor { + ha-locations-editor { margin-top: 16px; } a { diff --git a/src/panels/config/zone/ha-config-zone.ts b/src/panels/config/zone/ha-config-zone.ts index e5f6883d20..ad1526f0bf 100644 --- a/src/panels/config/zone/ha-config-zone.ts +++ b/src/panels/config/zone/ha-config-zone.ts @@ -31,11 +31,8 @@ import { saveCoreConfig } from "../../../data/core"; import { subscribeEntityRegistry } from "../../../data/entity_registry"; import { createZone, - defaultRadiusColor, deleteZone, fetchZones, - homeRadiusColor, - passiveRadiusColor, updateZone, Zone, ZoneMutableParams, @@ -73,6 +70,15 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { private _getZones = memoizeOne( (storageItems: Zone[], stateItems: HassEntity[]): MarkerLocation[] => { + const computedStyles = getComputedStyle(this); + const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color"); + const passiveRadiusColor = computedStyles.getPropertyValue( + "--secondary-text-color" + ); + const homeRadiusColor = computedStyles.getPropertyValue( + "--primary-color" + ); + const stateLocations: MarkerLocation[] = stateItems.map( (entityState) => ({ id: entityState.entity_id, @@ -86,7 +92,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { ? homeRadiusColor : entityState.attributes.passive ? passiveRadiusColor - : defaultRadiusColor, + : zoneRadiusColor, location_editable: entityState.entity_id === "zone.home" && this._canEditCore, radius_editable: false, @@ -94,7 +100,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { ); const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({ ...zone, - radius_color: zone.passive ? passiveRadiusColor : defaultRadiusColor, + radius_color: zone.passive ? passiveRadiusColor : zoneRadiusColor, location_editable: true, radius_editable: true, })); @@ -274,7 +280,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { } } - protected updated(changedProps: PropertyValues) { + public willUpdate(changedProps: PropertyValues) { super.updated(changedProps); const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if (oldHass && this._stateItems) { @@ -410,8 +416,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { if (this.narrow) { return; } - await this.updateComplete; this._activeEntry = created.id; + await this.updateComplete; + await this._map?.updateComplete; this._map?.fitMarker(created.id); } @@ -427,8 +434,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { if (this.narrow || !fitMap) { return; } - await this.updateComplete; this._activeEntry = entry.id; + await this.updateComplete; + await this._map?.updateComplete; this._map?.fitMarker(entry.id); } diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts index 9c93f077ee..5a6b89ee92 100644 --- a/src/panels/logbook/ha-panel-logbook.ts +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -1,4 +1,5 @@ import { mdiRefresh } from "@mdi/js"; +import "@material/mwc-icon-button"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import { css, html, LitElement, PropertyValues } from "lit"; @@ -9,7 +10,6 @@ import "../../components/entity/ha-entity-picker"; import "../../components/ha-circular-progress"; import "../../components/ha-date-range-picker"; import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; -import "../../components/ha-icon-button"; import "../../components/ha-menu-button"; import { clearLogbookCache, diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 493ee46823..e6c825d9b9 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -1,14 +1,5 @@ -import { HassEntity } from "home-assistant-js-websocket"; -import { - Circle, - CircleMarker, - LatLngTuple, - Layer, - Map, - Marker, - Polyline, - TileLayer, -} from "leaflet"; +import { HassEntities, HassEntity } from "home-assistant-js-websocket"; +import { LatLngTuple } from "leaflet"; import { css, CSSResultGroup, @@ -17,32 +8,106 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { - LeafletModuleType, - replaceTileLayer, - setupLeafletMap, -} from "../../../common/dom/setup-leaflet-map"; +import { customElement, property, query, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateDomain } from "../../../common/entity/compute_state_domain"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import { debounce } from "../../../common/util/debounce"; import parseAspectRatio from "../../../common/util/parse-aspect-ratio"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import { fetchRecent } from "../../../data/history"; import { HomeAssistant } from "../../../types"; -import "../../map/ha-entity-marker"; +import "../../../components/map/ha-entity-marker"; import { findEntities } from "../common/find-entities"; -import { installResizeObserver } from "../common/install-resize-observer"; import { processConfigEntities } from "../common/process-config-entities"; import { EntityConfig } from "../entity-rows/types"; import { LovelaceCard } from "../types"; import { MapCardConfig } from "./types"; +import "../../../components/map/ha-map"; +import { mdiImageFilterCenterFocus } from "@mdi/js"; +import type { HaMap, HaMapPaths } from "../../../components/map/ha-map"; +import memoizeOne from "memoize-one"; +const MINUTE = 60000; + +const COLORS = [ + "#0288D1", + "#00AA00", + "#984ea3", + "#00d2d5", + "#ff7f00", + "#af8d00", + "#7f80cd", + "#b3e900", + "#c42e60", + "#a65628", + "#f781bf", + "#8dd3c7", +]; @customElement("hui-map-card") class HuiMapCard extends LitElement implements LovelaceCard { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) + public isPanel = false; + + @state() + private _history?: HassEntity[][]; + + @state() + private _config?: MapCardConfig; + + @query("ha-map") + private _map?: HaMap; + + private _date?: Date; + + private _configEntities?: string[]; + + private _colorDict: Record = {}; + + private _colorIndex = 0; + + public setConfig(config: MapCardConfig): void { + if (!config) { + throw new Error("Error in card configuration."); + } + + if (!config.entities?.length && !config.geo_location_sources) { + throw new Error( + "Either entities or geo_location_sources must be specified" + ); + } + if (config.entities && !Array.isArray(config.entities)) { + throw new Error("Entities need to be an array"); + } + if ( + config.geo_location_sources && + !Array.isArray(config.geo_location_sources) + ) { + throw new Error("Geo_location_sources needs to be an array"); + } + + this._config = config; + this._configEntities = (config.entities + ? processConfigEntities(config.entities) + : [] + ).map((entity) => entity.entity); + + this._cleanupHistory(); + } + + public getCardSize(): number { + if (!this._config?.aspect_ratio) { + return 7; + } + + const ratio = parseAspectRatio(this._config.aspect_ratio); + const ar = + ratio && ratio.w > 0 && ratio.h > 0 + ? `${((100 * ratio.h) / ratio.w).toFixed(2)}` + : "100"; + return 1 + Math.floor(Number(ar) / 25) || 3; + } + public static async getConfigElement() { await import("../editor/config-elements/hui-map-card-editor"); return document.createElement("hui-map-card-editor"); @@ -66,129 +131,6 @@ class HuiMapCard extends LitElement implements LovelaceCard { return { type: "map", entities: foundEntities }; } - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ type: Boolean, reflect: true }) - public isPanel = false; - - @property() - private _history?: HassEntity[][]; - - private _date?: Date; - - @property() - private _config?: MapCardConfig; - - private _configEntities?: EntityConfig[]; - - // eslint-disable-next-line - private Leaflet?: LeafletModuleType; - - private _leafletMap?: Map; - - private _tileLayer?: TileLayer; - - private _resizeObserver?: ResizeObserver; - - private _debouncedResizeListener = debounce( - () => { - if (!this.isConnected || !this._leafletMap) { - return; - } - this._leafletMap.invalidateSize(); - }, - 250, - false - ); - - private _mapItems: Array = []; - - private _mapZones: Array = []; - - private _mapPaths: Array = []; - - private _colorDict: Record = {}; - - private _colorIndex = 0; - - private _colors: string[] = [ - "#0288D1", - "#00AA00", - "#984ea3", - "#00d2d5", - "#ff7f00", - "#af8d00", - "#7f80cd", - "#b3e900", - "#c42e60", - "#a65628", - "#f781bf", - "#8dd3c7", - ]; - - public setConfig(config: MapCardConfig): void { - if (!config) { - throw new Error("Error in card configuration."); - } - - if (!config.entities?.length && !config.geo_location_sources) { - throw new Error( - "Either entities or geo_location_sources must be specified" - ); - } - if (config.entities && !Array.isArray(config.entities)) { - throw new Error("Entities need to be an array"); - } - if ( - config.geo_location_sources && - !Array.isArray(config.geo_location_sources) - ) { - throw new Error("Geo_location_sources needs to be an array"); - } - - this._config = config; - this._configEntities = config.entities - ? processConfigEntities(config.entities) - : []; - - this._cleanupHistory(); - } - - public getCardSize(): number { - if (!this._config?.aspect_ratio) { - return 7; - } - - const ratio = parseAspectRatio(this._config.aspect_ratio); - const ar = - ratio && ratio.w > 0 && ratio.h > 0 - ? `${((100 * ratio.h) / ratio.w).toFixed(2)}` - : "100"; - return 1 + Math.floor(Number(ar) / 25) || 3; - } - - public connectedCallback(): void { - super.connectedCallback(); - this._attachObserver(); - if (this.hasUpdated) { - this.loadMap(); - } - } - - public disconnectedCallback(): void { - super.disconnectedCallback(); - - if (this._leafletMap) { - this._leafletMap.remove(); - this._leafletMap = undefined; - this.Leaflet = undefined; - } - - if (this._resizeObserver) { - this._resizeObserver.unobserve(this._mapEl); - } - } - protected render(): TemplateResult { if (!this._config) { return html``; @@ -196,22 +138,29 @@ class HuiMapCard extends LitElement implements LovelaceCard { return html`
-
- + + > + +
`; } - protected shouldUpdate(changedProps) { + protected shouldUpdate(changedProps: PropertyValues) { if (!changedProps.has("hass") || changedProps.size > 1) { return true; } @@ -228,7 +177,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { // Check if any state has changed for (const entity of this._configEntities) { - if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) { + if (oldHass.states[entity] !== this.hass!.states[entity]) { return true; } } @@ -238,17 +187,12 @@ class HuiMapCard extends LitElement implements LovelaceCard { protected firstUpdated(changedProps: PropertyValues): void { super.firstUpdated(changedProps); - if (this.isConnected) { - this.loadMap(); - } const root = this.shadowRoot!.getElementById("root"); if (!this._config || this.isPanel || !root) { return; } - this._attachObserver(); - if (!this._config.aspect_ratio) { root.style.paddingBottom = "100%"; return; @@ -263,172 +207,86 @@ class HuiMapCard extends LitElement implements LovelaceCard { } protected updated(changedProps: PropertyValues): void { - if (changedProps.has("hass") || changedProps.has("_history")) { - this._drawEntities(); - this._fitMap(); - } - if (changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (oldHass && oldHass.themes.darkMode !== this.hass.themes.darkMode) { - this._replaceTileLayer(); - } - } - if ( - changedProps.has("_config") && - changedProps.get("_config") !== undefined - ) { - this.updateMap(changedProps.get("_config") as MapCardConfig); - } - if (this._config?.hours_to_show && this._configEntities?.length) { - const minute = 60000; if (changedProps.has("_config")) { this._getHistory(); - } else if (Date.now() - this._date!.getTime() >= minute) { + } else if (Date.now() - this._date!.getTime() >= MINUTE) { this._getHistory(); } } } - private get _mapEl(): HTMLDivElement { - return this.shadowRoot!.getElementById("map") as HTMLDivElement; + private _fitMap() { + this._map?.fitMap(); } - private async loadMap(): Promise { - [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( - this._mapEl, - this._config!.dark_mode ?? this.hass.themes.darkMode - ); - this._drawEntities(); - this._leafletMap.invalidateSize(); - this._fitMap(); - } - - private _replaceTileLayer() { - const map = this._leafletMap; - const config = this._config; - const Leaflet = this.Leaflet; - if (!map || !config || !Leaflet || !this._tileLayer) { - return; - } - this._tileLayer = replaceTileLayer( - Leaflet, - map, - this._tileLayer, - this._config!.dark_mode ?? this.hass.themes.darkMode - ); - } - - private updateMap(oldConfig: MapCardConfig): void { - const map = this._leafletMap; - const config = this._config; - const Leaflet = this.Leaflet; - if (!map || !config || !Leaflet || !this._tileLayer) { - return; - } - if (this._config!.dark_mode !== oldConfig.dark_mode) { - this._replaceTileLayer(); - } - if ( - config.entities !== oldConfig.entities || - config.geo_location_sources !== oldConfig.geo_location_sources - ) { - this._drawEntities(); - } - map.invalidateSize(); - this._fitMap(); - } - - private _fitMap(): void { - if (!this._leafletMap || !this.Leaflet || !this._config || !this.hass) { - return; - } - const zoom = this._config.default_zoom; - if (this._mapItems.length === 0) { - this._leafletMap.setView( - new this.Leaflet.LatLng( - this.hass.config.latitude, - this.hass.config.longitude - ), - zoom || 14 - ); - return; - } - - const bounds = this.Leaflet.featureGroup(this._mapItems).getBounds(); - this._leafletMap.fitBounds(bounds.pad(0.5)); - - if (zoom && this._leafletMap.getZoom() > zoom) { - this._leafletMap.setZoom(zoom); - } - } - - private _getColor(entityId: string) { - let color; - if (this._colorDict[entityId]) { - color = this._colorDict[entityId]; - } else { - color = this._colors[this._colorIndex]; - this._colorIndex = (this._colorIndex + 1) % this._colors.length; - this._colorDict[entityId] = color; + private _getColor(entityId: string): string { + let color = this._colorDict[entityId]; + if (color) { + return color; } + color = COLORS[this._colorIndex % COLORS.length]; + this._colorIndex++; + this._colorDict[entityId] = color; return color; } - private _drawEntities(): void { - const hass = this.hass; - const map = this._leafletMap; - const config = this._config; - const Leaflet = this.Leaflet; - if (!hass || !map || !config || !Leaflet) { - return; - } - - if (this._mapItems) { - this._mapItems.forEach((marker) => marker.remove()); - } - const mapItems: Layer[] = (this._mapItems = []); - - if (this._mapZones) { - this._mapZones.forEach((marker) => marker.remove()); - } - const mapZones: Layer[] = (this._mapZones = []); - - if (this._mapPaths) { - this._mapPaths.forEach((marker) => marker.remove()); - } - const mapPaths: Layer[] = (this._mapPaths = []); - - const allEntities = this._configEntities!.concat(); - - // Calculate visible geo location sources - if (config.geo_location_sources) { - const includesAll = config.geo_location_sources.includes("all"); - for (const entityId of Object.keys(hass.states)) { - const stateObj = hass.states[entityId]; - if ( - computeDomain(entityId) === "geo_location" && - (includesAll || - config.geo_location_sources.includes(stateObj.attributes.source)) - ) { - allEntities.push({ entity: entityId }); - } + private _getEntities = memoizeOne( + ( + states: HassEntities, + config: MapCardConfig, + configEntities?: string[] + ) => { + if (!states || !config) { + return undefined; } - } - // DRAW history - if (this._config!.hours_to_show && this._history) { - for (const entityStates of this._history) { + let entities = configEntities || []; + + if (config.geo_location_sources) { + const geoEntities: string[] = []; + // Calculate visible geo location sources + const includesAll = config.geo_location_sources.includes("all"); + for (const stateObj of Object.values(states)) { + if ( + computeDomain(stateObj.entity_id) === "geo_location" && + (includesAll || + config.geo_location_sources.includes(stateObj.attributes.source)) + ) { + geoEntities.push(stateObj.entity_id); + } + } + + entities = [...entities, ...geoEntities]; + } + + return entities.map((entity) => ({ + entity_id: entity, + color: this._getColor(entity), + })); + } + ); + + private _getHistoryPaths = memoizeOne( + ( + config: MapCardConfig, + history?: HassEntity[][] + ): HaMapPaths[] | undefined => { + if (!config.hours_to_show || !history) { + return undefined; + } + + const paths: HaMapPaths[] = []; + + for (const entityStates of history) { if (entityStates?.length <= 1) { continue; } - const entityId = entityStates[0].entity_id; - // filter location data from states and remove all invalid locations - const path = entityStates.reduce( - (accumulator: LatLngTuple[], state) => { - const latitude = state.attributes.latitude; - const longitude = state.attributes.longitude; + const points = entityStates.reduce( + (accumulator: LatLngTuple[], entityState) => { + const latitude = entityState.attributes.latitude; + const longitude = entityState.attributes.longitude; if (latitude && longitude) { accumulator.push([latitude, longitude] as LatLngTuple); } @@ -437,162 +295,15 @@ class HuiMapCard extends LitElement implements LovelaceCard { [] ) as LatLngTuple[]; - // DRAW HISTORY - for ( - let markerIndex = 0; - markerIndex < path.length - 1; - markerIndex++ - ) { - const opacityStep = 0.8 / (path.length - 2); - const opacity = 0.2 + markerIndex * opacityStep; - - // DRAW history path dots - mapPaths.push( - Leaflet.circleMarker(path[markerIndex], { - radius: 3, - color: this._getColor(entityId), - opacity, - interactive: false, - }) - ); - - // DRAW history path lines - const line = [path[markerIndex], path[markerIndex + 1]]; - mapPaths.push( - Leaflet.polyline(line, { - color: this._getColor(entityId), - opacity, - interactive: false, - }) - ); - } + paths.push({ + points, + color: this._getColor(entityStates[0].entity_id), + gradualOpacity: 0.8, + }); } + return paths; } - - // DRAW entities - for (const entity of allEntities) { - const entityId = entity.entity; - const stateObj = hass.states[entityId]; - if (!stateObj) { - continue; - } - const title = computeStateName(stateObj); - const { - latitude, - longitude, - passive, - icon, - radius, - entity_picture: entityPicture, - gps_accuracy: gpsAccuracy, - } = stateObj.attributes; - - if (!(latitude && longitude)) { - continue; - } - - if (computeStateDomain(stateObj) === "zone") { - // DRAW ZONE - if (passive) { - continue; - } - - // create icon - let iconHTML = ""; - if (icon) { - const el = document.createElement("ha-icon"); - el.setAttribute("icon", icon); - iconHTML = el.outerHTML; - } else { - const el = document.createElement("span"); - el.innerHTML = title; - iconHTML = el.outerHTML; - } - - // create marker with the icon - mapZones.push( - Leaflet.marker([latitude, longitude], { - icon: Leaflet.divIcon({ - html: iconHTML, - iconSize: [24, 24], - className: this._config!.dark_mode - ? "dark" - : this._config!.dark_mode === false - ? "light" - : "", - }), - interactive: false, - title, - }) - ); - - // create circle around it - mapZones.push( - Leaflet.circle([latitude, longitude], { - interactive: false, - color: "#FF9800", - radius, - }) - ); - - continue; - } - - // DRAW ENTITY - // create icon - const entityName = title - .split(" ") - .map((part) => part[0]) - .join("") - .substr(0, 3); - - // create market with the icon - mapItems.push( - Leaflet.marker([latitude, longitude], { - icon: Leaflet.divIcon({ - // Leaflet clones this element before adding it to the map. This messes up - // our Polymer object and we can't pass data through. Thus we hack like this. - html: ` - - `, - iconSize: [48, 48], - className: "", - }), - title: computeStateName(stateObj), - }) - ); - - // create circle around if entity has accuracy - if (gpsAccuracy) { - mapItems.push( - Leaflet.circle([latitude, longitude], { - interactive: false, - color: this._getColor(entityId), - radius: gpsAccuracy, - }) - ); - } - } - - this._mapItems.forEach((marker) => map.addLayer(marker)); - this._mapZones.forEach((marker) => map.addLayer(marker)); - this._mapPaths.forEach((marker) => map.addLayer(marker)); - } - - private async _attachObserver(): Promise { - // Observe changes to map size and invalidate to prevent broken rendering - - if (!this._resizeObserver) { - await installResizeObserver(); - this._resizeObserver = new ResizeObserver(this._debouncedResizeListener); - } - this._resizeObserver.observe(this); - } + ); private async _getHistory(): Promise { this._date = new Date(); @@ -601,9 +312,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { return; } - const entityIds = this._configEntities!.map((entity) => entity.entity).join( - "," - ); + const entityIds = this._configEntities!.join(","); const endTime = new Date(); const startTime = new Date(); startTime.setHours(endTime.getHours() - this._config!.hours_to_show!); @@ -624,7 +333,6 @@ class HuiMapCard extends LitElement implements LovelaceCard { if (stateHistory.length < 1) { return; } - this._history = stateHistory; } @@ -636,13 +344,10 @@ class HuiMapCard extends LitElement implements LovelaceCard { this._history = undefined; } else { // remove unused entities - const configEntityIds = this._configEntities?.map( - (configEntity) => configEntity.entity - ); this._history = this._history!.reduce( (accumulator: HassEntity[][], entityStates) => { const entityId = entityStates[0].entity_id; - if (configEntityIds?.includes(entityId)) { + if (this._configEntities?.includes(entityId)) { accumulator.push(entityStates); } return accumulator; @@ -660,7 +365,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { height: 100%; } - #map { + ha-map { z-index: 0; border: none; position: absolute; @@ -671,7 +376,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { background: inherit; } - ha-icon-button { + mwc-icon-button { position: absolute; top: 75px; left: 3px; @@ -685,14 +390,6 @@ class HuiMapCard extends LitElement implements LovelaceCard { :host([ispanel]) #root { height: 100%; } - - .dark { - color: #ffffff; - } - - .light { - color: #000000; - } `; } } diff --git a/src/panels/lovelace/components/hui-input-list-editor.ts b/src/panels/lovelace/components/hui-input-list-editor.ts index ae64dea7b7..ea92b47553 100644 --- a/src/panels/lovelace/components/hui-input-list-editor.ts +++ b/src/panels/lovelace/components/hui-input-list-editor.ts @@ -29,6 +29,7 @@ export class HuiInputListEditor extends LitElement { .index=${index} @value-changed=${this._valueChanged} @blur=${this._consolidateEntries} + @keydown=${this._handleKeyDown} > - - -
- - -
- `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - entityId: { - type: String, - value: "", - }, - - entityName: { - type: String, - value: null, - }, - - entityPicture: { - type: String, - value: null, - }, - - entityColor: { - type: String, - value: null, - }, - }; - } - - ready() { - super.ready(); - this.addEventListener("click", (ev) => this.badgeTap(ev)); - } - - badgeTap(ev) { - ev.stopPropagation(); - if (this.entityId) { - this.fire("hass-more-info", { entityId: this.entityId }); - } - } -} - -customElements.define("ha-entity-marker", HaEntityMarker); diff --git a/src/panels/map/ha-panel-map.js b/src/panels/map/ha-panel-map.js deleted file mode 100644 index 108c984765..0000000000 --- a/src/panels/map/ha-panel-map.js +++ /dev/null @@ -1,263 +0,0 @@ -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { - replaceTileLayer, - setupLeafletMap, -} from "../../common/dom/setup-leaflet-map"; -import { computeStateDomain } from "../../common/entity/compute_state_domain"; -import { computeStateName } from "../../common/entity/compute_state_name"; -import { navigate } from "../../common/navigate"; -import "../../components/ha-icon"; -import "../../components/ha-menu-button"; -import { defaultRadiusColor } from "../../data/zone"; -import "../../layouts/ha-app-layout"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import "../../styles/polymer-ha-style"; -import "./ha-entity-marker"; - -/* - * @appliesMixin LocalizeMixin - */ -class HaPanelMap extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - - - - -
[[localize('panel.map')]]
- -
-
-
-
- `; - } - - static get properties() { - return { - hass: { - type: Object, - observer: "drawEntities", - }, - narrow: Boolean, - }; - } - - connectedCallback() { - super.connectedCallback(); - this.loadMap(); - } - - async loadMap() { - this._darkMode = this.hass.themes.darkMode; - [this._map, this.Leaflet, this._tileLayer] = await setupLeafletMap( - this.$.map, - this._darkMode - ); - this.drawEntities(this.hass); - this._map.invalidateSize(); - this.fitMap(); - } - - disconnectedCallback() { - if (this._map) { - this._map.remove(); - } - } - - computeShowEditZone(hass) { - return !__DEMO__ && hass.user.is_admin; - } - - openZonesEditor() { - navigate("/config/zone"); - } - - fitMap() { - let bounds; - - if (this._mapItems.length === 0) { - this._map.setView( - new this.Leaflet.LatLng( - this.hass.config.latitude, - this.hass.config.longitude - ), - 14 - ); - } else { - bounds = new this.Leaflet.latLngBounds( - this._mapItems.map((item) => item.getLatLng()) - ); - this._map.fitBounds(bounds.pad(0.5)); - } - } - - drawEntities(hass) { - /* eslint-disable vars-on-top */ - const map = this._map; - if (!map) return; - - if (this._darkMode !== this.hass.themes.darkMode) { - this._darkMode = this.hass.themes.darkMode; - this._tileLayer = replaceTileLayer( - this.Leaflet, - map, - this._tileLayer, - this.hass.themes.darkMode - ); - } - - if (this._mapItems) { - this._mapItems.forEach(function (marker) { - marker.remove(); - }); - } - const mapItems = (this._mapItems = []); - - if (this._mapZones) { - this._mapZones.forEach(function (marker) { - marker.remove(); - }); - } - const mapZones = (this._mapZones = []); - - Object.keys(hass.states).forEach((entityId) => { - const entity = hass.states[entityId]; - - if ( - entity.state === "home" || - !("latitude" in entity.attributes) || - !("longitude" in entity.attributes) - ) { - return; - } - - const title = computeStateName(entity); - let icon; - - if (computeStateDomain(entity) === "zone") { - // DRAW ZONE - if (entity.attributes.passive) return; - - // create icon - let iconHTML = ""; - if (entity.attributes.icon) { - const el = document.createElement("ha-icon"); - el.setAttribute("icon", entity.attributes.icon); - iconHTML = el.outerHTML; - } else { - const el = document.createElement("span"); - el.innerHTML = title; - iconHTML = el.outerHTML; - } - - icon = this.Leaflet.divIcon({ - html: iconHTML, - iconSize: [24, 24], - className: "icon", - }); - - // create marker with the icon - mapZones.push( - this.Leaflet.marker( - [entity.attributes.latitude, entity.attributes.longitude], - { - icon: icon, - interactive: false, - title: title, - } - ).addTo(map) - ); - - // create circle around it - mapZones.push( - this.Leaflet.circle( - [entity.attributes.latitude, entity.attributes.longitude], - { - interactive: false, - color: defaultRadiusColor, - radius: entity.attributes.radius, - } - ).addTo(map) - ); - - return; - } - - // DRAW ENTITY - // create icon - const entityPicture = entity.attributes.entity_picture || ""; - const entityName = title - .split(" ") - .map(function (part) { - return part.substr(0, 1); - }) - .join(""); - /* Leaflet clones this element before adding it to the map. This messes up - our Polymer object and we can't pass data through. Thus we hack like this. */ - icon = this.Leaflet.divIcon({ - html: - "", - iconSize: [45, 45], - className: "", - }); - - // create market with the icon - mapItems.push( - this.Leaflet.marker( - [entity.attributes.latitude, entity.attributes.longitude], - { - icon: icon, - title: computeStateName(entity), - } - ).addTo(map) - ); - - // create circle around if entity has accuracy - if (entity.attributes.gps_accuracy) { - mapItems.push( - this.Leaflet.circle( - [entity.attributes.latitude, entity.attributes.longitude], - { - interactive: false, - color: "#0288D1", - radius: entity.attributes.gps_accuracy, - } - ).addTo(map) - ); - } - }); - } -} - -customElements.define("ha-panel-map", HaPanelMap); diff --git a/src/panels/map/ha-panel-map.ts b/src/panels/map/ha-panel-map.ts new file mode 100644 index 0000000000..d391668c9d --- /dev/null +++ b/src/panels/map/ha-panel-map.ts @@ -0,0 +1,103 @@ +import { mdiPencil } from "@mdi/js"; +import "@material/mwc-icon-button"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { property } from "lit/decorators"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { navigate } from "../../common/navigate"; +import "../../components/ha-svg-icon"; +import "../../components/ha-menu-button"; +import "../../layouts/ha-app-layout"; +import { HomeAssistant } from "../../types"; +import "../../components/map/ha-map"; +import { haStyle } from "../../resources/styles"; + +class HaPanelMap extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow!: boolean; + + private _entities: string[] = []; + + protected render() { + return html` + + + + +
${this.hass.localize("panel.map")}
+ ${!__DEMO__ && this.hass.user?.is_admin + ? html`` + : ""} +
+
+ +
+ `; + } + + private _openZonesEditor() { + navigate("/config/zone"); + } + + public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (!changedProps.has("hass")) { + return; + } + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + this._getStates(oldHass); + } + + private _getStates(oldHass?: HomeAssistant) { + let changed = false; + const personSources = new Set(); + const locationEntities: string[] = []; + Object.values(this.hass!.states).forEach((entity) => { + if ( + entity.state === "home" || + !("latitude" in entity.attributes) || + !("longitude" in entity.attributes) + ) { + return; + } + locationEntities.push(entity.entity_id); + if (computeStateDomain(entity) === "person" && entity.attributes.source) { + personSources.add(entity.attributes.source); + } + if (oldHass?.states[entity.entity_id] !== entity) { + changed = true; + } + }); + + if (changed) { + this._entities = locationEntities.filter( + (entity) => !personSources.has(entity) + ); + } + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + ha-map { + height: calc(100vh - var(--header-height)); + } + `, + ]; + } +} + +customElements.define("ha-panel-map", HaPanelMap); + +declare global { + interface HTMLElementTagNameMap { + "ha-panel-map": HaPanelMap; + } +} diff --git a/src/panels/profile/ha-push-notifications-row.js b/src/panels/profile/ha-push-notifications-row.js index 429173ffd2..69f2e54246 100644 --- a/src/panels/profile/ha-push-notifications-row.js +++ b/src/panels/profile/ha-push-notifications-row.js @@ -1,5 +1,4 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/iron-label/iron-label"; import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index f3e9dcb757..82439c105e 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -29,10 +29,10 @@ documentContainer.innerHTML = ` --disabled-text-color: #bdbdbd; /* main interface colors */ - --primary-color: #03a9f4; + --primary-color: ${DEFAULT_PRIMARY_COLOR}; --dark-primary-color: #0288d1; --light-primary-color: #b3e5fC; - --accent-color: #ff9800; + --accent-color: ${DEFAULT_ACCENT_COLOR}; --divider-color: rgba(0, 0, 0, .12); --scrollbar-thumb-color: rgb(194, 194, 194); diff --git a/yarn.lock b/yarn.lock index 8fd9b8db50..eae3bc00de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2104,13 +2104,6 @@ "@polymer/iron-meta" "^3.0.0-pre.26" "@polymer/polymer" "^3.0.0" -"@polymer/iron-image@^3.0.1": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@polymer/iron-image/-/iron-image-3.0.2.tgz#425ee6269634e024dbea726a91a61724ae4402b6" - integrity sha512-VyYtnewGozDb5sUeoLR1OvKzlt5WAL6b8Od7fPpio5oYL+9t061/nTV8+ZMrpMgF2WgB0zqM/3K53o3pbK5v8Q== - dependencies: - "@polymer/polymer" "^3.0.0" - "@polymer/iron-input@^3.0.0-pre.26", "@polymer/iron-input@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@polymer/iron-input/-/iron-input-3.0.1.tgz#dc866a25107f9b38d9ca4512dd9a3e51b78b4915" @@ -2120,13 +2113,6 @@ "@polymer/iron-validatable-behavior" "^3.0.0-pre.26" "@polymer/polymer" "^3.0.0" -"@polymer/iron-label@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@polymer/iron-label/-/iron-label-3.0.1.tgz#170247dc50d63f4e2ae6c80711dbf5b64fa953d6" - integrity sha512-MkIZ1WfOy10pnIxRwTVPfsoDZYlqMkUp0hmimMj0pGRHmrc9n5phuJUY1pC+S7WoKP1/98iH2qnXQukPGTzoVA== - dependencies: - "@polymer/polymer" "^3.0.0" - "@polymer/iron-list@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@polymer/iron-list/-/iron-list-3.0.2.tgz#9e6b80e503328dc29217dbe26f94faa47adb4124"