Kartenansicht

Konfiguration

Bei der Liste der Bebauungspläne macht es Sinn sich die Lage der Pläne auch auf einer dynamischen Übersichtskarte anzeigen zu lassen. Hierzu kann django-leaflet genutzt werden. Im ersten Schritt muss man aber die Geodaten mit in den View übernehmen. Theoretisch wäre es ausreichend das Geometrie Feld geltungsbereich zu nutzen. Wenn man aber etwas Interaktion haben will, z.B. eine Selektierbarkeit einzelner Objekte im Viewer, dann ist es besser, ein gesamtes Geometrieobjekt mit ausgewählten Attributen zu verwenden. Hierzu überschreiben wir den die get_context_data Funktion der ListView um eine Serialisierung der Geoemtrien der aktuellen Seite und nennen sie markers

xplanung_light/views.py

from django.core.serializers import serialize
import json
# ...
class BPlanListView(SingleTableView):
# ...
def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["markers"] = json.loads(
            serialize("geojson", context['table'].page.object_list.data, geometry_field='geltungsbereich')
        )
        return context

komserv2/settings.py

# ...
SERIALIZATION_MODULES = {
    "geojson": "django.contrib.gis.serializers.geojson", 
}
# ...

Die Marker wollen wir in einem LeafletViewer darstellen. Hierzu müssen wir das Template bearbeiten. Aber zunächst brauchen wir noch die js-lib turf, die geometrische Operationen zur Verfügung stellt.

komserv/xplanung_light/templates/layout.html

<!-- ... -->
<head>
  <!-- ... -->
  <script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
  <!-- ... -->
</head>
<!-- ... -->

komserv/xplanung_light/templates/xplanung_light/bplan_list.html

{% extends "xplanung_light/layout.html" %}
{% load leaflet_tags %}
{% block title %}
    Liste der Bebauungspläne
{% endblock %}
{% load render_table from django_tables2 %}
{% block content %}
{{ markers|json_script:"markers-data" }}
<script>
<!-- javascript-Part - siehe nächster Abschnitt -->
</script>
{% leaflet_map "bplan_list_map" callback="window.map_init_basic" %}
<p><a href="{% url 'bplan-create' %}">BPlan anlegen</a></p>
{% render_table table %}
<p>Anzahl: {{ object_list.count }}</p>
{% endblock %}

In dem Javascript für den Leaflet-Client steckt jetzt etwas mehr Logik.

  • Die marker werden in die das HTML-Element mit der id markers-data übetragen

  • Leaflet parsed das Json und baut daraus features

  • Die Features werden anhand von Attributen unterschiedlich gefärbt

  • Es gibt ein click-Event, dass die Polygone abfragt und ein PopUp erzeugt

Javascript im Script-Tag des templates komserv/xplanung_light/templates/xplanung_light/bplan_list.html

    let mapGlobal = {};
    function map_init_basic (map, options) {
        mapGlobal = map;
        //https://stackoverflow.com/questions/43007019/leaflet-event-how-to-propagate-to-overlapping-layers
        const data = document.getElementById("markers-data");
        const markers = JSON.parse(data.textContent);
        map.setZoom(14);
        let feature = L.geoJSON(markers, {
                style: function(feature) {
                    switch (feature.properties.planart) {
                        case '1000': return {color: "#ff0000"};
                        case '10000':   return {color: "#0000ff"};
                    }
                }//,
                //onEachFeature: onEachFeature
                //zoomToBounds: zoomToBounds
            }
        )
            /*.bindPopup(function (layer) {
                return layer
                    .feature.properties.generic_id;
            })*/
        .addTo(map);       
        map.fitBounds(feature.getBounds());
        /*
        map.on('moveend', function() { 
            const bbox_field = document.getElementById("id_bbox");
            //bbox_field.value = "test";
            //alert(JSON.stringify(map.getBounds()));
            const bounds = map.getBounds();
            bbox_field.value = bounds._southWest.lng + "," + bounds._southWest.lat + "," + bounds._northEast.lng + "," + bounds._northEast.lat;
       });
       */
       /*function onEachFeature(feature, layer) {
            layer.on({
                click: zoomToFeature
            });
            //featureByName[feature.properties.name] = layer;
        }*/
        /*
        function zoomToBounds(bounds) {
            alert(JSON.stringify(bounds));
            //featureByName[feature.properties.name] = layer;
        }
        */
        /*
        function zoomToFeature(e) {
            map.fitBounds(e.target.getBounds());
        }
        */
        var popup = L.popup()
        map.on('click', e => {
            //var thisMap = map;
            const { lat, lng } = e.latlng;
            const point = turf.point([lng, lat]);
            const polygonsClicked = [];
            //console.log(map._layers)
            for (var id in map._layers) {
                const layer = map._layers[id]
                if (typeof layer.feature !== "undefined"){
                    //map._layers.forEach((p, i) => {
                    //const polygon= p.toGeoJSON();
                    const polygon = layer.feature;
                    //console.log(polygon)
                    //console.log(point)
                    if (turf.booleanPointInPolygon(point, polygon)) polygonsClicked.push(layer);
                }
            }
            if (polygonsClicked.length > 0) {
                popupContent = "Dokument(e):<br>";
                for (var id in polygonsClicked) {
                    //console.log(polygonsClicked[id]);
                    bounds = polygonsClicked[id].getBounds();
                    //console.log(bounds);
                    popupContent += "<a onclick='mapGlobal.fitBounds([[" + bounds._southWest.lat + ", " + bounds._southWest.lng + "], [" + bounds._northEast.lat + ", " + bounds._northEast.lng + "]]);'>+</a> ";
                    popupContent += "<b>" + polygonsClicked[id].feature.properties.title + "</b> (" + polygonsClicked[id].feature.properties.date_of_document + ") - " + polygonsClicked[id].feature.properties.description +  '<br>';
                    popupContent += "<a href='../" + polygonsClicked[id].feature.properties.pk + "/pdf/' target='_blank'>Download: " + polygonsClicked[id].feature.properties.pk + " (" + polygonsClicked[id].feature.properties.document_class + ")" + "</a><br>";
                }
                popup
                    .setLatLng(e.latlng)
                    .setContent(popupContent)
                    .openOn(map);
            } else {
                /*
                popup
                .setLatLng(e.latlng)
                .setContent("You clicked the map at " + e.latlng.toString())
                .openOn(map);
                */
            }
        });
    }

Anpassung der Höhe im css-File

komserv/xplanung_light/static/xplanung_light/site.css

#bplan_list_map { height: 180px; }

Kartenanzeige im Client

http://127.0.0.1:8000/bplan/

Kartenanzeige im Client

Suche

Installation django-filter

python3 -m pip install django-filter

komserv/settings.py

# ...
INSTALLED_APPS = [
    # ...
    'django_filters',
    # ...
]
# ...

Erstellen einer Filter Klasse

komserv/xplanung_light/filter.py

from django_filters import FilterSet, CharFilter, ModelChoiceFilter
from .models import BPlan, AdministrativeOrganization
from django.contrib.gis.geos import Polygon
from django.db.models import Q

def bbox_filter(queryset, value):
    #print("value from bbox_filter: " + value)
    # extract bbox from cs numerical values
    geom = Polygon.from_bbox(value.split(','))
    #print(geom)
    # 7.51461,50.31417,7.51563,50.31544
    return queryset.filter(geltungsbereich__bboverlaps=geom)


# https://stackoverflow.com/questions/68592837/custom-filter-with-django-filters
class BPlanFilter(FilterSet):

    name = CharFilter(lookup_expr='icontains')
    bbox = CharFilter(method='bbox_filter', label='BBOX')
    gemeinde = ModelChoiceFilter(queryset=AdministrativeOrganization.objects.only("pk", "name", "type"))

    class Meta:
        model = BPlan
        fields = ["name", "gemeinde", "planart", "bbox"]

    def bbox_filter(self, queryset, name, value):
        #print("name from DocumentFilter.bbox_filter: " + name)
        return bbox_filter(queryset, value)

Anpassen des Views

  • Import der Klassen

  • Erben von FilterView

  • neues Attribut filterset_class

  • fixe Definition des templates - sonst sucht django nach bplan_filter.html - und das existiert nicht

  • Überschreiben von get_queryset

komserv/xplanung_light/views.py

# ...
from .filter import BPlanFilter
from django_filters.views import FilterView
# ...
class BPlanListView(FilterView, SingleTableView):
    model = BPlan
    table_class = BPlanTable
    template_name = 'xplanung_light/bplan_list.html'
    success_url = reverse_lazy("bplan-list") 
    filterset_class = BPlanFilter

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["markers"] = json.loads(
            serialize("geojson", context['table'].page.object_list.data, geometry_field='geltungsbereich')
        )
        return context

    def get_queryset(self):
        qs = super().get_queryset()
        self.filter_set = BPlanFilter(self.request.GET, queryset=qs)
        return self.filter_set.qs
# ...

Konfiguration der Filterfunktionen im template

templates/xplanung_light/bplan_list.html

<!-- ... -->
{% leaflet_map "bplan_list_map" callback="window.map_init_basic" %}
Filter
<!-- add boostrap form css - if wished -->
{% load django_bootstrap5 %}
<form method="get" action="">
    {{ filter.form.as_p }}
    <input type="submit" /><a href="{% url 'bplan-list' %}">Filter löschen</a>.</p>
</form>
<p><a href="{% url 'bplan-create' %}">BPlan anlegen</a></p>
<!-- ... -->

Darstellung der Filter

http://127.0.0.1:8000/bplan/

Darstellung der Filter