Initial monorepo: readin, sensorapi, noise (Node22, npm ci, axios1.x, i18next-fs-backend)

This commit is contained in:
rxf
2026-07-25 11:35:41 +00:00
commit 2df1f79617
97 changed files with 13547 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+276
View File
@@ -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: &nbsp;<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
})
+15
View File
@@ -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}`
}
+30
View File
@@ -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')
}
+303
View File
@@ -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
+12
View File
@@ -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);
}
File diff suppressed because one or more lines are too long
+99
View File
@@ -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: '&copy; <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
}
);
}
+210
View File
@@ -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>' +
'&nbsp;&nbsp;' + dba[i] + (i == 0 ? '+' : '') + '<br />'
}
div_color.innerHTML += '&nbsp;<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
}
+17
View File
@@ -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})`)
}
+141
View File
@@ -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 + '">&#9679;&nbsp;</span>' +
this.series.name + ':&nbsp; <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
}
});
}
+27
View File
@@ -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;
}
+487
View File
@@ -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 */
+1
View File
@@ -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"}
+257
View File
@@ -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