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.
- `[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.
- `[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

6
build
View file

@ -28,7 +28,7 @@ cache_dir="./.cache";
build_output_folder="./app";
# Database settings for ssh port forwarding task
database_host="db.connectedhumber.org";
database_host="ch-kimsufi";
database_name="aq_db";
database_user="www-data";
@ -277,6 +277,10 @@ task_docs() {
# ██ ██
# ██████ ██
task_ci() {
if [[ -z "${TERM}" ]]; then
export TERM=xterm-256color;
fi
tasks_run setup setup-dev
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.
default_zoom: 12,
default_reading_type: "PM25",
// The number of minutes to round dates to when making time-based HTTP API requests.
// Very useful for improving cache hit rates.
date_rounding_interval: 6,

View file

@ -3,8 +3,9 @@
import { set_hidpi_canvas, pixel_ratio } from './Helpers/Canvas.mjs';
import { RenderGradient } from './Helpers/GradientHelpers.mjs';
import gradients from './Gradients.mjs';
class Guage {
class Gauge {
constructor(in_canvas) {
this.canvas = in_canvas;
@ -13,9 +14,30 @@ class Guage {
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 }) {
this.spec = spec;
this.max = max;
this.render();
}
render() {
@ -89,4 +111,4 @@ class Guage {
}
}
export default Guage;
export default Gauge;

View file

@ -1,5 +1,6 @@
"use strict";
import chroma from 'chroma-js';
var PM25 = {
/*
@ -60,7 +61,7 @@ var PM10 = {
var humidity = {
max: 100,
gradient: {
"0": "hsla(176, 77%, 40%, 0)",
"0": "hsla(52, 36%, 61%, 0.5)",
"50": "hsl(176, 77%, 40%)",
"100": "blue"
}
@ -103,6 +104,11 @@ var specs = {
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 {
PM10, PM25,

View file

@ -10,45 +10,128 @@ 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, in_device_data) {
constructor(in_map_manager, in_device_data) {
super();
this.map = in_map;
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
});
}
async setup() {
// Add a marker for each device
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 displaymobile devices by adding additional logic here
if(typeof device.latitude != "number" || typeof device.longitude != "number")
continue;
this.add_device_marker(device);
// 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();
}
// Display this layer
this.map.addLayer(this.layer);
// 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);
}
add_device_marker(device) {
/**
* 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)
autoPanPadding: L.point(100, 100),
icon
}
);
// Create the popup

View file

@ -8,15 +8,14 @@ import 'leaflet-easyprint';
import Config from './Config.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 ReadingsData from './ReadingsData.mjs';
import UI from './UI.mjs';
class MapManager {
constructor() {
console.log(Config);
this.readings_data = new ReadingsData();
}
async setup() {
@ -55,65 +54,14 @@ class MapManager {
Promise.all([
this.setup_device_markers.bind(this)()
.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"));
// 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() {
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);
}
// NOTE: We tried leaflet-time-dimension for changing the time displayed, but it didn't work out
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();
}
@ -149,8 +97,7 @@ class MapManager {
"OpenStreetMap": this.layer_openstreet
}, { // Overlay(s)
"Devices": this.device_markers.layer,
// FUTURE: Have 1 heatmap layer per reading type?
"Heatmap": this.overlay.layer
// "Heatmap": this.overlay.layer
}, { // 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 VoronoiCell from './VoronoiCell.mjs';
import Guage from '../Guage.mjs';
import Gauge from '../Gauge.mjs';
import Specs from './OverlaySpecs.mjs';
import Vector2 from '../Helpers/Vector2.mjs';
@ -37,7 +37,7 @@ class VoronoiManager {
}
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 GetFromUrl from './Helpers/GetFromUrl.mjs';
import Gauge from './Gauge.mjs';
// import Tour from './Tour.mjs';
@ -40,6 +41,9 @@ class UI {
this.map_manager = in_map_manager;
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.tour_enabled = false;
@ -63,7 +67,8 @@ class UI {
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");
}).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();
}

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"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
@ -15,7 +13,7 @@
viewBox="0 0 13.562857 21.42"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="marker.svg">
<defs
id="defs2">
@ -68,11 +66,12 @@
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1825"
inkscape:window-width="1831"
inkscape:window-height="1047"
inkscape:window-x="95"
inkscape:window-x="89"
inkscape:window-y="33"
inkscape:window-maximized="1" />
inkscape:window-maximized="1"
inkscape:document-rotation="0" />
<metadata
id="metadata5">
<rdf:RDF>
@ -91,10 +90,11 @@
id="layer1"
transform="translate(-27.03016,-48.232836)">
<path
style="fill:url(#linearGradient844);fill-opacity:1;stroke:none;stroke-width:0.46500003;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.95686275"
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"
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 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"
inkscape:connector-curvature="0" />
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<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"
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
$device_id = !empty($_GET["device-id"]) ? intval($_GET["device-id"]) : null;
$days_to_analyse = intval($_GET["days"] ?? "1") - 1;
$format = $_GET["format"] ?? "json";
if(!in_array($format, ["json", "csv"])) {
@ -54,7 +53,7 @@ class ListReadingTypes implements IAction {
if(!is_int($device_id))
$data = $this->types_repo->get_all_types();
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");
// 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.
* @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.
*/
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

@ -33,7 +33,6 @@ class MariaDBDeviceRepository implements IDeviceRepository {
public static $column_type_power = "power";
public static $column_type_software = "Software";
public static $column_type_notes = "Other";
// ------------------------------------------------------------------------
@ -109,11 +108,6 @@ class MariaDBDeviceRepository implements IDeviceRepository {
$s = $this->get_static;
$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(
"SELECT
{$s("table_name")}.{$s("column_device_id")} AS id,
@ -126,8 +120,6 @@ class MariaDBDeviceRepository implements IDeviceRepository {
FROM {$s("table_name")}
JOIN {$s("table_name_type")} ON
{$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
AND {$s("table_name")}.{$s("column_visible")} != 0;", [
"device_id" => $device_id

View file

@ -81,27 +81,37 @@ class MariaDBMeasurementTypeRepository implements IMeasurementTypeRepository {
)->fetchAll();
}
public function get_types_by_device($device_id, $days_to_analyse = -1) {
public function get_types_by_device($device_id) {
$data = [
"device_id" => $device_id
];
if($days_to_analyse >= 0)
$data["days"] = $days_to_analyse;
$s = $this->get_static;
$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(
"SELECT
{$s("table_name")}.*,
COUNT({$s("table_name")}.{$s("column_id")}) AS count
FROM {$o(MariaDBMeasurementDataRepository::class, "table_name_values")}
JOIN {$o(MariaDBMeasurementDataRepository::class, "table_name_metadata")} ON
{$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")}
JOIN {$s("table_name")} ON
{$s("table_name")}.{$s("column_id")} = {$o(MariaDBMeasurementDataRepository::class, "table_name_values")}.{$o(MariaDBMeasurementDataRepository::class, "column_values_reading_type")}
WHERE
{$o(MariaDBMeasurementDataRepository::class, "table_name_metadata")}.{$o(MariaDBMeasurementDataRepository::class, "column_metadata_device_id")} = :device_id
" . ($days_to_analyse >= 0 ? "AND DATEDIFF(NOW(),s_or_r) = :days" : "") . "
"SELECT {$s("table_name")}.*
FROM {$o($repo_device, "table_name")}
JOIN {$o($repo_sensor, "table_name_assoc")}
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($repo_sensor, "table_name")}
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 {$o($repo_sensor, "table_name_rtassoc")}
ON {$o($repo_sensor, "table_name")}.{$o($repo_sensor, "col_id")} = {$o($repo_sensor, "table_name_rtassoc")}.{$o($repo_sensor, "col_rtassoc_sensor_id")}
JOIN {$s("table_name")}
ON {$o($repo_sensor, "table_name_rtassoc")}.{$o($repo_sensor, "col_rtassoc_rvt_id")} = {$s("table_name")}.{$s("column_id")}
WHERE {$o($repo_device, "table_name")}.{$o($repo_device, "column_device_id")} = :device_id
GROUP BY {$s("table_name")}.{$s("column_id")};",
$data
)->fetchAll();

View file

@ -19,6 +19,10 @@ class MariaDBSensorRepository implements ISensorRepository {
public static $col_assoc_device_id = "device_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";
// ------------------------------------------------------------------------

14822
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",
"dependencies": {
"chart.js": "^2.9.3",
"chart.js": "^2.9.4",
"chroma-js": "^2.1.0",
"d3-delaunay": "^5.2.1",
"dom-create-element-query-selector": "github:hekigan/dom-create-element-query-selector",
"dom-create-element-query-selector": "^1.0.5",
"event-emitter-es6": "^1.1.5",
"iso8601-js-period": "^0.2.1",
"leaflet": "^1.6.0",
"leaflet": "^1.7.1",
"leaflet-easyprint": "^2.1.9",
"leaflet-fullscreen": "^1.0.2",
"leaflet.markercluster": "^1.4.1",
"moment": "^2.24.0",
"moment": "^2.29.1",
"nanomodal": "^5.1.1",
"shepherd.js": "^7.1.4",
"smartsettings": "^1.2.3",
@ -36,22 +35,23 @@
"xml-writer": "^1.7.0"
},
"devDependencies": {
"@types/chart.js": "^2.9.21",
"@types/chroma-js": "^2.0.0",
"@types/d3-delaunay": "^4.1.0",
"@types/chart.js": "^2.9.30",
"@types/chroma-js": "^2.1.3",
"@types/event-emitter-es6": "^1.1.0",
"@types/leaflet": "^1.5.12",
"@types/leaflet": "^1.5.23",
"@types/leaflet-fullscreen": "^1.0.4",
"nightdocs": "^1.0.9",
"postcss": "^8.2.6",
"postcss-copy": "^7.1.0",
"postcss-import": "^12.0.1",
"rollup": "^2.7.2",
"postcss-import": "^14.0.0",
"rollup": "^2.39.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-json": "^4.0.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-terser": "^5.3.0"
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^7.0.2"
},
"docpress": {
"github": "ConnectedHumber/Air-Quality-Web",

View file

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

View file

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