353 lines
14 KiB
HTML
Executable File
353 lines
14 KiB
HTML
Executable File
<!DOCTYPE HTML>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Air Quality Visualization</title>
|
|
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
<link rel="stylesheet" href="resources/bootstrap.min.css">
|
|
<link rel="stylesheet" href="resources/leaflet.css">
|
|
<script src="resources/jquery-3.5.1.min.js"></script>
|
|
<script src="resources/bootstrap.bundle.min.js"></script>
|
|
<script src="resources/leaflet.js"></script>
|
|
<style type="text/css">
|
|
body {
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
html, body, #map {
|
|
height: 100%;
|
|
}
|
|
|
|
body {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.leaflet-marker-icon{
|
|
background:none;
|
|
color:#fff;
|
|
white-space:nowrap;
|
|
padding:2px;
|
|
border:none;
|
|
}
|
|
.name{
|
|
margin:-12px 0 0 10px;
|
|
background-color:rgba(0,17,35,0.5);
|
|
border-radius:5px;
|
|
padding:3px 5px;
|
|
}
|
|
.name a{
|
|
color:#fff !important;
|
|
text-decoration:none;
|
|
}
|
|
.leaflet-popup-content td{
|
|
vertical-align:top;
|
|
}
|
|
|
|
.image-overlay {
|
|
image-rendering: crisp-edges;
|
|
}
|
|
|
|
#forecast-slider-wrapper {
|
|
flex-grow: 1;
|
|
margin-left: 5px;
|
|
display: flex;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
#forecast-slider {
|
|
width: 100%;
|
|
}
|
|
|
|
#preload {
|
|
z-index: -1;
|
|
position: fixed;
|
|
}
|
|
|
|
#windrose {
|
|
position: fixed;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 1000;
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
}
|
|
|
|
#colorbar {
|
|
display: flex;
|
|
position: fixed;
|
|
left: 0;
|
|
width: 50px;
|
|
top: 0;
|
|
bottom: 0;
|
|
margin-top: 56px;
|
|
z-index: 1000;
|
|
flex-direction: column;
|
|
font-size: 12px;
|
|
}
|
|
|
|
#colorbar div {
|
|
flex-grow: 1;
|
|
color: #fff;
|
|
text-align:center;
|
|
overflow: hidden;
|
|
line-height: 1;
|
|
display:flex;
|
|
align-items: center;
|
|
}
|
|
|
|
#colorbar div span {
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
#map {
|
|
margin-left: 50px;
|
|
margin-top: 56px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
|
<a class="navbar-brand" href="#">Air Quality Visualization</a>
|
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
|
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
<ul class="navbar-nav mr-auto">
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
Metrics
|
|
</a>
|
|
<div class="dropdown-menu" id="metric">
|
|
</div>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#" id="windrose-link">Windrose</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link disabled" href="#">|</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#">Forecast</a>
|
|
</li>
|
|
</ul>
|
|
<div id="forecast-slider-wrapper">
|
|
<input type="range" min="0" max="1000" value="0" id="forecast-slider">
|
|
</div>
|
|
<ul class="navbar-nav mr-auto float-right">
|
|
<li class="nav-item">
|
|
<a class="nav-link disabled" href="#">|</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="results/all.csv" target="_blank">Download Data</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
<div id="map"></div>
|
|
<div id="windrose" class="d-none">
|
|
<img src="results/windrose.png"/>
|
|
</div>
|
|
<div id="colorbar">
|
|
<div id="colorbar-0" style="background-color: #ff2900"></div>
|
|
<div id="colorbar-1" style="background-color: #ff8200"></div>
|
|
<div id="colorbar-2" style="background-color: #ffd700"></div>
|
|
<div id="colorbar-3" style="background-color: #c7ff30"></div>
|
|
<div id="colorbar-4" style="background-color: #7aff7d"></div>
|
|
<div id="colorbar-5" style="background-color: #30ffc7"></div>
|
|
<div id="colorbar-6" style="background-color: #00c5ff"></div>
|
|
<div id="colorbar-7" style="background-color: #0069ff"></div>
|
|
<div id="colorbar-8" style="background-color: #0009ff"></div>
|
|
<div id="colorbar-9" style="background-color: #0000b2"></div>
|
|
</div>
|
|
<div id="preload"></div>
|
|
<script>
|
|
"use strict";
|
|
window.onload = function() {
|
|
let map = L.map("map", {
|
|
center: [30.5186465, 120.7249815],
|
|
zoom: 16,
|
|
zoomControl: false,
|
|
minZoom: 15,
|
|
maxZoom: 20,
|
|
maxBounds: [[30.48,120.68],[30.55,120.77]],
|
|
});
|
|
let mapLayers = {
|
|
'Amap / Streets':L.tileLayer('//webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {
|
|
maxZoom: 20,
|
|
maxNativeZoom: 18,
|
|
minZoom: 3,
|
|
attribution: "Amap.cn",
|
|
subdomains: "1234"
|
|
}).addTo(map),
|
|
'GeoQ / Streets':L.tileLayer('//map.geoq.cn/ArcGIS/rest/services/ChinaOnlineStreetPurplishBlue/MapServer/tile/{z}/{y}/{x}', {
|
|
maxZoom: 20,
|
|
maxNativeZoom: 16,
|
|
minZoom: 3,
|
|
attribution: "GeoQ.cn"
|
|
})
|
|
}
|
|
L.control.layers(mapLayers, {}, {
|
|
position: 'topright',
|
|
collapsed: true
|
|
}).addTo(map);
|
|
L.control.zoom({
|
|
zoomInTitle: 'Zoom In',
|
|
zoomOutTitle: 'Zoom Out'
|
|
}).addTo(map);
|
|
// map.on('click', function(e) {
|
|
// console.log(e.latlng);
|
|
// });
|
|
|
|
let setOverlay = function(filename) {
|
|
if(setOverlay._lastLayer) {
|
|
if(setOverlay._lastLayer._url == filename) return;
|
|
map.removeLayer(setOverlay._lastLayer);
|
|
}
|
|
setOverlay._lastLayer = L.imageOverlay(filename, [
|
|
[30.512, 120.719],
|
|
[30.525, 120.735]
|
|
], {
|
|
'opacity': 0.5,
|
|
'className': 'image-overlay'
|
|
}).addTo(map);
|
|
};
|
|
|
|
let selectionUpdate = function() {
|
|
let progressImages = 11;
|
|
|
|
// let metric = document.getElementById("metric").value;
|
|
let metric = document.dataType
|
|
|
|
if(metric != selectionUpdate.metric) {
|
|
// User selected new metric
|
|
|
|
// Regenerate preloaders
|
|
document.getElementById('preload').innerHTML = '';
|
|
for(let i = 0; i < progressImages; i++) {
|
|
let element = document.createElement('img');
|
|
element.src = 'results/' + metric + '_' + i + '.png';
|
|
document.getElementById('preload').appendChild(element);
|
|
}
|
|
|
|
// Reload colorbar
|
|
$.getJSON('results/' + metric + '.json', function(data) {
|
|
console.log(data);
|
|
document.getElementById("colorbar-0").innerHTML = '<span>' + data[0].toFixed(4) + '<br/>~<br/>' + data[1].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-1").innerHTML = '<span>' + data[1].toFixed(4) + '<br/>~<br/>' + data[2].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-2").innerHTML = '<span>' + data[2].toFixed(4) + '<br/>~<br/>' + data[3].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-3").innerHTML = '<span>' + data[3].toFixed(4) + '<br/>~<br/>' + data[4].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-4").innerHTML = '<span>' + data[4].toFixed(4) + '<br/>~<br/>' + data[5].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-5").innerHTML = '<span>' + data[5].toFixed(4) + '<br/>~<br/>' + data[6].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-6").innerHTML = '<span>' + data[6].toFixed(4) + '<br/>~<br/>' + data[7].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-7").innerHTML = '<span>' + data[7].toFixed(4) + '<br/>~<br/>' + data[8].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-8").innerHTML = '<span>' + data[8].toFixed(4) + '<br/>~<br/>' + data[9].toFixed(4) + '</span>';
|
|
document.getElementById("colorbar-9").innerHTML = '<span>' + data[9].toFixed(4) + '<br/>~<br/>' + data[10].toFixed(4) + '</span>';
|
|
});
|
|
}
|
|
|
|
let slider = document.getElementById("forecast-slider");
|
|
let progress = (slider.value - slider.min) / (slider.max - slider.min) * progressImages;
|
|
if(progress > progressImages - 1) progress = progressImages - 1;
|
|
setOverlay('results/' + metric + '_' + Math.floor(progress) + '.png')
|
|
};
|
|
|
|
let metrics = {
|
|
// 'bme680_co2' : 'CO2 (BME680)',
|
|
'bme680_hum' : 'Humidity (BME680)',
|
|
// 'bme680_iaq' : 'Indoor Air Quality (BME680)',
|
|
'bme680_prs' : 'Pressure (BME680)',
|
|
'bme680_tmp' : 'Temperature (BME680)',
|
|
// 'bme680_tvoc' : 'TVOC (BME680)',
|
|
'mics_co' : 'CO (MICS6814)',
|
|
'mics_no2' : 'NO2 (MICS6814)',
|
|
'pms5003_pm1' : 'PM1 (PMS5003)',
|
|
'pms5003_pm2_5': 'PM2.5 (PMS5003)',
|
|
'pms5003_pm10' : 'PM10 (PMS5003)',
|
|
// 'stm32_vbat' : 'STM32 Battery Voltage',
|
|
// 'stm32_tmp' : 'STM32 Chip Temperature',
|
|
// 'stm32_vref' : 'STM32 3.3V Rail Voltage',
|
|
};
|
|
|
|
let units = {
|
|
// 'bme680_co2' : 'ppm',
|
|
'bme680_hum' : '%',
|
|
// 'bme680_iaq' : '',
|
|
'bme680_prs' : 'Pa',
|
|
'bme680_tmp' : '°C',
|
|
// 'bme680_tvoc' : 'ppm',
|
|
'mics_co' : 'mg/m3',
|
|
'mics_no2' : 'ug/m3',
|
|
'pms5003_pm1' : 'ug/m3',
|
|
'pms5003_pm2_5': 'ug/m3',
|
|
'pms5003_pm10' : 'ug/m3',
|
|
// 'stm32_vbat' : 'V',
|
|
// 'stm32_tmp' : '°C',
|
|
// 'stm32_vref' : 'V',
|
|
};
|
|
|
|
|
|
$.getJSON('results/all.json', function(data) {
|
|
L.geoJson(data, {
|
|
pointToLayer: function(feature, latlng) {
|
|
// console.log(feature);
|
|
|
|
let popup = "<div><table>";
|
|
for(let key in metrics) {
|
|
popup += "<tr><td>" + metrics[key]
|
|
+ "</td><td class='text-right'>" + parseFloat(feature.properties[key]).toFixed(4)
|
|
+ "</td><td>" + units[key] + "</td></tr>";
|
|
}
|
|
popup += "</table></div>";
|
|
|
|
L.circleMarker(latlng, {
|
|
radius: 1,
|
|
color: '#fff',
|
|
opacity: 1,
|
|
fill: true,
|
|
weight: 2,
|
|
fillColor: '#fff',
|
|
fillOpacity: 1
|
|
}).addTo(map);
|
|
L.marker(latlng, {
|
|
icon: L.divIcon({
|
|
iconSize: null,
|
|
html: '<div class="name"><a href="#" onclick="return false">'
|
|
+ feature.properties.id
|
|
+ '</a></div>'
|
|
})
|
|
}).bindPopup(popup).addTo(map);
|
|
}
|
|
});
|
|
});
|
|
|
|
let updateMetricsDropdown = function() {
|
|
let ele = document.getElementById("metric");
|
|
ele.innerHTML = '';
|
|
for(let key in metrics) {
|
|
let link = document.createElement('a');
|
|
link.href = '#';
|
|
link.text = metrics[key];
|
|
link.classList.add('dropdown-item');
|
|
if(key == document.dataType) link.classList.add('active');
|
|
link.onclick = function() { document.dataType = key; updateMetricsDropdown(); selectionUpdate(); };
|
|
ele.appendChild(link);
|
|
}
|
|
};
|
|
|
|
document.dataType = Object.keys(metrics)[0];
|
|
updateMetricsDropdown();
|
|
|
|
document.getElementById("forecast-slider").oninput = selectionUpdate;
|
|
// document.getElementById("metric").onchange = selectionUpdate;
|
|
selectionUpdate();
|
|
|
|
document.getElementById('windrose-link').onclick = function() {
|
|
$('#windrose').toggleClass('d-none');
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |