Love: Added map card (#1412)
* Added map card * Prefer arrow functions. * Fix lint errors. * Extract Leaflet setup. Debounce events. Cleanup. * Cleanup. * Add disconnectedCallback. More cleanup.pull/1420/head
parent
594c1d6615
commit
e649d37c05
|
@ -0,0 +1,22 @@
|
|||
import Leaflet from 'leaflet';
|
||||
|
||||
// Sets up a Leaflet map on the provided DOM element
|
||||
export default function setupLeafletMap(mapElement) {
|
||||
const map = Leaflet.map(mapElement);
|
||||
const style = document.createElement('link');
|
||||
style.setAttribute('href', '/static/images/leaflet/leaflet.css');
|
||||
style.setAttribute('rel', 'stylesheet');
|
||||
mapElement.parentNode.appendChild(style);
|
||||
map.setView([51.505, -0.09], 13);
|
||||
Leaflet.tileLayer(
|
||||
`https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}${Leaflet.Browser.retina ? '@2x.png' : '.png'}`,
|
||||
{
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
return map;
|
||||
}
|
|
@ -68,6 +68,7 @@ class HuiEntitiesCard extends PolymerElement {
|
|||
element._filterRawConfig,
|
||||
{ entities: entitiesList }
|
||||
));
|
||||
element.isPanel = this.isPanel;
|
||||
}
|
||||
}
|
||||
customElements.define('hui-entity-filter-card', HuiEntitiesCard);
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
import Leaflet from 'leaflet';
|
||||
|
||||
import '../../map/ha-entity-marker.js';
|
||||
|
||||
import setupLeafletMap from '../../../common/dom/setup-leaflet-map.js';
|
||||
import processConfigEntities from '../common/process-config-entities.js';
|
||||
import computeStateDomain from '../../../common/entity/compute_state_domain.js';
|
||||
import computeStateName from '../../../common/entity/compute_state_name.js';
|
||||
import debounce from '../../../common/util/debounce.js';
|
||||
|
||||
Leaflet.Icon.Default.imagePath = '/static/images/leaflet';
|
||||
|
||||
class HuiMapCard extends PolymerElement {
|
||||
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;
|
||||
}
|
||||
|
||||
#map {
|
||||
z-index: 0;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([is-panel]) #root {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card id="card" header="[[_config.title]]">
|
||||
<div id="root">
|
||||
<div id="map"></div>
|
||||
</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;
|
||||
}
|
||||
|
||||
this.$.root.style.paddingTop = this._config.aspect_ratio || '100%';
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
if (!config) {
|
||||
throw new Error('Error in card configuration.');
|
||||
}
|
||||
|
||||
this._configEntities = processConfigEntities(config.entities);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
let ar = this._config.aspect_ratio || '100%';
|
||||
ar = ar.substr(0, ar.length - 1);
|
||||
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._map = setupLeafletMap(this.$.map);
|
||||
this._drawEntities(this.hass);
|
||||
|
||||
setTimeout(() => {
|
||||
this._resetMap();
|
||||
this._fitMap();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
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() {
|
||||
if (this._mapItems.length === 0) {
|
||||
this._map.setView(
|
||||
new Leaflet.LatLng(this.hass.config.core.latitude, this.hass.config.core.longitude),
|
||||
14
|
||||
);
|
||||
} else {
|
||||
const bounds = new Leaflet.latLngBounds(this._mapItems.map(item => item.getLatLng()));
|
||||
this._map.fitBounds(bounds.pad(0.5));
|
||||
}
|
||||
}
|
||||
|
||||
_drawEntities(hass) {
|
||||
const map = this._map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._mapItems) {
|
||||
this._mapItems.forEach(marker => marker.remove());
|
||||
}
|
||||
const mapItems = this._mapItems = [];
|
||||
|
||||
this._configEntities.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 = Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className: '',
|
||||
});
|
||||
|
||||
// create market with the icon
|
||||
mapItems.push(Leaflet.marker([latitude, longitude], {
|
||||
icon: markerIcon,
|
||||
interactive: false,
|
||||
title: title,
|
||||
}).addTo(map));
|
||||
|
||||
// create circle around it
|
||||
mapItems.push(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 || '');
|
||||
|
||||
/* 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 = Leaflet.divIcon({
|
||||
html: el.outerHTML,
|
||||
iconSize: [48, 48],
|
||||
className: '',
|
||||
});
|
||||
|
||||
// create market with the icon
|
||||
mapItems.push(Leaflet.marker([latitude, longitude], {
|
||||
icon: markerIcon,
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hui-map-card', HuiMapCard);
|
|
@ -7,6 +7,7 @@ import '../cards/hui-glance-card';
|
|||
import '../cards/hui-history-graph-card.js';
|
||||
import '../cards/hui-horizontal-stack-card.js';
|
||||
import '../cards/hui-iframe-card.js';
|
||||
import '../cards/hui-map-card.js';
|
||||
import '../cards/hui-markdown-card.js';
|
||||
import '../cards/hui-media-control-card.js';
|
||||
import '../cards/hui-picture-card.js';
|
||||
|
@ -27,6 +28,7 @@ const CARD_TYPES = [
|
|||
'history-graph',
|
||||
'horizontal-stack',
|
||||
'iframe',
|
||||
'map',
|
||||
'markdown',
|
||||
'media-control',
|
||||
'picture',
|
||||
|
|
|
@ -52,6 +52,13 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||
}
|
||||
#view {
|
||||
min-height: calc(100vh - 112px);
|
||||
/**
|
||||
* Since we only set min-height, if child nodes need percentage
|
||||
* heights they must use absolute positioning so we need relative
|
||||
* positioning here.
|
||||
*
|
||||
* https://www.w3.org/TR/CSS2/visudet.html#the-height-property
|
||||
*/
|
||||
position: relative;
|
||||
}
|
||||
#view.tabs-hidden {
|
||||
|
@ -207,6 +214,7 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||
const viewConfig = this.config.views[this._curView];
|
||||
if (viewConfig.panel) {
|
||||
view = createCardElement(viewConfig.cards[0]);
|
||||
view.isPanel = true;
|
||||
} else {
|
||||
view = document.createElement('hui-view');
|
||||
view.config = viewConfig;
|
||||
|
|
|
@ -11,6 +11,7 @@ import './ha-entity-marker.js';
|
|||
import computeStateDomain from '../../common/entity/compute_state_domain.js';
|
||||
import computeStateName from '../../common/entity/compute_state_name.js';
|
||||
import LocalizeMixin from '../../mixins/localize-mixin.js';
|
||||
import setupLeafletMap from '../../common/dom/setup-leaflet-map.js';
|
||||
|
||||
Leaflet.Icon.Default.imagePath = '/static/images/leaflet';
|
||||
|
||||
|
@ -57,21 +58,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
|
|||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
var map = this._map = Leaflet.map(this.$.map);
|
||||
var style = document.createElement('link');
|
||||
style.setAttribute('href', '/static/images/leaflet/leaflet.css');
|
||||
style.setAttribute('rel', 'stylesheet');
|
||||
this.$.map.parentNode.appendChild(style);
|
||||
map.setView([51.505, -0.09], 13);
|
||||
Leaflet.tileLayer(
|
||||
`https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}${Leaflet.Browser.retina ? '@2x.png' : '.png'}`,
|
||||
{
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
}
|
||||
).addTo(map);
|
||||
var map = this._map = setupLeafletMap(this.$.map);
|
||||
|
||||
this.drawEntities(this.hass);
|
||||
|
||||
|
@ -81,6 +68,12 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
|
|||
}, 1);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this._map) {
|
||||
this._map.remove();
|
||||
}
|
||||
}
|
||||
|
||||
fitMap() {
|
||||
var bounds;
|
||||
|
||||
|
|
Loading…
Reference in New Issue