mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-09 02:16:35 +00:00
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.
This commit is contained in:
parent
594c1d6615
commit
e649d37c05
22
src/common/dom/setup-leaflet-map.js
Normal file
22
src/common/dom/setup-leaflet-map.js
Normal 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: '© <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,
|
element._filterRawConfig,
|
||||||
{ entities: entitiesList }
|
{ entities: entitiesList }
|
||||||
));
|
));
|
||||||
|
element.isPanel = this.isPanel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define('hui-entity-filter-card', HuiEntitiesCard);
|
customElements.define('hui-entity-filter-card', HuiEntitiesCard);
|
||||||
|
257
src/panels/lovelace/cards/hui-map-card.js
Normal file
257
src/panels/lovelace/cards/hui-map-card.js
Normal 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);
|
@ -7,6 +7,7 @@ import '../cards/hui-glance-card';
|
|||||||
import '../cards/hui-history-graph-card.js';
|
import '../cards/hui-history-graph-card.js';
|
||||||
import '../cards/hui-horizontal-stack-card.js';
|
import '../cards/hui-horizontal-stack-card.js';
|
||||||
import '../cards/hui-iframe-card.js';
|
import '../cards/hui-iframe-card.js';
|
||||||
|
import '../cards/hui-map-card.js';
|
||||||
import '../cards/hui-markdown-card.js';
|
import '../cards/hui-markdown-card.js';
|
||||||
import '../cards/hui-media-control-card.js';
|
import '../cards/hui-media-control-card.js';
|
||||||
import '../cards/hui-picture-card.js';
|
import '../cards/hui-picture-card.js';
|
||||||
@ -27,6 +28,7 @@ const CARD_TYPES = [
|
|||||||
'history-graph',
|
'history-graph',
|
||||||
'horizontal-stack',
|
'horizontal-stack',
|
||||||
'iframe',
|
'iframe',
|
||||||
|
'map',
|
||||||
'markdown',
|
'markdown',
|
||||||
'media-control',
|
'media-control',
|
||||||
'picture',
|
'picture',
|
||||||
|
@ -52,6 +52,13 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
}
|
}
|
||||||
#view {
|
#view {
|
||||||
min-height: calc(100vh - 112px);
|
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;
|
position: relative;
|
||||||
}
|
}
|
||||||
#view.tabs-hidden {
|
#view.tabs-hidden {
|
||||||
@ -207,6 +214,7 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
|||||||
const viewConfig = this.config.views[this._curView];
|
const viewConfig = this.config.views[this._curView];
|
||||||
if (viewConfig.panel) {
|
if (viewConfig.panel) {
|
||||||
view = createCardElement(viewConfig.cards[0]);
|
view = createCardElement(viewConfig.cards[0]);
|
||||||
|
view.isPanel = true;
|
||||||
} else {
|
} else {
|
||||||
view = document.createElement('hui-view');
|
view = document.createElement('hui-view');
|
||||||
view.config = viewConfig;
|
view.config = viewConfig;
|
||||||
|
@ -11,6 +11,7 @@ import './ha-entity-marker.js';
|
|||||||
import computeStateDomain from '../../common/entity/compute_state_domain.js';
|
import computeStateDomain from '../../common/entity/compute_state_domain.js';
|
||||||
import computeStateName from '../../common/entity/compute_state_name.js';
|
import computeStateName from '../../common/entity/compute_state_name.js';
|
||||||
import LocalizeMixin from '../../mixins/localize-mixin.js';
|
import LocalizeMixin from '../../mixins/localize-mixin.js';
|
||||||
|
import setupLeafletMap from '../../common/dom/setup-leaflet-map.js';
|
||||||
|
|
||||||
Leaflet.Icon.Default.imagePath = '/static/images/leaflet';
|
Leaflet.Icon.Default.imagePath = '/static/images/leaflet';
|
||||||
|
|
||||||
@ -57,21 +58,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
|
|||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
var map = this._map = Leaflet.map(this.$.map);
|
var map = this._map = setupLeafletMap(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);
|
|
||||||
|
|
||||||
this.drawEntities(this.hass);
|
this.drawEntities(this.hass);
|
||||||
|
|
||||||
@ -81,6 +68,12 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
|
|||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (this._map) {
|
||||||
|
this._map.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fitMap() {
|
fitMap() {
|
||||||
var bounds;
|
var bounds;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user