Merge branch 'dev'

This commit is contained in:
Starbeamrainbowlabs 2021-03-04 18:35:17 +00:00
commit 307534f112
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
22 changed files with 10380 additions and 4881 deletions

View file

@ -5,9 +5,17 @@ The data displayed has been produced by low-cost devices developed by [Connected
This is the changelog for the air quality web interface and its associated HTTP API. This is the changelog for the air quality web interface and its associated HTTP API.
- `[API]` refers to changes to the [HTTP API](https://aq.connectedhumber.org/__nightdocs/05-API-Docs.html). - `[API]` refers to changes to the [HTTP API](https://sensors.connectedhumber.org/__nightdocs/05-API-Docs.html).
- `[Code]` refers to internal changes to the code that have no direct impact on the web interface or the HTTP API, but are significant enough to warrant note. - `[Code]` refers to internal changes to the code that have no direct impact on the web interface or the HTTP API, but are significant enough to warrant note.
- `[Docs]` refers to changes to the [documentation](https://aq.connectedhumber.org/__nightdocs/00-Welcome.html). - `[Docs]` refers to changes to the [documentation](https://sensors.connectedhumber.org/__nightdocs/00-Welcome.html).
## v0.14
- Remove the heatmap background overlay :-(
- Colour device markers based on the latest reading
- Properly handle switching to a reading type that doesn't have any readings at all
- [API] Optimise `list-reading-types` action, which necessitated a database schema update and the removal of the `count` property on returned objects
- [API] Drastically optimise `device-info` by removing a redundant JOIN
## v0.13.6 ## v0.13.6

6
build
View file

@ -28,7 +28,7 @@ cache_dir="./.cache";
build_output_folder="./app"; build_output_folder="./app";
# Database settings for ssh port forwarding task # Database settings for ssh port forwarding task
database_host="db.connectedhumber.org"; database_host="ch-kimsufi";
database_name="aq_db"; database_name="aq_db";
database_user="www-data"; database_user="www-data";
@ -277,6 +277,10 @@ task_docs() {
# ██ ██ # ██ ██
# ██████ ██ # ██████ ██
task_ci() { task_ci() {
if [[ -z "${TERM}" ]]; then
export TERM=xterm-256color;
fi
tasks_run setup setup-dev tasks_run setup setup-dev
NODE_ENV="production" tasks_run client docs archive; NODE_ENV="production" tasks_run client docs archive;

View file

@ -10,6 +10,8 @@ export default {
// The default zoom level to use when loading the page. // The default zoom level to use when loading the page.
default_zoom: 12, default_zoom: 12,
default_reading_type: "PM25",
// The number of minutes to round dates to when making time-based HTTP API requests. // The number of minutes to round dates to when making time-based HTTP API requests.
// Very useful for improving cache hit rates. // Very useful for improving cache hit rates.
date_rounding_interval: 6, date_rounding_interval: 6,

View file

@ -3,8 +3,9 @@
import { set_hidpi_canvas, pixel_ratio } from './Helpers/Canvas.mjs'; import { set_hidpi_canvas, pixel_ratio } from './Helpers/Canvas.mjs';
import { RenderGradient } from './Helpers/GradientHelpers.mjs'; import { RenderGradient } from './Helpers/GradientHelpers.mjs';
import gradients from './Gradients.mjs';
class Guage { class Gauge {
constructor(in_canvas) { constructor(in_canvas) {
this.canvas = in_canvas; this.canvas = in_canvas;
@ -13,9 +14,30 @@ class Guage {
this.context = this.canvas.getContext("2d"); this.context = this.canvas.getContext("2d");
} }
/**
* Sets the reading type to display on this gauge.
* Pulls gradient definitions from Gradients.mjs.
* @param {string} new_reading_type The reading type code to display.
*/
set_reading_type(new_reading_type) {
if(typeof gradients[new_reading_type] == "undefined") {
console.warn(`[Gauge] Warning: Unknown reading type ${new_reading_type} (defaulting to "unknown")`);
new_reading_type = "unknown";
}
this.set_spec(gradients[new_reading_type]);
}
/**
* Sets the gradient spec to display.
* Automatically re-renders the gauge for convenience.
* @param {Object} arg The gradient spec to set display.
*/
set_spec({ gradient: spec, max }) { set_spec({ gradient: spec, max }) {
this.spec = spec; this.spec = spec;
this.max = max; this.max = max;
this.render();
} }
render() { render() {
@ -89,4 +111,4 @@ class Guage {
} }
} }
export default Guage; export default Gauge;

View file

@ -1,5 +1,6 @@
"use strict"; "use strict";
import chroma from 'chroma-js';
var PM25 = { var PM25 = {
/* /*
@ -60,7 +61,7 @@ var PM10 = {
var humidity = { var humidity = {
max: 100, max: 100,
gradient: { gradient: {
"0": "hsla(176, 77%, 40%, 0)", "0": "hsla(52, 36%, 61%, 0.5)",
"50": "hsl(176, 77%, 40%)", "50": "hsl(176, 77%, 40%)",
"100": "blue" "100": "blue"
} }
@ -103,6 +104,11 @@ var specs = {
unknown unknown
}; };
for(let spec of Object.values(specs)) {
spec.chroma = chroma.scale(Object.values(spec.gradient))
.domain(Object.keys(spec.gradient));
}
export default specs; export default specs;
export { export {
PM10, PM25, PM10, PM25,

View file

@ -10,45 +10,128 @@ import Emitter from 'event-emitter-es6';
import Config from './Config.mjs'; import Config from './Config.mjs';
import DeviceReadingDisplay from './DeviceReadingDisplay.mjs'; import DeviceReadingDisplay from './DeviceReadingDisplay.mjs';
import MarkerGenerator from './MarkerGenerator.mjs';
import GetFromUrl from './Helpers/GetFromUrl.mjs'; import GetFromUrl from './Helpers/GetFromUrl.mjs';
import { human_time_since } from './Helpers/DateHelper.mjs'; import { human_time_since } from './Helpers/DateHelper.mjs';
class LayerDeviceMarkers extends Emitter { class LayerDeviceMarkers extends Emitter {
constructor(in_map, in_device_data) { constructor(in_map_manager, in_device_data) {
super(); super();
this.map = in_map; this.map_manager = in_map_manager;
this.device_data = in_device_data; this.device_data = in_device_data;
this.marker_generator = new MarkerGenerator();
// Create a new clustering layer // 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.markerClusterGroup({
// this.layer = L.layerGroup({ // this.layer = L.layerGroup({
zoomToBoundsOnClick: false zoomToBoundsOnClick: false
}); });
// 3: Fetch the latest readings data
// --------------------------------------------------------------------
let device_values;
try {
device_values = await this.map_manager.readings_data.fetch(reading_type, datetime);
}
catch(error) {
alert(error);
device_values = new Map();
} }
async setup() { // 4: Add a marker for each device
// Add a marker for each device // --------------------------------------------------------------------
let has_data = 0, has_no_data = 0, total = 0;
for (let device of this.device_data.devices) { for (let device of this.device_data.devices) {
// If the device doesn't have a location, we're not interested // 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 // FUTURE: We might be able to display mobile devices by adding additional logic here
if(typeof device.latitude != "number" || typeof device.longitude != "number") if(typeof device.latitude != "number" || typeof device.longitude != "number")
continue; continue;
this.add_device_marker(device);
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++;
} }
// Display this layer total++;
this.map.addLayer(this.layer); }
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`);
} }
add_device_marker(device) {
// Create the marker // Create the marker
let marker = L.marker( let marker = L.marker(
L.latLng(device.latitude, device.longitude), L.latLng(device.latitude, device.longitude),
{ // See https://leafletjs.com/reference-1.4.0.html#marker { // See https://leafletjs.com/reference-1.4.0.html#marker
title: `Device: ${device.name}`, title: `Device: ${device.name}`,
autoPan: true, autoPan: true,
autoPanPadding: L.point(100, 100) autoPanPadding: L.point(100, 100),
icon
} }
); );
// Create the popup // Create the popup

View file

@ -8,15 +8,14 @@ import 'leaflet-easyprint';
import Config from './Config.mjs'; import Config from './Config.mjs';
import LayerDeviceMarkers from './LayerDeviceMarkers.mjs'; import LayerDeviceMarkers from './LayerDeviceMarkers.mjs';
import VoronoiManager from './Overlay/VoronoiManager.mjs';
// import LayerHeatmap from './LayerHeatmap.mjs';
// import LayerHeatmapGlue from './LayerHeatmapGlue.mjs';
import DeviceData from './DeviceData.mjs'; import DeviceData from './DeviceData.mjs';
import ReadingsData from './ReadingsData.mjs';
import UI from './UI.mjs'; import UI from './UI.mjs';
class MapManager { class MapManager {
constructor() { constructor() {
console.log(Config); console.log(Config);
this.readings_data = new ReadingsData();
} }
async setup() { async setup() {
@ -55,65 +54,14 @@ class MapManager {
Promise.all([ Promise.all([
this.setup_device_markers.bind(this)() this.setup_device_markers.bind(this)()
.then(() => console.info("[map] Device markers loaded successfully.")), .then(() => console.info("[map] Device markers loaded successfully.")),
this.setup_overlay.bind(this)()
.then(this.setup_layer_control.bind(this))
]).then(() => document.querySelector("main").classList.remove("working-visual")); ]).then(() => document.querySelector("main").classList.remove("working-visual"));
// Add the heatmap
// console.info("[map] Loading heatmap....");
// this.setup_heatmap()
// .then(() => console.info("[map] Heatmap loaded successfully."))
// // ...and the time dimension
// .then(this.setup_time_dimension.bind(this))
// .then(() => console.info("[map] Time dimension initialised."));
} }
async setup_overlay() { // NOTE: We tried leaflet-time-dimension for changing the time displayed, but it didn't work out
this.overlay = new VoronoiManager(this.device_data, this.map);
await this.overlay.setup();
// No need to do this here, as it does it automatically
// await this.overlay.set_data(new Date(), "PM25");
}
setup_time_dimension() {
// FUTURE: Replace leaflet-time-dimension with our own solution that's got a better ui & saner API?
this.layer_time = new L.TimeDimension({
period: "PT1H", // 1 hour
timeInterval: `2019-01-01T12:00:00Z/${new Date().toISOString()}`
});
//this.layer_time.on("timeloading", console.log.bind(null, "timeloading"));
this.layer_time_player = new L.TimeDimension.Player({
transitionTime: 500,
loop: false,
startOver: true,
buffer: 10 // Default: 5
}, this.layer_time);
this.layer_time_control = new L.Control.TimeDimension({
player: this.layer_time_player,
timeDimension: this.layer_time,
position: "bottomright",
autoplay: false,
minSpeed: 1,
speedStep: 0.25,
maxSpeed: 15,
timeSliderDragUpdate: false
});
this.map.addControl(this.layer_time_control);
// Create the time dimension <---> heatmap glue object
this.layer_heatmap_glue = new LayerHeatmapGlue(
this.layer_time,
this.heatmap
);
this.layer_heatmap_glue.attachTo(this.map);
}
async setup_device_markers() { async setup_device_markers() {
this.device_markers = new LayerDeviceMarkers(this.map, this.device_data); this.device_markers = new LayerDeviceMarkers(this, this.device_data);
await this.device_markers.setup(); await this.device_markers.setup();
} }
@ -149,8 +97,7 @@ class MapManager {
"OpenStreetMap": this.layer_openstreet "OpenStreetMap": this.layer_openstreet
}, { // Overlay(s) }, { // Overlay(s)
"Devices": this.device_markers.layer, "Devices": this.device_markers.layer,
// FUTURE: Have 1 heatmap layer per reading type? // "Heatmap": this.overlay.layer
"Heatmap": this.overlay.layer
}, { // Options }, { // Options
}); });

View file

@ -0,0 +1,46 @@
"use strict";
import chroma from 'chroma-js';
import marker_svg from '../marker-embed.svg';
import gradients from './Gradients.mjs';
class MarkerGenerator {
constructor() {
}
marker_default() {
return marker_svg.replace("{{colour_a}}", "#1975c8")
.replace("{{colour_b}}", "#5ea6d5")
.replace("{{colour_dark}}", "#2e6d99");
}
marker(value, type) {
let col = this.get_colour(value, type);
let result = marker_svg.replace(/\{\{colour_a\}\}/g, col.darken(0.25).hex())
.replace(/\{\{colour_b\}\}/g, col.brighten(0.25).hex())
.replace(/\{\{colour_dark\}\}/g, col.darken(1))
.replace(/\{\{id_grad\}\}/g, `marker-grad-${btoa(`${type}-${value}`).replace(/\+/g, "-").replace(/\//g, "_")}`);
return result;
}
/**
* Fetches the colour for a given value of a given type.
* Currently a gradient is not used - i.e. a value is coloured according to
* the last threshold it crossed in the associated gradient definition.
* TODO: Calculate the gradient instead.
* @param {number} value The value to calculate the colour for.
* @param {string} type The type of measurement value we're working with. Determines the gradient to use.
* @return {chroma} The calculated colour.
*/
get_colour(value, type) {
if(typeof gradients[type] == "undefined") {
console.warn(`[MarkerGenerator] Warning: Unknown gradient type '${type}', using unknown instead.`);
type = "unknown";
}
return gradients[type].chroma(value);
}
}
export default MarkerGenerator;

View file

@ -7,7 +7,7 @@ import Config from '../Config.mjs';
import VoronoiOverlay from './VoronoiOverlay.mjs'; import VoronoiOverlay from './VoronoiOverlay.mjs';
import VoronoiCell from './VoronoiCell.mjs'; import VoronoiCell from './VoronoiCell.mjs';
import Guage from '../Guage.mjs'; import Gauge from '../Gauge.mjs';
import Specs from './OverlaySpecs.mjs'; import Specs from './OverlaySpecs.mjs';
import Vector2 from '../Helpers/Vector2.mjs'; import Vector2 from '../Helpers/Vector2.mjs';
@ -37,7 +37,7 @@ class VoronoiManager {
} }
setup_guage() { setup_guage() {
this.guage = new Guage(document.getElementById("canvas-guage")); this.guage = new Gauge(document.getElementById("canvas-guage"));
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------

View file

@ -0,0 +1,38 @@
"use strict";
import Config from './Config.mjs';
import GetFromUrl from './Helpers/GetFromUrl.mjs';
class ReadingsData {
constructor() {
}
/**
* Fetches the data from all currently active devices at a given datetime
* and for a given reading type.
* @param {string} reading_type The reading type to fetch data for.
* @param {String|Date} [datetime="now"] The datetime to fetch the data for.
* @return {Promise<Map>} A promise that resolves to a Map keyed by device IDs that contains the data objects returned by the fetch-data API action.
*/
async fetch(reading_type, datetime = "now") {
if(datetime instanceof Date)
datetime = datetime.toISOString();
// TODO: memoize this
let data = await this.__make_request(reading_type, datetime);
let result = new Map();
for(let item of data)
result.set(item.device_id, item);
return result;
}
async __make_request(reading_type, datetime) {
return JSON.parse(await GetFromUrl(
`${Config.api_root}?action=fetch-data&datetime=${encodeURIComponent(datetime)}&reading_type=${encodeURIComponent(reading_type)}`
));
}
}
export default ReadingsData;

View file

@ -5,6 +5,7 @@ import NanoModal from 'nanomodal';
import Config from './Config.mjs'; import Config from './Config.mjs';
import GetFromUrl from './Helpers/GetFromUrl.mjs'; import GetFromUrl from './Helpers/GetFromUrl.mjs';
import Gauge from './Gauge.mjs';
// import Tour from './Tour.mjs'; // import Tour from './Tour.mjs';
@ -40,6 +41,9 @@ class UI {
this.map_manager = in_map_manager; this.map_manager = in_map_manager;
this.ui_panel = new SmartSettings("Settings"); this.ui_panel = new SmartSettings("Settings");
this.gauge = new Gauge(document.getElementById("canvas-guage"));
this.gauge.set_reading_type(Config.default_reading_type);
// this.ui_panel.watch((event) => console.log(event)); // this.ui_panel.watch((event) => console.log(event));
this.tour_enabled = false; this.tour_enabled = false;
@ -63,7 +67,8 @@ class UI {
document.querySelector("main").classList.add("working-visual"); document.querySelector("main").classList.add("working-visual");
await this.map_manager.overlay.update_reading_type(new_type); await this.map_manager.device_markers.update_markers(new_type);
this.gauge.set_reading_type(new_type);
document.querySelector("main").classList.remove("working-visual"); document.querySelector("main").classList.remove("working-visual");
}).bind(this) }).bind(this)
}, },
@ -89,7 +94,7 @@ class UI {
}) })
} }
]); ]);
this.ui_panel.setIndex("Reading Type", this.reading_types.findIndex((type) => type.short_descr == "PM25")); this.ui_panel.setIndex("Reading Type", this.reading_types.findIndex((type) => type.short_descr == Config.default_reading_type));
if(this.tour_enabled) await this.tour.run_once(); if(this.tour_enabled) await this.tour.run_once();
} }

View file

@ -0,0 +1 @@
<svg width="21.7725" height="34.385" version="1.1" viewBox="0 0 4.6085 7.2783" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient id="{{id_grad}}" x1="25.492" x2="25.557" y1="65.023" y2="44.643" gradientTransform="matrix(.33982 0 0 .33982 -6.3929 -14.983)" gradientUnits="userSpaceOnUse"><stop stop-color="{{colour_a}}" offset="0"/><stop stop-color="{{colour_b}}" offset="1"/></linearGradient><clipPath id="c"><path d="m40.048 68.631c-3.078-6.038-3.783-7.984-3.538-9.768 0.439-3.208 3.281-5.696 6.505-5.696 2.457 0 4.418 1.149 5.704 3.343 0.913 1.559 1.082 3.161 0.533 5.069-0.455 1.582-5.895 12.541-6.226 12.541-0.098 0-1.439-2.47-2.978-5.49zm4.445-7.594c1.168-1.168 0.58-3.06-1.079-3.477-1.301-0.326-2.675 1.155-2.368 2.554 0.345 1.572 2.28 2.09 3.448 0.922z" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".321" stroke-width="1.39"/></clipPath></defs><path d="m0.086822 2.4041c-0.039079-1.2465 1.1491-2.3381 2.217-2.3254 1.6322 0.19404 2.5079 1.4466 2.1195 2.8589-0.15462 0.5376-2.0032 4.2617-2.1157 4.2617-0.66809-1.378-2.0406-3.2681-2.2211-4.7952zm2.7196 0.34866c0.79076-1.4035-1.3749-1.4731-1.1717-0.31331 0.11724 0.5342 0.77479 0.71022 1.1717 0.31331z" fill="url(#{{id_grad}})" stroke-width=".33982"/><path transform="matrix(.33982 0 0 .33982 -12.313 -17.988)" d="m40.048 68.631c-3.078-6.038-3.783-7.984-3.538-9.768 0.439-3.208 3.281-5.696 6.505-5.696 2.457 0 4.418 1.149 5.704 3.343 0.913 1.559 1.082 3.161 0.533 5.069-0.455 1.582-5.895 12.541-6.226 12.541-0.098 0-1.439-2.47-2.978-5.49zm4.445-7.594c1.168-1.168 0.58-3.06-1.079-3.477-1.301-0.326-2.675 1.155-2.368 2.554 0.345 1.572 2.28 2.09 3.448 0.922z" clip-path="url(#c)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".321" stroke-width="1.39"/><path d="m1.2962 5.334c-1.046-2.0518-1.2855-2.7131-1.2023-3.3194 0.14918-1.0901 1.115-1.9356 2.2105-1.9356 0.83494 0 1.5013 0.39045 1.9383 1.136 0.31026 0.52978 0.36769 1.0742 0.18112 1.7225-0.15462 0.5376-2.0032 4.2617-2.1157 4.2617-0.033302 0-0.489-0.83936-1.012-1.8656zm1.5105-2.5806c0.39691-0.39691 0.1971-1.0398-0.36667-1.1816-0.44211-0.11078-0.90902 0.39249-0.80469 0.8679 0.11724 0.5342 0.77479 0.71022 1.1717 0.31331z" fill="none" stroke="{{colour_dark}}" stroke-opacity=".956" stroke-width=".15802"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
@ -15,7 +13,7 @@
viewBox="0 0 13.562857 21.42" viewBox="0 0 13.562857 21.42"
version="1.1" version="1.1"
id="svg8" id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="marker.svg"> sodipodi:docname="marker.svg">
<defs <defs
id="defs2"> id="defs2">
@ -68,11 +66,12 @@
fit-margin-left="0" fit-margin-left="0"
fit-margin-right="0" fit-margin-right="0"
fit-margin-bottom="0" fit-margin-bottom="0"
inkscape:window-width="1825" inkscape:window-width="1831"
inkscape:window-height="1047" inkscape:window-height="1047"
inkscape:window-x="95" inkscape:window-x="89"
inkscape:window-y="33" inkscape:window-y="33"
inkscape:window-maximized="1" /> inkscape:window-maximized="1"
inkscape:document-rotation="0" />
<metadata <metadata
id="metadata5"> id="metadata5">
<rdf:RDF> <rdf:RDF>
@ -91,10 +90,11 @@
id="layer1" id="layer1"
transform="translate(-27.03016,-48.232836)"> transform="translate(-27.03016,-48.232836)">
<path <path
style="fill:url(#linearGradient844);fill-opacity:1;stroke:none;stroke-width:0.46500003;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.95686275" style="fill:url(#linearGradient844);fill-opacity:1;stroke:none;stroke-width:0.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.956863"
d="m 30.844224,63.930232 c -3.078474,-6.038878 -3.783282,-7.984375 -3.538785,-9.76818 0.439793,-3.20864 3.281231,-5.696716 6.505776,-5.696716 2.457647,0 4.418161,1.149131 5.704071,3.343371 0.913856,1.559378 1.082523,3.161787 0.533646,5.069884 -0.455307,1.582814 -5.895833,12.541745 -6.226308,12.541745 -0.09882,0 -1.439103,-2.470547 -2.9784,-5.490104 z m 4.445635,-7.594744 c 1.16819,-1.168191 0.580604,-3.060887 -1.079611,-3.477574 -1.30134,-0.326616 -2.675892,1.155891 -2.368663,2.554695 0.345401,1.572603 2.280617,2.090534 3.448274,0.922879 z" d="m 27.286537,55.308491 c -0.115852,-3.668092 3.911381,-6.767612 6.524678,-6.843155 4.803399,0.571307 7.380181,4.257602 6.237717,8.413255 -0.455307,1.582814 -5.895833,12.541745 -6.226308,12.541745 -1.966961,-4.055469 -6.005853,-9.61727 -6.536087,-14.111845 z m 8.003322,1.026997 c 2.327368,-4.130254 -4.046386,-4.335875 -3.448274,-0.922879 0.345401,1.572603 2.280617,2.090534 3.448274,0.922879 z"
id="path842" id="path842"
inkscape:connector-curvature="0" /> inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<path <path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.39091456;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.32129965" style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.39091456;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.32129965"
d="m 40.048125,68.631745 c -3.078476,-6.03888 -3.783284,-7.98438 -3.538787,-9.76818 0.439793,-3.20864 3.281227,-5.69672 6.505777,-5.69672 2.45765,0 4.41816,1.14913 5.70407,3.34337 0.91386,1.55938 1.08252,3.16179 0.53365,5.06989 -0.45531,1.58281 -5.89584,12.54174 -6.22631,12.54174 -0.0988,0 -1.43911,-2.47055 -2.9784,-5.4901 z m 4.44563,-7.59475 c 1.16819,-1.16819 0.58061,-3.06088 -1.07961,-3.47757 -1.30134,-0.32662 -2.67589,1.15589 -2.36866,2.55469 0.3454,1.57261 2.28062,2.09054 3.44827,0.92288 z" d="m 40.048125,68.631745 c -3.078476,-6.03888 -3.783284,-7.98438 -3.538787,-9.76818 0.439793,-3.20864 3.281227,-5.69672 6.505777,-5.69672 2.45765,0 4.41816,1.14913 5.70407,3.34337 0.91386,1.55938 1.08252,3.16179 0.53365,5.06989 -0.45531,1.58281 -5.89584,12.54174 -6.22631,12.54174 -0.0988,0 -1.43911,-2.47055 -2.9784,-5.4901 z m 4.44563,-7.59475 c 1.16819,-1.16819 0.58061,-3.06088 -1.07961,-3.47757 -1.30134,-0.32662 -2.67589,1.15589 -2.36866,2.55469 0.3454,1.57261 2.28062,2.09054 3.44827,0.92288 z"

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -37,7 +37,6 @@ class ListReadingTypes implements IAction {
// 1: Parse & validate parameters // 1: Parse & validate parameters
$device_id = !empty($_GET["device-id"]) ? intval($_GET["device-id"]) : null; $device_id = !empty($_GET["device-id"]) ? intval($_GET["device-id"]) : null;
$days_to_analyse = intval($_GET["days"] ?? "1") - 1;
$format = $_GET["format"] ?? "json"; $format = $_GET["format"] ?? "json";
if(!in_array($format, ["json", "csv"])) { if(!in_array($format, ["json", "csv"])) {
@ -54,7 +53,7 @@ class ListReadingTypes implements IAction {
if(!is_int($device_id)) if(!is_int($device_id))
$data = $this->types_repo->get_all_types(); $data = $this->types_repo->get_all_types();
else else
$data = $this->types_repo->get_types_by_device($device_id, $days_to_analyse); $data = $this->types_repo->get_types_by_device($device_id);
$this->perfcounter->end("sql"); $this->perfcounter->end("sql");
// 1.5: Validate data from database // 1.5: Validate data from database

View file

@ -33,8 +33,7 @@ interface IMeasurementTypeRepository {
/** /**
* Gets the all the measurement types ever reported by a given device id. * Gets the all the measurement types ever reported by a given device id.
* @param int $device_id The id of the device to get the reading types for. * @param int $device_id The id of the device to get the reading types for.
* @param int $day_to_analyse The number of days worth fo data to analyse. Defaults to -1, which is everything. Set to 0 for readings recorded in the last 24 hours, which is much faster.
* @return string[] A list of device ids. * @return string[] A list of device ids.
*/ */
public function get_types_by_device(int $device_id, int $days_to_analyse = -1); public function get_types_by_device(int $device_id);
} }

View file

@ -35,7 +35,6 @@ class MariaDBDeviceRepository implements IDeviceRepository {
public static $column_type_notes = "Other"; public static $column_type_notes = "Other";
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
/** /**
@ -109,11 +108,6 @@ class MariaDBDeviceRepository implements IDeviceRepository {
$s = $this->get_static; $s = $this->get_static;
$o = $this->get_static_extra; $o = $this->get_static_extra;
$data_repo_class = MariaDBMeasurementDataRepository::class;
$data_repo_table_meta = $o($data_repo_class, "table_name_metadata");
$data_repo_col_datetime = "$data_repo_table_meta.{$o($data_repo_class, "column_metadata_datetime")}";
$data_repo_col_device_id = "$data_repo_table_meta.{$o($data_repo_class, "column_metadata_device_id")}";
$query_result = $this->database->query( $query_result = $this->database->query(
"SELECT "SELECT
{$s("table_name")}.{$s("column_device_id")} AS id, {$s("table_name")}.{$s("column_device_id")} AS id,
@ -126,8 +120,6 @@ class MariaDBDeviceRepository implements IDeviceRepository {
FROM {$s("table_name")} FROM {$s("table_name")}
JOIN {$s("table_name_type")} ON JOIN {$s("table_name_type")} ON
{$s("table_name")}.{$s("column_device_type")} = {$s("table_name_type")}.{$s("column_type_id")} {$s("table_name")}.{$s("column_device_type")} = {$s("table_name_type")}.{$s("column_type_id")}
JOIN $data_repo_table_meta ON
$data_repo_col_device_id = {$s("table_name")}.{$s("column_device_id")}
WHERE {$s("table_name")}.{$s("column_device_id")} = :device_id WHERE {$s("table_name")}.{$s("column_device_id")} = :device_id
AND {$s("table_name")}.{$s("column_visible")} != 0;", [ AND {$s("table_name")}.{$s("column_visible")} != 0;", [
"device_id" => $device_id "device_id" => $device_id

View file

@ -81,27 +81,37 @@ class MariaDBMeasurementTypeRepository implements IMeasurementTypeRepository {
)->fetchAll(); )->fetchAll();
} }
public function get_types_by_device($device_id, $days_to_analyse = -1) { public function get_types_by_device($device_id) {
$data = [ $data = [
"device_id" => $device_id "device_id" => $device_id
]; ];
if($days_to_analyse >= 0)
$data["days"] = $days_to_analyse;
$s = $this->get_static; $s = $this->get_static;
$o = $this->get_static_extra; $o = $this->get_static_extra;
$repo_device = MariaDBDeviceRepository::class;
$repo_sensor = MariaDBSensorRepository::class;
/*
SELECT reading_value_types.*
FROM devices
JOIN device_sensors ON device_sensors.device_id = devices.device_id
JOIN sensors ON sensors.id = device_sensors.sensors_id
JOIN sensor_reading_value_types ON sensor_reading_value_types.sensor_id = sensors.id
JOIN reading_value_types ON reading_value_types.id = sensor_reading_value_types.reading_value_types_id
WHERE devices.device_id=10
GROUP BY reading_value_types.id;
*/
return $this->database->query( return $this->database->query(
"SELECT "SELECT {$s("table_name")}.*
{$s("table_name")}.*, FROM {$o($repo_device, "table_name")}
COUNT({$s("table_name")}.{$s("column_id")}) AS count JOIN {$o($repo_sensor, "table_name_assoc")}
FROM {$o(MariaDBMeasurementDataRepository::class, "table_name_values")} ON {$o($repo_sensor, "table_name_assoc")}.{$o($repo_sensor, "col_assoc_device_id")} = {$o($repo_device, "table_name")}.{$o($repo_device, "column_device_id")}
JOIN {$o(MariaDBMeasurementDataRepository::class, "table_name_metadata")} ON JOIN {$o($repo_sensor, "table_name")}
{$o(MariaDBMeasurementDataRepository::class, "table_name_metadata")}.{$o(MariaDBMeasurementDataRepository::class, "column_metadata_id")} = {$o(MariaDBMeasurementDataRepository::class, "table_name_values")}.{$o(MariaDBMeasurementDataRepository::class, "column_values_reading_id")} ON {$o($repo_sensor, "table_name_assoc")}.{$o($repo_sensor, "col_assoc_sensor_id")} = {$o($repo_sensor, "table_name")}.{$o($repo_sensor, "col_id")}
JOIN {$s("table_name")} ON JOIN {$o($repo_sensor, "table_name_rtassoc")}
{$s("table_name")}.{$s("column_id")} = {$o(MariaDBMeasurementDataRepository::class, "table_name_values")}.{$o(MariaDBMeasurementDataRepository::class, "column_values_reading_type")} ON {$o($repo_sensor, "table_name")}.{$o($repo_sensor, "col_id")} = {$o($repo_sensor, "table_name_rtassoc")}.{$o($repo_sensor, "col_rtassoc_sensor_id")}
WHERE JOIN {$s("table_name")}
{$o(MariaDBMeasurementDataRepository::class, "table_name_metadata")}.{$o(MariaDBMeasurementDataRepository::class, "column_metadata_device_id")} = :device_id ON {$o($repo_sensor, "table_name_rtassoc")}.{$o($repo_sensor, "col_rtassoc_rvt_id")} = {$s("table_name")}.{$s("column_id")}
" . ($days_to_analyse >= 0 ? "AND DATEDIFF(NOW(),s_or_r) = :days" : "") . " WHERE {$o($repo_device, "table_name")}.{$o($repo_device, "column_device_id")} = :device_id
GROUP BY {$s("table_name")}.{$s("column_id")};", GROUP BY {$s("table_name")}.{$s("column_id")};",
$data $data
)->fetchAll(); )->fetchAll();

View file

@ -19,6 +19,10 @@ class MariaDBSensorRepository implements ISensorRepository {
public static $col_assoc_device_id = "device_id"; public static $col_assoc_device_id = "device_id";
public static $col_assoc_sensor_id = "sensors_id"; public static $col_assoc_sensor_id = "sensors_id";
public static $table_name_rtassoc = "sensor_reading_value_types";
public static $col_rtassoc_sensor_id = "sensor_id";
public static $col_rtassoc_rvt_id = "reading_value_types_id";
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------

6374
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,17 +18,16 @@
}, },
"homepage": "https://github.com/sbrl/ConnectedHumber-Air-Quality-Interface#readme", "homepage": "https://github.com/sbrl/ConnectedHumber-Air-Quality-Interface#readme",
"dependencies": { "dependencies": {
"chart.js": "^2.9.3", "chart.js": "^2.9.4",
"chroma-js": "^2.1.0", "chroma-js": "^2.1.0",
"d3-delaunay": "^5.2.1", "dom-create-element-query-selector": "^1.0.5",
"dom-create-element-query-selector": "github:hekigan/dom-create-element-query-selector",
"event-emitter-es6": "^1.1.5", "event-emitter-es6": "^1.1.5",
"iso8601-js-period": "^0.2.1", "iso8601-js-period": "^0.2.1",
"leaflet": "^1.6.0", "leaflet": "^1.7.1",
"leaflet-easyprint": "^2.1.9", "leaflet-easyprint": "^2.1.9",
"leaflet-fullscreen": "^1.0.2", "leaflet-fullscreen": "^1.0.2",
"leaflet.markercluster": "^1.4.1", "leaflet.markercluster": "^1.4.1",
"moment": "^2.24.0", "moment": "^2.29.1",
"nanomodal": "^5.1.1", "nanomodal": "^5.1.1",
"shepherd.js": "^7.1.4", "shepherd.js": "^7.1.4",
"smartsettings": "^1.2.3", "smartsettings": "^1.2.3",
@ -36,22 +35,23 @@
"xml-writer": "^1.7.0" "xml-writer": "^1.7.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chart.js": "^2.9.21", "@types/chart.js": "^2.9.30",
"@types/chroma-js": "^2.0.0", "@types/chroma-js": "^2.1.3",
"@types/d3-delaunay": "^4.1.0",
"@types/event-emitter-es6": "^1.1.0", "@types/event-emitter-es6": "^1.1.0",
"@types/leaflet": "^1.5.12", "@types/leaflet": "^1.5.23",
"@types/leaflet-fullscreen": "^1.0.4", "@types/leaflet-fullscreen": "^1.0.4",
"nightdocs": "^1.0.9", "nightdocs": "^1.0.9",
"postcss": "^8.2.6",
"postcss-copy": "^7.1.0", "postcss-copy": "^7.1.0",
"postcss-import": "^12.0.1", "postcss-import": "^14.0.0",
"rollup": "^2.7.2", "rollup": "^2.39.0",
"rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-json": "^4.0.0", "rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-postcss": "^3.1.1", "rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.3.0" "rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^7.0.2"
}, },
"docpress": { "docpress": {
"github": "ConnectedHumber/Air-Quality-Web", "github": "ConnectedHumber/Air-Quality-Web",

View file

@ -8,6 +8,7 @@ import postcss from 'rollup-plugin-postcss';
import { terser } from "rollup-plugin-terser"; import { terser } from "rollup-plugin-terser";
import replace from 'rollup-plugin-replace'; import replace from 'rollup-plugin-replace';
import json from 'rollup-plugin-json'; import json from 'rollup-plugin-json';
import { string } from 'rollup-plugin-string'
import postcss_import from 'postcss-import'; import postcss_import from 'postcss-import';
import postcss_copy from 'postcss-copy'; import postcss_copy from 'postcss-copy';
@ -40,6 +41,10 @@ let plugins = [
}), }),
string({
include: '**/*.svg'
}),
replace({ replace({
exclude: 'node_modules/**', exclude: 'node_modules/**',
values: { values: {

View file

@ -1 +1 @@
v0.13.4-dev v0.14-dev