GeoDjango – Fix Rendering Duplicate Geometry in Leaflet

djangogeodjangoleaflet

I have a GeoDjango app following this tutorial but substituting World Borders polygon data as included in the standard GeoDjango tutorial.

I have created serializers and a django rest api that displays geometry intersecting the current bounding box using leaflet. The problem I've noticed is that when I pan around the map, it renders duplicate polygons on top of each other each time I set a new bounding box extent request. This results in the map slowing down as the number of polygons increases and also results in an opaque symbology.

Initial symbology with one render of polygon
After rendering multiple geometry after panning around the map

How do I ensure each polygon only renders once and not each time the map location refreshes?

models.py

from django.contrib.gis.db import models

class WorldBorder(models.Model):
    # Regular Django fields corresponding to the attributes in the
    # world borders shapefile.
    name = models.CharField(max_length=50)
    area = models.IntegerField()
    pop2005 = models.IntegerField('Population 2005')
    fips = models.CharField('FIPS Code', max_length=2, null=True)
    iso2 = models.CharField('2 Digit ISO', max_length=2)
    iso3 = models.CharField('3 Digit ISO', max_length=3)
    un = models.IntegerField('United Nations Code')
    region = models.IntegerField('Region Code')
    subregion = models.IntegerField('Sub-Region Code')
    lon = models.FloatField()
    lat = models.FloatField()

    # GeoDjango-specific: a geometry field (MultiPolygonField)
    mpoly = models.MultiPolygonField()

    # Returns the string representation of the model.
    def __str__(self):
        return self.name

    class Meta:
        ordering = ['name']
        verbose_name_plural = "World borders"

World borders viewset:

"""World borders API views."""
from rest_framework import viewsets
from rest_framework_gis import filters


from .models import WorldBorder
from .serializers import WorldBorderSerializer


class WorldBorderViewSet(viewsets.ReadOnlyModelViewSet):
    """World border view set."""

    bbox_filter_field = "mpoly"
    filter_backends = (filters.InBBoxFilter,)
    queryset = WorldBorder.objects.all()
    bbox_filter_include_overlapping = True
    serializer_class = WorldBorderSerializer

serializers.py

from rest_framework_gis import serializers

from .models import WorldBorder


class WorldBorderSerializer(serializers.GeoFeatureModelSerializer):
    """world border GeoJSON serializer."""

    class Meta:
        """World border serializer meta class."""

        fields = ("id", "name")
        geo_field = "mpoly"
        model = WorldBorder

leaflet map js

const copy = "© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors";
const url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const osm = L.tileLayer(url, { attribution: copy });
const map = L.map("map", { layers: [osm] });
map.
locate()
  .on("locationfound", (e) => map.setView(e.latlng, 8))
  .on("locationerror", () => map.setView([0, 0], 5));

async function load_world_borders() {
    const world_border_url = `/api/world/?in_bbox=${map.getBounds().toBBoxString()}`
    const response = await fetch(world_border_url)
    const geojson = await response.json()
    return geojson
}
  
async function render_world_borders() {
    const world_borders = await load_world_borders();
    L.geoJSON(world_borders)
        .bindPopup((layer) => layer.feature.properties.name)
        .addTo(map);
}
  
  map.on("moveend", render_world_borders);

edit – I have adjusted leaflet js as follows:

async function render_world_borders() {
    const world_borders = await load_world_borders();
    L.geoJSON(world_borders)
        .bindPopup((layer) => layer.feature.properties.name)
        .addTo(map)
        .myTag = "myGeoJSON";
}

var removeMarkers = function() {
    map.eachLayer( function(layer) {

      if ( layer.myTag &&  layer.myTag === "myGeoJSON") {
        map.removeLayer(layer)
          }

        });

}

map.on("moveend", removeMarkers);

map.on("moveend", render_world_borders);

This now removes the layers then re-renders. It seems to work better, but I notice particularly when zoomed out that the rendering of the same feature is still occurring as you can see in the image below.

example of polygon features rendering more than once

Best Answer

Since world borders GeoJSON data is requested by bounding box, you indeed have to clear previous features when loading new ones after map zoom or pan.

The simplest was to do that is to first create world borders layer at global level and then when new data is fetched, first clean it and than add new data.

Code could then look something like this:

const bordersLayer = L.geoJSON(null)
  .bindPopup((layer) => layer.feature.properties.name)
  .addTo(map);

async function render_world_borders() {
  bordersLayer.clearLayers();
  const world_borders = await load_world_borders();
  bordersLayer.addData(world_borders);
}
Related Question