import 'https://unpkg.com/leaflet-ant-path@1.1.2/dist/leaflet-ant-path.js?module'; window.L.Icon.Default.imagePath = "/static/images/leaflet"; const latitude_offset = -0.00205; const longitude_offset = 0.00407; const fireEvent = (node, type, detail, options) => { options = options || {}; detail = detail === null || detail === undefined ? {} : detail; const event = new Event(type, { bubbles: options.bubbles === undefined ? true : options.bubbles, cancelable: Boolean(options.cancelable), composed: options.composed === undefined ? true : options.composed, }); event.detail = detail; node.dispatchEvent(event); return event; }; function setupLeafletMap(mapElement) { const map = window.L.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], 10); window.L.tileLayer( 'http://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', { subdomains: "1234", minZoom: 5, maxZoom: 18, } ).addTo(map); return map; } function isValidEntityId(entityId) { return /^(\w+)\.(\w+)$/.test(entityId); } function processConfigEntities(entities) { if (!entities || !Array.isArray(entities)) { throw new Error("Entities need to be an array"); } return entities.map((entityConf, index) => { if ( typeof entityConf === "object" && !Array.isArray(entityConf) && entityConf.type ) { return entityConf; } if (typeof entityConf === "string") { entityConf = { entity: entityConf }; } else if (typeof entityConf === "object" && !Array.isArray(entityConf)) { if (!entityConf.entity) { throw new Error( `Entity object at position ${index} is missing entity field.` ); } } else { throw new Error(`Invalid entity specified at position ${index}.`); } if (!isValidEntityId(entityConf.entity)) { throw new Error( `Invalid entity ID at position ${index}: ${entityConf.entity}` ); } return entityConf; }); } function computeStateDomain(stateObj) { return stateObj.entity_id.substr(0, stateObj.entity_id.indexOf(".")); } function computeObjectId(entityId) { return entityId.substr(entityId.indexOf(".") + 1); } function computeStateName(stateObj) { if (stateObj._entityDisplay === undefined) { stateObj._entityDisplay = stateObj.attributes.friendly_name || computeObjectId(stateObj.entity_id).replace(/_/g, " "); } return stateObj._entityDisplay; } function debounce(func, wait, immediate) { let timeout; return function (...args) { const context = this; const later = () => { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } class CNMapCard extends Polymer.Element { static get template() { return Polymer.html `
`; } 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); let now = new Date(); let startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString(); this._configEntities.forEach((entity) => { const entityId = entity.entity; this.hass.callApi('GET', `history/period/${startTime}?filter_entity_id=${entityId}`).then((data) => { data = data[0]; if (!data) return; let latlngs = []; data.forEach((log) => { if (log.attributes.latitude && log.attributes.longitude) { latlngs.push([log.attributes.latitude + latitude_offset, log.attributes.longitude + longitude_offset]); } }); let antPolyline = new L.Polyline.AntPath(latlngs); antPolyline.addTo(this._map); }) }); 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() { const zoom = this._config.default_zoom; if (this._mapItems.length === 0) { this._map.setView( new window.L.LatLng( this.hass.config.latitude, this.hass.config.longitude ), zoom || 10 ); return; } const bounds = new window.L.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 = []); this._configEntities.forEach((entity) => { const entityId = entity.entity; if (!(entityId in hass.states)) { return; } const stateObj = hass.states[entityId]; const title = computeStateName(stateObj); let { latitude, longitude, passive, icon, radius, entity_picture: entityPicture, gps_accuracy: gpsAccuracy, } = stateObj.attributes; if (!(latitude && longitude)) { return; } latitude += latitude_offset; longitude += longitude_offset; 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 = window.L.divIcon({ html: iconHTML, iconSize: [24, 24], className: "", }); // create market with the icon mapItems.push( window.L.marker([latitude, longitude], { icon: markerIcon, interactive: false, title: title, }).addTo(map) ); // create circle around it mapItems.push( window.L.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 = window.L.divIcon({ html: el.outerHTML, iconSize: [48, 48], className: "", }); // create market with the icon mapItems.push( window.L.marker([latitude, longitude], { icon: markerIcon, title: computeStateName(stateObj), }).addTo(map) ); // create circle around if entity has accuracy if (gpsAccuracy) { mapItems.push( window.L.circle([latitude, longitude], { interactive: false, color: "#0288D1", radius: gpsAccuracy, }).addTo(map) ); } }); } } customElements.define("cn-map-card", CNMapCard);