Skip to content

Commit

Permalink
Merge pull request #114 from arthur-schnitzler/274-plot-relations-geo…
Browse files Browse the repository at this point in the history
…json-to-map

274 plot relations geojson to map
  • Loading branch information
csae8092 authored Dec 10, 2024
2 parents 0264edb + 9e4722c commit 38a46f3
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 6 deletions.
6 changes: 6 additions & 0 deletions network/management/commands/edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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"),
),
]
Original file line number Diff line number Diff line change
@@ -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)"
),
),
]
12 changes: 12 additions & 0 deletions network/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +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_id = models.IntegerField(
verbose_name="ID der Quelle", help_text="ID der Quelle"
)
Expand All @@ -66,6 +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_id = models.IntegerField(
verbose_name="ID des Ziels", help_text="ID des Ziels"
)
Expand Down
2 changes: 2 additions & 0 deletions network/templates/network/list_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ <h1 class="display-1 text-center">
<a type="button" class="btn btn-outline-primary" href="{% url 'network:data' %}{% querystring %}&format=csv">CSV</a>
<a type="button" class="btn btn-outline-primary" href="{% url 'network:data' %}{% querystring %}&format=cosmograph">JSON</a>
<a type="button" class="btn btn-outline-primary" href="{% url 'network:network' %}{% querystring %}&format=cosmograph">Als Netzwerk</a>
<a type="button" class="btn btn-outline-primary" href="{% url 'network:geojson' %}{% querystring %}">GeoJson</a>
<a type="button" class="btn btn-outline-primary" href="{% url 'network:map' %}{% querystring %}">Karte</a>
</div>
</div>

Expand Down
105 changes: 105 additions & 0 deletions network/templates/network/map.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Map{% endblock %}
{% block scriptHeader %}
{% endblock %}
{% block content %}
<style>
#map {
width: 100%;
height: 700px;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.heat/0.2.0/leaflet-heat.js"></script>
<div class="container-fluid pt-3">
<h1 class="display-3 text-center">Beziehungen zu Orten</h1>
<div id="mapcontainer" class="p-4">
<div id="legend"></div>
<div id="map"></div>
</div>
</div>
<span id="url" class="visually-hidden" aria-hidden="true">{% url 'network:geojson' %}{% querystring %}</span>

<script type="text/javascript">
const url = document.getElementById("url").textContent;
console.log("fetching data")
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error("Geojson response was not ok");
}
return response.json();
})
.then(data => {
var map = L.map('map')
console.log(data["metadata"])
var OSMBaseLayer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);

var CartoDB_PositronNoLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
maxZoom: 20
});

var CartoDB_DarkMatterNoLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
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);
}
});
markers.addTo(map);
geojsonLayer.eachLayer(layer => markers.addLayer(layer));

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 => {
console.error("Something went wrong:", error);
});

</script>
{% endblock %}
6 changes: 4 additions & 2 deletions network/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from django.urls import path
from network.views import EdgeListViews, network_data, NetworkView
from network.views import EdgeListViews, network_data, NetworkView, edges_as_geojson, MapView


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/", edges_as_geojson, name="geojson"),
path("map/", MapView.as_view(), name="map"),
]
37 changes: 37 additions & 0 deletions network/utils.py
Original file line number Diff line number Diff line change
@@ -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"]
28 changes: 28 additions & 0 deletions network/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -36,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
Expand All @@ -51,6 +56,29 @@ class EdgeListViews(GenericListView):
template_name = "network/list_view.html"


def edges_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"])
data["metadata"] = {"number of objects": len(df)}
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
Expand Down
10 changes: 6 additions & 4 deletions templates/partials/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{% static 'img/browserconfig.xml' %}">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css" />
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>

<script type="text/javascript">
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
Expand Down

0 comments on commit 38a46f3

Please sign in to comment.