Initial monorepo: readin, sensorapi, noise (Node22, npm ci, axios1.x, i18next-fs-backend)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
build_and_copy.sh
|
||||
Dockerfile_noise
|
||||
.vscode
|
||||
node_modules
|
||||
log
|
||||
.env*
|
||||
.gitignore
|
||||
deploy.sh
|
||||
Readme.*
|
||||
@@ -0,0 +1,10 @@
|
||||
# API Configuration
|
||||
API_KEY=your_api_key_here
|
||||
APIHOST=http://localhost:3005
|
||||
|
||||
# MongoDB Configuration (if needed)
|
||||
MONGO_ROOT_USERNAME=admin
|
||||
MONGO_ROOT_PASSWORD=your_password_here
|
||||
LOCALDIR=/path/to/local/dir
|
||||
REPO_USER=your_username
|
||||
REPO_PASSWORD=your_password
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
log
|
||||
.env
|
||||
Readme.pdf
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/bin/www.js",
|
||||
"env": {
|
||||
"PORT": "3003",
|
||||
"APIHOST": "http://localhost:3005",
|
||||
// "MONGOHOST": "reception",
|
||||
// "MONGOUSRP": "admin:mongo4noise",
|
||||
// "MONGOAUTH": "true",
|
||||
// "INFLUXHOST": "reception",
|
||||
// "INFLUXTOKEN": "gHGGgjaK0lmM6keMa01JeuDpqOE_vRq8UimsU4QKb2miI5BDh2PfWynEbwKizdJapXy8jVbTat5mVZLQTNmSdw==",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
ADD package.json /tmp/package.json
|
||||
ADD package-lock.json /tmp/package-lock.json
|
||||
RUN cd /tmp && npm ci
|
||||
RUN mkdir -p /opt/app && cp -a /tmp/node_modules /tmp/package.json /opt/app/
|
||||
WORKDIR /opt/app
|
||||
ADD . /opt/app/
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ=Europe/Berlin
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN deluser --remove-home node
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Develop und Deploy der Noise-Anwendung
|
||||
rxf 2025-08-08
|
||||
## Develop local ( auf esprimo !)
|
||||
* via VS die APP **sensorapi** auf dem *esprimo* starten
|
||||
* als **mongo** direkt die auf *ionos* verwenden
|
||||
* in VS die app **laerm_web_sensorapi** editieren und starten
|
||||
* im Browser aufrufen: **esprimo:3000**
|
||||
|
||||
## Deploy auf *ionos*
|
||||
* überprüfen, ob **portainer** richtig läuft, ggf. neu installieren
|
||||
* prüfen, ob alle Container richtig laufen, vor Allem der **watchtower**, ggf. das Stack neu starten
|
||||
* in VS (auf esprimo) das script **deploy.sh** aufrufen
|
||||
* auf **ionos** sollte nun via **watchtower** die neue Version geladen werden (spätestens nach 1 Minute)
|
||||
** nun im Browser **https://noise.fuerst-stuttgart.de** aufrufen und testen
|
||||
|
||||
## Deploy auf *rbsensors*
|
||||
* wenn **deplay.sh** noch nicht gestartet wurde (via VS auf esprimo), diese aufrufen.
|
||||
* Portainer auf rbsensors aufrufen: **192.168.51.22:9000**
|
||||
* über **Images** das neueste Image (**noise:latest**) vom Repository *Reinhard* holen
|
||||
* das Stack **noisesensor** aufrufen
|
||||
* den Container **noise** löschen
|
||||
* via *edit* das Stack neu starten
|
||||
* und nun im Browser mit **laerm.rbsensors.de** testen
|
||||
|
||||
| Datum | Version | Bemerkung |
|
||||
|-------|---------|---------|
|
||||
| 2025-08-08 | 1.0.1 | Typos verbessert |
|
||||
| 2024-12-20| 1.0.0 | erste Version |
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'dotenv/config'
|
||||
import createError from 'http-errors'
|
||||
import logger from 'morgan'
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import i18next from 'i18next'
|
||||
import i18nextMiddleware from 'i18next-http-middleware'
|
||||
import Backend from 'i18next-fs-backend'
|
||||
|
||||
const app = express()
|
||||
|
||||
export let url2call
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
import * as indexRouter from './routes/index.js'
|
||||
import apiRouter from './routes/api.js'
|
||||
|
||||
i18next
|
||||
.use(Backend)
|
||||
.use(i18nextMiddleware.LanguageDetector)
|
||||
.init({
|
||||
backend: {
|
||||
loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json'
|
||||
},
|
||||
fallbackLng: 'de',
|
||||
debug: false,
|
||||
preload: ['de', 'en']
|
||||
});
|
||||
|
||||
app.use(i18nextMiddleware.handle(i18next));
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'pug');
|
||||
|
||||
app.get('*', (req, res, next) => {
|
||||
const subdom = req.headers.host.split('.')
|
||||
if (subdom[0] === 'laerm') {
|
||||
subdom[0] = 'noise'
|
||||
}
|
||||
// app.set('category',subdom[0])
|
||||
app.set('category','noise')
|
||||
next()
|
||||
})
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname,'node_modules')));
|
||||
|
||||
app.use('/api', apiRouter)
|
||||
app.use('/srv', apiRouter)
|
||||
app.use('/', indexRouter.router);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function(req, res, next) {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
export default app
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
import app from '../app.js'
|
||||
import Debug from 'debug'
|
||||
import http from 'http'
|
||||
const debug = Debug('laerm-web-sensorapi:server')
|
||||
|
||||
|
||||
/* var app = require('../app');
|
||||
var debug = require('debug')('laerm-web-sensorapi:server');
|
||||
var http = require('http');
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Build Docker-Container
|
||||
#
|
||||
# Call: buildit.sh name [target]
|
||||
#
|
||||
# The Dockerfile must be named like Dockerfile_name
|
||||
#
|
||||
# 2018-09-20 rxf
|
||||
# - before sending docker image to remote, tag actual remote image
|
||||
#
|
||||
# 2018-09-14 rxf
|
||||
# - first Version
|
||||
#
|
||||
|
||||
set -x
|
||||
port=""
|
||||
orgName=noise
|
||||
name=noise
|
||||
|
||||
usage()
|
||||
{
|
||||
echo "Usage build_and_copy.sh [-p port] [-n name] target"
|
||||
echo " Build docker container $name and copy to target"
|
||||
echo "Params:"
|
||||
echo " target: Where to copy the container to "
|
||||
echo " -p port: ssh port (default 22)"
|
||||
echo " -n name: new name for container (default: $orgName)"
|
||||
}
|
||||
|
||||
while getopts n:p:h? o
|
||||
do
|
||||
case "$o" in
|
||||
n) name="$OPTARG";;
|
||||
p) port="-p $OPTARG";;
|
||||
h) usage; exit 0;;
|
||||
*) usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
if [[ -z "$target" ]]; then
|
||||
target=$1
|
||||
shift
|
||||
else
|
||||
echo "bad option $1"
|
||||
# exit 1
|
||||
shift
|
||||
fi
|
||||
done
|
||||
|
||||
docker build -f Dockerfile_$orgName --no-cache -t $name .
|
||||
|
||||
dat=`date +%Y%m%d%H%M`
|
||||
|
||||
if [ "$target" == "localhost" ]
|
||||
then
|
||||
docker tag $name $name:V_$dat
|
||||
exit
|
||||
fi
|
||||
|
||||
ssh $port $target "docker tag $name $name:V_$dat"
|
||||
docker save $name | bzip2 | pv | ssh $port $target 'bunzip2 | docker load'
|
||||
@@ -0,0 +1,565 @@
|
||||
// Prepare all data to plot the charts
|
||||
import * as utils from '../charts/utilities.js'
|
||||
import { translate as trans } from '../routes/index.js'
|
||||
|
||||
/**********************************************
|
||||
* Live-Data
|
||||
* *
|
||||
* @param params
|
||||
* @param values
|
||||
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params}}
|
||||
*
|
||||
***********************************************/
|
||||
export const liveData = (params, values, req) => {
|
||||
let series1 = []
|
||||
let series2 = []
|
||||
let series3 = []
|
||||
let infoTafel
|
||||
|
||||
// save the current (actual) values here
|
||||
let aktVal = {}
|
||||
|
||||
|
||||
// Put values into the arrays
|
||||
let cnt = 0
|
||||
values.forEach((x) => {
|
||||
let dat = new Date(x.datetime).getTime()
|
||||
series1.push([dat, x.LAeq])
|
||||
series2.push([dat, x.LA_max])
|
||||
series3.push([dat, x.LA_min]) // put data and value into series array
|
||||
})
|
||||
|
||||
if (values.length != 0) {
|
||||
const lastidx = values.length - 1
|
||||
// Aktuelle Werte speichern
|
||||
aktVal['LAeq'] = values[lastidx].LAeq
|
||||
aktVal['LAMax'] = values[lastidx].LA_max
|
||||
aktVal['LAMin'] = values[lastidx].LA_min
|
||||
|
||||
// InfoTafel füllen
|
||||
infoTafel =
|
||||
'<table class="infoTafel"><tr >' +
|
||||
'<th colspan="3">' + trans("ActualValues") + '</th>' +
|
||||
'</tr><tr>' +
|
||||
'<td>LAeq</td><td>' + (aktVal.LAeq).toFixed(1) + '</td><td>dbA</td>'
|
||||
infoTafel +=
|
||||
'</tr><tr>' +
|
||||
'<td>LAmin</td><td>' + (aktVal.LAMin).toFixed(0) + '</td><td>dbA</td>'
|
||||
infoTafel +=
|
||||
'</tr><tr>' +
|
||||
'<td>LAmax</td><td>' + (aktVal.LAMax).toFixed(0) + '</td><td>dbA</td>'
|
||||
infoTafel +=
|
||||
'</tr></table>' +
|
||||
'</div>'
|
||||
}
|
||||
let txtMeldung = false
|
||||
|
||||
|
||||
// Plot-Options
|
||||
let options = utils.createGlobObtions('dd.LL HH:mm:ss')
|
||||
|
||||
let series_LAeq = {
|
||||
name: 'LAeq',
|
||||
data: series1,
|
||||
color: utils.colors.eq,
|
||||
yAxis: 0,
|
||||
zIndex: 1,
|
||||
marker: {
|
||||
enabled: false,
|
||||
symbol: 'square',
|
||||
},
|
||||
visible: true,
|
||||
fillColor: {
|
||||
linearGradient: [0, 600, 0, 0],
|
||||
stops: [
|
||||
[0, 'rgb(238,223,236)'],
|
||||
[1, 'rgb(238,223,236)']
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
let series_LAMax = {
|
||||
name: 'LAmax',
|
||||
data: series2,
|
||||
color: utils.colors.max,
|
||||
yAxis: 0,
|
||||
zIndex: 0,
|
||||
marker: {
|
||||
enabled: false,
|
||||
symbol: 'square',
|
||||
},
|
||||
visible: true,
|
||||
fillColor: {
|
||||
linearGradient: [0, 600, 0, 0],
|
||||
stops: [
|
||||
[0, 'rgb(255,233,236)'],
|
||||
[1, 'rgb(255,233,236)']
|
||||
]
|
||||
},
|
||||
// fillOpacity: 0.2,
|
||||
}
|
||||
|
||||
let series_LAMin = {
|
||||
name: 'LAmin',
|
||||
data: series3,
|
||||
color: utils.colors.min,
|
||||
yAxis: 0,
|
||||
zIndex: 6,
|
||||
marker: {
|
||||
enabled: false,
|
||||
symbol: 'square',
|
||||
},
|
||||
visible: true,
|
||||
fillColor: {
|
||||
linearGradient: [0, 600, 0, 0],
|
||||
stops: [
|
||||
[0, 'rgb(219,216,217)'],
|
||||
[1, 'rgb(219,216,217)']
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
let yAxis_LAeq = { // 1
|
||||
title: {
|
||||
text: 'dbA',
|
||||
style: {
|
||||
color: 'black'
|
||||
}
|
||||
},
|
||||
max: utils.noise_ymax,
|
||||
// min: utils.noise_ymin,
|
||||
min: 10,
|
||||
// opposite: true,
|
||||
tickAmount: 11,
|
||||
useHTML: true,
|
||||
}
|
||||
|
||||
options.series = []
|
||||
options.yAxis = []
|
||||
options.series[0] = series_LAeq
|
||||
options.series[1] = series_LAMin
|
||||
options.title.text = trans('NoiseLevelsLast24Hours')
|
||||
options.subtitle.useHTML = true
|
||||
options.subtitle.text = `${trans("25minlogAverageValues")}<br /> `
|
||||
options.series[2] = series_LAMax
|
||||
// options.yAxis[2] = yAxis_LAMin
|
||||
options.yAxis[0] = yAxis_LAeq
|
||||
// options.yAxis[1] = yAxis_LAMax
|
||||
options.chart.zoomType = 'x'
|
||||
options.chart.type = 'area'
|
||||
options.chart.resetZoomButton= {
|
||||
position: {
|
||||
align: 'left',
|
||||
verticalAlign: 'bottom',
|
||||
x: 20,
|
||||
y: -50,
|
||||
},
|
||||
relativeTo: 'chart'
|
||||
}
|
||||
|
||||
// options.tooltip.formatter = function () {
|
||||
// let y = Math.round(this.y * 100) / 100
|
||||
// return '<div style="border: 2px solid ' + this.point.color + '; padding: 3px;">' +
|
||||
// DateTime.fromMillis(this.x, {zone: 'utc'}).toFormat("dd.LL - HH:mm:ss") + '<br />' +
|
||||
// '<span style="color: ' + this.point.color + '">● </span>' +
|
||||
// this.series.name + ': <b>' + y + '</b></div>'
|
||||
// }
|
||||
|
||||
|
||||
let noDataTafel = '<div class="errTafel">' +
|
||||
'Für heute liegen leider keine Daten vor!<br /> Bitte den Sensor überprüfen!\' <br />' +
|
||||
'</div>'
|
||||
|
||||
|
||||
let navy = 20
|
||||
let navbreit = 55
|
||||
let navtxt = ['-24h', '-12h', 'live', '+12h', '+24h']
|
||||
let navtime = [-24, -12, 0, 12, 24]
|
||||
|
||||
return { options: options, params: params, info: {class: 'infoTafel', text:infoTafel}}
|
||||
}
|
||||
|
||||
|
||||
/********************************************
|
||||
* havg - Data
|
||||
*
|
||||
* @param params
|
||||
* @param values
|
||||
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params, options2: any}}
|
||||
*
|
||||
********************************************/
|
||||
export const havgData = (params, values) => {
|
||||
let series1 = []
|
||||
let series2 = []
|
||||
// Put values into the series
|
||||
let cnt = 0
|
||||
values.forEach((x) => {
|
||||
if (x.n_AVG != -1) {
|
||||
let dat = new Date(x.datetime).getTime() // retrieve the date
|
||||
series1.push([dat, x.n_AVG])
|
||||
series2.push([dat, x.peakcount])
|
||||
}
|
||||
})
|
||||
|
||||
// Plot-Options
|
||||
let options = utils.createGlobObtions()
|
||||
options.xAxis.tickInterval = 6 * 3600 * 1000
|
||||
// options.xAxis.plotBands = utils.calcWeekends(data, true)
|
||||
options.xAxis.minTickInterval = 3600 * 1000
|
||||
|
||||
options.yAxis = [
|
||||
{
|
||||
title: {
|
||||
text: 'dbA',
|
||||
useHTML: true,
|
||||
},
|
||||
min: utils.noise_ymin,
|
||||
tickAmount: 10,
|
||||
gridLineColor: 'lightgray'
|
||||
}, {
|
||||
title: {
|
||||
text: 'Counts',
|
||||
useHTML: true,
|
||||
},
|
||||
tickAmount: 10,
|
||||
allowDecimals: false,
|
||||
gridLineColor: 'lightgray',
|
||||
opposite: true
|
||||
}
|
||||
]
|
||||
options.series = [
|
||||
{
|
||||
name: `${trans('MeanValueLAeq')}`,
|
||||
data: series1,
|
||||
color: utils.colors.eq,
|
||||
type: 'column',
|
||||
zIndex: 2,
|
||||
yAxis: 0
|
||||
}, {
|
||||
name: `${trans('Peaks')}`,
|
||||
data: series2,
|
||||
color: utils.colors.peaks,
|
||||
type: 'column',
|
||||
yAxis: 1,
|
||||
zIndex: 2
|
||||
}
|
||||
]
|
||||
// options.plotOptions.column = {
|
||||
// pointWidth: 8,
|
||||
// groupPadding: 0.5
|
||||
// }
|
||||
options.plotOptions.column = {
|
||||
pointWidth: 8,
|
||||
groupPadding: 0.1
|
||||
}
|
||||
|
||||
options.title.text = `${trans('HourlyMeanValues')} / ${trans('PeaksOver')} ${params.peak} dbA`
|
||||
options.subtitle.text = `${trans('HourlyMeanValuesOfLAeqForEachFullHour')} / ${trans('NumberOfPeaksPerHour')}<br /> `
|
||||
options.subtitle.useHTML = true
|
||||
options.chart.zoomType = 'x'
|
||||
options.chart.resetZoomButton= {
|
||||
position: {
|
||||
align: 'left',
|
||||
x: 20,
|
||||
y: 300,
|
||||
},
|
||||
relativeTo: 'chart'
|
||||
}
|
||||
|
||||
return { options: options, params: params}
|
||||
|
||||
}
|
||||
|
||||
/********************************************
|
||||
* davg - Data
|
||||
*
|
||||
* @param params
|
||||
* @param values
|
||||
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params, options2: any}}
|
||||
*
|
||||
********************************************/
|
||||
|
||||
export const davgData = (params, values) => {
|
||||
let series1 = []
|
||||
let series2 = []
|
||||
|
||||
// Put values into the serieses
|
||||
let cnt = 0
|
||||
values.forEach((x) => {
|
||||
if (x.n_AVG != -1) {
|
||||
let dat = new Date(x.datetime).getTime() // retrieve the date
|
||||
series1.push([dat, x.n_AVG])
|
||||
series2.push([dat, x.peakcount])
|
||||
}
|
||||
})
|
||||
// Plot-Options
|
||||
let options = utils.createGlobObtions()
|
||||
// options.xAxis.plotBands = utils.calcWeekends(values, true)
|
||||
options.xAxis.tickInterval = 24 * 3600 * 1000
|
||||
options.xAxis.title.text = 'Date'
|
||||
// options.xAxis.labels = {
|
||||
// formatter: function () {
|
||||
// return this.axis.defaultLabelFormatter.call(this);
|
||||
// }
|
||||
// }
|
||||
|
||||
options.chart.type = 'column'
|
||||
|
||||
options.yAxis = [
|
||||
{
|
||||
title: {
|
||||
text: 'dbA',
|
||||
useHTML: true,
|
||||
},
|
||||
min: utils.noise_ymin,
|
||||
tickAmount: 10,
|
||||
gridLineColor: 'lightgray',
|
||||
}, {
|
||||
title: {
|
||||
text: 'Counts',
|
||||
useHTML: true,
|
||||
},
|
||||
tickAmount: 10,
|
||||
allowDecimals: false,
|
||||
gridLineColor: 'lightgray',
|
||||
opposite: true
|
||||
}
|
||||
]
|
||||
|
||||
options.series = [
|
||||
{
|
||||
name: `${trans('MeanValueLAeq')}`,
|
||||
data: series1,
|
||||
color: utils.colors.eq,
|
||||
type: 'column',
|
||||
zIndex: 2,
|
||||
yAxis: 0
|
||||
},{
|
||||
name: `${trans('Peaks')}`,
|
||||
data: series2,
|
||||
color: utils.colors.peaks,
|
||||
type: 'column',
|
||||
yAxis: 1,
|
||||
zIndex: 2,
|
||||
}
|
||||
]
|
||||
options.plotOptions.column = {
|
||||
pointWidth: 20,
|
||||
groupPadding: 0.1
|
||||
}
|
||||
options.title.text = `${trans('DailyMeanValues')} / ${trans('PeaksOver')} ${params.peak} dbA`
|
||||
options.subtitle.text = `${trans('DailyMeanValuesOfLAeqForEachFullDay')} / ${trans('NumberOfPeaksPerDay')}<br /> `
|
||||
options.subtitle.useHTML = true
|
||||
options.chart.zoomType = 'x'
|
||||
options.chart.resetZoomButton = {
|
||||
position: {
|
||||
align: 'left',
|
||||
// verticalAlign: 'bottom',
|
||||
x: 20,
|
||||
y: 300,
|
||||
},
|
||||
relativeTo: 'chart'
|
||||
}
|
||||
|
||||
return {options: options, params: params}
|
||||
|
||||
}
|
||||
|
||||
/********************************************
|
||||
* dayNight - Data
|
||||
*
|
||||
* @param params
|
||||
* @param values
|
||||
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params}}
|
||||
*
|
||||
********************************************/
|
||||
export const dayNightData = (params, values) => {
|
||||
|
||||
let series1 = []
|
||||
let series2 = []
|
||||
// Put values into the serieses
|
||||
let cnt = 0
|
||||
values.forEach((x) => {
|
||||
if (x.n_dayAVG > 0) {
|
||||
let dat = new Date(x.datetime).getTime() // retrieve the date
|
||||
series1.push([dat, x.n_dayAVG])
|
||||
series2.push([dat, x.n_nightAVG])
|
||||
}
|
||||
})
|
||||
// Plot-Options
|
||||
let options = utils.createGlobObtions()
|
||||
// options.xAxis.plotBands = utils.calcWeekends(data, true)
|
||||
options.xAxis.tickInterval = 24 * 3600 * 1000
|
||||
options.xAxis.title.text = trans('Date')
|
||||
// options.xAxis.labels = {
|
||||
// formatter: function () {
|
||||
// return this.axis.defaultLabelFormatter.call(this);
|
||||
// }
|
||||
// }
|
||||
|
||||
options.chart.type = 'column'
|
||||
options.yAxis = {
|
||||
title: {
|
||||
text: 'dbA',
|
||||
useHTML: true,
|
||||
},
|
||||
min: utils.noise_ymin,
|
||||
tickAmount: 10,
|
||||
gridLineColor: 'lightgray',
|
||||
}
|
||||
options.series = [
|
||||
{
|
||||
name: `${trans('Day')}`,
|
||||
data: series1,
|
||||
color: utils.colors.eq,
|
||||
type: 'column',
|
||||
zIndex: 2,
|
||||
},
|
||||
{
|
||||
name: `${trans('Night')}`,
|
||||
data: series2,
|
||||
color: utils.colors.max,
|
||||
type: 'column',
|
||||
zIndex: 2,
|
||||
},
|
||||
]
|
||||
options.plotOptions.series = {
|
||||
animation: false
|
||||
}
|
||||
options.plotOptions.column = {
|
||||
pointWidth: 20,
|
||||
groupPadding: 0.3
|
||||
}
|
||||
options.title.text = trans('AverageValuesDuringTheDayAndNightHours')
|
||||
options.subtitle.text = `${trans('DailyMeanValuesOfLAeqFrom6h00To22h00')}<br />${trans('NightlyMeanValuesOfLAeqFrom22h00To6h00')}<br /> `
|
||||
options.subtitle.useHTML = true
|
||||
options.chart.zoomType = 'x'
|
||||
options.chart.resetZoomButton = {
|
||||
position: {
|
||||
align: 'left',
|
||||
// verticalAlign: 'bottom',
|
||||
x: 20,
|
||||
y: 300,
|
||||
},
|
||||
relativeTo: 'chart'
|
||||
}
|
||||
return {options: options, params: params}
|
||||
}
|
||||
|
||||
/***********************************
|
||||
Lden - Data
|
||||
*
|
||||
* @param params
|
||||
* @param values
|
||||
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params}}
|
||||
**********************************/
|
||||
export const ldenData = (params, values) => {
|
||||
|
||||
let series1 = []
|
||||
|
||||
// Put values into the series
|
||||
let cnt = 0
|
||||
values.forEach((x) => {
|
||||
if (!((x.lden === -1) || (x.datetime === undefined))) {
|
||||
let dat = new Date(x.datetime).getTime() // retrieve the date
|
||||
series1.push([dat, x.lden])
|
||||
}
|
||||
})
|
||||
// Plot-Options
|
||||
let options = utils.createGlobObtions()
|
||||
|
||||
// options.xAxis.plotBands = utils.calcWeekends(data, true)
|
||||
options.xAxis.tickInterval = 24 * 3600 * 1000
|
||||
options.xAxis.title.text = trans('Date')
|
||||
options.xAxis.labels = {
|
||||
formatter: function () {
|
||||
let lbl = this.axis.defaultLabelFormatter.call(this);
|
||||
this.value += 86400000;
|
||||
let lbl1 = this.axis.defaultLabelFormatter.call(this);
|
||||
return parseInt(lbl) + '/' + lbl1;
|
||||
}
|
||||
}
|
||||
options.chart.type = 'column'
|
||||
options.yAxis = {
|
||||
title: {
|
||||
text: 'dbA',
|
||||
useHTML: true,
|
||||
},
|
||||
min: utils.noise_ymin,
|
||||
tickAmount: 10,
|
||||
gridLineColor: 'lightgray',
|
||||
}
|
||||
options.series = [
|
||||
{
|
||||
name: 'Lden',
|
||||
data: series1,
|
||||
color: utils.colors.eq,
|
||||
type: 'column',
|
||||
zIndex: 2,
|
||||
}
|
||||
]
|
||||
options.plotOptions.series = {
|
||||
animation: false
|
||||
}
|
||||
options.plotOptions.column = {
|
||||
pointWidth: 20,
|
||||
groupPadding: 0.1
|
||||
}
|
||||
options.title.text = trans('LDEN-Index')
|
||||
options.subtitle.text = `${trans('DailyMeanValuesOfLDEN-Index')}<br /> `
|
||||
options.subtitle.useHTML = true
|
||||
options.chart.zoomType = 'x'
|
||||
options.chart.resetZoomButton = {
|
||||
position: {
|
||||
align: 'left',
|
||||
// verticalAlign: 'bottom',
|
||||
x: 20,
|
||||
y: 300,
|
||||
},
|
||||
relativeTo: 'chart'
|
||||
}
|
||||
/* options.tooltip.formatter = function () {
|
||||
let d = DateTime.fromMillis(this.x, {zone: 'utc'})
|
||||
let d1 = d.plus({days: 1})
|
||||
let txt = d.toFormat('dd') + '/' + d1.toFormat('dd.LL')
|
||||
return '<div style="border: 2px solid ' + this.point.color + '; padding: 3px;">' +
|
||||
' ' + txt + '<br />' +
|
||||
'<span style="color: ' + this.point.color + '">● </span>' +
|
||||
this.series.name + ': <b>' +
|
||||
Highcharts.numberFormat(this.y, 1) +
|
||||
'</b></div>';
|
||||
}
|
||||
|
||||
*/
|
||||
return {options: options, params: params}
|
||||
}
|
||||
|
||||
/***********************************
|
||||
Map - Data
|
||||
*
|
||||
* @param params
|
||||
* @param values
|
||||
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params}}
|
||||
**********************************/
|
||||
export const mapData = (params, values) => {
|
||||
let actsensors = 0
|
||||
for (let x of values) {
|
||||
if (x.value >= 0) {
|
||||
actsensors++
|
||||
}
|
||||
}
|
||||
return {
|
||||
...params,
|
||||
values: values,
|
||||
actsensors: `${trans("ActiveSensors")} ${actsensors}`,
|
||||
registered: `(${trans("registered")} ${params.count})`,
|
||||
valfromtxt: trans("ValuesFrom"),
|
||||
popuptxt: {
|
||||
offline: trans('offline'),
|
||||
sensor: trans('Sensor'),
|
||||
lastseen: trans('LastSeen'),
|
||||
showchart: trans('ShowChart')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// Utility routine for plotting the data
|
||||
|
||||
import { translate as trans } from '../routes/index.js'
|
||||
|
||||
let language = 'en'
|
||||
|
||||
export const colors = {'eq': '#0000FF', 'max': '#FF0000', 'min': '#008000', 'peaks': '#DAA520'};
|
||||
export const noise_ymin = 20;
|
||||
export const noise_ymax = 120;
|
||||
|
||||
export function createGlobObtions() {
|
||||
// Options, die für alle Plots identisch sind
|
||||
let globObject = {
|
||||
chart: {
|
||||
spacingRight: 20,
|
||||
spacingLeft: 20,
|
||||
spacingTop: 25,
|
||||
backgroundColor: {
|
||||
linearGradient: [0, 400, 0, 0],
|
||||
stops: [
|
||||
[0, '#eee'],//[0, '#ACD0AA'], //[0, '#A18D99'], // [0, '#886A8B'], // [0, '#F2D0B5'],
|
||||
[1, '#fff']
|
||||
]
|
||||
},
|
||||
type: 'line',
|
||||
borderWidth: '2',
|
||||
// events: {
|
||||
// selection: function (event) {
|
||||
// if (event.xAxis) {
|
||||
// doUpdate = false;
|
||||
// } else {
|
||||
// doUpdate = true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
title: {
|
||||
align: 'left',
|
||||
style: {'fontSize': '25px'},
|
||||
useHTML: true,
|
||||
},
|
||||
subtitle: {
|
||||
align: 'left',
|
||||
},
|
||||
tooltip: {
|
||||
valueDecimals: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
useHTML: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
title: {
|
||||
text: trans('datetime'),
|
||||
},
|
||||
gridLineWidth: 2,
|
||||
labels: {
|
||||
formatter: function () {
|
||||
let v = this.axis.defaultLabelFormatter.call(this);
|
||||
if (v.indexOf(':') == -1) {
|
||||
return '<span style="font-weight:bold;color:red">' + v + '<span>';
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
enabled: true,
|
||||
layout: 'horizontal',
|
||||
// verticalAlign: 'top',
|
||||
borderWidth: 1,
|
||||
align: 'center',
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
animation: false,
|
||||
turboThreshold: 0,
|
||||
marker: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
return globObject;
|
||||
}
|
||||
|
||||
export function calcWeekends(data, isyear) {
|
||||
/* let weekend = [];
|
||||
let oldDay = 8;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let mom = moment(data[i].date);
|
||||
if (isyear) {
|
||||
mom = moment(data[i]._id)
|
||||
}
|
||||
let day = mom.day();
|
||||
let st = mom.startOf('day');
|
||||
if (day != oldDay) {
|
||||
if (day == 6) {
|
||||
weekend.push({
|
||||
color: 'rgba(169,235,158,0.4)',
|
||||
from: st.valueOf(),
|
||||
to: st.add(1, 'days').valueOf(),
|
||||
zIndex: 0
|
||||
})
|
||||
} else if (day == 0) {
|
||||
weekend.push({
|
||||
color: 'rgba(169,235,158,0.4)',
|
||||
from: st.valueOf(),
|
||||
to: st.add(1, 'days').valueOf(),
|
||||
zIndex: 0
|
||||
})
|
||||
}
|
||||
oldDay = day;
|
||||
}
|
||||
}
|
||||
return weekend;
|
||||
*/}
|
||||
|
||||
export function calcDays(data, isyear) {
|
||||
let days = [];
|
||||
if (data.length == 0) {
|
||||
return days
|
||||
}
|
||||
let oldday = moment(data[0].date).day();
|
||||
if (isyear) {
|
||||
oldday = moment(data[0]._id).day();
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let m = moment(data[i].date);
|
||||
if (isyear) {
|
||||
m = moment(data[i]._id);
|
||||
}
|
||||
let tag = m.day()
|
||||
if (tag != oldday) {
|
||||
m.startOf('day');
|
||||
days.push({color: 'lightgray', value: m.valueOf(), width: 1, zIndex: 2});
|
||||
oldday = tag;
|
||||
}
|
||||
}
|
||||
return days;
|
||||
};
|
||||
|
||||
export const setLanguage = (lng) => {
|
||||
language = lng
|
||||
}
|
||||
|
||||
export const getLanguage = () => {
|
||||
return language
|
||||
}
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Angepasst aus dem readin-/sensorapi-deploy.sh (urspr. /Projekte/Logbuch/deploy.sh)
|
||||
# Nur linux/amd64, nativer Build auf esprimo (amd64) -> kein buildx/qemu,
|
||||
# eigener Dockerfile-Name (Dockerfile_noise). Image-Name: noise.
|
||||
|
||||
REGISTRY="docker.citysensor.de"
|
||||
IMAGE_NAME="noise"
|
||||
TAG="${1:-latest}"
|
||||
PLATFORM="linux/amd64"
|
||||
DOCKERFILE="Dockerfile_noise"
|
||||
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
BUILD_DATE=$(date +%d.%m.%Y)
|
||||
|
||||
echo "=========================================="
|
||||
echo "noise (laerm_web) Deploy Script (amd64)"
|
||||
echo "=========================================="
|
||||
echo "Registry: ${REGISTRY}"
|
||||
echo "Image: ${IMAGE_NAME}"
|
||||
echo "Tag: ${TAG}"
|
||||
echo "Platform: ${PLATFORM}"
|
||||
echo "Dockerfile: ${DOCKERFILE}"
|
||||
echo "Build-Datum: ${BUILD_DATE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Hinweis: Login wird uebersprungen, weil die Credentials fuer ${REGISTRY}
|
||||
# bereits in ~/.docker/config.json liegen.
|
||||
# Bei Bedarf manuell: docker login ${REGISTRY}
|
||||
|
||||
echo ">>> Baue Image (nur ${PLATFORM}, nativ) ..."
|
||||
docker build \
|
||||
--platform "${PLATFORM}" \
|
||||
-f "${DOCKERFILE}" \
|
||||
-t "${FULL_IMAGE}" \
|
||||
.
|
||||
echo ""
|
||||
|
||||
echo ">>> Pushe ${FULL_IMAGE} ..."
|
||||
docker push "${FULL_IMAGE}"
|
||||
echo ""
|
||||
|
||||
# Falls ein anderer Tag als 'latest' gebaut wurde, zusaetzlich latest setzen+pushen
|
||||
if [ "${TAG}" != "latest" ]; then
|
||||
echo ">>> Tagge zusaetzlich als :latest ..."
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
|
||||
docker push "${REGISTRY}/${IMAGE_NAME}:latest"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Build + Push fertig: ${FULL_IMAGE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Danach Stack aktualisieren:"
|
||||
echo " cd /opt/stacks/esprimo/noisesensors && docker compose up -d noise"
|
||||
@@ -0,0 +1,5 @@
|
||||
####Neues Sensor-API
|
||||
|
||||
* Programm **sensor_web_api**. Läuft auf Port 3000
|
||||
* Aufruf über: **laerm.test.me:3000** (oder feinstaub.test... oder multigeiger.test...)
|
||||
* Zugriff auf die DB mit dem Programm **SensorApi**. Dieses reagiert auf Port 3005 und muss im Hintergrund laufen!
|
||||
@@ -0,0 +1,5 @@
|
||||
Changes ab AUG-23
|
||||
|
||||
- i18n in /:sid in idex.js rein
|
||||
- Messung und Anzeige auf Console der Ladezeit der 5 Pages im Hintergrund
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"NoiseMeasurement": "Lärm-Messung",
|
||||
"Settings": "Einstellungen",
|
||||
"Info": "Info",
|
||||
"StartDate": "Start Datum",
|
||||
"NumberOfDays": "So viele Tage anzeigen",
|
||||
"Only_for_live_chart": "Nur bei 'live' Grafik",
|
||||
"Count_peaks_over": "Zähle Spitzen über",
|
||||
"Close": "Schließen",
|
||||
"Save_changes": "Änderungen speichern",
|
||||
"Language": "Sprache",
|
||||
"Map": "Karte",
|
||||
"Live": "Live",
|
||||
"Hour_AVG": "Stundenmittel",
|
||||
"Day_AVG": "Tagesmittel",
|
||||
"Day_Night": "Tag/Nacht-Werte",
|
||||
"ActualValues": "Aktuelle Werte",
|
||||
"NoiseLevelsLast24Hours": "Schallpegel der letzten 24 Stunden",
|
||||
"ValuesFrom": "Werte vom",
|
||||
"ActiveSensors": "Aktive Sensoren:",
|
||||
"registered": "registriert:",
|
||||
"ShowChart": "Grafik anzeigen",
|
||||
"Sensor": "Sensor",
|
||||
"offline": "offline",
|
||||
"LastSeen": "Letzter Kontakt",
|
||||
"datetime": "Datum/Zeit",
|
||||
"25minlogAverageValues": "2.5min log. Mittelwerte",
|
||||
"HourlyMeanValues": "Stündliche Mittelwerte",
|
||||
"PeaksOver": "Spitzen über",
|
||||
"HourlyMeanValuesOfLAeqForEachFullHour": "Stunden-Mittelwerte von LAeq jeweils für eine volle Stunde",
|
||||
"NumberOfPeaksPerHour": "Anzahl der Spitzen pro Stunde",
|
||||
"DailyMeanValues": "Tages-Mittelwerte",
|
||||
"DailyMeanValuesOfLAeqForEachFullDay": "Tages-Mittelwerte von LAeq jeweils für einen vollen Tag",
|
||||
"NumberOfPeaksPerDay": "Anzahl der Spitzen pro Tag",
|
||||
"AverageValuesDuringTheDayAndNightHours": "Mittelwerte während der Tag- und Nachtstunden",
|
||||
"DailyMeanValuesOfLAeqFrom6h00To22h00": "Tages-Mittelwerte des LAeq jeweils von 6:00 bis 22:00 Uhr",
|
||||
"NightlyMeanValuesOfLAeqFrom22h00To6h00": "Nacht-Mittelwerte des LAeq jeweils von 22:00 bis 6:00 Uhr",
|
||||
"LDEN-Index": "L<sub>DEN</sub>-Index",
|
||||
"DailyMeanValuesOfLDEN-Index": "Tages-Mittelwerte des L<sub>DEN</sub>-Index",
|
||||
"Date": "Datum",
|
||||
"Version": "Version",
|
||||
"from": "vom",
|
||||
"RemoveFromMap": "Sensoren nicht anzeigen, die nicht gesendet haben seit",
|
||||
"weeks": "Wochen",
|
||||
"ESYSCALL": "Fehler vom Serveraufruf: ",
|
||||
"Error": "Fehler",
|
||||
"MeanValueLAeq": "Mittelwert LAeq",
|
||||
"Peaks": "Spitzen",
|
||||
"Day": "Tag",
|
||||
"Night": "Nacht",
|
||||
"CenterMap": "Karte zentrieren auf",
|
||||
"MyLocation": "Mein Standort"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"NoiseMeasurement": "Lärm-Messung",
|
||||
"Settings": "Einstellungen",
|
||||
"Info": "Info",
|
||||
"StartDate": "Start Datum",
|
||||
"NumberOfDays": "So viele Tage anzeigen",
|
||||
"Only_for_live_chart": "Nur bei 'live' Grafik",
|
||||
"Count_peaks_over": "Zähle Spitzen über",
|
||||
"Close": "Schließen",
|
||||
"Save_changes": "Änderungen speichern",
|
||||
"Language": "Sprache",
|
||||
"Map": "Karte",
|
||||
"Live": "Live",
|
||||
"Hour_AVG": "Stundenmittel",
|
||||
"Day_AVG": "Tagesmittel",
|
||||
"Day_Night": "Tag/Nacht-Werte",
|
||||
"ActualValues": "Aktuelle Werte",
|
||||
"NoiseLevelsLast24Hours": "Schallpegel der letzten 24 Stunden",
|
||||
"ValuesFrom": "Werte vom",
|
||||
"ActiveSensors": "Aktive Sensoren:",
|
||||
"registered": "registriert:",
|
||||
"ShowChart": "Grafik anzeigen",
|
||||
"Sensor": "Sensor",
|
||||
"offline": "offline",
|
||||
"LastSeen": "Letzter Kontakt",
|
||||
"datetime": "Datum/Zeit",
|
||||
"25minlogAverageValues": "2.5min log. Mittelwerte",
|
||||
"HourlyMeanValues": "Stündliche Mittelwerte",
|
||||
"PeaksOver": "Spitzen über",
|
||||
"HourlyMeanValuesOfLAeqForEachFullHour": "Stunden-Mittelwerte von LAeq jeweils für eine volle Stunde",
|
||||
"NumberOfPeaksPerHour": "Anzahl der Spitzen pro Stunde",
|
||||
"DailyMeanValues": "Tages-Mittelwerte",
|
||||
"DailyMeanValuesOfLAeqForEachFullDay": "Tages-Mittelwerte von LAeq jeweils für einen vollen Tag",
|
||||
"NumberOfPeaksPerDay": "Anzahl der Spitzen pro Tag",
|
||||
"AverageValuesDuringTheDayAndNightHours": "Mittelwerte während der Tag- und Nachtstunden",
|
||||
"DailyMeanValuesOfLAeqFrom6h00To22h00": "Tages-Mittelwerte des LAeq jeweils von 6:00 bis 22:00 Uhr",
|
||||
"NightlyMeanValuesOfLAeqFrom22h00To6h00": "Nacht-Mittelwerte des LAeq jeweils von 22:00 bis 6:00 Uhr",
|
||||
"LDEN-Index": "L<sub>DEN</sub>-Index",
|
||||
"DailyMeanValuesOfLDEN-Index": "Tages-Mittelwerte des L<sub>DEN</sub>-Index",
|
||||
"Date": "Datum",
|
||||
"Version": "Version",
|
||||
"from": "vom",
|
||||
"RemoveFromMap": "Sensoren nicht anzeigen, die nicht gesendet haben seit",
|
||||
"weeks": "Wochen",
|
||||
"CenterMap": "Center map on"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"NoiseMeasurement": "Noise Measurement",
|
||||
"Settings": "Settings",
|
||||
"Info": "Info",
|
||||
"StartDate": "Start date",
|
||||
"NumberOfDays": "Show so many days",
|
||||
"Only_for_live_chart": "Only for 'live' chart",
|
||||
"Count_peaks_over": "Count peaks over",
|
||||
"Close": "Close",
|
||||
"Save_changes": "Save changes",
|
||||
"Language": "Language",
|
||||
"Map": "Map",
|
||||
"Live": "Live",
|
||||
"Hour_AVG": "Hour_AVG",
|
||||
"Day_AVG": "Day_AVG",
|
||||
"Day_Night": "Day_Night",
|
||||
"ActualValues": "Actual Values",
|
||||
"NoiseLevelsLast24Hours": "Noise levels last 24 hours",
|
||||
"ValuesFrom": "Values from ",
|
||||
"ActiveSensors": "Active Sensors:",
|
||||
"registered": "registered:",
|
||||
"ShowChart": "Show chart",
|
||||
"Sensor": "Sensor",
|
||||
"offline": "offline",
|
||||
"LastSeen": "Last seen",
|
||||
"datetime": "Date/Time",
|
||||
"25minlogAverageValues": "2.5min log average values",
|
||||
"HourlyMeanValues": "Hourly mean values",
|
||||
"PeaksOver": "Peaks over",
|
||||
"HourlyMeanValuesOfLAeqForEachFullHour": "Hourly mean values of LAeq for each full hour",
|
||||
"NumberOfPeaksPerHour": "Number of peaks per hour",
|
||||
"DailyMeanValues": "Daily mean values",
|
||||
"DailyMeanValuesOfLAeqForEachFullDay": "Daily mean values of LAeq for each full day",
|
||||
"NumberOfPeaksPerDay": "Number of peaks per day",
|
||||
"AverageValuesDuringTheDayAndNightHours": "Average values during the day and night hours",
|
||||
"DailyMeanValuesOfLAeqFrom6h00To22h00": "Daily mean values of LAeq from 6h00 to 22h00",
|
||||
"NightlyMeanValuesOfLAeqFrom22h00To6h00": "Nightly mean values of LAeq from 22h00 to 6h00",
|
||||
"LDEN-Index": "L<sub>DEN</sub>-Index",
|
||||
"DailyMeanValuesOfLDEN-Index": "Daily mean values of L<sub>DEN</sub>-Index",
|
||||
"Date": "Date",
|
||||
"Version": "Version",
|
||||
"from": "from",
|
||||
"RemoveFromMap": "Don't show sensors that have not sent for",
|
||||
"weeks": "weeks",
|
||||
"ESYSCALL": "Error from Syscall: ",
|
||||
"Error": "Error",
|
||||
"MeanValueLAeq": "Mean value LAeq",
|
||||
"Peaks": "Peaks",
|
||||
"Day": "Day",
|
||||
"Night": "Night",
|
||||
"CenterMap": "Center map on",
|
||||
"MyLocation": "My Location"
|
||||
}
|
||||
Generated
+2117
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "laerm-web-sensorapi",
|
||||
"version": "3.3.1",
|
||||
"date": "2026-06-30 18:00 UTC",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node bin/www.js >>/var/log/noise.log 2>&1"
|
||||
},
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"www": "./bin/www.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~2.6.9",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.18.2",
|
||||
"http-errors": "~1.6.3",
|
||||
"i18next": "^22.4.14",
|
||||
"i18next-express-middleware": "^2.0.0",
|
||||
"i18next-http-middleware": "^3.3.0",
|
||||
"i18next-fs-backend": "^2.0.0",
|
||||
"luxon": "^3.3.0",
|
||||
"morgan": "^1.10.1",
|
||||
"pug": "^3.0.2",
|
||||
"sass": "^1.58.3",
|
||||
"spin.js": "^4.1.2"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,276 @@
|
||||
// Utility routine for plotting the data
|
||||
|
||||
import {DateTime} from './luxon.min.js'
|
||||
export const colors = {'eq': '#0000FF', 'max': '#FF0000', 'min': '#008000', 'peaks': '#DAA520'};
|
||||
|
||||
// Defaults für die Mittelwertbildungen
|
||||
let txtMeldung = false; // falls keine Daten da sind, Text melden
|
||||
const avgTime = 30; // defaul average time für particulate matter
|
||||
let doUpdate = true; // update every 5 min
|
||||
let optSidsArray = []; // Arrray der letzten 5 Einträge
|
||||
let nbrofdaysforavg = 8; // Stundenmittel Anzahl Tagte (default = 5)
|
||||
let nbrofdaysfordaynight = 30; // use 30 days or day/night graphic
|
||||
let stucols = 1; // Anzeige nur Balken (StundenMittel)
|
||||
let activeTab = 'maptab'; // active TAB ID
|
||||
|
||||
export const noise_ymin = 30; // lowest value on y-axis for noise
|
||||
export const noise_ymax = 120; // highest value on y-axis for noise
|
||||
export let peaklim = 70; // threshold for peak count
|
||||
|
||||
export const url = '/srv'
|
||||
|
||||
export function createGlobObtions() {
|
||||
// Options, die für alle Plots identisch sind
|
||||
let globObject = {
|
||||
chart: {
|
||||
accessibility: {
|
||||
enabled: false
|
||||
},
|
||||
height: 600,
|
||||
// width: 1000,
|
||||
spacingRight: 20,
|
||||
spacingLeft: 20,
|
||||
spacingTop: 25,
|
||||
backgroundColor: {
|
||||
linearGradient: [0, 400, 0, 0],
|
||||
stops: [
|
||||
[0, '#eee'],//[0, '#ACD0AA'], //[0, '#A18D99'], // [0, '#886A8B'], // [0, '#F2D0B5'],
|
||||
[1, '#fff']
|
||||
]
|
||||
},
|
||||
type: 'line',
|
||||
borderWidth: '2',
|
||||
useHTML: true,
|
||||
events: {
|
||||
selection: function (event) {
|
||||
if (event.xAxis) {
|
||||
doUpdate = false;
|
||||
} else {
|
||||
doUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
// text: 'Feinstaub über 1 Tag',
|
||||
align: 'left',
|
||||
style: {'fontSize': '25px'},
|
||||
useHTML: true,
|
||||
},
|
||||
subtitle: {
|
||||
// text: 'Gemessene Werte und ' + avgTime + 'min-gleitende Mittelwerte',
|
||||
align: 'left',
|
||||
},
|
||||
tooltip: {
|
||||
valueDecimals: 1,
|
||||
backgroundColor: 0,
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
useHTML: true,
|
||||
},
|
||||
labels: {
|
||||
useHTML: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
title: {
|
||||
text: 'date/time',
|
||||
},
|
||||
gridLineWidth: 2,
|
||||
labels: {
|
||||
formatter: function () {
|
||||
let v = this.axis.defaultLabelFormatter.call(this);
|
||||
if (v.indexOf(':') == -1) {
|
||||
return '<span style="font-weight:bold;color:red">' + v + '<span>';
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
enabled: true,
|
||||
layout: 'horizontal',
|
||||
// verticalAlign: 'top',
|
||||
borderWidth: 1,
|
||||
align: 'center',
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
animation: false,
|
||||
turboThreshold: 0,
|
||||
marker: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
return globObject;
|
||||
}
|
||||
|
||||
export function calcWeekends(data, isyear) {
|
||||
let weekend = [];
|
||||
let oldDay = 8;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let mom = moment(data[i].date);
|
||||
if (isyear) {
|
||||
mom = moment(data[i]._id)
|
||||
}
|
||||
let day = mom.day();
|
||||
let st = mom.startOf('day');
|
||||
if (day != oldDay) {
|
||||
if (day == 6) {
|
||||
weekend.push({
|
||||
color: 'rgba(169,235,158,0.4)',
|
||||
from: st.valueOf(),
|
||||
to: st.add(1, 'days').valueOf(),
|
||||
zIndex: 0
|
||||
})
|
||||
} else if (day == 0) {
|
||||
weekend.push({
|
||||
color: 'rgba(169,235,158,0.4)',
|
||||
from: st.valueOf(),
|
||||
to: st.add(1, 'days').valueOf(),
|
||||
zIndex: 0
|
||||
})
|
||||
}
|
||||
oldDay = day;
|
||||
}
|
||||
}
|
||||
return weekend;
|
||||
}
|
||||
|
||||
export function calcDays(data, isyear) {
|
||||
let days = [];
|
||||
if (data.length == 0) {
|
||||
return days
|
||||
}
|
||||
let oldday = moment(data[0].date).day();
|
||||
if (isyear) {
|
||||
oldday = moment(data[0]._id).day();
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let m = moment(data[i].date);
|
||||
if (isyear) {
|
||||
m = moment(data[i]._id);
|
||||
}
|
||||
let tag = m.day()
|
||||
if (tag != oldday) {
|
||||
m.startOf('day');
|
||||
days.push({color: 'lightgray', value: m.valueOf(), width: 1, zIndex: 2});
|
||||
oldday = tag;
|
||||
}
|
||||
}
|
||||
return days;
|
||||
};
|
||||
|
||||
export async function addSensorID2chart(chart, sensor, width) {
|
||||
let indoor = sensor.indoor || false
|
||||
let addr = await addAddress(sensor.sid)
|
||||
chart.renderer.label(
|
||||
`Sensor: <span class="bigger">${sensor.sid}</span> ${indoor ? ' (indoor)' : ''}<br /><br />${addr.street}<br />${addr.plz} ${addr.city}`,
|
||||
width / 2 - 150, 70)
|
||||
.css({
|
||||
fontSize: '14pt',
|
||||
'font-weight': 'bold',
|
||||
})
|
||||
.attr({
|
||||
zIndex: 3,
|
||||
}).add();
|
||||
}
|
||||
|
||||
export const showError = (err) => {
|
||||
const dialog = document.getElementById('dialogError')
|
||||
const body = dialog.querySelector('.dialog-body')
|
||||
body.innerHTML = err
|
||||
dialog.showModal()
|
||||
}
|
||||
|
||||
export const showReset = () => {
|
||||
const dialog = document.getElementById('dialogReset')
|
||||
dialog.showModal()
|
||||
}
|
||||
|
||||
// Setup dialog close buttons
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const errorClose = document.querySelector('#dialogError .dialog-close')
|
||||
if (errorClose) {
|
||||
errorClose.addEventListener('click', () => {
|
||||
document.getElementById('dialogError').close()
|
||||
})
|
||||
}
|
||||
|
||||
const resetClose = document.querySelector('#dialogReset .dialog-close')
|
||||
if (resetClose) {
|
||||
resetClose.addEventListener('click', () => {
|
||||
document.getElementById('dialogReset').close()
|
||||
})
|
||||
}
|
||||
|
||||
const resetOk = document.querySelector('#btnResetOk')
|
||||
if (resetOk) {
|
||||
resetOk.addEventListener('click', () => {
|
||||
document.getElementById('dialogReset').close()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// remove the taps (if shownig the map)
|
||||
export const removeTabs = () => {
|
||||
document.querySelector('#navi').style.display = 'none'
|
||||
}
|
||||
|
||||
// show thw tabs again
|
||||
export const showTabs = () => {
|
||||
if (activeTab !== 'maptab') {
|
||||
document.querySelector('#navi').style.display = 'block'
|
||||
}
|
||||
}
|
||||
|
||||
export const setCurrentTab = (tab) => {
|
||||
activeTab = tab
|
||||
}
|
||||
|
||||
export const getCurrentTab = () => {
|
||||
return activeTab
|
||||
}
|
||||
|
||||
export const fetchfromserver = async (url) => {
|
||||
const ret = await fetch(encodeURI(url))
|
||||
.catch(e => {
|
||||
showError(e)
|
||||
});
|
||||
// return await ret.json()
|
||||
let x = await ret.json()
|
||||
return x
|
||||
}
|
||||
|
||||
export const addAddress = async (sid) => {
|
||||
let url = `/srv/getaddress?sensorid=${sid}`
|
||||
let erg = await fetchfromserver(url)
|
||||
if (!erg.err) {
|
||||
return erg.address
|
||||
} else {
|
||||
return "no address"
|
||||
}
|
||||
}
|
||||
|
||||
export const cityCoords = async (city) => {
|
||||
let url = `/srv/getcitycoords?city=${city}`
|
||||
let erg = await fetchfromserver(url)
|
||||
if (!erg.err) {
|
||||
return erg.coords
|
||||
} else {
|
||||
return "no coordinates"
|
||||
}
|
||||
}
|
||||
|
||||
// table to distribute the different charts
|
||||
export const ttIndex = Object.freeze({
|
||||
map: 0,
|
||||
live: 1,
|
||||
havg: 2,
|
||||
davg: 3,
|
||||
daynight: 4,
|
||||
lden: 5
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
// set correct language (will be called by chglang.pug)
|
||||
let currentLang
|
||||
const navLang = navigator.language.slice(0,2)
|
||||
const selectedLang = localStorage.getItem('curlang')
|
||||
if (!selectedLang) {
|
||||
currentLang = navLang
|
||||
} else {
|
||||
currentLang = selectedLang
|
||||
}
|
||||
if (sensorid !== '') {
|
||||
window.location.href = `/${sensorid}?lng=${currentLang}`
|
||||
} else {
|
||||
window.location.href = `?lng=${currentLang}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// all date and time functions
|
||||
import { DateTime } from './luxon.min.js'
|
||||
import {loadAll} from './showcharts.js'
|
||||
import {getCurrentTab} from "./chart_utilities.js";
|
||||
|
||||
|
||||
// Show date and time (every minute)
|
||||
// Will be called every second
|
||||
export const showDate = (sofort, params) => {
|
||||
let d = DateTime.now()
|
||||
if(sofort || d.second === 0) {
|
||||
document.querySelector('#h1datum').innerHTML = d.toFormat('yyyy-MM-dd HH:mm')
|
||||
}
|
||||
if (((d.minute % params.refreshRate) == 0) && (d.second == 15)) { // alle ganzen refreshRate Minuten, 15sec danach
|
||||
let start = 1
|
||||
console.log(params.refreshRate, 'Minuten um, Grafik wird erneuert');
|
||||
let ct = getCurrentTab()
|
||||
if (ct !== 'maptab') {
|
||||
loadAll(params, start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const formatJSDate = (d) => {
|
||||
return DateTime.fromJSDate(d).toFormat('yyyy-LL-dd HH:mm:ss')
|
||||
}
|
||||
|
||||
export const formatISODate = (d) => {
|
||||
return DateTime.fromISO(d).toFormat('yyyy-LL-dd HH:mm:ss')
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Client side javascript
|
||||
"use strict"
|
||||
|
||||
import * as map from './map.js'
|
||||
import * as dt from './datetime.js'
|
||||
import {loadAll, showChart, setNoUTC, tabtable} from './showcharts.js'
|
||||
import { DateTime } from './luxon.min.js'
|
||||
import {ttIndex, setCurrentTab, getCurrentTab, showError, fetchfromserver, cityCoords} from "./chart_utilities.js";
|
||||
import * as spin from './spinner.js'
|
||||
|
||||
|
||||
(async () => {
|
||||
|
||||
// global variables
|
||||
const params = { // set defaults
|
||||
center: {name: 'Stuttgart', coords: [48.778, 9.180]},
|
||||
zoom: 10,
|
||||
span: 1,
|
||||
peak: 70,
|
||||
weeks: 4,
|
||||
datetime: '',
|
||||
seldate: '',
|
||||
refreshRate: 5,
|
||||
sid: -1
|
||||
}
|
||||
|
||||
let minDate = new Date(2023, 0, 1)
|
||||
|
||||
const setting = [
|
||||
{
|
||||
typ: 'maptab',
|
||||
show: ['ccity', 'odth'],
|
||||
val: ['centercity', 'olderthan'],
|
||||
param: [["center","name"], 'weeks']
|
||||
},
|
||||
{
|
||||
typ: 'livetab',
|
||||
show: ['stday', 'nbday'],
|
||||
val: ['startday', 'nbrofdays'],
|
||||
param: ['seldate', 'span']
|
||||
},{
|
||||
typ: 'houravgtab',
|
||||
show: ['stday','pklim'],
|
||||
val: ['startday', 'peaklim'],
|
||||
param: ['seldate', 'peak']
|
||||
},{
|
||||
typ: 'dayavgtab',
|
||||
show: ['stday','pklim'],
|
||||
val: ['startday', 'peaklim'],
|
||||
param: ['seldate', 'peak']
|
||||
},{
|
||||
typ: 'daynighttab',
|
||||
show: ['stday'],
|
||||
val: ['startday'],
|
||||
param: ['seldate']
|
||||
},{
|
||||
typ: 'ldentab',
|
||||
show: ['stday'],
|
||||
val: ['startday'],
|
||||
param: ['seldate']
|
||||
}
|
||||
]
|
||||
// END global variables
|
||||
|
||||
// localStorage.clear()
|
||||
|
||||
|
||||
// Check new map center
|
||||
const checkNewCenter = (oldC, newC) => {
|
||||
if((oldC[0] !== newC[0]) || (oldC[1] !== newC[1])) {
|
||||
map.setNewCenter(newC)
|
||||
}
|
||||
}
|
||||
|
||||
// Register events
|
||||
// Button 'My Location' pressed
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btnMyLocation = document.querySelector('#btnMyLocation');
|
||||
if (btnMyLocation) {
|
||||
btnMyLocation.addEventListener('click', () => {
|
||||
map.goToMyLocation();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Button 'Save' pressed
|
||||
document.querySelector('#btnSave').addEventListener('click', async () => {
|
||||
let curtab = getCurrentTab()
|
||||
for(let i = 0; i < setting.length; i++) {
|
||||
if(setting[i].typ === getCurrentTab()) {
|
||||
for(let j = 0; j < setting[i].show.length; j++) {
|
||||
let elem = document.getElementById(setting[i].val[j])
|
||||
let val = elem.value
|
||||
if(elem.type === 'checkbox') {
|
||||
val = elem.checked
|
||||
}
|
||||
let x = setting[i].param[j]
|
||||
console.log('x: ', x)
|
||||
if ( Array.isArray(x)) {
|
||||
params[setting[i].param[j][0]][setting[i].param[j][1]] = val
|
||||
} else {
|
||||
params[setting[i].param[j]] = val
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if (curtab !== 'maptab') {
|
||||
let starttime
|
||||
if ((params.seldate === 'today') || (params.seldate === 'heute') || (DateTime.now().toFormat('yyyy-MM-dd') === params.seldate)) {
|
||||
starttime = ''
|
||||
} else {
|
||||
starttime = DateTime.fromFormat(params.seldate, 'yyyy-MM-dd').toUTC().toISO()
|
||||
}
|
||||
params.datetime = starttime
|
||||
} else {
|
||||
const oldCoords = params.center.coords
|
||||
params.center.coords = await cityCoords(params.center.name)
|
||||
checkNewCenter(oldCoords, params.center.coords)
|
||||
localStorage.setItem('centercity',JSON.stringify(params.center))
|
||||
}
|
||||
let newlng = document.querySelector('#sellan input:checked').id
|
||||
let oldlng = localStorage.getItem('curlang')
|
||||
localStorage.setItem('curlang', newlng)
|
||||
if(newlng !== oldlng) {
|
||||
location.reload()
|
||||
}
|
||||
document.getElementById('dialogSettings').close()
|
||||
spin.spinner.spin(spin.spindiv)
|
||||
await loadAll(params,0, curtab)
|
||||
spin.spinner.stop()
|
||||
// ToDo:
|
||||
// Load ALL incl. MAP und LIVE hier, d.h. an loadAll einen zusätzlichen Parameter übergeben
|
||||
// if(params.sid !== undefined) {
|
||||
// await loadAll(params)
|
||||
// }
|
||||
})
|
||||
|
||||
// Btn 'Settings' pressed
|
||||
document.querySelector('#btnSet').addEventListener('click', () => {
|
||||
const curtab = getCurrentTab()
|
||||
const dialog = document.getElementById('dialogSettings')
|
||||
|
||||
// Set max and min dates for date input
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const minDateStr = minDate.toISOString().split('T')[0]
|
||||
const startdayInput = document.getElementById('startday')
|
||||
startdayInput.max = today
|
||||
startdayInput.min = minDateStr
|
||||
|
||||
// Convert seldate format if needed
|
||||
let seldate = params.seldate
|
||||
if (seldate === 'today' || seldate === 'heute') {
|
||||
seldate = today
|
||||
}
|
||||
|
||||
startdayInput.value = seldate
|
||||
document.getElementById('nbrofdays').value = params.span
|
||||
document.getElementById('peaklim').value = params.peak
|
||||
document.getElementById('olderthan').value = params.weeks
|
||||
const centercity = JSON.parse(localStorage.getItem('centercity'))
|
||||
document.getElementById('centercity').value = centercity.name
|
||||
|
||||
// select different infos depending on the selected tab
|
||||
// first clear all
|
||||
document.querySelector('#ccity').style.display = 'none'
|
||||
document.querySelector('#stday').style.display = 'none'
|
||||
document.querySelector('#nbday').style.display = 'none'
|
||||
document.querySelector('#pklim').style.display = 'none'
|
||||
document.querySelector('#odth').style.display = 'none'
|
||||
for(let i = 0; i < setting.length; i++) {
|
||||
if(setting[i].typ === curtab) {
|
||||
for(let j = 0; j < setting[i].show.length; j++) {
|
||||
document.querySelector(`#${setting[i].show[j]}`).style.display = 'inline'
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
dialog.showModal()
|
||||
})
|
||||
|
||||
// Close button for settings dialog
|
||||
document.querySelector('#dialogSettings .dialog-close').addEventListener('click', () => {
|
||||
document.getElementById('dialogSettings').close()
|
||||
})
|
||||
|
||||
// Close button (secondary button)
|
||||
document.querySelector('#btnClose').addEventListener('click', () => {
|
||||
document.getElementById('dialogSettings').close()
|
||||
})
|
||||
|
||||
const getSensorType = async (sid) => {
|
||||
let url = `/srv/getoneproperty?sensorid=${sid}`
|
||||
const data = await fetchfromserver(url)
|
||||
return data.props
|
||||
}
|
||||
|
||||
// main function:
|
||||
// show the map if called w/o any parameter
|
||||
// if called with a sensor-ID, show the live chart for this sensor
|
||||
async function main() {
|
||||
setNoUTC() // no UTC for HighCharts
|
||||
// set correct language
|
||||
let currentLang = localStorage.getItem('curlang')
|
||||
if ((currentLang === null) || (currentLang === '')) {
|
||||
currentLang = 'de'
|
||||
localStorage.setItem('curlang', currentLang)
|
||||
}
|
||||
let centercity
|
||||
try {
|
||||
centercity = JSON.parse(localStorage.getItem('centercity'))
|
||||
} catch (e) {
|
||||
centercity = params.center
|
||||
}
|
||||
if((centercity === null) || (centercity.name === '')) {
|
||||
centercity = params.center
|
||||
}
|
||||
localStorage.setItem('centercity', JSON.stringify(centercity))
|
||||
params.center = centercity
|
||||
if(currentLang === 'en') {
|
||||
document.querySelector('#en').checked = true
|
||||
params.seldate = 'today'
|
||||
} else {
|
||||
document.querySelector('#de').checked = true
|
||||
params.seldate = 'heute'
|
||||
}
|
||||
console.log(currentLang)
|
||||
if (parseInt(sysparams.csid) !== -1) {
|
||||
history.pushState({}, '', `/${sysparams.csid}`)
|
||||
} else {
|
||||
history.pushState({}, '', '/')
|
||||
}
|
||||
|
||||
dt.showDate(true, params)
|
||||
// and show date/time every minute
|
||||
setInterval(() => dt.showDate(false, params), 1000)
|
||||
|
||||
// initialise tabs
|
||||
const triggerTabList = [].slice.call(document.querySelectorAll('.tab-button'))
|
||||
triggerTabList.forEach(function (triggerEl) {
|
||||
triggerEl.addEventListener('click', function (event) {
|
||||
event.preventDefault()
|
||||
|
||||
// Remove active from all tabs
|
||||
triggerTabList.forEach(tab => tab.classList.remove('active'))
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'))
|
||||
|
||||
// Add active to clicked tab
|
||||
triggerEl.classList.add('active')
|
||||
const targetId = triggerEl.getAttribute('data-target')
|
||||
document.getElementById(targetId).classList.add('active')
|
||||
|
||||
let newtab = event.currentTarget.id
|
||||
setCurrentTab(newtab)
|
||||
let tab = tabtable.find(tab => tab.id === newtab)
|
||||
if (document.querySelector(`#${tab.id}err`).innerHTML !== '') { // if there is an error message, clear it
|
||||
showError(document.querySelector(`#${tab.id}err`).innerHTML)
|
||||
}
|
||||
if (newtab === 'maptab') {
|
||||
map.showMap(params)
|
||||
}
|
||||
})
|
||||
})
|
||||
const csid = parseInt(sysparams.csid)
|
||||
if(csid !== -1) {
|
||||
spin.spinner.spin(spin.spindiv)
|
||||
// check, if sid is of right type (noise)
|
||||
const props = await getSensorType(csid)
|
||||
if (props.type !== 'noise') {
|
||||
// wrong sensor type
|
||||
spin.spinner.stop()
|
||||
showError('Sensor ID ' + csid + ' is not of type noise')
|
||||
return
|
||||
}
|
||||
params.center.coords = [props.location[0].loc.coordinates[1], props.location[0].loc.coordinates[0]]
|
||||
params.sid = csid
|
||||
let t = triggerTabList[1]
|
||||
|
||||
// Switch to live tab
|
||||
triggerTabList.forEach(tab => tab.classList.remove('active'))
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'))
|
||||
t.classList.add('active')
|
||||
document.getElementById('t_live').classList.add('active')
|
||||
|
||||
setCurrentTab('livetab')
|
||||
let err = await showChart(params, ttIndex.live)
|
||||
spin.spinner.stop()
|
||||
if (err.error) {
|
||||
let errtxt = document.querySelector(`#${tabtable[ttIndex.live].id}err`).innerHTML
|
||||
if (errtxt !== '') {
|
||||
showError(errtxt)
|
||||
}
|
||||
}
|
||||
await loadAll(params)
|
||||
} else {
|
||||
// show the map
|
||||
map.showMap(params)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
||||
})()
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
import { DateTime } from './luxon.min.js'
|
||||
|
||||
export function logit(str) {
|
||||
let s = `${DateTime.now().toISO()} => ${str}`;
|
||||
console.log(s);
|
||||
}
|
||||
|
||||
export function logerror(str) {
|
||||
let s = `${DateTime.utc().toISO()} => *** ERROR *** ${str}`;
|
||||
console.log(s);
|
||||
}
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,99 @@
|
||||
// all function related to the map
|
||||
|
||||
import * as utils from "./chart_utilities.js"
|
||||
import * as mapn from './map_noise.js'
|
||||
import * as mapu from './map_utilities.js'
|
||||
|
||||
const mapparams = {
|
||||
map: null,
|
||||
bounds: null,
|
||||
popuptext: '',
|
||||
clickedSensor: 0,
|
||||
APIURL: '/srv/getmapdata?',
|
||||
MAXZOOM: 19
|
||||
}
|
||||
|
||||
export async function showMap(params) {
|
||||
if(sysparams.category === 'noise') {
|
||||
utils.removeTabs()
|
||||
if(mapparams.map && mapparams.map.remove) {
|
||||
mapparams.map.off();
|
||||
mapparams.map.remove();
|
||||
}
|
||||
}
|
||||
mapparams.map = L.map('map', {scrollWheelZoom: false}).setView(params.center.coords, parseInt(params.zoom))
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: mapparams.MAXZOOM,
|
||||
attribution: '© <a href="http://www.js.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(mapparams.map)
|
||||
mapparams.bounds = mapparams.map.getBounds();
|
||||
|
||||
mapparams.map.on('moveend', async function () {
|
||||
mapparams.bounds = mapparams.map.getBounds()
|
||||
await buildMarkers(params, mapparams)
|
||||
});
|
||||
|
||||
if(sysparams.category === 'noise'){
|
||||
mapn.buildLegend(mapparams)
|
||||
}
|
||||
|
||||
let lastdate = await buildMarkers(params, mapparams)
|
||||
mapu.showLastDate(lastdate, mapparams);
|
||||
}
|
||||
|
||||
const buildMarkers = async (params, mapparams) => {
|
||||
return mapn.buildMarkers(params, mapparams)
|
||||
}
|
||||
|
||||
export const setNewCenter = (center) => {
|
||||
mapparams.map.setView(center)
|
||||
}
|
||||
|
||||
export const goToMyLocation = () => {
|
||||
if (!navigator.geolocation) {
|
||||
alert('Geolocation wird von diesem Browser nicht unterstützt');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
const accuracy = position.coords.accuracy;
|
||||
|
||||
// Karte zur aktuellen Position zentrieren
|
||||
mapparams.map.setView([lat, lon], 15);
|
||||
|
||||
// Optional: Marker an aktueller Position setzen
|
||||
const myLocationMarker = L.marker([lat, lon], {
|
||||
icon: L.icon({
|
||||
iconUrl: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCI+PGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iOCIgZmlsbD0iIzQyODVGNCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIzIi8+PGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTUiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzQyODVGNCIgc3Ryb2tlLXdpZHRoPSIyIiBvcGFjaXR5PSIwLjMiLz48L3N2Zz4=',
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20]
|
||||
})
|
||||
}).addTo(mapparams.map);
|
||||
|
||||
myLocationMarker.bindPopup(`📍 Ihr Standort<br/>Genauigkeit: ±${Math.round(accuracy)}m`).openPopup();
|
||||
},
|
||||
(error) => {
|
||||
let errorMsg = 'Position konnte nicht ermittelt werden.';
|
||||
switch(error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
errorMsg = 'Bitte erlauben Sie den Zugriff auf Ihren Standort.';
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
errorMsg = 'Standortinformationen sind nicht verfügbar.';
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
errorMsg = 'Die Anfrage ist abgelaufen.';
|
||||
break;
|
||||
}
|
||||
alert(errorMsg);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 5000,
|
||||
maximumAge: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
|
||||
import * as dt from './datetime.js'
|
||||
import * as utils from "./chart_utilities.js"
|
||||
import { showChart, loadAll} from './showcharts.js'
|
||||
import * as mapu from './map_utilities.js'
|
||||
import * as spin from './spinner.js'
|
||||
|
||||
export const colorscale = ['#d53e4f', '#fc8d59', '#fee08b', '#e6f598', '#99d594', '#3288bd', '#808080'];
|
||||
const dba = [100, 80, 60, 40, 20, 0, -999];
|
||||
|
||||
export function getColor(d) {
|
||||
let val = parseInt(d);
|
||||
for (let i = 0; i < dba.length; i++) {
|
||||
if (val >= dba[i]) {
|
||||
return (colorscale[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const buildLegend = (mpp) => {
|
||||
let legend = L.control({position: 'bottomright'})
|
||||
legend.onAdd = function () {
|
||||
let div = L.DomUtil.create('div', 'info legend')
|
||||
let div_color = L.DomUtil.create('div', 'info legend inner', div)
|
||||
div_color.innerHTML += 'dbA<br />';
|
||||
// loop through our density intervals and generate a label with a colored square for each interval
|
||||
for (let i = 0; i < dba.length - 1; i++) {
|
||||
div_color.innerHTML +=
|
||||
'<i style="background:' + colorscale[i] + '"></i>' +
|
||||
' ' + dba[i] + (i == 0 ? '+' : '') + '<br />'
|
||||
}
|
||||
div_color.innerHTML += ' <i style="background:' + getColor(dba[dba.length - 1]) + '"></i> offline'
|
||||
return div
|
||||
};
|
||||
legend.addTo(mpp.map)
|
||||
}
|
||||
|
||||
export async function buildMarkers(params, mpp) {
|
||||
const url = mpp.APIURL + `type=noise&box=${mpp.bounds.toBBoxString()}`
|
||||
let sensors = await utils.fetchfromserver(url)
|
||||
if (!sensors.err) {
|
||||
let markers = L.markerClusterGroup({
|
||||
spiderfyOnMaxZoom: true,
|
||||
showCoverageOnHover: false,
|
||||
zoomToBoundsOnClick: true,
|
||||
// disableClusteringAtZoom: 14,
|
||||
iconCreateFunction: function (cluster) {
|
||||
let mymarkers = cluster.getAllChildMarkers();
|
||||
let color = getMedian(mymarkers);
|
||||
return new L.Icon({
|
||||
iconUrl: buildIcon(color, cluster.getChildCount(), 0),
|
||||
iconSize: [35, 35]
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
for (let x of sensors.values) {
|
||||
if ((params.weeks > 0) && (x.weeks > params.weeks)) {
|
||||
continue
|
||||
}
|
||||
if (x.value <= -5) {
|
||||
continue
|
||||
}
|
||||
let marker = L.marker([x.location[1], x.location[0]], {
|
||||
icon: new L.Icon({
|
||||
iconUrl: buildIcon(getColor(parseInt(x.value)), 0, x.indoor),
|
||||
iconSize: [35, 35],
|
||||
}),
|
||||
name: x.id,
|
||||
value: x.value,
|
||||
url: '/graph?sid=' + x.id,
|
||||
lastseen: dt.formatISODate(x.lastseen),
|
||||
indoor: x.indoor
|
||||
})
|
||||
.on('click', e => onMarkerClick(e, true, sensors.popuptxt, params, mpp)) // define click- and
|
||||
markers.addLayer(marker);
|
||||
}
|
||||
mpp.map.addLayer(markers);
|
||||
return sensors.lastdate
|
||||
} else {
|
||||
utils.showError(sensors.err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function buildIcon(color, n, indoor) {
|
||||
let x = 100;
|
||||
if (n < 10) {
|
||||
x = 200;
|
||||
} else if (n < 100) {
|
||||
x = 150;
|
||||
}
|
||||
let txtColor = "black";
|
||||
if (color == colorscale[5]) {
|
||||
txtColor = "white"
|
||||
}
|
||||
let icon = '<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600">' +
|
||||
'<circle cx="300" cy="300" r="250" fill="' + color + '" stroke="black" stroke-width="20" />';
|
||||
if (n !== 0) {
|
||||
icon +=
|
||||
'<text id="marker_text" x="' + x + '" ' +
|
||||
'y="400" font-size="1500%" font-family="Verdana,Lucida Sans Unicode,sans-serif" ' +
|
||||
'fill="' + txtColor + '">' + n + '</text>';
|
||||
}
|
||||
if(indoor) {
|
||||
icon += '<line x1="125" y1="475" x2="475" y2="125" stroke="black" stroke-width="20" />'
|
||||
}
|
||||
icon += '</svg>';
|
||||
return encodeURI("data:image/svg+xml," + icon).replace(new RegExp('#', 'g'), '%23');
|
||||
}
|
||||
|
||||
|
||||
async function bauPopupText(item, txts) {
|
||||
let addr = ''
|
||||
let offlinetext = `
|
||||
<tr><td colspan="2"><span style="color:red;">${txts.offline}</span></td></tr>
|
||||
<tr><td>${txts.lastseen}:</td><td>${item.lastseen}</td></tr>`
|
||||
|
||||
let normaltext = `
|
||||
<tr></tr><tr><td>LA_max:</td><td>${item.value}</td></tr>`
|
||||
let popuptext = `
|
||||
<div id="infoTitle">
|
||||
<h4>${txts.sensor}: ${item.name}</h4>
|
||||
<div class="addr">${addr}</div>
|
||||
<div id="infoTable">
|
||||
<table>
|
||||
<tr><td colspan="2"><span style="color:limegreen;">${item.indoor==1 ? "indoor" : ""}</span></td></tr>
|
||||
${item.value < 0 ? offlinetext : normaltext}
|
||||
</table>
|
||||
<div id="infoBtn">
|
||||
<a href="#" class="speciallink">${txts.showchart}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
return popuptext
|
||||
}
|
||||
|
||||
async function onMarkerClick(e, click, txts, params, mpp) {
|
||||
let item = e.target.options;
|
||||
let popuptext = await bauPopupText(item, txts)
|
||||
e.target.bindPopup(popuptext).openPopup(); // show the popup
|
||||
let addr = await utils.addAddress(item.name)
|
||||
document.querySelector('#infoTitle .addr').innerHTML = `${addr.street}<br />${addr.plz} ${addr.city}`
|
||||
let link = document.querySelector('.speciallink')
|
||||
link.addEventListener('click', async function (ev) {
|
||||
console.log(`Event clicked: ${mpp.clickedSensor}`)
|
||||
params.sid = item.name
|
||||
history.pushState({}, '', `/${params.sid}`)
|
||||
utils.setCurrentTab('livetab')
|
||||
spin.spinner.spin(spin.spindiv)
|
||||
let ok = await showChart(params, utils.ttIndex.live)
|
||||
spin.spinner.stop()
|
||||
if (ok) {
|
||||
// Switch to live tab
|
||||
let triggerEl = document.querySelector(`#livetab`)
|
||||
document.querySelectorAll('.tab-button').forEach(tab => tab.classList.remove('active'))
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'))
|
||||
triggerEl.classList.add('active')
|
||||
document.getElementById('t_live').classList.add('active')
|
||||
|
||||
params.center.coords = [e.latlng.lat, e.latlng.lng]
|
||||
ev.preventDefault()
|
||||
await loadAll(params)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// With all Markers in cluster (markers) calculate the median
|
||||
// of the values. With this median fetch the color and return it.
|
||||
// If there are 'offline' sensors (value == -1) strip then before
|
||||
// calculating the median. If there are only offline sensor, return
|
||||
// color of value==-1 (dark gray).
|
||||
function getMedian(markers, value) {
|
||||
markers.sort(function (a, b) { // first sort, the lowest first
|
||||
let y1 = a.options.value;
|
||||
let y2 = b.options.value;
|
||||
if (y1 < y2) {
|
||||
return -1;
|
||||
}
|
||||
if (y2 < y1) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
// console.log(markers);
|
||||
let i = 0; // now find the 'offlines' (value == -1)
|
||||
for (i = 0; i < markers.length; i++) {
|
||||
if (markers[i].options.value > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
markers.splice(0, i); // remove these from array
|
||||
let lang = markers.length;
|
||||
if (lang > 1) { //
|
||||
if ((lang % 2) == 1) { // uneven ->
|
||||
return getColor(markers[(Math.floor(lang / 2))].options.value); // median is in the middle
|
||||
} else { // evaen ->
|
||||
lang = lang / 2; // median is mean of both middle values
|
||||
// console.log(lang);
|
||||
let wert = (markers[lang - 1].options.value +
|
||||
markers[lang].options.value) / 2;
|
||||
return getColor(wert);
|
||||
}
|
||||
} else if (lang == 1) { // only one marker -> return its color
|
||||
return getColor(markers[0].options.value);
|
||||
}
|
||||
return getColor(-1); // only offlines
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as utils from "./chart_utilities.js"
|
||||
import * as dt from './datetime.js'
|
||||
|
||||
// Show the last date below tha map grafics
|
||||
export async function showLastDate(ld, mpp) {
|
||||
let url
|
||||
if(sysparams) {
|
||||
url = mpp.APIURL + 'type=noise'
|
||||
} else {
|
||||
url = mpp.APIURL + 'type=radiactivity'
|
||||
}
|
||||
let sensors = await utils.fetchfromserver(url)
|
||||
document.querySelector('#mapdate').innerText = `${sensors.valfromtxt} ${dt.formatISODate(ld).slice(0,-3)}`
|
||||
document.querySelector('#actsensors').innerText = `${sensors.actsensors} ${sensors.registered}`
|
||||
// $('#actsensors').html(`${erg.count} aktive Sensoren (angemeldet ${allsens})`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as utils from "./chart_utilities.js";
|
||||
import { DateTime } from './luxon.min.js'
|
||||
import * as map from "./map.js";
|
||||
import {fetchfromserver, showError} from "./chart_utilities.js";
|
||||
import * as spin from './spinner.js'
|
||||
|
||||
export const tabtable = [
|
||||
{id: 'maptab', type: 'map', container: 'map', func: map.showMap, div: null, dformat: null},
|
||||
{id: 'livetab', type: 'live', container: 'dlive', func: showChart, div: 2, dformat: "dd.LL - HH:mm:ss"},
|
||||
{id: 'houravgtab', type: 'havg', container: 'dhour', func: showChart, div: 1, dformat: "dd.LL - HH'h'"},
|
||||
{id: 'dayavgtab', type: 'davg', container: 'dday', func: showChart, div: 1, dformat: "dd.LL"},
|
||||
{id: 'daynighttab', type: 'daynight', container: 'ddaynight', func: showChart, div: 1, dformat: "dd.LL"},
|
||||
{id: 'ldentab', type: 'lden', container: 'dlden', func: showChart, div: 1, dformat: "dd.LL"},
|
||||
]
|
||||
|
||||
|
||||
export async function showChart(params, index) {
|
||||
let container = tabtable[index].container
|
||||
let typ = tabtable[index].type
|
||||
let id = tabtable[index].id
|
||||
function form() {
|
||||
for (let item of tabtable) {
|
||||
if (item.type === typ) {
|
||||
const d = DateTime.fromMillis(this.x)
|
||||
const d1 = d.plus({days: 1})
|
||||
let fmt = d.toFormat(item.dformat)
|
||||
if (((typ === 'daynight') && (this.series.name.startsWith('N'))) || (typ === 'lden')) {
|
||||
fmt = `${d.toFormat('dd')}/${d1.toFormat('dd.LL')}`
|
||||
}
|
||||
if (this.series.name === 'Peaks') {
|
||||
item.div = 0
|
||||
}
|
||||
return '<div style="border: 2px solid ' + this.point.color + '; padding: 3px;">' +
|
||||
fmt + '<br />' +
|
||||
'<span style="color: ' + this.point.color + '">● </span>' +
|
||||
this.series.name + ': <b>' +
|
||||
Highcharts.numberFormat(this.y, item.div) +
|
||||
'</b></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function xformat() {
|
||||
if (typ === 'lden') {
|
||||
let lbl = this.axis.defaultLabelFormatter.call(this);
|
||||
this.value += 86400000;
|
||||
let lbl1 = this.axis.defaultLabelFormatter.call(this);
|
||||
return parseInt(lbl) + '/' + lbl1;
|
||||
} else if ((typ === 'live') || (typ === 'havg')) {
|
||||
let v = this.axis.defaultLabelFormatter.call(this);
|
||||
if (v.indexOf(':') == -1) {
|
||||
return '<span style="font-weight:bold;color:red">' + v + '<span>';
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
} else {
|
||||
return this.axis.defaultLabelFormatter.call(this);
|
||||
}
|
||||
}
|
||||
let url = `/srv/getsensordata?sensorid=${params.sid}&data=${typ}&datetime=${params.datetime}`
|
||||
if (typ === 'live') {
|
||||
url += `&span=${params.span}`
|
||||
} else if ((typ === 'havg') || (typ === 'davg')) {
|
||||
url += `&peak=${params.peak}`
|
||||
}
|
||||
console.log(`fetch ${typ} from server ${url}`)
|
||||
|
||||
let errdiv = document.querySelector(`#${id}err`)
|
||||
errdiv.innerHTML = ''
|
||||
|
||||
let erg = await fetchfromserver(url)
|
||||
if(sysparams.category === 'noise') {
|
||||
utils.showTabs()
|
||||
}
|
||||
if (!erg.err) {
|
||||
erg.options.xAxis.labels.formatter = xformat
|
||||
erg.options.tooltip.formatter = form
|
||||
let chart = Highcharts.chart(container, erg.options, function (chart) {
|
||||
utils.addSensorID2chart(chart, {
|
||||
sid: erg.params.sid,
|
||||
indoor: erg.params.indoor
|
||||
}, document.getElementById(container).offsetWidth)
|
||||
if(erg.info) {
|
||||
let wbreit = window.innerWidth
|
||||
let cbreit = chart.chartWidth
|
||||
let infoposx = wbreit - ((wbreit-cbreit)/2) -200
|
||||
let infoposy = chart.plotTop - 80
|
||||
chart.renderer.label(
|
||||
erg.info.text,
|
||||
infoposx,
|
||||
infoposy,
|
||||
'rect', 0, 0, true)
|
||||
.css({
|
||||
fontSize: '10pt',
|
||||
color: 'green'
|
||||
})
|
||||
.attr({
|
||||
zIndex: 5,
|
||||
}).add();
|
||||
}
|
||||
})
|
||||
return {error: false}
|
||||
} else {
|
||||
errdiv.innerHTML = erg.err
|
||||
return {error: true}
|
||||
}
|
||||
}
|
||||
|
||||
export const loadAll = async (params, start = 2, curtab = '') => {
|
||||
console.log('now load all in Background')
|
||||
let messzeit = DateTime.now()
|
||||
if (start == 0) {
|
||||
map.showMap(params)
|
||||
start++
|
||||
}
|
||||
if (params.sid !== -1) {
|
||||
for (let i = start; i < tabtable.length; i++) {
|
||||
let err = await showChart(params, i)
|
||||
if(tabtable[i].id === curtab) {
|
||||
spin.spinner.stop()
|
||||
let errtxt = document.querySelector(`#${tabtable[i].id}err`).innerHTML
|
||||
if (errtxt !== '') {
|
||||
showError(errtxt)
|
||||
}
|
||||
}
|
||||
}
|
||||
let dauer = DateTime.now().diff(messzeit,['seconds']).toObject().seconds
|
||||
console.log(`now all loaded. Zeitdauer: ${dauer}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const setNoUTC = () => {
|
||||
Highcharts.setOptions({
|
||||
time: {
|
||||
useUTC: false // Don't use UTC on the charts
|
||||
},
|
||||
accessibility: { // supress warning concerning accessability
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Aufbau eine Spinner, der während des Ladens von AJAX-Requests angezeigt wird
|
||||
|
||||
import { Spinner } from '/spin.js/spin.js'
|
||||
|
||||
const spinneropts = {
|
||||
lines: 13, // The number of lines to draw
|
||||
length: 38, // The length of each line
|
||||
width: 17, // The line thickness
|
||||
radius: 45, // The radius of the inner circle
|
||||
scale: 1, // Scales overall size of the spinner
|
||||
corners: 1, // Corner roundness (0..1)
|
||||
speed: 1, // Rounds per second
|
||||
rotate: 0, // The rotation offset
|
||||
animation: 'spinner-line-fade-quick', // The CSS animation name for the lines
|
||||
direction: 1, // 1: clockwise, -1: counterclockwise
|
||||
color: '#404040', // CSS color or array of colors
|
||||
fadeColor: 'transparent', // CSS color or array of colors
|
||||
top: '50%', // Top position relative to parent
|
||||
left: '50%', // Left position relative to parent
|
||||
shadow: '0 0 1px transparent', // Box-shadow for the lines
|
||||
zIndex: 2000000000, // The z-index (defaults to 2e9)
|
||||
className: 'spinner', // The CSS class to assign to the spinner
|
||||
position: 'absolute', // Element positioning
|
||||
};
|
||||
|
||||
export const spinner = new Spinner(spinneropts)
|
||||
export const spindiv = document.getElementById('spinner')
|
||||
@@ -0,0 +1,60 @@
|
||||
.marker-cluster-small {
|
||||
background-color: rgba(181, 226, 140, 0.6);
|
||||
}
|
||||
.marker-cluster-small div {
|
||||
background-color: rgba(110, 204, 57, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-medium {
|
||||
background-color: rgba(241, 211, 87, 0.6);
|
||||
}
|
||||
.marker-cluster-medium div {
|
||||
background-color: rgba(240, 194, 12, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-large {
|
||||
background-color: rgba(253, 156, 115, 0.6);
|
||||
}
|
||||
.marker-cluster-large div {
|
||||
background-color: rgba(241, 128, 23, 0.6);
|
||||
}
|
||||
|
||||
/* IE 6-8 fallback colors */
|
||||
.leaflet-oldie .marker-cluster-small {
|
||||
background-color: rgb(181, 226, 140);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-small div {
|
||||
background-color: rgb(110, 204, 57);
|
||||
}
|
||||
|
||||
.leaflet-oldie .marker-cluster-medium {
|
||||
background-color: rgb(241, 211, 87);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-medium div {
|
||||
background-color: rgb(240, 194, 12);
|
||||
}
|
||||
|
||||
.leaflet-oldie .marker-cluster-large {
|
||||
background-color: rgb(253, 156, 115);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-large div {
|
||||
background-color: rgb(241, 128, 23);
|
||||
}
|
||||
|
||||
.marker-cluster {
|
||||
background-clip: padding-box;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.marker-cluster div {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 15px;
|
||||
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.marker-cluster span {
|
||||
line-height: 30px;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
|
||||
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
}
|
||||
|
||||
.leaflet-cluster-spider-leg {
|
||||
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
|
||||
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
|
||||
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
|
||||
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
|
||||
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
body {
|
||||
font-family: Verdana, "Lucida Sans Unicode", sans-serif;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0000EE;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: lightgray;
|
||||
}
|
||||
header #hline1 {
|
||||
margin: 0px 1vw 5px 1vw;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
header #h1name {
|
||||
font-size: 148%;
|
||||
clear: both;
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
header #buttonsRight {
|
||||
float: right;
|
||||
margin-right: 1%;
|
||||
}
|
||||
header #buttonsRight #btnSet {
|
||||
margin-right: 20px;
|
||||
}
|
||||
header #buttonsRight #btnHelp {
|
||||
margin-right: 20px;
|
||||
}
|
||||
header #buttonsRight #h1datum {
|
||||
float: right;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#fenster {
|
||||
margin-left: 0.5vw;
|
||||
background: white;
|
||||
padding-top: 5px;
|
||||
width: 99vw;
|
||||
}
|
||||
#fenster #navi {
|
||||
float: left;
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
|
||||
#map {
|
||||
margin-left: 0.5vw;
|
||||
margin-top: -50px;
|
||||
width: 98vw;
|
||||
height: 80vh;
|
||||
border-radius: 10px;
|
||||
border: solid 1px seagreen;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
#map #infoTable table, #map #infoTable tr, #map #infoTable td {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
border: none !important;
|
||||
}
|
||||
#map #infoBtn {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
#map #btnMyLocation {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
#map #btnMyLocation:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
#map #btnMyLocation:active {
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
#mapdateactsens {
|
||||
width: 98vw;
|
||||
font-size: 70%;
|
||||
padding: 10px 0 10px 0;
|
||||
overflow: auto;
|
||||
}
|
||||
#mapdateactsens #mapdate, #mapdateactsens #actsensors {
|
||||
width: 33%;
|
||||
float: left;
|
||||
}
|
||||
#mapdateactsens #actsensors {
|
||||
text-align: left;
|
||||
margin-left: 1vw;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: lightgray;
|
||||
}
|
||||
footer #author {
|
||||
font-size: 80%;
|
||||
width: 98vw;
|
||||
margin: auto;
|
||||
padding-bottom: 30px;
|
||||
padding-top: 10px;
|
||||
height: 50px;
|
||||
}
|
||||
footer #author #mailadr {
|
||||
float: left;
|
||||
}
|
||||
footer #author #versn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 6px 8px;
|
||||
font: 12px Arial, Helvetica, sans-serif;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.info h4 {
|
||||
margin: 0 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.legend {
|
||||
line-height: 18px;
|
||||
color: #555;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.legend i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
float: left;
|
||||
margin-right: 0px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.leglabel {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.ndmarker {
|
||||
position: absolute;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
margin-top: 44px;
|
||||
}
|
||||
|
||||
#dlive, #dhour, #dday, #ddaynight, #dlden {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
#dialogError {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border: red solid 2px;
|
||||
}
|
||||
|
||||
.highcharts-tooltip {
|
||||
zIndex: 5;
|
||||
}
|
||||
|
||||
.settings {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.thelabels {
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.theInputs {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.rows {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#startday {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
#nbrofdays, #peaklim, #olderthan {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.addit {
|
||||
width: 20%;
|
||||
border: 1px solid blue;
|
||||
}
|
||||
|
||||
.klein {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.infoTafel th, td {
|
||||
margin: auto;
|
||||
text-align: left;
|
||||
padding: 0 10px;
|
||||
border: 1px solid black;
|
||||
background-color: rgba(238, 238, 238, 0.6);
|
||||
}
|
||||
|
||||
.infoTafel th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.infoTafel tr {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
.infoTafel, .legend {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#stday, #nbday, #pklim, #odth {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#first {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addr {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bigger {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highcharts-container {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.errdiv {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #dee2e6;
|
||||
}
|
||||
|
||||
#btnSet, #btnHelp {
|
||||
background-color: #2D6AF8;
|
||||
color: white;
|
||||
border-color: #2D6AF8;
|
||||
}
|
||||
|
||||
#btnSet:hover, #btnHelp:hover {
|
||||
background-color: #1e5ae8;
|
||||
}
|
||||
|
||||
#btnMyLocation {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
background-color: #f1f1f1;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 0;
|
||||
margin: 0 0.5vw;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background-color: #2D6AF8;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px 4px 0 0;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background-color: #1e5ae8;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: white;
|
||||
color: #2D6AF8;
|
||||
border-bottom: 2px solid white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Dialog Styles */
|
||||
dialog {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 0;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dialog-button.primary {
|
||||
background-color: #2D6AF8;
|
||||
color: white;
|
||||
border-color: #2D6AF8;
|
||||
}
|
||||
|
||||
.dialog-button.primary:hover {
|
||||
background-color: #1e5ae8;
|
||||
}
|
||||
|
||||
.dialog-button.secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.dialog-button.secondary:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
/* Radio buttons */
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-label input[type="radio"] {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["style.sass","style.css"],"names":[],"mappings":"AAYA;EACE,uDAAA;EACA,eAAA;EACA,WAAA;EACA,iBAAA;EACA,kBAAA;EACA,YAAA;ACXF;;ADcA;EACE,YAAA;ACXF;;ADaA;EACE,YAAA;EACA,WAAA;EACA,kBAAA;EAEA,eAAA;EACA,YAAA;EACA,kBAAA;ACXF;;ADaA;EACE,cAAA;ACVF;;ADYA;EACE,2BA9BoB;ACqBtB;ADWE;EACE,uBAAA;EACA,iBAAA;EACA,oBAAA;EACA,cAAA;ACTJ;ADWE;EACE,eAAA;EACA,WAAA;EACA,WAAA;EACA,gBAAA;ACTJ;ADWE;EACE,YAAA;EACA,gBAAA;ACTJ;ADUI;EACE,kBAAA;ACRN;ADSI;EACE,kBAAA;ACPN;ADQI;EACE,YAAA;EACA,eAAA;ACNN;;ADQA;EACE,kBAAA;EACA,iBAAA;EACA,gBAAA;EACA,WAAA;ACLF;ADOE;EACE,WAAA;EACA,kBAAA;ACLJ;;ADOA;EACE,kBAAA;EACA,iBAAA;EACA,WAAA;EACA,YAAA;EACA,mBA7Ec;EA8Ed,0BAAA;EACA,gBAAA;EACA,UAAA;ACJF;ADME;EACE,YAAA;EACA,kBAAA;EACA,uBAAA;ACJJ;ADKE;EACE,gBAAA;EACA,kBAAA;ACHJ;;ADMA;EACE,WAAA;EACA,cAAA;EACA,sBAAA;EACA,cAAA;ACHF;ADKE;EACE,UAAA;EACA,WAAA;ACHJ;ADIE;EACE,gBAAA;EACA,gBAAA;ACFJ;;ADIA;EACE,2BAlGoB;ACiGtB;ADGE;EACE,cAAA;EACA,WAAA;EACA,YAAA;EACA,oBAAA;EACA,iBAAA;EACA,YAAA;ACDJ;ADGI;EACE,WAAA;ACDN;ADGI;EACE,YAAA;ACDN;;ADIA;EACE,gBAAA;EACA,uCAAA;EACA,oCAAA;EACA,uCAAA;EACA,kBAAA;ACDF;;ADGA;EACE,eAAA;EACA,WAAA;ACAF;;ADEA;EACE,iBAAA;EACA,WAAA;EACA,eAAA;ACCF;;ADCA;EACE,WAAA;EACA,YAAA;EACA,WAAA;EACA,iBAAA;EACA,UAAA;ACEF;;ADAA;EACE,iBAAA;ACGF;;ADDA;EACE,kBAAA;EACA,eAAA;ACIF;;ADFA;EACE,gBAAA;ACKF;;ADHA;EACE,WAAA;EACA,YAAA;EACA,YAAA;ACMF;;ADJA;EACE,UAAA;ACOF;;ADLA;EACE,qBAAA;ACQF;;ADNA;EACE,SAAA;ACSF;;ADNA;EACE,gBAAA;ACSF;;ADPA;EACE,UAAA;EACA,WAAA;ACUF;;ADPA;EAEE,WAAA;ACSF;;ADNA;EACE,WAAA;EACA,cAAA;ACSF;;ADPA;EACE,WAAA;EACA,qBAAA;EACA,mBAAA;ACUF;;ADRA;EACE,UAAA;ACWF;;ADTA;EACE,UAAA;ACYF;;ADVA;EACE,UAAA;EACA,sBAAA;ACaF;;ADXA;EACE,cAAA;ACcF;;ADZA;EACE,YAAA;EACA,gBAAA;EACA,eAAA;EACA,uBAAA;EACA,0CAAA;ACeF;;ADbA;EACE,kBAAA;ACgBF;;ADdA;EACE,YAAA;ACiBF;;ADfA;EACE;IACE,aAAA;ECkBF;AACF;ADjBA;EACE,yBArNa;EAsNb,WAAA;ACmBF;;ADjBA;EACE,cAzNa;EA0Nb,yBAAA;ACoBF;;ADjBA;EACE,aAAA;ACoBF;;ADfA;EACE,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,kBAAA;ACkBF;;ADhBA;EACE,iBAAA;EACA,mBAAA;ACmBF;;ADjBA;EACE,eAAA;EACA,iBAAA;ACoBF;;ADlBA;EACE,iBAAA;ACqBF;;ADnBA;EACE,aAAA;ACsBF","file":"style.css"}
|
||||
@@ -0,0 +1,257 @@
|
||||
$corner-radius: 10px
|
||||
$border-color: seagreen
|
||||
$background: rgb(146, 154, 146)
|
||||
$divcolor: rgb(162, 227, 162)
|
||||
$table-stripes: rgb(157, 179, 157)
|
||||
$fieldset-color: rgb(208, 235, 208)
|
||||
$button-color: darkslategrey
|
||||
$header_footer_color: lightgray
|
||||
$checkboxsize: 1.5
|
||||
$inboxwidth: 70px
|
||||
$navtab-color: #2D6AF8
|
||||
|
||||
body
|
||||
font-family: Verdana, 'Lucida Sans Unicode', sans-serif
|
||||
font-size: 16px
|
||||
width: 100%
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
height: 100%
|
||||
// background-color: rgba(0,0,0,0.9)
|
||||
|
||||
html
|
||||
height: 100%
|
||||
|
||||
#wrapper
|
||||
margin: auto
|
||||
width: 100%
|
||||
text-align: center
|
||||
// background-color: #e9e9e9
|
||||
margin-top: 5px
|
||||
height: 100%
|
||||
position: relative
|
||||
|
||||
a
|
||||
color: #0000EE
|
||||
|
||||
header
|
||||
background-color: $header_footer_color
|
||||
|
||||
#hline1
|
||||
margin: 0px 1vw 5px 1vw
|
||||
padding-top: 10px
|
||||
padding-bottom: 10px
|
||||
overflow: auto
|
||||
|
||||
#h1name
|
||||
font-size: 148%
|
||||
clear: both
|
||||
float: left
|
||||
text-align: left
|
||||
|
||||
#buttonsRight
|
||||
float: right
|
||||
margin-right: 1%
|
||||
#btnSet
|
||||
margin-right: 20px
|
||||
#btnHelp
|
||||
margin-right: 20px
|
||||
#h1datum
|
||||
float: right
|
||||
margin-top: 5px
|
||||
|
||||
#fenster
|
||||
margin-left: 0.5vw
|
||||
background: white
|
||||
padding-top: 5px
|
||||
width: 99vw
|
||||
|
||||
#navi
|
||||
float: left
|
||||
margin-left: 0.5vw
|
||||
|
||||
#map
|
||||
margin-left: 0.5vw
|
||||
margin-top: -50px
|
||||
width: 98vw
|
||||
height: 80vh
|
||||
border-radius: $corner-radius
|
||||
border: solid 1px $border-color
|
||||
overflow: hidden
|
||||
z-index: 1
|
||||
|
||||
#infoTable table, #infoTable tr, #infoTable td
|
||||
margin: auto
|
||||
text-align: center
|
||||
border: none !important
|
||||
#infoBtn
|
||||
margin-top: 10px
|
||||
text-align: center
|
||||
|
||||
|
||||
#mapdateactsens
|
||||
width: 98vw
|
||||
font-size: 70%
|
||||
padding: 10px 0 10px 0
|
||||
overflow: auto
|
||||
|
||||
#mapdate, #actsensors
|
||||
width: 33%
|
||||
float: left
|
||||
#actsensors
|
||||
text-align: left
|
||||
margin-left: 1vw
|
||||
|
||||
footer
|
||||
background-color: $header_footer_color
|
||||
|
||||
#author
|
||||
font-size: 80%
|
||||
width: 98vw
|
||||
margin: auto
|
||||
padding-bottom: 30px
|
||||
padding-top: 10px
|
||||
height: 50px
|
||||
|
||||
#mailadr
|
||||
float: left
|
||||
|
||||
#versn
|
||||
float: right
|
||||
|
||||
|
||||
.info
|
||||
padding: 6px 8px
|
||||
font: 12px Arial, Helvetica, sans-serif
|
||||
background: rgba(255,255,255,0.8)
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.2)
|
||||
border-radius: 5px
|
||||
|
||||
.info h4
|
||||
margin: 0 0 5px
|
||||
color: #777
|
||||
|
||||
.legend
|
||||
line-height: 18px
|
||||
color: #555
|
||||
font-size: 110%
|
||||
|
||||
.legend i
|
||||
width: 18px
|
||||
height: 18px
|
||||
float: left
|
||||
margin-right: 0px
|
||||
opacity: 1
|
||||
|
||||
.leglabel
|
||||
padding-top: 20px
|
||||
|
||||
.ndmarker
|
||||
position: absolute
|
||||
font-size: 24px
|
||||
|
||||
.tab-pane
|
||||
margin-top: 44px
|
||||
|
||||
#dlive, #dhour, #dday, #ddaynight, #dlden
|
||||
width: 100%
|
||||
margin: auto
|
||||
height: 80vh
|
||||
|
||||
#dialogError
|
||||
color: red
|
||||
|
||||
.modal-content
|
||||
border: red solid 2px
|
||||
|
||||
.highcharts-tooltip
|
||||
zIndex: 5
|
||||
|
||||
|
||||
.settings
|
||||
text-align: left
|
||||
|
||||
.thelabels
|
||||
width: 50%
|
||||
float: left
|
||||
// border: 1px solid green
|
||||
|
||||
.theInputs
|
||||
//width: 60%
|
||||
float: left
|
||||
// border: 1px red solid
|
||||
|
||||
.rows
|
||||
width: 100%
|
||||
display: block
|
||||
|
||||
.column
|
||||
width: 100%
|
||||
display: inline-block
|
||||
margin-bottom: 15px
|
||||
|
||||
#startday
|
||||
width: 40%
|
||||
|
||||
#nbrofdays, #peaklim, #olderthan
|
||||
width: 13%
|
||||
|
||||
.addit
|
||||
width: 20%
|
||||
border: 1px solid blue
|
||||
|
||||
.klein
|
||||
font-size: 80%
|
||||
|
||||
.infoTafel th,td
|
||||
margin: auto
|
||||
text-align: left
|
||||
padding: 0 10px
|
||||
border: 1px solid black
|
||||
background-color: rgba(238,238,238,0.6)
|
||||
|
||||
.infoTafel th
|
||||
text-align: center
|
||||
|
||||
.infoTafel tr
|
||||
height: 20px
|
||||
|
||||
@media only screen and (max-width: 700px)
|
||||
.infoTafel, .legend
|
||||
display: none
|
||||
|
||||
.nav-tabs .nav-item .nav-link
|
||||
background-color: $navtab-color
|
||||
color: #FFF
|
||||
|
||||
.nav-tabs .nav-item .nav-link.active
|
||||
color: $navtab-color
|
||||
background-color: #FFFFFF
|
||||
|
||||
// for settings: set all to 'none'
|
||||
#stday, #nbday, #pklim, #odth
|
||||
display: none
|
||||
|
||||
|
||||
|
||||
// for first page to show 'Loading ...'
|
||||
#first
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
text-align: center
|
||||
|
||||
.addr
|
||||
font-weight: bold
|
||||
margin-bottom: 10px
|
||||
|
||||
.bigger
|
||||
font-size: 120%
|
||||
font-weight: bold
|
||||
|
||||
.highcharts-container
|
||||
margin-top: -10px
|
||||
|
||||
.errdiv
|
||||
display: none
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import express from 'express'
|
||||
import axios from 'axios'
|
||||
import * as prep from '../charts/preparecharts.js'
|
||||
import { getLanguage } from '../charts/utilities.js'
|
||||
import { translate as trans } from '../routes/index.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
const disttable = [
|
||||
{type: 'live', func: prep.liveData },
|
||||
{type: 'havg', func: prep.havgData },
|
||||
{type: 'davg', func: prep.davgData },
|
||||
{type: 'daynight', func: prep.dayNightData },
|
||||
{type: 'lden', func: prep.ldenData },
|
||||
{type: 'map', func: prep.mapData },
|
||||
]
|
||||
|
||||
let APIHOST = process.env.APIHOST || 'http://localhost:3005'
|
||||
const API_KEY = process.env.API_KEY
|
||||
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/:cmd', async function(req, res, next) {
|
||||
// req.i18n.changeLanguage(req.query.lng)
|
||||
const calledAs = req.baseUrl
|
||||
const cmd = req.params.cmd
|
||||
const pretty = (req.query.pretty !== undefined)
|
||||
const lng = getLanguage()
|
||||
|
||||
let url = APIHOST + '/api' + req.originalUrl.slice(4) + `&lng=${lng}`
|
||||
try {
|
||||
const response = await axios.get(encodeURI(url) , {
|
||||
headers: {'X-API-Key': API_KEY}
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
res.json({err: `${trans('ESYSCALL')} status=${response.status}`})
|
||||
}
|
||||
if(response.data.err) {
|
||||
res.json(response.data)
|
||||
return
|
||||
}
|
||||
// if called as '/api' then directly return the data
|
||||
if ((calledAs === '/api') || (cmd === 'getoneproperty') || (cmd === 'getaddress') || (cmd === 'getcitycoords')) {
|
||||
if(pretty) {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(JSON.stringify(response.data,null,2))
|
||||
} else {
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.json(response.data)
|
||||
}
|
||||
return
|
||||
}
|
||||
const options = response.data.options
|
||||
for(let x of disttable) {
|
||||
if (x.type === options.data) {
|
||||
let ret = x.func(options, response.data.values, req)
|
||||
res.json(ret)
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
res.json({err: `${trans('ESYSCALL')} ${e}`})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
|
||||
|
||||
// ToDo
|
||||
// Hier : getsensordata und getmapdata unterbringen und damit den externen Server aufrufen
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import express from 'express'
|
||||
import pkg from '../package.json' with { type: "json" }
|
||||
import { setLanguage } from '../charts/utilities.js'
|
||||
|
||||
const router = express.Router()
|
||||
const { name, version, date } = pkg
|
||||
|
||||
const renderOpts = (sid, cat) => {
|
||||
return {
|
||||
title: 'Noise',
|
||||
version: version,
|
||||
date: date.slice(0, 10),
|
||||
csensor: sid,
|
||||
category: cat
|
||||
}
|
||||
}
|
||||
|
||||
let i18n;
|
||||
|
||||
const translate = (x) => {
|
||||
return i18n.t(x)
|
||||
}
|
||||
|
||||
const getIndex = (cat) => {
|
||||
return 'index_noise'
|
||||
}
|
||||
|
||||
|
||||
/* GET home page. If there is no parameter 'lng=xx', check stored language and call
|
||||
te page again with correct language (chglang.js))
|
||||
*/
|
||||
router.get('/', function (req, res, next) {
|
||||
// req.i18n.changeLanguage('de')
|
||||
if (req.query.lng === undefined) {
|
||||
res.render('chglang', {sensorid: ''})
|
||||
} else {
|
||||
i18n = req.i18n
|
||||
let opts = renderOpts(-1, req.app.get('category'))
|
||||
setLanguage(req.query.lng)
|
||||
res.render(getIndex(opts.category), opts)
|
||||
}
|
||||
})
|
||||
|
||||
//
|
||||
// router.get('/', function(req, res, next) {
|
||||
// // req.i18n.changeLanguage('de')
|
||||
// res.render('index', renderOpts(-1))
|
||||
// })
|
||||
|
||||
|
||||
router.get('/:sid', async function (req, res, next) {
|
||||
const sid = req.params.sid
|
||||
let opts
|
||||
if (req.query.lng === undefined) {
|
||||
res.render('chglang', { sensorid: sid })
|
||||
} else {
|
||||
i18n = req.i18n
|
||||
if (isNaN(sid)) {
|
||||
opts = renderOpts(-1, req.app.get('category'))
|
||||
} else {
|
||||
opts = renderOpts(sid, req.app.get('category'))
|
||||
}
|
||||
res.render(getIndex(opts.category), opts)
|
||||
}
|
||||
})
|
||||
|
||||
//export default router
|
||||
|
||||
export { router, translate }
|
||||
@@ -0,0 +1,13 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1" charset="utf-8")
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
script.
|
||||
const sensorid = '#{sensorid}';
|
||||
|
||||
body
|
||||
#first
|
||||
h1 Loading...
|
||||
script(type="module" src="/javascripts/checklang.js")
|
||||
@@ -0,0 +1,6 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
||||
@@ -0,0 +1,114 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
header
|
||||
#hline1
|
||||
#h1name #{t("NoiseMeasurement")}
|
||||
#buttonsRight
|
||||
button#btnSet(value='set') #{t("Settings")}
|
||||
// button#btnHelp(value='help') #{t("Info")}
|
||||
#h1datum
|
||||
|
||||
#fenster
|
||||
#spinner
|
||||
#navi
|
||||
.tabs#tablist
|
||||
button.tab-button.active#maptab(data-target="t_map" type="button" role="tab" aria-controls="map" aria-selected="true") #{t("Map")}
|
||||
button.tab-button#livetab(data-target="t_live" type="button" role="tab" aria-controls="live") #{t("Live")}
|
||||
button.tab-button#houravgtab(data-target="t_houravg" type="button" role="tab" aria-controls="houravg") #{t("Hour_AVG")}
|
||||
button.tab-button#dayavgtab(data-target="t_dayavg" type="button" role="tab" aria-controls="dayavg") #{t("Day_AVG")}
|
||||
button.tab-button#daynighttab(data-target="t_daynight" type="button" role="tab" aria-controls="daynight") #{t("Day_Night")}
|
||||
button.tab-button#ldentab(data-target="t_lden" type="button" role="tab" aria-controls="lden") L<sub>DEN</sub>-Index
|
||||
.tab-content#thetabs
|
||||
.tab-pane.active#t_map(role="tabpanel")
|
||||
#map
|
||||
// button#btnMyLocation(type="button" title=t("MyLocation"))
|
||||
span 📍
|
||||
#maptaberr.errdiv
|
||||
#mapdateactsens
|
||||
#actsensors
|
||||
#mapdate
|
||||
.tab-pane#t_live(role="tabpanel")
|
||||
#dlive
|
||||
#livetaberr.errdiv
|
||||
.tab-pane#t_houravg(role="tabpanel")
|
||||
#dhour
|
||||
#houravgtaberr.errdiv
|
||||
.tab-pane#t_dayavg(role="tabpanel")
|
||||
#dday
|
||||
#dayavgtaberr.errdiv
|
||||
.tab-pane#t_daynight(role="tabpanel")
|
||||
#ddaynight
|
||||
#daynighttaberr.errdiv
|
||||
.tab-pane#t_lden(role="tabpanel")
|
||||
#dlden
|
||||
#ldentaberr.errdiv
|
||||
|
||||
|
||||
// Error-Dialog
|
||||
dialog#dialogError
|
||||
.dialog-content
|
||||
.dialog-header
|
||||
h3.dialog-title #{t("Error")}
|
||||
button.dialog-close(type="button" aria-label="Close") ×
|
||||
.dialog-body
|
||||
.dialog-footer
|
||||
|
||||
|
||||
// Settings-Dialog
|
||||
dialog#dialogSettings
|
||||
.dialog-content
|
||||
.dialog-header
|
||||
h3.dialog-title #{t("Settings")}
|
||||
button.dialog-close(type="button" aria-label="Close") ×
|
||||
.dialog-body.settings
|
||||
.rows
|
||||
.column#ccity
|
||||
label.thelabels(for='city') #{t("CenterMap")}:
|
||||
input.theInputs#centercity(name="centercity")
|
||||
.column#stday
|
||||
label.thelabels(for='startday') #{t("StartDate")}:
|
||||
input.theInputs#startday(type='date' name='startday')
|
||||
.column
|
||||
.column#nbday
|
||||
label.thelabels(for='nbrofdays')
|
||||
| #{t("NumberOfDays")}:<br />
|
||||
input.theInputs#nbrofdays(type='number' name='nbrofdays' min="1" max="10")
|
||||
span.klein (max. 10)
|
||||
.column#pklim
|
||||
label.thelabels(for='peaklim')
|
||||
| #{t("Count_peaks_over")}
|
||||
input.theInputs#peaklim(type='number' name='peaklim' min="10" max="130")
|
||||
| dbA
|
||||
span.klein (max. 130)
|
||||
.column#odth
|
||||
label.thelabels(for='olderthan')
|
||||
| #{t("RemoveFromMap")}
|
||||
input.theInputs#olderthan(type='number' name='olderthan' min="0" max="52")
|
||||
| #{t("weeks")}
|
||||
span.klein (max. 52)
|
||||
.column
|
||||
.column#sellan
|
||||
label.thelabels
|
||||
| #{t("Language")}:
|
||||
.radio-group
|
||||
label.radio-label
|
||||
input#de(type="radio" name="lanbut")
|
||||
span Deutsch
|
||||
label.radio-label
|
||||
input#en(type="radio" name="lanbut" checked)
|
||||
span English
|
||||
.dialog-footer
|
||||
button.dialog-button.secondary#btnClose(type="button") #{t("Close")}
|
||||
button.dialog-button.primary#btnSave(type="button") #{t("Save_changes")}
|
||||
|
||||
|
||||
// Reset-Dialog
|
||||
dialog#dialogReset
|
||||
.dialog-content
|
||||
.dialog-header
|
||||
h3.dialog-title Info
|
||||
button.dialog-close(type="button" aria-label="Close") ×
|
||||
.dialog-body
|
||||
.dialog-footer
|
||||
button.dialog-button.primary#btnResetOk(type="button") OK
|
||||
@@ -0,0 +1,37 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1" charset="utf-8")
|
||||
link(rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
|
||||
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
|
||||
crossorigin="")
|
||||
link(rel='stylesheet', href='/stylesheets/MarkerCluster.css')
|
||||
link(rel='stylesheet', href='/spin.js/spin.css')
|
||||
|
||||
|
||||
script.
|
||||
const sysparams = {
|
||||
version: '#{version}',
|
||||
date: '#{date}',
|
||||
csid: '#{csensor}',
|
||||
category: '#{category}'
|
||||
};
|
||||
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
|
||||
body
|
||||
#wrapper
|
||||
block content
|
||||
footer
|
||||
#author
|
||||
#mailadr
|
||||
a(href="mailto:rexfue@gmail.com") mailto:rexfue@gmail.com
|
||||
#versn #{t('Version')}: #{version} #{t('from')} #{date}
|
||||
|
||||
script(src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
|
||||
integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
|
||||
crossorigin="")
|
||||
script(src="/javascripts/leaflet.markercluster.js")
|
||||
script(src="https://code.highcharts.com/highcharts.js")
|
||||
script(type="module" src="/javascripts/global.js")
|
||||
Reference in New Issue
Block a user