diff --git a/app/templates/cities/detail.html b/app/templates/cities/detail.html index 2594462..496bb43 100644 --- a/app/templates/cities/detail.html +++ b/app/templates/cities/detail.html @@ -2,7 +2,7 @@ {% load i18n %} {% load static %} -{% block title %}{% trans "City/municipality" %}: {{ station.id }}{% endblock title %} +{% block title %}{% trans "City/municipality" %}: {{ city.name }}{% endblock title %} {% block styles %} @@ -13,157 +13,283 @@ + + {% endblock styles %} {% block content %} -
-
-
-
+
+
+
+
{% trans "Air quality" %}
+ +
+ {% trans "Method: AQI by U.S. EPA NAAQS" %} +
+
+
+
-
+

{{ city.name }}

-

Aktuell sind {{ city.station_count }} Stationen aktiv

-

Durchschnittswerte der letzten Stunde

+

Es gibt {{ city.station_count }} Citizen Science Stationen in {{ city.name }}. Bitte beachten Sie, dass einzelne Messstationen durch ungünstige Aufstellorte die Mittelwerte verfälschen können. In unserer Methodik setzen wir jedoch gezielte statistische Verfahren ein, um diese Einflüsse so gut wie möglich zu korrigieren.

+

Mittelwerte der letzten Stunde (1h-Mittelwert)

{% for value in city.values %} {% if value.dimension in "PM1.0,PM2.5,PM10.0,Humidity,Temperature" %} -
+
{{ value.dimension }}
-

{{ value.value|floatformat:2 }} {{ value.unit }}

+

{{ value.value|floatformat:2 }} {{ value.unit }}

Berechnung aus {{ value.value_count }} Werten von {{ value.station_count }} Stationen
{% endif %} {% endfor %} -

Achtung, die Daten stammen aus Citizen Science Quellen. Bei einer niedrigen Anzahl an Stationen, können die Durchschnittswerte unter Umständen leicht verschoben sein.

+ + + // Define color steps for PM1.0 + const colorStepsPM1 = [ + [0, [120, 1, 0.75], "{% trans 'Good' %}"], // Grün + [9, [60, 1, 1], "{% trans 'Moderate' %}"], // Gelb + [35, [30, 1, 1], "{% trans 'Unhealthy for sensitive groups' %}"], // Orange + [55, [0, 1, 1], "{% trans 'Unhealthy' %}"], // Rot + [125,[300, 1, 0.7], "{% trans 'Very unhealthy' %}"], // Violett + [250.5,[330, 1, 0.5], "{% trans 'Hazardous' %}"] // Dunkelrot/Braun +]; + +const colorStepsPM25 = [ + [0, [120, 1, 0.75], "{% trans 'Good' %}"], + [9, [60, 1, 1], "{% trans 'Moderate' %}"], + [35, [30, 1, 1], "{% trans 'Unhealthy for sensitive groups' %}"], + [55, [0, 1, 1], "{% trans 'Unhealthy' %}"], + [125,[300, 1, 0.7], "{% trans 'Very unhealthy' %}"], + [250.5,[330, 1, 0.5], "{% trans 'Hazardous' %}"] +]; + +const colorStepsPM10 = [ + [0, [120, 1, 0.75], "{% trans 'Good' %}"], + [54, [60, 1, 1], "{% trans 'Moderate' %}"], + [154, [30, 1, 1], "{% trans 'Unhealthy for sensitive groups' %}"], + [254, [0, 1, 1], "{% trans 'Unhealthy' %}"], + [354, [300, 1, 0.7], "{% trans 'Very unhealthy' %}"], + [424, [330, 1, 0.5], "{% trans 'Hazardous' %}"], +]; + +const colorStepsArray = [colorStepsPM1, colorStepsPM25, colorStepsPM10]; + +function interpolate(a, b, fraction) { +return [ + a[0] + (b[0] - a[0]) * fraction, + a[1] + (b[1] - a[1]) * fraction, + a[2] + (b[2] - a[2]) * fraction +]; +} + +function getColorForPM(value, colorSteps) { +if (isNaN(value)) return [0, 0, 0.7]; // Grau für ungültige Werte +for (let i = 0; i < colorSteps.length - 1; i++) { + if (value >= colorSteps[i][0] && value < colorSteps[i + 1][0]) { + const fraction = (value - colorSteps[i][0]) / (colorSteps[i + 1][0] - colorSteps[i][0]); + return interpolate(colorSteps[i][1], colorSteps[i + 1][1], fraction); + } +} +return colorSteps[colorSteps.length - 1][1]; +} + +function hsvToRgb(h, s, v) { +h = h % 360; // Ensure h is within 0-360 degrees +let c = v * s; +let x = c * (1 - Math.abs(((h / 60) % 2) - 1)); +let m = v - c; +let rPrime, gPrime, bPrime; + +if (h < 60) { + rPrime = c; gPrime = x; bPrime = 0; +} else if (h < 120) { + rPrime = x; gPrime = c; bPrime = 0; +} else if (h < 180) { + rPrime = 0; gPrime = c; bPrime = x; +} else if (h < 240) { + rPrime = 0; gPrime = x; bPrime = c; +} else if (h < 300) { + rPrime = x; gPrime = 0; bPrime = c; +} else { + rPrime = c; gPrime = 0; bPrime = x; +} + +let r = Math.round((rPrime + m) * 255); +let g = Math.round((gPrime + m) * 255); +let b = Math.round((bPrime + m) * 255); + +return [r, g, b]; +} + +function getMean(arr) { +var acc = 0; +var count = 0; +for(let i = 0; i < arr.length; i++) { + if(isNaN(arr[i])) continue; + acc += Number(arr[i]); + count++; +} +if(count == 0) return NaN; +return acc / count; +} + +function createLegend(pmTypeIndex, colorSteps) { +var legendDiv = document.getElementById('legend'); + +// Clear legend +legendDiv.innerHTML = ''; + +// Loop through the color steps +for (var i = 0; i < colorSteps.length; i++) { + var from = colorSteps[i][0]; + var to = colorSteps[i + 1] ? colorSteps[i + 1][0] : '+'; + var hsv = colorSteps[i][1]; + var description = colorSteps[i][2]; // Health impact description + + const rgb = hsvToRgb(hsv[0], hsv[1], hsv[2]); + const colorString = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; + + var item = document.createElement('div'); + item.style.display = 'flex'; + item.style.flexDirection = 'row'; + item.style.alignItems = 'center'; + item.style.marginBottom = '4px'; + + var colorBox = document.createElement('span'); + colorBox.style.background = colorString; + colorBox.style.width = '18px'; + colorBox.style.height = '18px'; + colorBox.style.display = 'inline-block'; + colorBox.style.marginRight = '8px'; + colorBox.style.border = '1px solid #ccc'; + colorBox.style.borderRadius = '50%'; + + var labelText = document.createElement('span'); + labelText.innerHTML = `${from}${to !== '+' ? '–' + to : '+'} µg/m³ | ${description}`; + labelText.style.fontSize = '0.8rem'; + + item.appendChild(colorBox); + item.appendChild(labelText); + legendDiv.appendChild(item); +} +} + +function showPM(selection) { +var pmTypeIndex = selection.selectedIndex; // 0 for PM1.0, 1 for PM2.5, 2 for PM10.0 +var colorSteps = colorStepsArray[pmTypeIndex]; +addMarkerLayer(pmTypeIndex, colorSteps); +createLegend(pmTypeIndex, colorSteps); +} + +var map = L.map('map').setView([{{ city.coordinates.1|stringformat:"f" }}, {{ city.coordinates.0|stringformat:"f" }}], 13); + +const tiles = L.tileLayer.grayscale('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { +maxZoom: 19, +attribution: 'CC BY 2024 Luftdaten.at | © OpenStreetMap' +}).addTo(map); + +var stationData; + +async function fetchMarkerData() { +const api_url = "{{API_URL}}"; +const response = await fetch(`${api_url}/station/current/all`); + +stationData = []; + +// Parse response CSV +const text = await response.text(); +const items = text.split("\n"); +for (let row in items) { + if (items[row].length == 0 || row == 0) continue; + var data = items[row].split(","); + const [stationID, lat, lon, pm1, pm25, pm10] = data; + const marker = {stationID, lat, lon, pm1, pm25, pm10}; + stationData.push(marker); +} +} + +fetchMarkerData().then(() => { +var defaultPmTypeIndex = 1; // Index for PM2.5 +var defaultColorSteps = colorStepsArray[defaultPmTypeIndex]; +addMarkerLayer(defaultPmTypeIndex, defaultColorSteps); +createLegend(defaultPmTypeIndex, defaultColorSteps); +}); + +var markerLayer; + +// Add markers for 0: pm1, 1: pm25, 2: pm10 +async function addMarkerLayer(pmTypeIndex, colorSteps) { +if (markerLayer != null) { + map.removeLayer(markerLayer); +} + +// Marker layer +markerLayer = L.markerClusterGroup({ + iconCreateFunction: function (cluster) { + const meanPM = getMean(cluster.getAllChildMarkers().map(marker => marker.pm)); + const colorArrayHSV = getColorForPM(meanPM, colorSteps); + const rgb = hsvToRgb(colorArrayHSV[0], colorArrayHSV[1], colorArrayHSV[2]); + const colorString = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; + const brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; + const textColor = brightness > 125 ? "black" : "white"; + const html = `
${isNaN(meanPM) ? '' : meanPM.toFixed(1)}
`; + return L.divIcon({iconSize: [40, 40], className: '', html: html}); + }, + showCoverageOnHover: false, +}); + +const markerList = []; + +// Add markers for each station +for (let stationIndex in stationData) { + const station = stationData[stationIndex]; + const pmValue = station[['pm1', 'pm25', 'pm10'][pmTypeIndex]]; + const colorArrayHSV = getColorForPM(pmValue, colorSteps); + const rgb = hsvToRgb(colorArrayHSV[0], colorArrayHSV[1], colorArrayHSV[2]); + const colorString = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; + const brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; + const textColor = brightness > 125 ? "black" : "white"; + + const html = ` + +
+ + ${isNaN(pmValue) ? '' : Number(pmValue).toFixed(1)} + +
+
`; + + var marker = L.marker([station.lat, station.lon], {icon: L.divIcon({iconSize: [40, 40], className: '', html: html})}); + marker.pm = pmValue; // Add a custom attribute to marker + markerList.push(marker); +} + +// Add markers to map +markerLayer.addLayers(markerList); +map.addLayer(markerLayer); +} + {% endblock content %} \ No newline at end of file