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
Jerad Meisner 2018-07-07 02:36:01 -07:00 committed by Paulus Schoutsen
parent 594c1d6615
commit e649d37c05
6 changed files with 298 additions and 15 deletions

View File

@ -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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
minZoom: 0,
maxZoom: 20,
}
).addTo(map);
return map;
}

View File

@ -68,6 +68,7 @@ class HuiEntitiesCard extends PolymerElement {
element._filterRawConfig,
{ entities: entitiesList }
));
element.isPanel = this.isPanel;
}
}
customElements.define('hui-entity-filter-card', HuiEntitiesCard);

View File

@ -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);

View File

@ -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',

View File

@ -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;

View File

@ -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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <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;