Skip to content

Commit

Permalink
add app cities
Browse files Browse the repository at this point in the history
  • Loading branch information
silvioheinze committed Dec 6, 2024
1 parent 326ef46 commit 50bfe1b
Show file tree
Hide file tree
Showing 13 changed files with 277 additions and 1 deletion.
Empty file added app/cities/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions app/cities/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
6 changes: 6 additions & 0 deletions app/cities/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class CitiesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'cities'
Empty file.
3 changes: 3 additions & 0 deletions app/cities/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.db import models

# Create your models here.
3 changes: 3 additions & 0 deletions app/cities/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
7 changes: 7 additions & 0 deletions app/cities/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path
from .views import CitiesListView, CitiesDetailView

urlpatterns = [
path('', CitiesListView, name='cities-list'),
path('<str:pk>/', CitiesDetailView, name='cities-detail'),
]
67 changes: 67 additions & 0 deletions app/cities/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import requests
from datetime import datetime, timezone, timedelta
from collections import defaultdict
from django.shortcuts import render, get_object_or_404
from django.http import Http404
from django.conf import settings
from main.enums import Dimension, SensorModel, OutputFormat, Precision, Order

def CitiesDetailView(request, pk):
api_url = f"{settings.API_URL}/city/current?city_slug={pk}"
print(api_url)
try:
response = requests.get(api_url)
response.raise_for_status() # Prüft, ob die Anfrage erfolgreich war
station_data = response.json() # Daten im JSON-Format

# Extract information from the JSON
geometry = station_data.get("geometry", {})
properties = station_data.get("properties", {})

# Prepare city info
city_info = {
'name': properties.get('name'),
'country': properties.get('country'),
'timezone': properties.get('timezone'),
'coordinates': geometry.get('coordinates'),
'last_updated': properties.get('time'),
'values': []
}

# Parse "values" and translate dimensions
values = properties.get("values", [])
for value in values:
dimension_id = value.get("dimension")
translated_dimension = Dimension.get_name(dimension_id)
city_info['values'].append({
'dimension': translated_dimension,
'value': value.get("value"),
'unit': Dimension.get_unit(dimension_id)
})

except requests.exceptions.HTTPError as err:
raise Http404(f"City {pk} not found: {err}")

except requests.exceptions.RequestException as e:
return render(request, 'stations/error.html', {'error': str(e)})

# Render die Daten im Template
return render(request, 'cities/detail.html', {'city': city_info})

def CitiesListView(request):
api_url = f"{settings.API_URL}/city/all"

try:
# Perform the GET request
response = requests.get(api_url)
response.raise_for_status() # Raise an error for unsuccessful requests
data = response.json() # Parse JSON response

# Extract and sort the list of cities alphabetically by name
cities = data.get("cities", [])
cities = sorted(cities, key=lambda city: city.get("name", "").lower()) # Sort case-insensitively

except requests.exceptions.HTTPError as err:
raise Http404(f"Cities not found.")

return render(request, "cities/list.html", {"cities": cities})
3 changes: 2 additions & 1 deletion app/main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@
'accounts.apps.AccountsConfig',
'api.apps.ApiConfig',
'campaign.apps.CampaignConfig',
'cities.apps.CitiesConfig',
'devices.apps.DevicesConfig',
'pages.apps.PagesConfig',
'workshops.apps.WorkshopsConfig',
'stations.apps.StationsConfig',
'workshops.apps.WorkshopsConfig',
]

MIDDLEWARE = [
Expand Down
1 change: 1 addition & 0 deletions app/main/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
path("", include("pages.urls")),
path("api/", include("api.urls")),
path("campaigns/", include("campaign.urls")),
path("cities/", include("cities.urls")),
path("devices/", include("devices.urls")),
path("stations/", include("stations.urls")),
path("workshops/", include("workshops.urls")),
Expand Down
1 change: 1 addition & 0 deletions app/templates/_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
{% else %}
<li class="nav-item"><a class="nav-link" href="{% url 'workshops-list' %}">{% trans "Workshops" %}</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'stations-list' %}">{% trans "Stations" %}</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'cities-list' %}">{% trans "Cities" %}</a></li>
{% if user.is_authenticated and user.is_superuser %}
<li class="nav-item"><a class="nav-link" href="{% url 'device-list' %}">{% trans "Devices" %}</a></li>
{% endif %}
Expand Down
167 changes: 167 additions & 0 deletions app/templates/cities/detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
{% extends "_base.html" %}
{% load i18n %}
{% load static %}

{% block title %}{% trans "City/municipality" %}: {{ station.id }}{% endblock title %}

{% block styles %}
<!-- Leaflet CSS -->
<link rel="stylesheet" href="{% static 'css/leaflet.css' %}" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
<link rel="stylesheet" href="{% static 'css/MarkerCluster.css' %}" crossorigin="">
<link rel="stylesheet" href="{% static 'css/MarkerCluster.Default.css' %}" crossorigin="">

<!-- Leaflet JS -->
<script src="{% static 'js/leaflet.js' %}" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="{% static 'js/chart.umd.js' %}" crossorigin=""></script>
{% endblock styles %}

{% block content %}
<div class="row">
<div class="col-lg-12 mb-4">
<div id="map" style="width: 100%; height: 400px;"></div>
</div>
</div>

<div class="row">
<div class="col-lg-12">
<h1>{{ city.name }}</h1>
</div>
</div>
<div class="row">
<h3>Aktuelle Durchschnittswerte</h3>
{% for value in city.values %}
{% if value.dimension in "PM1.0,PM2.5,PM10.0,Humidity,Temperature" %}
<div class="col-sm-4 mb-4">
<div class="card text-center">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">{{ value.dimension }}</h6>
<p class="card-text display-4">{{ value.value|floatformat:2 }} {{ value.unit }}</p>
</div>
</div>
</div>
{% endif %}
{% endfor %}
<p>Achtung, die Daten stammen aus Citizen Science Quellen. Bei einer niedrigen Anzahl an Stationen, können die Durchschnittswerte unter Umständen leicht verschoben sein.</p>
</div>

<script>
// Define color steps for PM2.5
const colorStepsPM25 = [
[0, [120, 1, 0.75]],
[9, [60, 1, 1]],
[35, [30, 1, 1]],
[55, [0, 1, 1]],
[125, [300, 1, 0.7]],
[250.5, [330, 1, 0.5]]
];

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];
}

// Initialisierung der Leaflet-Karte
var map = L.map('map').setView([{{ city.coordinates.1|stringformat:"f" }}, {{ city.coordinates.0|stringformat:"f" }}], 13);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '<a href="https://luftdaten.at">CC BY 2024 Luftdaten.at</a> | &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);

async function fetchMarkerData() {
const api_url = "{{API_URL}}";
const response = await fetch(`${api_url}/station/current/all`);

const 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, pm25};
stationData.push(marker);
}
return stationData;
}

async function addMarkersForPM25() {
const stationData = await fetchMarkerData();

// Add markers for PM2.5
for (let stationIndex in stationData) {
const station = stationData[stationIndex];
const pmValue = station.pm25;
const colorArrayHSV = getColorForPM(pmValue, colorStepsPM25);
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 = `
<a
class="hiddenlink clickable"
href="/stations/${station.stationID}"
>
<div style="height: 3.5em; width: 3.5em; background-color: ${colorString}; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10pt;">
<span style="color: ${textColor};">
${isNaN(pmValue) ? '' : Number(pmValue).toFixed(1)}
</span>
</div>
</a>`;

L.marker([station.lat, station.lon], {
icon: L.divIcon({iconSize: [40, 40], className: '', html: html})
}).addTo(map);
}
}

// Add markers for PM2.5
addMarkersForPM25();
</script>


{% endblock content %}
17 changes: 17 additions & 0 deletions app/templates/cities/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "_base.html" %}
{% load i18n %}
{% load static %}

{% block title %}{% trans "Cities and municipalities" %}{% endblock title %}

{% block styles %}
{% endblock styles %}

{% block content %}
<h2>{% trans "Cities and municipalities" %}</h2>
<p>
{% for city in cities %}
<a href="/cities/{{ city.slug }}">{{ city.name }}</a> ({{ city.country.name }}){% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endblock content %}

0 comments on commit 50bfe1b

Please sign in to comment.