"use strict"; import L from 'leaflet'; import 'leaflet.markercluster'; // import CreateElement from 'dom-create-element-query-selector'; // We're using the git repo for now until an update is released, and rollup doesn't like that apparently import CreateElement from '../../node_modules/dom-create-element-query-selector/src/index.js'; import tabs from 'tabs'; import Emitter from 'event-emitter-es6'; import Config from './Config.mjs'; import DeviceReadingDisplay from './DeviceReadingDisplay.mjs'; import MarkerGenerator from './MarkerGenerator.mjs'; import GetFromUrl from './Helpers/GetFromUrl.mjs'; import { human_time_since } from './Helpers/DateHelper.mjs'; class LayerDeviceMarkers extends Emitter { constructor(in_map_manager, in_device_data) { super(); this.map_manager = in_map_manager; this.device_data = in_device_data; this.marker_generator = new MarkerGenerator(); // Create a new clustering layer this.layer = null; } /** * Performs initial setup of the device markers layer. * @return {Promise} A Promise that resolves when the initial setup is complete */ async setup() { await this.update_markers(Config.default_reading_type, "now"); } /** * Replaces all existing device markers (if any) with those for a given * reading type and datetime. * @param {string} reading_type The reading type to use to colour the markers. * @param {String|Date} [datetime="now"] The datetime of the data to use to colour the markers (default: the special keyword "now", which indicates to fetch data for the current time) * @return {Promise} A Promise that resolves when the markers have been updated. */ async update_markers(reading_type, datetime = "now") { // 1: Remove the old layer, if present // -------------------------------------------------------------------- if(this.layer !== null) this.map_manager.map.removeLayer(this.layer); // 2: Create a new layer // -------------------------------------------------------------------- this.layer = L.markerClusterGroup({ // this.layer = L.layerGroup({ zoomToBoundsOnClick: false }); // 3: Fetch the latest readings data // -------------------------------------------------------------------- let device_values = await this.map_manager.readings_data.fetch(reading_type, datetime); // 4: Add a marker for each device // -------------------------------------------------------------------- let has_data = 0, has_no_data = 0, total = 0; for (let device of this.device_data.devices) { // If the device doesn't have a location, we're not interested // FUTURE: We might be able to display mobile devices by adding additional logic here if(typeof device.latitude != "number" || typeof device.longitude != "number") continue; console.log(`[LayerDeviceMarkers] id =`, device.id, `name =`, device.name, `location: (`, device.latitude, `,`, device.longitude, `)`); if(device_values.has(device.id)) { this.add_device_marker(device, reading_type, device_values.get(device.id).value); console.log(`has value`); has_data++; } else { this.add_device_marker(device, "unknown"); console.log(`doesn't have value`); has_no_data++; } total++; } console.log(`[LayerDeviceMarkers] has_data`, has_data, `has_no_data`, has_no_data, `total`, total); // 5: Display the new layer // -------------------------------------------------------------------- this.map_manager.map.addLayer(this.layer); } /** * Adds a single device marker with a given reading type and value. * @param {Object} device The object representing the device to add. * @param {string} reading_type The reading type to use when colouring the marker. The special "unknown" reading type causes a default blue marker to be shown (regardless of the value passed). * @param {number} value The reading value to use when colouring the marker. */ add_device_marker(device, reading_type, value) { let icon; if(reading_type !== "unknown") { icon = L.divIcon({ className: "device-marker-icon", html: this.marker_generator.marker(value, reading_type), iconSize: L.point(17.418, 27.508), iconAnchor: L.point(8.71, 27.16) }); console.log(`[LayerDeviceMarkers/add_device_marker] got value`); } else { icon = L.divIcon({ className: "device-marker-icon icon-unknown", html: this.marker_generator.marker_default(), iconSize: L.point(17.418, 27.508), iconAnchor: L.point(8.71, 27.16) }); console.log(`[LayerDeviceMarkers/add_device_marker] unknown value`); } // Create the marker let marker = L.marker( L.latLng(device.latitude, device.longitude), { // See https://leafletjs.com/reference-1.4.0.html#marker title: `Device: ${device.name}`, autoPan: true, autoPanPadding: L.point(100, 100), icon } ); // Create the popup let popup = L.popup({ className: "popup-device", maxWidth: 640, autoPanPadding: L.point(100, 100) }).setContent("⌛ Loading..."); // TODO: Display a nice loading animation here marker.on("popupopen", this.marker_popup_open_handler.bind(this, device.id)); marker.bindPopup(popup); this.layer.addLayer(marker); } async marker_popup_open_handler(device_id, event) { if(typeof device_id !== "number") throw new Error("Error: Invalid device id passed."); console.info("Fetching device info for device", device_id); let device_info = JSON.parse(await GetFromUrl(`${Config.api_root}?action=device-info&device-id=${device_id}`)); device_info.last_seen = new Date(`${device_info.last_seen}+0000`); // Force parsing as UTC device_info.location = [ device_info.latitude, device_info.longitude ]; delete device_info.latitude; delete device_info.longitude; event.popup.setContent(this.render_device_info(device_info)); this.emit("marker-popup-opened"); } render_device_info(device_info) { let result = document.createDocumentFragment(); // ---------------------------------- result.appendChild(CreateElement("h2.device-name", `Device: ${device_info.name}` )); result.querySelector(".device-name").dataset.id = device_info.id; result.querySelector(".device-name").dataset.last_seen = device_info.last_seen; result.querySelector(".device-name").dataset.minutes_ago = human_time_since(device_info.last_seen); // ---------------------------------- // Select a tab by default window.location = "#tab-data"; let tabContainer = CreateElement("div.tab-container", CreateElement("ul.tabs", CreateElement("li", CreateElement("a.tab", "Info")), CreateElement("li", CreateElement("a.tab.active", "Data")) ), CreateElement("div.tab-panes", CreateElement("div.device-params.tab-pane") // The tab pane for the graph is added dynamically below ) ); result.appendChild(tabContainer); // ---------------------------------- let params_container = tabContainer.querySelector(".device-params"); let info_list = []; for(let property in device_info) { // Filter out properties we're handling specially if(["id", "name", "other"].includes(property)) continue; // Ensure the property is a string - giving special handling to // some property values let value = device_info[property]; if(typeof value == "undefined" || value === null) value = "(not specified)"; // If the value isn't a string, but is still 'truthy' if(typeof value != "string") { switch(property) { case "location": value = `(${value[0]}, ${value[1]})`; break; case "sensors": value = CreateElement("table", ...value.map((sensor) => CreateElement("tr", CreateElement("td", sensor.type), CreateElement("td", sensor.description) )) ); break; default: value = value.toString(); break; } } info_list.push(CreateElement( "tr.device-property", CreateElement("th.name", property.split("_").map((word) => word[0].toUpperCase()+word.slice(1)).join(" ")), CreateElement("td.value", value) )); } params_container.appendChild( CreateElement("table.device-property-table", ...info_list) ); params_container.appendChild(CreateElement("p.device-notes", CreateElement("em", device_info.other || "") )); // ---------------------------------- let chart_device_data = new DeviceReadingDisplay(Config, device_info.id); chart_device_data.setup("PM25").then(() => console.info("[layer/markers] Device chart setup complete!") ); chart_device_data.display.classList.add("tab-pane", "active"); tabContainer.querySelector(".tab-panes").appendChild(chart_device_data.display); tabs(tabContainer); // ---------------------------------- return result; } } export default LayerDeviceMarkers;