mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +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",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/codemirror": "^0.0.71",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/memoize-one": "^4.1.0",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"babel-eslint": "^10",
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { Map } from "leaflet";
|
||||
|
||||
// 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
|
||||
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet"))
|
||||
.default;
|
||||
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
|
||||
|
||||
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 { LovelaceCardEditor } from "../../types";
|
||||
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 { processEditorEntities } from "../process-editor-entities";
|
||||
import { EntityConfig } from "../../entity-rows/types";
|
||||
@ -37,10 +37,10 @@ const cardConfigStruct = struct({
|
||||
|
||||
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
private _config?: MapCardConfig;
|
||||
private _configEntities?: EntityConfig[];
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
public setConfig(config: MapCardConfig): void {
|
||||
config = cardConfigStruct(config);
|
||||
this._config = config;
|
||||
this._configEntities = processEditorEntities(config.entities);
|
||||
@ -62,10 +62,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
return this._config!.default_zoom || NaN;
|
||||
}
|
||||
|
||||
get _entities(): string[] {
|
||||
return this._config!.entities || [];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
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"
|
||||
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@*":
|
||||
version "6.1.0"
|
||||
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"
|
||||
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":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece"
|
||||
|
Loading…
x
Reference in New Issue
Block a user