TransWikia.com

Leaflet is behaving unexpectedly when trying to load more than one layer

Geographic Information Systems Asked by Ricardo Oliveira on May 14, 2021

I had a hard time trying to create a good title for my issue since it is a little hard to explain. But I will do my best.

I’m trying to create a web map to show three distinct layers, each from a different time period. The user choose a year and then the layer is loaded. For this to happen Geoserver is serving three layers using WFS. Now what follows is weird.

  • Sometimes when the web map is loaded, or refreshed, some layers are not encoded.
  • Sometimes layers end-up in the wrong year option.
  • But sometimes everything works, but requires that I refresh the page several times.

My code is not behaving as it should all the time. At first I thought that this should be something related to Geoserver (I posted this question a while ago following this line of though). But now I suspect that the issue is in my Javascript code, I friend of mine mentioned something relate to syncronous and asycrounouns code but I have not idea how to start debugging it.

This is my code (changed a few things to make it easier to read):

http://plnkr.co/edit/g1bHqJ1F2TcOlzieW1G8

<html>

<head>
<title>Mapping Past Denver</title>
<link rel="stylesheet" href="Plugins/leaflet-0.7.3/leaflet.css" />
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="stylesheet" href="Plugins/Leaflet.label-master/dist/leaflet.label.css" />
<link rel="stylesheet" href="Plugins/fullscreen/Control.FullScreen.css" />
<script src="Plugins/leaflet-0.7.3/leaflet-src.js"></script>
<script src="Plugins/jquery-1.11.2.min.js"></script>
<script src="Plugins/Leaflet.label-master/dist/leaflet.label.js"></script>
<script src="Plugins/fullscreen/Control.FullScreen.js"></script>
</head>

<body>
<div id="map">
    <script>
        $(document).ready(function () {
            var map = L.map('map', {
                fullscreenControl: true,
                fullscreenControlOptions: {
                    position: 'topleft'
                }
            }).setView([39.752171, -104.998817], 17);

            //the base map
            L.tileLayer('https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png', {
                maxZoom: 20,
                attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
                    '<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
                    'Imagery © <a href="http://mapbox.com">Mapbox</a>',
                id: 'examples.map-20v6611k'
            }).addTo(map);

            //create the datasets that will hold the layers      
            var dataset1 = new L.layerGroup();
            var dataset2 = new L.layerGroup();
            var dataset3 = new L.layerGroup();
            //JSON request 
            $.ajax({
                url: "http://localhost:8080/geoserver/PastDenver/ows?service=WFS&version=2.0.0&request=GetFeature&typeName=PastDenver:dataset3&maxFeatures=300&outputFormat=text/javascript&format_options=callback:getJson",
                dataType: 'jsonp',
                jsonpCallback: 'getJson',
                success: function (response) {
                    WFSLayers = L.geoJson(response, {
                        style: function (feature) {
                            return {
                                weight: 5,
                                color: '#6e7ce8',
                                weight: 2,
                                opacity: 1,
                                dashArray: '3',
                                fillOpacity: 0.7,
                            };
                        },
                        onEachFeature: effects
                    }).addTo(dataset1);
                }
            });


            $.ajax({
                url: "http://localhost:8080/geoserver/PastDenver/ows?service=WFS&version=2.0.0&request=GetFeature&typeName=PastDenver:dataset1&maxFeatures=300&outputFormat=text/javascript&format_options=callback:getJson",
                dataType: 'jsonp',
                jsonpCallback: 'getJson',
                success: function (response) {
                    WFSLayer2 = L.geoJson(response, {
                        style: function (feature) {
                            return {
                                weight: 5,
                                color: '#e31424',
                                weight: 2,
                                opacity: 1,
                                dashArray: '3',
                                fillOpacity: 0.7,
                            };
                        },
                        onEachFeature: effects
                    }).addTo(dataset2);
                }
            });


            $.ajax({
                url: "http://localhost:8080/geoserver/PastDenver/ows?service=WFS&version=2.0.0&request=GetFeature&typeName=PastDenver:dataset2&maxFeatures=300&outputFormat=text/javascript&format_options=callback:getJson",
                dataType: 'jsonp',
                jsonpCallback: 'getJson',
                success: function (response) {
                    WFSLayer3 = L.geoJson(response, {
                        style: function (feature) {
                            return {
                                weight: 5,
                                color: '#14e324',
                                weight: 2,
                                opacity: 1,
                                dashArray: '3',
                                fillOpacity: 0.7,
                            };
                        },
                        onEachFeature: effects
                    }).addTo(dataset3);
                }
            });


            //hover effect and popup effect
            function effects(feature, layer) {
                popupOptions = {
                    maxWidth: 200
                };
                if (feature.properties.name !== null) {
                    layer.bindLabel(feature.properties.name, popupOptions, {
                        noHide: true
                    });
                };
                layer.bindPopup("<b>Name: </b>" + feature.properties.name + "<br><b>Description: </b>" + feature.properties.descr + "<br><b>Floors: </b>" + feature.properties.floors + "<br><b>Material: </b>" + feature.properties.material);
                layer.on({
                    click: zoomToFeature
                });
            };


            //zoom to feature
            function zoomToFeature(e) {
                map.fitBounds(e.target.getBounds());
            }

            //group the datasets
            var overlayData = {
                "1887": dataset1,
                "1925": dataset2,
                "1961": dataset3
            };


            //create the layer control
            L.control.layers(overlayData, null, {
                collapsed: false
            }).addTo(map);

        });
    </script>
</div>


I did some quick research and it seems that the part that may causing issue is the AJAX call, more and more I’m suspecting that this a case of asynchronous code. Still, I haven’t found a clear way to fix it. Also, I don’t understand why my ajax calls are putting the data in different variables (sometime 1887 data is attributed to dataset3).


It seems that the issue is indeed AJAX making the requests at the same time. But yet, I haven’t find a clear way to fix this issue. Probably deferred objects is the best solution here but I’m finding trouble applying it to the code.

4 Answers

After seeking for a more broad (jQuery and AJAX related) answer here I assigned to my jsonpCallbacks unique values, like the following:

//JSON request 
                $.ajax({
                    url: "http://localhost:8080/geoserver/PastDenver/ows?service=WFS&version=2.0.0&request=GetFeature&typeName=PastDenver:dataset3&maxFeatures=300&outputFormat=text/javascript&format_options=callback:getJson1",
                    dataType: 'jsonp',
                    jsonpCallback: 'getJson1',
                    success: function (response) {
                        WFSLayers = L.geoJson(response, {
                            style: function (feature) {
                                return {
                                    weight: 5,
                                    color: '#6e7ce8',
                                    weight: 2,
                                    opacity: 1,
                                    dashArray: '3',
                                    fillOpacity: 0.7,
                                };
                            },
                            onEachFeature: effects
                        }).addTo(dataset1);
                    }
                });

For each layer I assigned a specific callback name. Now everything seems to be working just fine, doesn't matter hoe many time I refresh the page, no errors are thrown into the console.

I will make this an answer since it solved my problem, not sure if this is a logical fix or not though.

Correct answer by Ricardo Oliveira on May 14, 2021

Your map is being shown in the browser before all the GeoJSON is loaded into your layers.

Try wrapping your JavaScript with jQuery's ready() method.

$(document).ready(function(){

  <put all your code in here>

 }); 

Answered by Ken on May 14, 2021

Two pieces of advice (which may or may not constitute an answer--haven't tested your code myself):

Use Promise Objects

Available within jQuery (as well as many other js libraries). Read the docs: essentially, a promise object allows you to work with variables that point to data that may still be loading asynchronously.

E.g.

var promiseData = $.Deferred();

$.ajax(url, {
  success: function(data){
    promiseData.resolve(data);
    // optionally, do other stuff with the data once it's fully loaded
  },
  error: function(jqXHR, textStatus, errorThrown){
    promiseData.reject(errorThrown);
  }
});

To interact with the promise object outside of the $.ajax() success callback, use the .done() handler:

promiseData.done(function(data){
  console.log(data);
});

Abstract Code into Functions

There's a lot of different ways to do this, but if you see yourself writing the same code over and over, chances are you could encapsulate that logic once in a function that you call with different argument parameters.

One approach would be something like:

var loadData = function(url, styleFunction){
  var wfsLayer = $.Deferred();
  $.ajax({
    url: url,
    dataType: 'jsonp',
    jsonpCallback: 'getJson',
    success: function(data){
      wfsLayer = L.geojson(data, {
        style: styleFunction,
        onEachFeature: function(feature, layer){
          <insert onEachFeature code>
        }
      }).addTo(dataSet1);
    }
  });

  return promiseData;
};

Then call the function with the correct arguments

var firstDataSet = loadData('http://localhost:8080...', function(feature){
  return {
    weight: 5,
    color: '#6e7ce8',
    opacity: 1
  };
});

and because we're working with promise objects, if you need to interact with firstDataSet, use the .done() handler.

There are likely still bugs in the code that this doesn't address, but organizing your code better should make it a lot easier to spot them.

Answered by James Conkling on May 14, 2021

This question already has an accepted answer, but it seems like the answer may be very specific to OP's particular code.

If anyone else is having trouble loading more than 1 leaflet layer with this whole asynchronous/synchronous problem, here is a solution I found and have tested for my personal use. It's working perfectly for me.

My problem: A leaflet map is supposed to load and display 3 geojson files, but often fails to load and display the third geojson file when the map is refreshed, seemingly randomly.

Solution in brief: Instead of using jquery/AJAX, use the Fetch API to return a promise object. I specifically followed the code from this github page.

Okay, so here are the specifics.

My original problematic code looked like this. It was being used to load and display a locally stored geojson file of hotel locations (point data). It was problematic and only loaded intermittently:

var hotels = new XMLHttpRequest();
    hotels.open(
        "GET",
        "/data/hotel_hostel_guestHouse.geojson",
    );
    hotels.setRequestHeader("Content-Type", "application/json");
    hotels.responseType = "json";
    hotels.onload = function() {
        if (beaches.status != 200) return;
        L.geoJSON(hotels.response).addTo(mymap)
        };
    hotels.send();

Okay, now here's code that works everytime using the Fetch API. It uses arrow functions for brevity.

If someone wants to copy and paste this, you're going to want to rename hotel_url to something more suitable for your use case, and change the url itself to point to your data. You'll also need to change the name of the variable mymap to your specific mapid.

var hotel_url = "/data/hotel_hostel_guestHouse.geojson"

    fetch(
        hotel_url
    ).then(
        res => res.json()
    ).then(
        data => L.geoJson(data).addTo(mymap)
    )

Answered by jawGIS on May 14, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP