From 3d69827c96ae42ec5d092e661706e9169eada36c Mon Sep 17 00:00:00 2001 From: Peter Andorfer Date: Mon, 9 Dec 2024 14:14:00 +0100 Subject: [PATCH 1/6] adds lat/lng for edges (#275) --- network/management/commands/edges.py | 6 +++ ...dge_source_lng_edge_target_lat_and_more.py | 33 +++++++++++++++ ...urce_lat_alter_edge_source_lng_and_more.py | 41 +++++++++++++++++++ network/models.py | 4 ++ 4 files changed, 84 insertions(+) create mode 100644 network/migrations/0003_edge_source_lat_edge_source_lng_edge_target_lat_and_more.py create mode 100644 network/migrations/0004_alter_edge_source_lat_alter_edge_source_lng_and_more.py diff --git a/network/management/commands/edges.py b/network/management/commands/edges.py index c7cd42e..95eb06d 100644 --- a/network/management/commands/edges.py +++ b/network/management/commands/edges.py @@ -41,6 +41,12 @@ def handle(self, *args, **kwargs): "start_date": x.start_date, "end_date": x.end_date, } + if source_kind == "place": + item["source_lat"] = source_obj.lat + item["source_lng"] = source_obj.lng + if target_kind == "place": + item["target_lat"] = target_obj.lat + item["target_lng"] = target_obj.lng try: Edge.objects.create(**item) except Exception as e: diff --git a/network/migrations/0003_edge_source_lat_edge_source_lng_edge_target_lat_and_more.py b/network/migrations/0003_edge_source_lat_edge_source_lng_edge_target_lat_and_more.py new file mode 100644 index 0000000..2b689f2 --- /dev/null +++ b/network/migrations/0003_edge_source_lat_edge_source_lng_edge_target_lat_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.3 on 2024-12-09 12:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("network", "0002_alter_edge_options_alter_edge_source_kind_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="edge", + name="source_lat", + field=models.FloatField(blank=True, null=True, verbose_name="Breitengrad"), + ), + migrations.AddField( + model_name="edge", + name="source_lng", + field=models.FloatField(blank=True, null=True, verbose_name="Längengrad"), + ), + migrations.AddField( + model_name="edge", + name="target_lat", + field=models.FloatField(blank=True, null=True, verbose_name="Breitengrad"), + ), + migrations.AddField( + model_name="edge", + name="target_lng", + field=models.FloatField(blank=True, null=True, verbose_name="Längengrad"), + ), + ] diff --git a/network/migrations/0004_alter_edge_source_lat_alter_edge_source_lng_and_more.py b/network/migrations/0004_alter_edge_source_lat_alter_edge_source_lng_and_more.py new file mode 100644 index 0000000..c20d51a --- /dev/null +++ b/network/migrations/0004_alter_edge_source_lat_alter_edge_source_lng_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1.3 on 2024-12-09 12:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("network", "0003_edge_source_lat_edge_source_lng_edge_target_lat_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="edge", + name="source_lat", + field=models.FloatField( + blank=True, null=True, verbose_name="Breitengrad (Start)" + ), + ), + migrations.AlterField( + model_name="edge", + name="source_lng", + field=models.FloatField( + blank=True, null=True, verbose_name="Längengrad (Start)" + ), + ), + migrations.AlterField( + model_name="edge", + name="target_lat", + field=models.FloatField( + blank=True, null=True, verbose_name="Breitengrad (Ziel)" + ), + ), + migrations.AlterField( + model_name="edge", + name="target_lng", + field=models.FloatField( + blank=True, null=True, verbose_name="Längengrad (Ziel)" + ), + ), + ] diff --git a/network/models.py b/network/models.py index 765e282..3c336ec 100644 --- a/network/models.py +++ b/network/models.py @@ -47,6 +47,8 @@ class Edge(models.Model): verbose_name="Art der Quelle", help_text="Art der Quelle (Person, Ort, Werk, Institution, Ereignis)", ) + source_lat = models.FloatField(blank=True, null=True, verbose_name="Breitengrad (Start)") + source_lng = models.FloatField(blank=True, null=True, verbose_name="Längengrad (Start)") source_id = models.IntegerField( verbose_name="ID der Quelle", help_text="ID der Quelle" ) @@ -66,6 +68,8 @@ class Edge(models.Model): verbose_name="Art des Ziels", help_text="Art des Ziels (Person, Ort, Werk, Institution, Ereignis)", ) + target_lat = models.FloatField(blank=True, null=True, verbose_name="Breitengrad (Ziel)") + target_lng = models.FloatField(blank=True, null=True, verbose_name="Längengrad (Ziel)") target_id = models.IntegerField( verbose_name="ID des Ziels", help_text="ID des Ziels" ) From 9b563b85c6843b867b64d99c24a42b3a71aea4bb Mon Sep 17 00:00:00 2001 From: Peter Andorfer Date: Mon, 9 Dec 2024 17:22:44 +0100 Subject: [PATCH 2/6] adds geojson endpoint for edges (#276) * adds geojson endpoint for edges * fix for test --- network/models.py | 16 ++++++++++++---- network/urls.py | 5 +++-- network/utils.py | 37 +++++++++++++++++++++++++++++++++++++ network/views.py | 23 +++++++++++++++++++++++ 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 network/utils.py diff --git a/network/models.py b/network/models.py index 3c336ec..3fb9c38 100644 --- a/network/models.py +++ b/network/models.py @@ -47,8 +47,12 @@ class Edge(models.Model): verbose_name="Art der Quelle", help_text="Art der Quelle (Person, Ort, Werk, Institution, Ereignis)", ) - source_lat = models.FloatField(blank=True, null=True, verbose_name="Breitengrad (Start)") - source_lng = models.FloatField(blank=True, null=True, verbose_name="Längengrad (Start)") + source_lat = models.FloatField( + blank=True, null=True, verbose_name="Breitengrad (Start)" + ) + source_lng = models.FloatField( + blank=True, null=True, verbose_name="Längengrad (Start)" + ) source_id = models.IntegerField( verbose_name="ID der Quelle", help_text="ID der Quelle" ) @@ -68,8 +72,12 @@ class Edge(models.Model): verbose_name="Art des Ziels", help_text="Art des Ziels (Person, Ort, Werk, Institution, Ereignis)", ) - target_lat = models.FloatField(blank=True, null=True, verbose_name="Breitengrad (Ziel)") - target_lng = models.FloatField(blank=True, null=True, verbose_name="Längengrad (Ziel)") + target_lat = models.FloatField( + blank=True, null=True, verbose_name="Breitengrad (Ziel)" + ) + target_lng = models.FloatField( + blank=True, null=True, verbose_name="Längengrad (Ziel)" + ) target_id = models.IntegerField( verbose_name="ID des Ziels", help_text="ID des Ziels" ) diff --git a/network/urls.py b/network/urls.py index e37ca70..6b723fc 100644 --- a/network/urls.py +++ b/network/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from network.views import EdgeListViews, network_data, NetworkView +from network.views import EdgeListViews, network_data, NetworkView, edegs_as_geojson app_name = "network" urlpatterns = [ path("edges/", EdgeListViews.as_view(), name="edges_browse"), - path("csv/", network_data, name="data"), + path("network-data/", network_data, name="data"), path("network/", NetworkView.as_view(), name="network"), + path("geojson-data/", edegs_as_geojson, name="network"), ] diff --git a/network/utils.py b/network/utils.py new file mode 100644 index 0000000..a2aa805 --- /dev/null +++ b/network/utils.py @@ -0,0 +1,37 @@ +import pandas as pd + + +def df_to_geojson_vect( + df: pd.DataFrame, properties: list, lat="latitude", lon="longitude" +) -> tuple: + """converts a dataframe into a geojson + taken from https://blog.finxter.com/5-best-ways-to-convert-a-pandas-dataframe-to-geojson/ + + Args: + df (pd.DataFrame): a pandas DataFrame + properties (list): column keys which should be used as properties + lat (str, optional): the name of the column holding the latitute. Defaults to 'latitude'. + lon (str, optional): the anem of the column holding the longitute. Defaults to 'longitude'. + + Returns: + tuple: (lat, long) + """ + features = df.apply( + lambda row: { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [row[lon], row[lat]], + }, + "properties": {prop: row[prop] for prop in properties}, + }, + axis=1, + ).tolist() + return {"type": "FeatureCollection", "features": features} + + +def get_coords(row): + if pd.isna(row["source_lat"]): + return (row["target_lat"], row["target_lng"]) + else: + return row["source_lat"], row["source_lng"] diff --git a/network/views.py b/network/views.py index 748a685..140a8a8 100644 --- a/network/views.py +++ b/network/views.py @@ -13,6 +13,7 @@ from network.forms import EdgeFilterFormHelper from network.models import Edge from network.tables import EdgeTable +from network.utils import get_coords, df_to_geojson_vect class NetworkView(TemplateView): @@ -51,6 +52,28 @@ class EdgeListViews(GenericListView): template_name = "network/list_view.html" +def edegs_as_geojson(request): + values_list = [x.name for x in Edge._meta.get_fields()] + qs = ( + Edge.objects.filter(edge_kind__icontains="place") + .exclude(source_lat__isnull=True, target_lat__isnull=True) + .exclude(edge_kind="placeplace") + ) + items = EdgeListFilter(request.GET, queryset=qs).qs.values_list(*values_list) + df = pd.DataFrame(list(items), columns=values_list) + try: + df["label"] = df[["source_label", "edge_label", "target_label"]].agg( + " ".join, axis=1 + ) + except ValueError: + return JsonResponse(data={}) + df[["latitude", "longitude"]] = df.apply( + lambda row: pd.Series(get_coords(row)), axis=1 + ) + data = df_to_geojson_vect(df, ["label", "edge_id"]) + return JsonResponse(data=data) + + def network_data(request): values_list = [x.name for x in Edge._meta.get_fields()] qs = EdgeListFilter(request.GET, queryset=Edge.objects.all()).qs From 9da29d6d555f49460be5dc3a635ada0d2341be1a Mon Sep 17 00:00:00 2001 From: csae8092 Date: Mon, 9 Dec 2024 17:43:10 +0100 Subject: [PATCH 3/6] wip [skip ci] --- network/templates/network/list_view.html | 2 + network/templates/network/map.html | 64 ++++++++++++++++++++++++ network/urls.py | 5 +- network/views.py | 7 ++- pmb/settings.py | 25 +++++++++ 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 network/templates/network/map.html diff --git a/network/templates/network/list_view.html b/network/templates/network/list_view.html index 4670364..f58a1e8 100644 --- a/network/templates/network/list_view.html +++ b/network/templates/network/list_view.html @@ -16,6 +16,8 @@

CSV JSON Als Netzwerk + GeoJson + Karte diff --git a/network/templates/network/map.html b/network/templates/network/map.html new file mode 100644 index 0000000..0925512 --- /dev/null +++ b/network/templates/network/map.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} +{% load static %} +{% load leaflet_tags %} +{% block title %}Map{% endblock %} +{% block scriptHeader %} +{% leaflet_js %} +{% leaflet_css %} +{% endblock %} +{% block content %} + + +
+ +
+ {% leaflet_map "yourmap" callback="window.map_init_basic" %} +
+ +
+ + + +{% endblock %} \ No newline at end of file diff --git a/network/urls.py b/network/urls.py index 6b723fc..0bc7a54 100644 --- a/network/urls.py +++ b/network/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from network.views import EdgeListViews, network_data, NetworkView, edegs_as_geojson +from network.views import EdgeListViews, network_data, NetworkView, edges_as_geojson, MapView app_name = "network" @@ -7,5 +7,6 @@ path("edges/", EdgeListViews.as_view(), name="edges_browse"), path("network-data/", network_data, name="data"), path("network/", NetworkView.as_view(), name="network"), - path("geojson-data/", edegs_as_geojson, name="network"), + path("geojson-data/", edges_as_geojson, name="network-as-geojson"), + path("map/", MapView.as_view(), name="map"), ] diff --git a/network/views.py b/network/views.py index 140a8a8..253b14d 100644 --- a/network/views.py +++ b/network/views.py @@ -37,6 +37,10 @@ def get_context_data(self, **kwargs): return context +class MapView(TemplateView): + template_name = "network/map.html" + + class EdgeListViews(GenericListView): model = Edge filter_class = EdgeListFilter @@ -52,7 +56,7 @@ class EdgeListViews(GenericListView): template_name = "network/list_view.html" -def edegs_as_geojson(request): +def edges_as_geojson(request): values_list = [x.name for x in Edge._meta.get_fields()] qs = ( Edge.objects.filter(edge_kind__icontains="place") @@ -71,6 +75,7 @@ def edegs_as_geojson(request): lambda row: pd.Series(get_coords(row)), axis=1 ) data = df_to_geojson_vect(df, ["label", "edge_id"]) + data["metadata"] = {"number of objects": len(df)} return JsonResponse(data=data) diff --git a/pmb/settings.py b/pmb/settings.py index ba3ee85..eb3a0b6 100644 --- a/pmb/settings.py +++ b/pmb/settings.py @@ -44,6 +44,7 @@ "django.contrib.staticfiles", "rest_framework", "browsing", + "leaflet", "crispy_forms", "crispy_bootstrap5", "django_tables2", @@ -336,3 +337,27 @@ ("schnitzler-zeitungen.", "schnitzler-zeitungen", "#8E8575"), ("schnitzler-mikrofilme.", "schnitzler-mikrofilme", "#6e7b8b"), ] + + +LEAFLET_CONFIG = { + "DEFAULT_CENTER": (47, 16), + "DEFAULT_ZOOM": 6, + "MIN_ZOOM": 3, + "OVERLAYS": [ + ( + "dinamlex", + "https://maps.acdh.oeaw.ac.at/mapserv?map=/data/test.map&mode=tile&layers=test&tilemode=gmap&tile={x}+{y}+{z}", + {"maxZoom": 18, "opacity": 0.7}, + ), + ( + "czoernig", + "https://maps.acdh.oeaw.ac.at/mapserv?map=/data/czoernig.map&mode=tile&layers=czoernig&tilemode=gmap&tile={x}+{y}+{z}", + {"maxZoom": 18, "opacity": 0.7}, + ), + ( + "tirol", + "https://maps.acdh.oeaw.ac.at/mapserv?map=/data/tirol.map&mode=tile&layers=tirol&tilemode=gmap&tile={x}+{y}+{z}", + {"maxZoom": 18, "opacity": 0.7}, + ), + ], +} \ No newline at end of file From 35beaf7173b016237a55e687f85327b1104aa201 Mon Sep 17 00:00:00 2001 From: csae8092 Date: Tue, 10 Dec 2024 08:15:07 +0100 Subject: [PATCH 4/6] some working example --- network/templates/network/list_view.html | 2 +- network/templates/network/map.html | 51 ++++++++++-------------- network/urls.py | 2 +- pmb/settings.py | 25 ------------ templates/partials/head.html | 10 +++-- 5 files changed, 29 insertions(+), 61 deletions(-) diff --git a/network/templates/network/list_view.html b/network/templates/network/list_view.html index f58a1e8..f263f26 100644 --- a/network/templates/network/list_view.html +++ b/network/templates/network/list_view.html @@ -16,7 +16,7 @@

CSV JSON Als Netzwerk - GeoJson + GeoJson Karte diff --git a/network/templates/network/map.html b/network/templates/network/map.html index 0925512..99eea1f 100644 --- a/network/templates/network/map.html +++ b/network/templates/network/map.html @@ -1,43 +1,25 @@ {% extends "base.html" %} {% load static %} -{% load leaflet_tags %} {% block title %}Map{% endblock %} {% block scriptHeader %} -{% leaflet_js %} -{% leaflet_css %} {% endblock %} {% block content %}
- -
- {% leaflet_map "yourmap" callback="window.map_init_basic" %} -
+

Map

+
- + {% endblock %} \ No newline at end of file diff --git a/network/urls.py b/network/urls.py index 0bc7a54..c25271d 100644 --- a/network/urls.py +++ b/network/urls.py @@ -7,6 +7,6 @@ path("edges/", EdgeListViews.as_view(), name="edges_browse"), path("network-data/", network_data, name="data"), path("network/", NetworkView.as_view(), name="network"), - path("geojson-data/", edges_as_geojson, name="network-as-geojson"), + path("geojson-data/", edges_as_geojson, name="geojson"), path("map/", MapView.as_view(), name="map"), ] diff --git a/pmb/settings.py b/pmb/settings.py index eb3a0b6..ba3ee85 100644 --- a/pmb/settings.py +++ b/pmb/settings.py @@ -44,7 +44,6 @@ "django.contrib.staticfiles", "rest_framework", "browsing", - "leaflet", "crispy_forms", "crispy_bootstrap5", "django_tables2", @@ -337,27 +336,3 @@ ("schnitzler-zeitungen.", "schnitzler-zeitungen", "#8E8575"), ("schnitzler-mikrofilme.", "schnitzler-mikrofilme", "#6e7b8b"), ] - - -LEAFLET_CONFIG = { - "DEFAULT_CENTER": (47, 16), - "DEFAULT_ZOOM": 6, - "MIN_ZOOM": 3, - "OVERLAYS": [ - ( - "dinamlex", - "https://maps.acdh.oeaw.ac.at/mapserv?map=/data/test.map&mode=tile&layers=test&tilemode=gmap&tile={x}+{y}+{z}", - {"maxZoom": 18, "opacity": 0.7}, - ), - ( - "czoernig", - "https://maps.acdh.oeaw.ac.at/mapserv?map=/data/czoernig.map&mode=tile&layers=czoernig&tilemode=gmap&tile={x}+{y}+{z}", - {"maxZoom": 18, "opacity": 0.7}, - ), - ( - "tirol", - "https://maps.acdh.oeaw.ac.at/mapserv?map=/data/tirol.map&mode=tile&layers=tirol&tilemode=gmap&tile={x}+{y}+{z}", - {"maxZoom": 18, "opacity": 0.7}, - ), - ], -} \ No newline at end of file diff --git a/templates/partials/head.html b/templates/partials/head.html index 2fb727e..017c69e 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -19,10 +19,12 @@ - - + + + + + + +
+

Beziehungen von und mit Orten

+
+
- +
@@ -24,27 +26,75 @@

Map

fetch(url) .then(response => { if (!response.ok) { - throw new Error("Network response was not ok"); + throw new Error("Geojson response was not ok"); } return response.json(); }) .then(data => { - var map = L.map('map').setView([48.20 , 16.37], 6); - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + var map = L.map('map') + console.log(data["metadata"]) + var OSMBaseLayer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); + + var CartoDB_PositronNoLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors © CARTO', + subdomains: 'abcd', + maxZoom: 20 + }); + + var CartoDB_DarkMatterNoLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors © CARTO', + subdomains: 'abcd', + maxZoom: 20 + }); + const markers = L.markerClusterGroup(); const geojsonLayer = L.geoJSON(data, { onEachFeature: function (feature, layer) { layer.bindPopup(feature.properties.label); }, pointToLayer: function (feature, latlng) { - return L.marker(latlng); // Convert GeoJSON points to Leaflet markers + return L.marker(latlng); } - }); + }); + markers.addTo(map); geojsonLayer.eachLayer(layer => markers.addLayer(layer)); - map.addLayer(markers); + + var heatData = []; + L.geoJSON(data, { + onEachFeature: function (feature, layer) { + if (feature.geometry.type === "Point") { + var lat = feature.geometry.coordinates[1]; + var lng = feature.geometry.coordinates[0]; + heatData.push([lat, lng]); + } + } + }); + + // Create the heatmap layer + var heatmapLayer = L.heatLayer(heatData, { + radius: 25, + blur: 10, + maxZoom: 17, + max: 0.7, + gradient: {0: 'white', 0.5: 'lime', 1: 'red'}, + + }); + + var baseMaps = { + "Base Layer": OSMBaseLayer, + "CartoDB hell": CartoDB_PositronNoLabels, + "CartoDB dunkel": CartoDB_DarkMatterNoLabels + }; + + const overlayMaps = { + "Marker Cluster": markers, + "Heatmap": heatmapLayer + }; + + L.control.layers(baseMaps, overlayMaps, { collapsed: false }).addTo(map); map.fitBounds(geojsonLayer.getBounds()); }) .catch(error => { From 9e4722c19788eb4f8d2ab8d5d856fd1c79fb7e8d Mon Sep 17 00:00:00 2001 From: csae8092 Date: Tue, 10 Dec 2024 10:03:16 +0100 Subject: [PATCH 6/6] better title by the one and only @AlexanderWatzinger [skip ci] --- network/templates/network/map.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/templates/network/map.html b/network/templates/network/map.html index 4f2e57d..f4a2193 100644 --- a/network/templates/network/map.html +++ b/network/templates/network/map.html @@ -12,7 +12,7 @@
-

Beziehungen von und mit Orten

+

Beziehungen zu Orten