mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Convert map card to Lit/TS (#2826)
* Convert map card to Lit/TS * Address comments
This commit is contained in:
parent
63e6506510
commit
90a1f7e51c
@ -107,6 +107,7 @@
|
|||||||
"@gfx/zopfli": "^1.0.9",
|
"@gfx/zopfli": "^1.0.9",
|
||||||
"@types/chai": "^4.1.7",
|
"@types/chai": "^4.1.7",
|
||||||
"@types/codemirror": "^0.0.71",
|
"@types/codemirror": "^0.0.71",
|
||||||
|
"@types/leaflet": "^1.4.3",
|
||||||
"@types/memoize-one": "^4.1.0",
|
"@types/memoize-one": "^4.1.0",
|
||||||
"@types/mocha": "^5.2.5",
|
"@types/mocha": "^5.2.5",
|
||||||
"babel-eslint": "^10",
|
"babel-eslint": "^10",
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
|
import { Map } from "leaflet";
|
||||||
|
|
||||||
// Sets up a Leaflet map on the provided DOM element
|
// Sets up a Leaflet map on the provided DOM element
|
||||||
export const setupLeafletMap = async (mapElement) => {
|
export type LeafletModuleType = typeof import("leaflet");
|
||||||
|
|
||||||
|
export const setupLeafletMap = async (
|
||||||
|
mapElement
|
||||||
|
): Promise<[Map, LeafletModuleType]> => {
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet"))
|
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType;
|
||||||
.default;
|
|
||||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
|
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
|
||||||
|
|
||||||
const map = Leaflet.map(mapElement);
|
const map = Leaflet.map(mapElement);
|
||||||
|
@ -1,369 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import "@polymer/paper-icon-button/paper-icon-button";
|
|
||||||
|
|
||||||
import "../../map/ha-entity-marker";
|
|
||||||
|
|
||||||
import { setupLeafletMap } from "../../../common/dom/setup-leaflet-map";
|
|
||||||
import { processConfigEntities } from "../common/process-config-entities";
|
|
||||||
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";
|
|
||||||
|
|
||||||
// should be interface when converted to TS
|
|
||||||
export const Config = {
|
|
||||||
title: "",
|
|
||||||
aspect_ratio: "",
|
|
||||||
default_zoom: 14,
|
|
||||||
entities: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
class HuiMapCard extends PolymerElement {
|
|
||||||
static async getConfigElement() {
|
|
||||||
await import(/* webpackChunkName: "hui-map-card-editor" */ "../editor/config-elements/hui-map-card-editor");
|
|
||||||
return document.createElement("hui-map-card-editor");
|
|
||||||
}
|
|
||||||
|
|
||||||
static getStubConfig() {
|
|
||||||
return { entities: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
:host([is-panel]) ha-card {
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
/**
|
|
||||||
* In panel mode we want a full height map. Since parent #view
|
|
||||||
* only sets min-height, we need absolute positioning here
|
|
||||||
*/
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
|
||||||
z-index: 0;
|
|
||||||
border: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
paper-icon-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 75px;
|
|
||||||
left: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([is-panel]) #root {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<ha-card id="card" header="[[_config.title]]">
|
|
||||||
<div id="root">
|
|
||||||
<div id="map"></div>
|
|
||||||
<paper-icon-button
|
|
||||||
on-click="_fitMap"
|
|
||||||
icon="hass:image-filter-center-focus"
|
|
||||||
title="Reset focus"
|
|
||||||
></paper-icon-button>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
observer: "_drawEntities",
|
|
||||||
},
|
|
||||||
_config: Object,
|
|
||||||
isPanel: {
|
|
||||||
type: Boolean,
|
|
||||||
reflectToAttribute: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this._debouncedResizeListener = debounce(this._resetMap.bind(this), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
|
|
||||||
if (!this._config || this.isPanel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
|
||||||
|
|
||||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
|
||||||
this.$.root.style.paddingBottom = `${((100 * ratio.h) / ratio.w).toFixed(
|
|
||||||
2
|
|
||||||
)}%`;
|
|
||||||
} else {
|
|
||||||
this.$.root.style.paddingBottom = "100%";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfig(config) {
|
|
||||||
if (!config) {
|
|
||||||
throw new Error("Error in card configuration.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.entities && !config.geo_location_sources) {
|
|
||||||
throw new Error(
|
|
||||||
"Either entities or geo_location_sources must be defined"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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._configGeoLocationSources = config.geo_location_sources;
|
|
||||||
this._configEntities = config.entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCardSize() {
|
|
||||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
|
||||||
let ar;
|
|
||||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
|
||||||
ar = `${((100 * ratio.h) / ratio.w).toFixed(2)}`;
|
|
||||||
} else {
|
|
||||||
ar = "100";
|
|
||||||
}
|
|
||||||
return 1 + Math.floor(ar / 25) || 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
// Observe changes to map size and invalidate to prevent broken rendering
|
|
||||||
// Uses ResizeObserver in Chrome, otherwise window resize event
|
|
||||||
if (typeof ResizeObserver === "function") {
|
|
||||||
this._resizeObserver = new ResizeObserver(() =>
|
|
||||||
this._debouncedResizeListener()
|
|
||||||
);
|
|
||||||
this._resizeObserver.observe(this.$.map);
|
|
||||||
} else {
|
|
||||||
window.addEventListener("resize", this._debouncedResizeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadMap() {
|
|
||||||
[this._map, this.Leaflet] = await setupLeafletMap(this.$.map);
|
|
||||||
this._drawEntities(this.hass);
|
|
||||||
this._map.invalidateSize();
|
|
||||||
this._fitMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
|
|
||||||
if (this._map) {
|
|
||||||
this._map.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._resizeObserver) {
|
|
||||||
this._resizeObserver.unobserve(this.$.map);
|
|
||||||
} else {
|
|
||||||
window.removeEventListener("resize", this._debouncedResizeListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_resetMap() {
|
|
||||||
if (!this._map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._map.invalidateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
_fitMap() {
|
|
||||||
const zoom = this._config.default_zoom;
|
|
||||||
if (this._mapItems.length === 0) {
|
|
||||||
this._map.setView(
|
|
||||||
new this.Leaflet.LatLng(
|
|
||||||
this.hass.config.latitude,
|
|
||||||
this.hass.config.longitude
|
|
||||||
),
|
|
||||||
zoom || 14
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bounds = new this.Leaflet.latLngBounds(
|
|
||||||
this._mapItems.map((item) => item.getLatLng())
|
|
||||||
);
|
|
||||||
this._map.fitBounds(bounds.pad(0.5));
|
|
||||||
|
|
||||||
if (zoom && this._map.getZoom() > zoom) {
|
|
||||||
this._map.setZoom(zoom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_drawEntities(hass) {
|
|
||||||
const map = this._map;
|
|
||||||
if (!map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._mapItems) {
|
|
||||||
this._mapItems.forEach((marker) => marker.remove());
|
|
||||||
}
|
|
||||||
const mapItems = (this._mapItems = []);
|
|
||||||
|
|
||||||
let allEntities = [];
|
|
||||||
if (this._configEntities) {
|
|
||||||
allEntities = allEntities.concat(this._configEntities);
|
|
||||||
}
|
|
||||||
if (this._configGeoLocationSources) {
|
|
||||||
Object.keys(this.hass.states).forEach((entityId) => {
|
|
||||||
const stateObj = this.hass.states[entityId];
|
|
||||||
if (
|
|
||||||
computeStateDomain(stateObj) === "geo_location" &&
|
|
||||||
(this._configGeoLocationSources.includes(
|
|
||||||
stateObj.attributes.source
|
|
||||||
) ||
|
|
||||||
this._configGeoLocationSources.includes("all"))
|
|
||||||
) {
|
|
||||||
allEntities.push(entityId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
allEntities = processConfigEntities(allEntities);
|
|
||||||
|
|
||||||
allEntities.forEach((entity) => {
|
|
||||||
const entityId = entity.entity;
|
|
||||||
if (!(entityId in hass.states)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const stateObj = hass.states[entityId];
|
|
||||||
const title = computeStateName(stateObj);
|
|
||||||
const {
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
passive,
|
|
||||||
icon,
|
|
||||||
radius,
|
|
||||||
entity_picture: entityPicture,
|
|
||||||
gps_accuracy: gpsAccuracy,
|
|
||||||
} = stateObj.attributes;
|
|
||||||
|
|
||||||
if (!(latitude && longitude)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let markerIcon;
|
|
||||||
let iconHTML;
|
|
||||||
let el;
|
|
||||||
|
|
||||||
if (computeStateDomain(stateObj) === "zone") {
|
|
||||||
// DRAW ZONE
|
|
||||||
if (passive) return;
|
|
||||||
|
|
||||||
// create icon
|
|
||||||
if (icon) {
|
|
||||||
el = document.createElement("ha-icon");
|
|
||||||
el.setAttribute("icon", icon);
|
|
||||||
iconHTML = el.outerHTML;
|
|
||||||
} else {
|
|
||||||
iconHTML = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
markerIcon = this.Leaflet.divIcon({
|
|
||||||
html: iconHTML,
|
|
||||||
iconSize: [24, 24],
|
|
||||||
className: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// create market with the icon
|
|
||||||
mapItems.push(
|
|
||||||
this.Leaflet.marker([latitude, longitude], {
|
|
||||||
icon: markerIcon,
|
|
||||||
interactive: false,
|
|
||||||
title: title,
|
|
||||||
}).addTo(map)
|
|
||||||
);
|
|
||||||
|
|
||||||
// create circle around it
|
|
||||||
mapItems.push(
|
|
||||||
this.Leaflet.circle([latitude, longitude], {
|
|
||||||
interactive: false,
|
|
||||||
color: "#FF9800",
|
|
||||||
radius: radius,
|
|
||||||
}).addTo(map)
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRAW ENTITY
|
|
||||||
// create icon
|
|
||||||
const entityName = title
|
|
||||||
.split(" ")
|
|
||||||
.map((part) => part[0])
|
|
||||||
.join("")
|
|
||||||
.substr(0, 3);
|
|
||||||
|
|
||||||
el = document.createElement("ha-entity-marker");
|
|
||||||
el.setAttribute("entity-id", entityId);
|
|
||||||
el.setAttribute("entity-name", entityName);
|
|
||||||
el.setAttribute("entity-picture", entityPicture || "");
|
|
||||||
|
|
||||||
/* this.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. */
|
|
||||||
markerIcon = this.Leaflet.divIcon({
|
|
||||||
html: el.outerHTML,
|
|
||||||
iconSize: [48, 48],
|
|
||||||
className: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// create market with the icon
|
|
||||||
mapItems.push(
|
|
||||||
this.Leaflet.marker([latitude, longitude], {
|
|
||||||
icon: markerIcon,
|
|
||||||
title: computeStateName(stateObj),
|
|
||||||
}).addTo(map)
|
|
||||||
);
|
|
||||||
|
|
||||||
// create circle around if entity has accuracy
|
|
||||||
if (gpsAccuracy) {
|
|
||||||
mapItems.push(
|
|
||||||
this.Leaflet.circle([latitude, longitude], {
|
|
||||||
interactive: false,
|
|
||||||
color: "#0288D1",
|
|
||||||
radius: gpsAccuracy,
|
|
||||||
}).addTo(map)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hui-map-card", HuiMapCard);
|
|
407
src/panels/lovelace/cards/hui-map-card.ts
Normal file
407
src/panels/lovelace/cards/hui-map-card.ts
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
|
import { Layer, Marker, Circle, Map } from "leaflet";
|
||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import "../../map/ha-entity-marker";
|
||||||
|
|
||||||
|
import {
|
||||||
|
setupLeafletMap,
|
||||||
|
LeafletModuleType,
|
||||||
|
} 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 parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import computeDomain from "../../../common/entity/compute_domain";
|
||||||
|
import { LovelaceCard } from "../types";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
|
import { EntityConfig } from "../entity-rows/types";
|
||||||
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
|
|
||||||
|
export interface MapCardConfig extends LovelaceCardConfig {
|
||||||
|
title: string;
|
||||||
|
aspect_ratio: string;
|
||||||
|
default_zoom?: number;
|
||||||
|
entities?: Array<EntityConfig | string>;
|
||||||
|
geo_location_sources?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("hui-map-card")
|
||||||
|
class HuiMapCard extends LitElement implements LovelaceCard {
|
||||||
|
public static async getConfigElement() {
|
||||||
|
await import(/* webpackChunkName: "hui-map-card-editor" */ "../editor/config-elements/hui-map-card-editor");
|
||||||
|
return document.createElement("hui-map-card-editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getStubConfig() {
|
||||||
|
return { entities: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
@property() public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public isPanel = false;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
private _config?: MapCardConfig;
|
||||||
|
private _configEntities?: EntityConfig[];
|
||||||
|
// tslint:disable-next-line
|
||||||
|
private Leaflet?: LeafletModuleType;
|
||||||
|
private _leafletMap?: Map;
|
||||||
|
// @ts-ignore
|
||||||
|
private _resizeObserver?: ResizeObserver;
|
||||||
|
private _debouncedResizeListener = debounce(
|
||||||
|
() => {
|
||||||
|
if (!this._leafletMap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._leafletMap.invalidateSize();
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
private _mapItems: Array<Marker | Circle> = [];
|
||||||
|
private _connected = false;
|
||||||
|
|
||||||
|
public setConfig(config: MapCardConfig): void {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Error in card configuration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.entities && !config.geo_location_sources) {
|
||||||
|
throw new Error(
|
||||||
|
"Either entities or geo_location_sources must be defined"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCardSize(): number {
|
||||||
|
if (!this._config) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
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._connected = true;
|
||||||
|
if (this.hasUpdated) {
|
||||||
|
this._attachObserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
|
||||||
|
if (this._leafletMap) {
|
||||||
|
this._leafletMap.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._resizeObserver) {
|
||||||
|
this._resizeObserver.unobserve(this._mapEl);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener("resize", this._debouncedResizeListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this._config) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-card id="card" .header=${this._config.title}>
|
||||||
|
<div id="root">
|
||||||
|
<div id="map"></div>
|
||||||
|
<paper-icon-button
|
||||||
|
@click=${this._fitMap}
|
||||||
|
icon="hass:image-filter-center-focus"
|
||||||
|
title="Reset focus"
|
||||||
|
></paper-icon-button>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this.loadMap();
|
||||||
|
const root = this.shadowRoot!.getElementById("root");
|
||||||
|
|
||||||
|
if (!this._config || this.isPanel || !root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._connected) {
|
||||||
|
this._attachObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||||
|
|
||||||
|
root.style.paddingBottom =
|
||||||
|
ratio && ratio.w > 0 && ratio.h > 0
|
||||||
|
? `${((100 * ratio.h) / ratio.w).toFixed(2)}%`
|
||||||
|
: (root.style.paddingBottom = "100%");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has("hass")) {
|
||||||
|
this._drawEntities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _mapEl(): HTMLDivElement {
|
||||||
|
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadMap(): Promise<void> {
|
||||||
|
[this._leafletMap, this.Leaflet] = await setupLeafletMap(this._mapEl);
|
||||||
|
this._drawEntities();
|
||||||
|
this._leafletMap.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.latLngBounds(
|
||||||
|
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
|
||||||
|
);
|
||||||
|
this._leafletMap.fitBounds(bounds.pad(0.5));
|
||||||
|
|
||||||
|
if (zoom && this._leafletMap.getZoom() > zoom) {
|
||||||
|
this._leafletMap.setZoom(zoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = []);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 marker with the icon
|
||||||
|
mapItems.push(
|
||||||
|
Leaflet.marker([latitude, longitude], {
|
||||||
|
icon: Leaflet.divIcon({
|
||||||
|
html: icon ? `<ha-icon icon="${icon}"></ha-icon>` : title,
|
||||||
|
iconSize: [24, 24],
|
||||||
|
className: "",
|
||||||
|
}),
|
||||||
|
interactive: false,
|
||||||
|
title,
|
||||||
|
}).addTo(map)
|
||||||
|
);
|
||||||
|
|
||||||
|
// create circle around it
|
||||||
|
mapItems.push(
|
||||||
|
Leaflet.circle([latitude, longitude], {
|
||||||
|
interactive: false,
|
||||||
|
color: "#FF9800",
|
||||||
|
radius,
|
||||||
|
}).addTo(map)
|
||||||
|
);
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<ha-entity-marker
|
||||||
|
entity-id="${entityId}"
|
||||||
|
entity-name="${entityName}"
|
||||||
|
entity-picture="${entityPicture || ""}
|
||||||
|
></ha-entity-marker>
|
||||||
|
`,
|
||||||
|
iconSize: [48, 48],
|
||||||
|
className: "",
|
||||||
|
}),
|
||||||
|
title: computeStateName(stateObj),
|
||||||
|
}).addTo(map)
|
||||||
|
);
|
||||||
|
|
||||||
|
// create circle around if entity has accuracy
|
||||||
|
if (gpsAccuracy) {
|
||||||
|
mapItems.push(
|
||||||
|
Leaflet.circle([latitude, longitude], {
|
||||||
|
interactive: false,
|
||||||
|
color: "#0288D1",
|
||||||
|
radius: gpsAccuracy,
|
||||||
|
}).addTo(map)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
:host([ispanel]) ha-card {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
/**
|
||||||
|
* In panel mode we want a full height map. Since parent #view
|
||||||
|
* only sets min-height, we need absolute positioning here
|
||||||
|
*/
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-card {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
z-index: 0;
|
||||||
|
border: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
paper-icon-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 75px;
|
||||||
|
left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([ispanel]) #root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-map-card": HuiMapCard;
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ import { EntitiesEditorEvent, EditorTarget } from "../types";
|
|||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import { LovelaceCardEditor } from "../../types";
|
import { LovelaceCardEditor } from "../../types";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { Config } from "../../cards/hui-alarm-panel-card";
|
import { MapCardConfig } from "../../cards/hui-map-card";
|
||||||
import { configElementStyle } from "./config-elements-style";
|
import { configElementStyle } from "./config-elements-style";
|
||||||
import { processEditorEntities } from "../process-editor-entities";
|
import { processEditorEntities } from "../process-editor-entities";
|
||||||
import { EntityConfig } from "../../entity-rows/types";
|
import { EntityConfig } from "../../entity-rows/types";
|
||||||
@ -37,10 +37,10 @@ const cardConfigStruct = struct({
|
|||||||
|
|
||||||
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
private _config?: Config;
|
private _config?: MapCardConfig;
|
||||||
private _configEntities?: EntityConfig[];
|
private _configEntities?: EntityConfig[];
|
||||||
|
|
||||||
public setConfig(config: Config): void {
|
public setConfig(config: MapCardConfig): void {
|
||||||
config = cardConfigStruct(config);
|
config = cardConfigStruct(config);
|
||||||
this._config = config;
|
this._config = config;
|
||||||
this._configEntities = processEditorEntities(config.entities);
|
this._configEntities = processEditorEntities(config.entities);
|
||||||
@ -62,10 +62,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
|||||||
return this._config!.default_zoom || NaN;
|
return this._config!.default_zoom || NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
get _entities(): string[] {
|
|
||||||
return this._config!.entities || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return html``;
|
return html``;
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -1653,6 +1653,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/freeport/-/freeport-1.0.21.tgz#73f6543ed67d3ca3fff97b985591598b7092066f"
|
resolved "https://registry.yarnpkg.com/@types/freeport/-/freeport-1.0.21.tgz#73f6543ed67d3ca3fff97b985591598b7092066f"
|
||||||
integrity sha1-c/ZUPtZ9PKP/+XuYVZFZi3CSBm8=
|
integrity sha1-c/ZUPtZ9PKP/+XuYVZFZi3CSBm8=
|
||||||
|
|
||||||
|
"@types/geojson@*":
|
||||||
|
version "7946.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.6.tgz#416f388a06b227784a2d91a88a53f14de05cd54b"
|
||||||
|
integrity sha512-f6qai3iR62QuMPPdgyH+LyiXTL2n9Rf62UniJjV7KHrbiwzLTZUKsdq0mFSTxAHbO7JvwxwC4tH0m1UnweuLrA==
|
||||||
|
|
||||||
"@types/glob-stream@*":
|
"@types/glob-stream@*":
|
||||||
version "6.1.0"
|
version "6.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc"
|
resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc"
|
||||||
@ -1725,6 +1730,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
|
resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
|
||||||
integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
|
integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
|
||||||
|
|
||||||
|
"@types/leaflet@^1.4.3":
|
||||||
|
version "1.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.4.3.tgz#62638cb73770eeaed40222042afbcc7b495f0cc4"
|
||||||
|
integrity sha512-jFRBSsPHi1EwQSwrN0cOJLdPhwOZsRl4IMxvm/2ShLh0YM5GfCtQXCzsrv8RE7DWL+AykXdYSAd9bFLWbZT4CQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/geojson" "*"
|
||||||
|
|
||||||
"@types/memoize-one@^4.1.0":
|
"@types/memoize-one@^4.1.0":
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece"
|
resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user