mirror of
https://github.com/ConnectedHumber/Air-Quality-Web
synced 2024-12-04 08:13:01 +00:00
Merge branch 'dev'
This commit is contained in:
commit
307534f112
22 changed files with 10380 additions and 4881 deletions
12
Changelog.md
12
Changelog.md
|
@ -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
6
build
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
});
|
||||
|
|
46
client_src/js/MarkerGenerator.mjs
Normal file
46
client_src/js/MarkerGenerator.mjs
Normal 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;
|
|
@ -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"));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
|
38
client_src/js/ReadingsData.mjs
Normal file
38
client_src/js/ReadingsData.mjs
Normal 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;
|
|
@ -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();
|
||||
}
|
||||
|
|
1
client_src/marker-embed.svg
Normal file
1
client_src/marker-embed.svg
Normal 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 |
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
14822
package-lock.json
generated
File diff suppressed because it is too large
Load diff
26
package.json
26
package.json
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
|||
v0.13.4-dev
|
||||
v0.14-dev
|
||||
|
|
Loading…
Reference in a new issue