|
|
|
<!doctype html>
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8" />
|
|
|
|
<title>openSenseMap data aggregation</title>
|
|
|
|
<style>
|
|
|
|
body {
|
|
|
|
font-family: sans-serif;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
details { padding: 10px; }
|
|
|
|
th, td { padding: 10px; text-align: right; }
|
|
|
|
tr:nth-child(odd) { background: #eee; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
|
|
|
|
<body>
|
|
|
|
<h1 id="title">loading ...</h1>
|
|
|
|
<h4 id="subtitle"></h4>
|
|
|
|
|
|
|
|
<div>
|
|
|
|
<button onclick="loadConfig('&phenomenon=Niederschlag&operation=sum')">Niederschlag</button>
|
|
|
|
<button onclick="loadConfig('&phenomenon=Temperatur&operation=arithmeticMean')">ø Temperatur</button>
|
|
|
|
<button onclick="loadConfig('&phenomenon=Temperatur&operation=max')">max. Temperatur</button>
|
|
|
|
<button onclick="loadConfig('&phenomenon=Temperatur&operation=min')">min. Temperatur</button>
|
|
|
|
<button onclick="loadConfig('&phenomenon=PM2.5&operation=max')">max. Feinstaub</button>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<button onclick="loadConfig('&windowSize=30 days', reset=false)">Monat</button>
|
|
|
|
<button onclick="loadConfig('&windowSize=1 week', reset=false)">Woche</button>
|
|
|
|
<button onclick="loadConfig('&windowSize=1 day', reset=false)">Tag</button>
|
|
|
|
<button onclick="loadConfig('&windowSize=1 hour', reset=false)">Stunde</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<details>
|
|
|
|
<summary>Advanced Usage</summary>
|
|
|
|
<p>
|
|
|
|
Shown data can be modified through the URL search query. Supported keys are:
|
|
|
|
<code>boxId, phenomenon, start, end, windowSize, operation</code>
|
|
|
|
For supported values check out the <a href="https://docs.opensensemap.org/#api-Statistics-descriptive" target="_blank">openSenseMap API docs</a>.
|
|
|
|
</p>
|
|
|
|
<p>
|
|
|
|
Example:
|
|
|
|
<code>
|
|
|
|
?boxId=5b26181b1fef04001b69093c&phenomenon=Temperatur&operation=max&windowSize=14 days&start=2019-10-01T00:00:00.000Z
|
|
|
|
</code>
|
|
|
|
</p>
|
|
|
|
</details>
|
|
|
|
|
|
|
|
<br/>
|
|
|
|
|
|
|
|
<div id="status"></div>
|
|
|
|
|
|
|
|
<table id="datatable"></table>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
const defaultConfig = {
|
|
|
|
boxId: '5b26181b1fef04001b69093c',
|
|
|
|
phenomenon: 'Niederschlag',
|
|
|
|
operation: 'sum',
|
|
|
|
start: new Date('2019-04-01').toISOString(),
|
|
|
|
end: new Date().toISOString(),
|
|
|
|
windowSize: '30 days',
|
|
|
|
}
|
|
|
|
|
|
|
|
const config = Object.assign(defaultConfig, parseQuery())
|
|
|
|
main(config).catch(console.error)
|
|
|
|
|
|
|
|
async function main (config) {
|
|
|
|
try {
|
|
|
|
const data = await fetchOsemDataAggregate(config)
|
|
|
|
|
|
|
|
const table = document.querySelector('#datatable')
|
|
|
|
populateTable(table, data, config)
|
|
|
|
|
|
|
|
const title = `${data['boxName']} - ${data['phenomenon']}`
|
|
|
|
document.querySelector('#title').innerText = title
|
|
|
|
document.title = title
|
|
|
|
const subtitle = `${config.operation} per ${config.windowSize}`
|
|
|
|
document.querySelector('#subtitle').innerText = subtitle
|
|
|
|
} catch (err) {
|
|
|
|
document.querySelector('#status').innerText = err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseQuery () {
|
|
|
|
const kvPairs = window.location.search
|
|
|
|
.slice(1) // remove leading ?
|
|
|
|
.split('&') // create list of { key: value } objects
|
|
|
|
.map(kv => {
|
|
|
|
[k,v] = kv.split('=')
|
|
|
|
return { [k]: decodeURIComponent(v) }
|
|
|
|
})
|
|
|
|
|
|
|
|
return Object.assign.apply(null, kvPairs) // combine objects
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchOsemDataAggregate (config) {
|
|
|
|
const {
|
|
|
|
boxId,
|
|
|
|
phenomenon,
|
|
|
|
operation,
|
|
|
|
start,
|
|
|
|
end,
|
|
|
|
windowSize,
|
|
|
|
} = config
|
|
|
|
|
|
|
|
const url = `https://api.opensensemap.org/statistics/descriptive?boxid=${boxId}&from-date=${start}&to-date=${end}&phenomenon=${phenomenon}&window=${windowSize}&operation=${operation}&columns=boxName,unit,phenomenon&format=json&download=false`
|
|
|
|
const res = await fetch(url)
|
|
|
|
return (await res.json())[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
function populateTable (table, data, config) {
|
|
|
|
const dateFormat = d => new Date(d).toLocaleDateString('de')
|
|
|
|
const valFormat = v => Math.round(v * (10 ** 2)) / (10 ** 2)
|
|
|
|
const addRow = (left, right) => {
|
|
|
|
const row = document.createElement('tr')
|
|
|
|
row.innerHTML = `<td>${left}</td><td>${right}</td>`
|
|
|
|
table.appendChild(row)
|
|
|
|
}
|
|
|
|
|
|
|
|
const dates = Object.keys(data).filter(k => k.startsWith('20')).sort()
|
|
|
|
|
|
|
|
for (let i = 0; i < dates.length; i++) {
|
|
|
|
const dateFrom = dates[i]
|
|
|
|
const dateTo = dates[i + 1] || new Date()
|
|
|
|
const val = data[dates[i]]
|
|
|
|
|
|
|
|
const left = `${dateFormat(dateFrom)} - ${dateFormat(dateTo)}`
|
|
|
|
const right = `${valFormat(val)} ${data['unit']}`
|
|
|
|
addRow(left, right)
|
|
|
|
}
|
|
|
|
|
|
|
|
// add aggregate of all values
|
|
|
|
let aggregate = null
|
|
|
|
switch (config.operation) {
|
|
|
|
case 'sum':
|
|
|
|
aggregate = dates.map(d => data[d]).reduce((s, v) => s += v, 0)
|
|
|
|
break
|
|
|
|
case 'arithmeticMean':
|
|
|
|
aggregate = dates.map(d => data[d]).reduce((s, v) => s += v, 0) / dates.length
|
|
|
|
break
|
|
|
|
case 'max':
|
|
|
|
aggregate = Math.max.apply(null, dates.map(d => data[d]))
|
|
|
|
break
|
|
|
|
case 'min':
|
|
|
|
aggregate = Math.min.apply(null, dates.map(d => data[d]))
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if (aggregate)
|
|
|
|
addRow(config.operation, `${valFormat(aggregate)} ${data['unit']}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// click handler of the UI buttons.
|
|
|
|
function loadConfig (queryString, reset = true) {
|
|
|
|
if (reset)
|
|
|
|
window.location.search = queryString // this assignment reloads the page
|
|
|
|
else
|
|
|
|
window.location.search += queryString
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|