mirror of
https://github.com/tomasriveral/ReSSPublica.git
synced 2026-08-11 18:28:38 +02:00
feeds: first complete draft basel air quality
This commit is contained in:
+361
-190
@@ -1,8 +1,10 @@
|
|||||||
from datetime import date
|
from datetime import date, timedelta, datetime
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import geopandas as gpd
|
import geopandas as gpd
|
||||||
from shapely import wkb
|
from shapely import wkb
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
from pathlib import Path
|
||||||
|
import copy
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger("resspublica")
|
logger = logging.getLogger("resspublica")
|
||||||
@@ -10,40 +12,90 @@ logger = logging.getLogger("resspublica")
|
|||||||
from .translations import *
|
from .translations import *
|
||||||
from .utils import *
|
from .utils import *
|
||||||
|
|
||||||
station_coordinates = {
|
start_date = pd.Timestamp("2026-01-01")
|
||||||
"12500": (47.558, 7.588), # https://luftqualitaet.ch/messnetz/station/blSIB
|
start_date_datetime = date.fromisoformat("2026-01-01") # a bit dumb we need those two formats
|
||||||
"12450": (47.500, 7.620), # https://luftqualitaet.ch/messnetz/station/blSIB
|
end_date = pd.Timestamp((date.today() - timedelta(days=1)).isoformat())
|
||||||
"12510": (47.450, 7.780), # https://luftqualitaet.ch/messnetz/station/blMUT
|
|
||||||
|
|
||||||
|
station_information = {
|
||||||
|
"100048": {
|
||||||
|
"name": "Basel Chrischona",
|
||||||
|
"coordinates": (47.571709338, 7.687073826)
|
||||||
|
},
|
||||||
|
"100050": {
|
||||||
|
"name": "Basel Feldbergstrasse",
|
||||||
|
"coordinates": (47.567022213, 7.594722533)
|
||||||
|
},
|
||||||
|
"100049": {
|
||||||
|
"name": "Basel St. Johannplatz",
|
||||||
|
"coordinates": (47.565950312, 7.582002453)
|
||||||
|
},
|
||||||
|
|
||||||
|
"12450": {
|
||||||
|
"name": "Sissach-Bützenen",
|
||||||
|
"coordinates": (47.465037653, 7.815429278)
|
||||||
|
},
|
||||||
|
"12510": {
|
||||||
|
"name": "A2 Hard",
|
||||||
|
"coordinates": (47.538075849, 7.648985359)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateBaselLuftqualitat(ASSETS):
|
pollutants = [
|
||||||
logger.info("Generating Luftqualitat map in Basel feed...")
|
"pm10",
|
||||||
logger.info("Preparing data...")
|
"pm2_5",
|
||||||
|
"no2",
|
||||||
|
"o3"
|
||||||
|
]
|
||||||
|
|
||||||
urls = [
|
pollutant_units = {
|
||||||
"https://data.bs.ch/api/v2/catalog/datasets/100048/exports/parquet",
|
"pm10": "µg/m³",
|
||||||
"https://data.bs.ch/api/v2/catalog/datasets/100050/exports/parquet",
|
"pm2_5": "µg/m³",
|
||||||
"https://data.bs.ch/api/v2/catalog/datasets/100093/exports/parquet",
|
"no2": "µg/m³",
|
||||||
"https://data.bs.ch/api/v2/catalog/datasets/100049/exports/parquet",
|
"o3": "µg/m³",
|
||||||
"https://data.bs.ch/api/v2/catalog/datasets/100178/exports/parquet",
|
}
|
||||||
"https://data.bs.ch/api/v2/catalog/datasets/100158/exports/parquet",
|
|
||||||
"https://data.bl.ch/api/v2/catalog/datasets/12500/exports/parquet",
|
|
||||||
|
# Fixed scale per pollutant so colors remain comparable day-to-day
|
||||||
|
pollutant_scales = {
|
||||||
|
"pm10": (0, 50),
|
||||||
|
"pm2_5": (0, 30),
|
||||||
|
"no2": (0, 100),
|
||||||
|
"o3": (0, 200),
|
||||||
|
}
|
||||||
|
|
||||||
|
def generateBaselLuftqualitat(ASSETS, CACHE):
|
||||||
|
|
||||||
|
urls = [
|
||||||
|
"https://data.bs.ch/api/explore/v2.1/catalog/datasets/100048/exports/parquet?where=datum_zeit>2026-01-01",
|
||||||
|
"https://data.bs.ch/api/explore/v2.1/catalog/datasets/100050/exports/parquet?where=datum_zeit>2026-01-01",
|
||||||
|
"https://data.bs.ch/api/explore/v2.1/catalog/datasets/100093/exports/parquet", # if we use where with this one it 400: Bad Request
|
||||||
|
"https://data.bs.ch/api/explore/v2.1/catalog/datasets/100049/exports/parquet?where=datum_zeit>2026-01-01",
|
||||||
|
"https://data.bs.ch/api/explore/v2.1/catalog/datasets/100178/exports/parquet",
|
||||||
"https://data.bl.ch/api/v2/catalog/datasets/12450/exports/parquet",
|
"https://data.bl.ch/api/v2/catalog/datasets/12450/exports/parquet",
|
||||||
"https://data.bl.ch/api/v2/catalog/datasets/12510/exports/parquet"
|
"https://data.bl.ch/api/v2/catalog/datasets/12510/exports/parquet"
|
||||||
]
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Load data
|
# Load data
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
dataframes = []
|
dataframes = []
|
||||||
|
|
||||||
for url in urls:
|
for url in urls:
|
||||||
logger.debug(f"Querying {url}...")
|
logger.debug(f"Querying {url}...")
|
||||||
df = pd.read_parquet(url)
|
df = pd.read_parquet(url)
|
||||||
|
|
||||||
station_id = url.split("/")[-3]
|
station_id = url.split("/")[-3]
|
||||||
|
|
||||||
df["station_id"] = station_id
|
df["station_id"] = station_id
|
||||||
|
|
||||||
|
# Always define station name
|
||||||
|
# Use dataset id as fallback until manually mapped
|
||||||
|
df["station_name"] = station_id
|
||||||
|
|
||||||
|
if station_id in station_information:
|
||||||
|
df["station_name"] = station_information[station_id]["name"]
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Normalize datetime to Swiss time
|
# Normalize datetime to Swiss time
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -53,7 +105,7 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
"anfangszeit",
|
"anfangszeit",
|
||||||
"messbeginn"
|
"messbeginn"
|
||||||
]
|
]
|
||||||
|
|
||||||
date_column = next(
|
date_column = next(
|
||||||
(
|
(
|
||||||
col
|
col
|
||||||
@@ -62,37 +114,40 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
),
|
),
|
||||||
None
|
None
|
||||||
)
|
)
|
||||||
|
|
||||||
if date_column is None:
|
if date_column is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"No date column found in {url}, skipping"
|
f"No date column found in {url}, skipping"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
df["date_time"] = pd.to_datetime(
|
df["date_time"] = pd.to_datetime(
|
||||||
df[date_column],
|
df[date_column],
|
||||||
errors="coerce",
|
errors="coerce",
|
||||||
utc=True
|
utc=True
|
||||||
)
|
)
|
||||||
|
|
||||||
df["date_time"] = (
|
df["date_time"] = (
|
||||||
df["date_time"]
|
df["date_time"]
|
||||||
.dt.tz_convert("Europe/Zurich")
|
.dt.tz_convert("Europe/Zurich")
|
||||||
.dt.tz_localize(None)
|
.dt.tz_localize(None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Add coordinates for BL datasets
|
# Add coordinates for BL datasets
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
if station_id in station_coordinates:
|
if station_id in station_information:
|
||||||
|
|
||||||
lat, lon = station_coordinates[station_id]
|
lat, lon = station_information[station_id]["coordinates"]
|
||||||
|
|
||||||
df["latitude"] = lat
|
df["latitude"] = lat
|
||||||
df["longitude"] = lon
|
df["longitude"] = lon
|
||||||
|
|
||||||
|
df["station_name"] = (
|
||||||
|
station_information[station_id]["name"]
|
||||||
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Convert long format datasets
|
# Convert long format datasets
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -100,7 +155,7 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
"parameter" in df.columns
|
"parameter" in df.columns
|
||||||
and "messwert" in df.columns
|
and "messwert" in df.columns
|
||||||
):
|
):
|
||||||
|
|
||||||
df = df.pivot_table(
|
df = df.pivot_table(
|
||||||
index=[
|
index=[
|
||||||
"date_time",
|
"date_time",
|
||||||
@@ -110,18 +165,18 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
values="messwert",
|
values="messwert",
|
||||||
aggfunc="mean"
|
aggfunc="mean"
|
||||||
).reset_index()
|
).reset_index()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Pollutant normalization
|
# Pollutant normalization
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
pollutant_mapping = {
|
pollutant_mapping = {
|
||||||
|
|
||||||
"pm10": [
|
"pm10": [
|
||||||
"pm10",
|
"pm10",
|
||||||
"pm10_stundenmittelwerte_ug_m3"
|
"pm10_stundenmittelwerte_ug_m3"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pm2_5": [
|
"pm2_5": [
|
||||||
"pm2_5",
|
"pm2_5",
|
||||||
"pm2.5",
|
"pm2.5",
|
||||||
@@ -134,7 +189,7 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
"feldbergstr2_pm25",
|
"feldbergstr2_pm25",
|
||||||
"stjohann2_pm25"
|
"stjohann2_pm25"
|
||||||
],
|
],
|
||||||
|
|
||||||
"no2": [
|
"no2": [
|
||||||
"no2",
|
"no2",
|
||||||
"no2_stundenmittelwerte_ug_m3",
|
"no2_stundenmittelwerte_ug_m3",
|
||||||
@@ -145,7 +200,7 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
"feldbergstr2_no2",
|
"feldbergstr2_no2",
|
||||||
"stjohann2_no2"
|
"stjohann2_no2"
|
||||||
],
|
],
|
||||||
|
|
||||||
"o3": [
|
"o3": [
|
||||||
"o3",
|
"o3",
|
||||||
"o3_stundenmittelwerte_ug_m3",
|
"o3_stundenmittelwerte_ug_m3",
|
||||||
@@ -158,27 +213,28 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
"stjohann2_o3"
|
"stjohann2_o3"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Melt everything into:
|
# Melt everything into:
|
||||||
# date_time | station_id | pollutant | value | geometry
|
# date_time | station_id | pollutant | value | geometry
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
for pollutant, candidates in pollutant_mapping.items():
|
for pollutant, candidates in pollutant_mapping.items():
|
||||||
|
|
||||||
for column in candidates:
|
for column in candidates:
|
||||||
|
|
||||||
if column not in df.columns:
|
if column not in df.columns:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
keep = [
|
keep = [
|
||||||
"date_time",
|
"date_time",
|
||||||
"station_id",
|
"station_id",
|
||||||
|
"station_name",
|
||||||
column
|
column
|
||||||
]
|
]
|
||||||
|
|
||||||
for extra in [
|
for extra in [
|
||||||
"geo_point_2d",
|
"geo_point_2d",
|
||||||
"latitude",
|
"latitude",
|
||||||
@@ -186,36 +242,36 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
]:
|
]:
|
||||||
if extra in df.columns:
|
if extra in df.columns:
|
||||||
keep.append(extra)
|
keep.append(extra)
|
||||||
|
|
||||||
|
|
||||||
tmp = df[keep].copy()
|
tmp = df[keep].copy()
|
||||||
|
|
||||||
tmp = tmp.rename(
|
tmp = tmp.rename(
|
||||||
columns={
|
columns={
|
||||||
column: "value"
|
column: "value"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
tmp["pollutant"] = pollutant
|
tmp["pollutant"] = pollutant
|
||||||
|
|
||||||
parts.append(tmp)
|
parts.append(tmp)
|
||||||
|
|
||||||
|
|
||||||
if not parts:
|
if not parts:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"No pollutants found in {url}"
|
f"No pollutants found in {url}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
df = pd.concat(
|
df = pd.concat(
|
||||||
parts,
|
parts,
|
||||||
ignore_index=True
|
ignore_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
dataframes.append(df)
|
dataframes.append(df)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Combine datasets
|
# Combine datasets
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -224,33 +280,36 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
ignore_index=True,
|
ignore_index=True,
|
||||||
sort=False
|
sort=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Geometry
|
# Geometry
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
def safe_load(x):
|
def safe_load(x):
|
||||||
try:
|
try:
|
||||||
return wkb.loads(x)
|
geom = wkb.loads(x)
|
||||||
|
if geom.is_empty:
|
||||||
|
return None
|
||||||
|
return geom
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
dataframe["geometry"] = None
|
dataframe["geometry"] = None
|
||||||
|
|
||||||
|
|
||||||
if "geo_point_2d" in dataframe.columns:
|
if "geo_point_2d" in dataframe.columns:
|
||||||
|
|
||||||
dataframe["geometry"] = dataframe[
|
dataframe["geometry"] = dataframe[
|
||||||
"geo_point_2d"
|
"geo_point_2d"
|
||||||
].apply(
|
].apply(
|
||||||
safe_load
|
safe_load
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Fill missing geometry from coordinates
|
# Fill missing geometry from coordinates
|
||||||
missing_geometry = dataframe["geometry"].isna()
|
missing_geometry = dataframe["geometry"].isna()
|
||||||
|
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
missing_geometry,
|
missing_geometry,
|
||||||
"geometry"
|
"geometry"
|
||||||
@@ -258,57 +317,14 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
dataframe.loc[missing_geometry, "longitude"],
|
dataframe.loc[missing_geometry, "longitude"],
|
||||||
dataframe.loc[missing_geometry, "latitude"]
|
dataframe.loc[missing_geometry, "latitude"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
geo = gpd.GeoDataFrame(
|
geo = gpd.GeoDataFrame(
|
||||||
dataframe,
|
dataframe,
|
||||||
geometry="geometry",
|
geometry="geometry",
|
||||||
crs="EPSG:4326"
|
crs="EPSG:4326"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Time filter
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
start = pd.Timestamp(
|
|
||||||
"2026-07-29 00:00:00"
|
|
||||||
)
|
|
||||||
|
|
||||||
end = pd.Timestamp(
|
|
||||||
"2026-07-29 23:59:59"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
geo = geo[
|
|
||||||
(geo["date_time"] >= start)
|
|
||||||
&
|
|
||||||
(geo["date_time"] <= end)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Average per station
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
averaged = (
|
|
||||||
geo
|
|
||||||
.groupby(
|
|
||||||
[
|
|
||||||
"station_id",
|
|
||||||
"pollutant",
|
|
||||||
"geometry"
|
|
||||||
],
|
|
||||||
as_index=False
|
|
||||||
)
|
|
||||||
["value"]
|
|
||||||
.mean()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
averaged = gpd.GeoDataFrame(
|
|
||||||
averaged,
|
|
||||||
geometry="geometry",
|
|
||||||
crs="EPSG:4326"
|
|
||||||
)
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Boundaries
|
# Boundaries
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -316,95 +332,250 @@ def generateBaselLuftqualitat(ASSETS):
|
|||||||
ASSETS /
|
ASSETS /
|
||||||
"swissBOUNDARIES3D_1_5_LV95_LN02.gdb"
|
"swissBOUNDARIES3D_1_5_LV95_LN02.gdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
cantons = gpd.read_file(
|
cantons = gpd.read_file(
|
||||||
gdb,
|
gdb,
|
||||||
layer="TLM_KANTONSGEBIET"
|
layer="TLM_KANTONSGEBIET"
|
||||||
)
|
)
|
||||||
|
|
||||||
basel_stadt = cantons[
|
basel_stadt = cantons[
|
||||||
cantons["KANTONSNUMMER"] == 12
|
cantons["KANTONSNUMMER"] == 12
|
||||||
].to_crs(averaged.crs)
|
].to_crs("EPSG:4326")
|
||||||
|
|
||||||
basel_land = cantons[
|
basel_land = cantons[
|
||||||
cantons["KANTONSNUMMER"] == 13
|
cantons["KANTONSNUMMER"] == 13
|
||||||
].to_crs(averaged.crs)
|
].to_crs("EPSG:4326")
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Plot
|
# Time filter
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
pollutants = [
|
logger.info("Generating daily image...")
|
||||||
"pm10",
|
for day in pd.date_range(start_date, end_date, freq="D"):
|
||||||
"pm2_5",
|
|
||||||
"no2",
|
|
||||||
"o3"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
if Path( CACHE / f"baselAirQuality-{day.strftime("%Y-%m-%d")}.png").exists():
|
||||||
fig, axes = plt.subplots(
|
logger.debug(f"Day {day.strftime("%Y-%m-%d")} is already cached. Skipping...")
|
||||||
1,
|
|
||||||
len(pollutants),
|
|
||||||
figsize=(20, 5)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
for ax, pollutant in zip(
|
|
||||||
axes,
|
|
||||||
pollutants
|
|
||||||
):
|
|
||||||
|
|
||||||
subset = averaged[
|
|
||||||
averaged["pollutant"] == pollutant
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
if subset.empty:
|
|
||||||
ax.set_visible(False)
|
|
||||||
continue
|
continue
|
||||||
|
logger.debug(f"Handling day {day.strftime("%Y-%m-%d")}...")
|
||||||
|
|
||||||
subset.plot(
|
next_day = day + pd.Timedelta(days=1)
|
||||||
ax=ax,
|
|
||||||
column="value",
|
geo_day = geo[
|
||||||
cmap="hot",
|
(geo["date_time"] >= day)
|
||||||
legend=True,
|
&
|
||||||
markersize=80
|
(geo["date_time"] < next_day)
|
||||||
)
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
basel_land.plot(
|
# Average per station
|
||||||
ax=ax,
|
# ---------------------------------------------------------
|
||||||
facecolor="none",
|
averaged = (
|
||||||
edgecolor="black",
|
geo_day.groupby(
|
||||||
linewidth=2
|
[
|
||||||
)
|
"station_id",
|
||||||
|
"station_name",
|
||||||
|
"pollutant",
|
||||||
basel_stadt.plot(
|
"geometry"
|
||||||
ax=ax,
|
],
|
||||||
facecolor="none",
|
as_index=False
|
||||||
edgecolor="black",
|
|
||||||
linewidth=1
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
for _, row in subset.iterrows():
|
|
||||||
ax.annotate(
|
|
||||||
row["station_id"],
|
|
||||||
(
|
|
||||||
row.geometry.x,
|
|
||||||
row.geometry.y
|
|
||||||
),
|
|
||||||
fontsize=8
|
|
||||||
)
|
)
|
||||||
|
["value"]
|
||||||
|
.mean()
|
||||||
ax.set_title(
|
)
|
||||||
pollutant
|
|
||||||
|
|
||||||
|
averaged = gpd.GeoDataFrame(
|
||||||
|
averaged,
|
||||||
|
geometry="geometry",
|
||||||
|
crs="EPSG:4326"
|
||||||
)
|
)
|
||||||
|
|
||||||
ax.axis("off")
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Plot
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
figsize=(14, 14)
|
||||||
|
)
|
||||||
|
|
||||||
|
axes = axes.flatten()
|
||||||
|
|
||||||
|
|
||||||
|
for ax, pollutant in zip(
|
||||||
|
axes,
|
||||||
|
pollutants
|
||||||
|
):
|
||||||
|
|
||||||
|
subset = averaged[
|
||||||
|
averaged["pollutant"] == pollutant
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if subset.empty:
|
||||||
|
ax.set_visible(False)
|
||||||
|
continue
|
||||||
|
|
||||||
|
vmin, vmax = pollutant_scales[pollutant]
|
||||||
|
|
||||||
|
subset.plot(
|
||||||
|
ax=ax,
|
||||||
|
column="value",
|
||||||
|
cmap="hot_r",
|
||||||
|
legend=True,
|
||||||
|
markersize=150,
|
||||||
|
vmin=vmin,
|
||||||
|
vmax=vmax
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
basel_land.plot(
|
||||||
|
ax=ax,
|
||||||
|
facecolor="none",
|
||||||
|
edgecolor="black",
|
||||||
|
linewidth=2
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
basel_stadt.plot(
|
||||||
|
ax=ax,
|
||||||
|
facecolor="none",
|
||||||
|
edgecolor="black",
|
||||||
|
linewidth=1
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.set_xlim(
|
||||||
|
7.45,
|
||||||
|
7.90
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.set_ylim(
|
||||||
|
47.35,
|
||||||
|
47.70
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# these offsets exist to avoid collision between names
|
||||||
|
label_offsets = {
|
||||||
|
"Basel Feldbergstrasse": (10, 8),
|
||||||
|
"Basel St. Johannplatz": (-10, -12),
|
||||||
|
"Basel Chrischona": (0, 8),
|
||||||
|
"Sissach-Bützenen": (0, 8),
|
||||||
|
"A2 Hard": (0, 8),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row in subset.iterrows():
|
||||||
|
|
||||||
|
dx, dy = label_offsets.get(
|
||||||
|
row["station_name"],
|
||||||
|
(0, 8)
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.annotate(
|
||||||
|
row["station_name"],
|
||||||
|
xy=(
|
||||||
|
row.geometry.x,
|
||||||
|
row.geometry.y
|
||||||
|
),
|
||||||
|
xytext=(dx, dy),
|
||||||
|
textcoords="offset points",
|
||||||
|
ha="center",
|
||||||
|
fontsize=9,
|
||||||
|
bbox=dict(
|
||||||
|
facecolor="white",
|
||||||
|
alpha=0.7,
|
||||||
|
edgecolor="none",
|
||||||
|
pad=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.set_title(
|
||||||
|
f"{pollutant} ({pollutant_units[pollutant]})"
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.axis("off")
|
||||||
|
|
||||||
|
for ax in axes[len(pollutants):]:
|
||||||
|
ax.set_visible(False)
|
||||||
|
fig.suptitle(day.strftime("%Y-%m-%d"))
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(CACHE / f"baselAirQuality-{day.strftime("%Y-%m-%d")}.png", dpi=120, bbox_inches="tight")
|
||||||
|
plt.close()
|
||||||
|
|
||||||
plt.tight_layout()
|
feeds = {
|
||||||
plt.show()
|
"fr": [],
|
||||||
|
"de": [],
|
||||||
|
"it": [],
|
||||||
|
"rm": [],
|
||||||
|
"en": []
|
||||||
|
}
|
||||||
|
|
||||||
|
yesterday = date.today() - timedelta(days=1)
|
||||||
|
current = start_date_datetime
|
||||||
|
|
||||||
|
while current <= yesterday:
|
||||||
|
dailyEntry = {}
|
||||||
|
dailyEntry["id"] = f"air-quality-basel-{current.isoformat()}"
|
||||||
|
dailyEntry["creationDate"] = current.isoformat()
|
||||||
|
dailyEntry["date"] = current.isoformat()
|
||||||
|
dailyEntry["source"] = "https://luftqualitaet.ch/"
|
||||||
|
dailyEntry["url"] = "https://luftqualitaet.ch/"
|
||||||
|
dailyEntry["text"] = f"<img src\"https://resspublica.tomasrivera.ch/images/baselAirQuality-{current.isoformat()}.png\"alt=\"basel air quality {current.isoformat()}\">"
|
||||||
|
|
||||||
|
for lang in ["fr", "de", "it", "rm", "en"]:
|
||||||
|
dailyEntry["title"] = f"{translatedAirQualityInBasel[lang]} {current.isoformat()}"
|
||||||
|
feeds[lang].append(copy.deepcopy(dailyEntry))
|
||||||
|
|
||||||
|
current += timedelta(days=1)
|
||||||
|
|
||||||
|
generateFeed(
|
||||||
|
translatedAirQualityInBasel["fr"],
|
||||||
|
f"Flux RSS des {translatedAirQualityInBasel["fr"]}",
|
||||||
|
translatedAirQualityInBaselCamelCase["fr"],
|
||||||
|
"fr",
|
||||||
|
["rss", "atom"],
|
||||||
|
datetime.fromisoformat(f"{yesterday.isoformat()} 23:59:59").replace(tzinfo=ZoneInfo("Europe/Zurich")),
|
||||||
|
feeds["fr"]
|
||||||
|
)
|
||||||
|
generateFeed(
|
||||||
|
translatedAirQualityInBasel["de"],
|
||||||
|
f"RSS-Feed für {translatedAirQualityInBasel['de']}",
|
||||||
|
translatedAirQualityInBaselCamelCase["de"],
|
||||||
|
"de",
|
||||||
|
["rss", "atom"],
|
||||||
|
datetime.fromisoformat(f"{yesterday.isoformat()} 23:59:59").replace(tzinfo=ZoneInfo("Europe/Zurich")),
|
||||||
|
feeds["de"]
|
||||||
|
)
|
||||||
|
|
||||||
|
generateFeed(
|
||||||
|
translatedAirQualityInBasel["en"],
|
||||||
|
f"RSS feed for {translatedAirQualityInBasel['en']}",
|
||||||
|
translatedAirQualityInBaselCamelCase["en"],
|
||||||
|
"en",
|
||||||
|
["rss", "atom"],
|
||||||
|
datetime.fromisoformat(f"{yesterday.isoformat()} 23:59:59").replace(tzinfo=ZoneInfo("Europe/Zurich")),
|
||||||
|
feeds["en"]
|
||||||
|
)
|
||||||
|
|
||||||
|
generateFeed(
|
||||||
|
translatedAirQualityInBasel["it"],
|
||||||
|
f"Feed RSS per {translatedAirQualityInBasel['it']}",
|
||||||
|
translatedAirQualityInBaselCamelCase["it"],
|
||||||
|
"it",
|
||||||
|
["rss", "atom"],
|
||||||
|
datetime.fromisoformat(f"{yesterday.isoformat()} 23:59:59").replace(tzinfo=ZoneInfo("Europe/Zurich")),
|
||||||
|
feeds["it"]
|
||||||
|
)
|
||||||
|
|
||||||
|
generateFeed(
|
||||||
|
translatedAirQualityInBasel["rm"],
|
||||||
|
f"Feed RSS per {translatedAirQualityInBasel['rm']}",
|
||||||
|
translatedAirQualityInBaselCamelCase["rm"],
|
||||||
|
"rm",
|
||||||
|
["rss", "atom"],
|
||||||
|
datetime.fromisoformat(f"{yesterday.isoformat()} 23:59:59").replace(tzinfo=ZoneInfo("Europe/Zurich")),
|
||||||
|
feeds["rm"]
|
||||||
|
)
|
||||||
|
|||||||
+1
-1
@@ -43,5 +43,5 @@ def main():
|
|||||||
if ( args.gen_bernReligionMap and date.today().day == 1 and date.today().month in [1, 5, 9]) or args.force_gen_bernReligionMap:
|
if ( args.gen_bernReligionMap and date.today().day == 1 and date.today().month in [1, 5, 9]) or args.force_gen_bernReligionMap:
|
||||||
generateBernReligionMap(ASSETS, CACHE)
|
generateBernReligionMap(ASSETS, CACHE)
|
||||||
if args.gen_baselLuftqualitat:
|
if args.gen_baselLuftqualitat:
|
||||||
generateBaselLuftqualitat(ASSETS)
|
generateBaselLuftqualitat(ASSETS, CACHE)
|
||||||
logging.info("Done")
|
logging.info("Done")
|
||||||
|
|||||||
@@ -96,3 +96,18 @@ translatedCityOfBern = {
|
|||||||
"de": "Stadt Bern",
|
"de": "Stadt Bern",
|
||||||
"fr": "Ville de Bern"
|
"fr": "Ville de Bern"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
translatedAirQualityInBasel = {
|
||||||
|
"fr": "Qualité de l'air à Bâle",
|
||||||
|
"en": "Air quality in Basel",
|
||||||
|
"de": "Luftqualität in Basel",
|
||||||
|
"it": "Qualità dell'aria a Basilea",
|
||||||
|
"rm": "Qualitad da l'aria a Basilea"
|
||||||
|
}
|
||||||
|
translatedAirQualityInBaselCamelCase = {
|
||||||
|
"fr": "qualiteDeLAirABale",
|
||||||
|
"en": "airQualityInBasel",
|
||||||
|
"de": "luftqualitatInBasel",
|
||||||
|
"it": "qualitaDell'AriaABasilea",
|
||||||
|
"rm": "qualitadDaLAriaABasilea"
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user