0

I'm attempting to implement a mapping functionality into my Laravel app, using Leaflet. For doing that, I've been analyzing, line by line, how it works the code of an existing example project, with such functionality:

https://github.com/nafiesl/laravel-leaflet-example

This project implements in its code, the basic usage of the tools I just need for my app: making a simple CRUD in the database of geographical data, using a map for handle it. After updating some modules used by that project (which were pretty outdated), I could achieve to run it locally in my computer (using PHP 8.2/Laravel 10).

After reading the Leaflet documentation, and analyzing a lot the code of this project (often using the browser console to debug), I've begun to understand the way Leaflet works, and the way it handle objects (such as GeoJSONs and Layers). I've been editing parts the original code for debugging purposes, such as its indentation, adding some variables for storing results of some processes, and also adding lots of 'console.log's, used for looking through the browser console, the kind of data that Leaflet handles (often using those mentioned variables into them).

Despite I've been understanding in general terms, until now, how the code I have works, there's something in it which really puzzles me, and I'm stuck on it while analyzing the code. Since the code has many levels of nested content (such as functions and objects), I will describe here just the code parts which are mainly relevant to my issue:

  • The code I have in my main Blade view, makes a GET request to the own API of the project (using Axios), for getting a GeoJSON object, with the geographical information needed to generate a layer. This GeoJSON object is generated and returned by the API, from data stored in our database. Currently, our GeoJSON object only consist in a "FeatureCollection". This is an object containing an array called "features". Each item of this array is a "Feature" object. Every "Feature" object here, contains some properties (such as a name,an address and timestamps), and a geometry, which define its geographical information. At the moment, we only have "Features" with a "Point" geometry type, with only a pair of coordinates numbers (latitude and longitude), associated to each one.

  • Using the GeoJSON object obtained by the request (if successful), the code makes a call to the 'L.geoJSON' method. These it's used for generating a Layer-type object (in this case, of markers), with the geographical information contained into the GeoJSON object. The 'L.geoJSON' method takes two arguments: the first one being the whole GeoJSON object, which will be parsed, and the second one being an object, containing the options to generate the Layer-type object (this second argument is optional, but it's used here).

  • The method 'pointToLayer' is defined here as an attribute, into the options object passed as second argument to 'L.geoJSON'. This method runs as many times as the number of items that the 'features' array, in the GeoJSON object, has in it. Each time this method runs, it handles a different "Feature" object contained in that array (every existing "Feature" will be handled here once). This method receives two arguments. The first one is 'geoJsonPoint', which will contain the entire "Feature" object, handled at the time that method is running. The second one is 'latlng', which will contain a plain object with just two attributes: 'lat' and 'lng'. These attributes will just adopt the values of the coordinates (latitude and longitude) of the "Point" geometry, defined into the currently handled "Feature", with a Number type. So, the object recieved in 'latlng' won't contain any information of a "Feature" but the coordinates of its "Point" geometry.

  • Then, into the 'pointToLayer' method, there's a call to 'L.marker', with only one argument passed to it: the 'latlng' object, received as argument by 'pointToLayer'. In the original code, the 'L.marker' method was chained to 'bindPopup', and included directly into the 'return' statement of 'pointToLayer' function. In my edited code, I'm storing the value returned just by 'L.marker' (without chaining) into a variable 'prueba', above the 'return' statement. This returned value is a Layer-type object.

The issue is this one:

When I look at the content of that variable 'prueba', printed by a 'console.log', I can see that I'm having a lot of information loaded here, into a Layer-type object. That information is correspondent to all the processed "Feature" properties (such as name and address, among others), and not only to its "Point" geometry coordinates (consisting in just two numbers).

My doubt is: Since I'm only passing to the 'L.marker' method, the object 'latlng' (containing just two coordinate numbers and nothing else) when assigning the value to 'prueba', where's the object contained in 'prueba' (after the assignment), getting from the information of those "Feature" properties?

Despite this isn't an unwanted behaviour (I want that properties information into my Layer), at this moment I'm just trying to understand how Leaflet works (for using it in my project). So, I'd like to understand what's going on here. I think this might be an issue not only for Leaflet, but for JavaScript in general.

When trying to print the result of a 'L.marker' call with the same parameter, directly within a 'console.log' (without previously storing it result in a variable), I'm also obtaining a Layer-type object. But in this case, just with the coordinates information, and not the "Feature" object properties (I try this just after printing the 'prueba' value). So, there must be something happening in the middle, while storing the value returned by 'L.marker', that I'm not able to see properly.

I'm here posting the most relevant code (located into <script> tags in my Blade view), with comments at the most relevant lines for the issue:

var map = L.map('mapid')      
            .setView( 
                [{{ config('leaflet.map_center_latitude') }},{{ config('leaflet.map_center_longitude') }}],
                {{ config('leaflet.zoom_level') }}
            );

//We're storing this result, just for debugging.
var tile = L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
    {attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}
).addTo(map); 


//This is a method from an external plugin, imported above in the document (not shown here).
var markers = L.markerClusterGroup();

//Here we make a call to the API, which return us a GeoJSON object if successful.
axios.get('{{ route('api.outlets.index') }}')
    .then(
        function (response) {
            
            var marker =
                L.geoJSON(
                    //'response.data' contains the entire GeoJSON object.
                    response.data,

                    {
                        pointToLayer: 
 
                            function(geoJsonPoint, latlng) {
                                //'geoJsonPoint' contains an entire Feature of the GeoJSON object,
                                //while 'latlng', just contains the coordinates of its "Point" type geometry.
                                console.log(geoJsonPoint);
                                console.log(latlng);


                                //HERE'S THE ISSUE:                                
                                var prueba=L.marker(latlng);    //We use this variable 'prueba' for debugging.
                                console.log("L.marker:");
                                console.log(prueba);
                                console.log(L.marker(latlng));
  
                                return prueba   //Here, we had 'L.marker(latlng)' before, instead of 'prueba'.
                                    .bindPopup(
                                        function (layer) {
                                            console.log("layer:");
                                            console.log(layer);
                                            return layer.feature.properties.map_popup_content;
                                        }
                                    );
                            }
                    }
                    


                );
            console.log("Contenido de marker y markers:");
            console.log(marker);
            
            markers.addLayer(marker);
            console.log(markers);
            
        }
    )
    .catch(function (error) {
        console.log(error);
    });

map.addLayer(markers);

@can('create', new App\Outlet)
    var theMarker;

    map.on('click', function onClick(e) {
        console.log("Contenido de Map y tile:");
        console.log(map);
        console.log(tile);
        console.log(e);
        let latitude = e.latlng.lat.toString().substring(0, 15);
        let longitude = e.latlng.lng.toString().substring(0, 15);

        if (theMarker != undefined) {
            map.removeLayer(theMarker);
        };

        var popupContent = "Your location : " + latitude + ", " + longitude + ".";
        popupContent += '<br><a href="{{ route('outlets.create') }}?latitude=' + latitude + '&longitude=' + longitude + '">Add new outlet here</a>';

        theMarker = L.marker([latitude, longitude]).addTo(map);
        theMarker.bindPopup(popupContent)
        .openPopup();
    });
@endcan

The whole GeoJSON object, returned by the API, is this one:

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "id": 1,
                "name": "Prueba",
                "address": "Casa de Lean",
                "latitude": "-3.330076616628",
                "longitude": "114.65040206909",
                "creator_id": 1,
                "created_at": "2023-03-15T00:49:43.000000Z",
                "updated_at": "2023-03-15T00:49:43.000000Z",
                "coordinate": "-3.330076616628, 114.65040206909",
                "map_popup_content": "<div class=\"my-2\"><strong>Outlet Name:</strong><br><a href=\"http://localhost:8000/outlets/1\" title=\"View Prueba Outlet detail\">Prueba</a></div><div class=\"my-2\"><strong>Coordinate:</strong><br>-3.330076616628, 114.65040206909</div>"
            },
            "geometry": {
                "type": "Point",
                "coordinates": [
                    "114.65040206909",
                    "-3.330076616628"
                ]
            }
        },
        {
            "type": "Feature",
            "properties": {
                "id": 2,
                "name": "Casa de Gaby",
                "address": "Liniers 557, Lomas del Mirador",
                "latitude": "-34.66036884245",
                "longitude": "-58.52985620498",
                "creator_id": 1,
                "created_at": "2023-03-15T02:14:02.000000Z",
                "updated_at": "2023-03-15T02:29:23.000000Z",
                "coordinate": "-34.66036884245, -58.52985620498",
                "map_popup_content": "<div class=\"my-2\"><strong>Outlet Name:</strong><br><a href=\"http://localhost:8000/outlets/2\" title=\"View Casa de Gaby Outlet detail\">Casa de Gaby</a></div><div class=\"my-2\"><strong>Coordinate:</strong><br>-34.66036884245, -58.52985620498</div>"
            },
            "geometry": {
                "type": "Point",
                "coordinates": [
                    "-58.52985620498",
                    "-34.66036884245"
                ]
            }
        }
    ]
}

Finally, the output obtained in my browser console, correspondent to the 'console.log's prints of 'prueba' and 'L.marker' method (located into the 'pointToLayer' defined function, highlighted with "HERE'S THE ISSUE" comment), is this one (in lines 198 and 199 of the client-side code, read by the browser):

enter image description here

Since our GeoJSON object, has currently two features, the method 'pointToLayer' runs twice, and so does those 'console.log's. Here, we're showing just the result of just one execution of both 'console.log's.

Additionaly, I'll share the result of the 'console.log's showing the values of the 'geoJsonPoint' and 'latlng' arguments into the 'pointToLayer' method (located a few lines above the other ones, lines 182 and 188 in the client-side code):

enter image description here

Does anyone have an idea of what's happening here? If there's more information needed, or something in the question that isn't clear, please tell me.

Thanks a lot!

Leandro

1 Answer 1

1

After reading a lot of documentation, some of it suggested by a teammate of a Telegram group, I've could find out what was actually happening.

First of all, 'console.log', at least on Chrome, has a very particular behavior: when an object is logged, the console initially shows the state of the object at the moment of the log, collapsed.

However, when expanded, it shows the state of the object at the moment of being expanded.

This is explained here: https://news.ycombinator.com/item?id=27525812#:~:text=The%20console%20log%20will%20synchronously,shows%20you%20the%20current%20state.

What was happening was this:

  • Into 'prueba', I've referenced the object returned by 'L.marker(latlng)'.
  • Below, I've printed the value of 'prueba' in the console.
  • After that, into the 'return' statement of method 'pointToLayer', I've chained the method 'bindPopup' to 'prueba', which mutates the content of the object referenced by the last one.

So, when expanding the log of 'prueba' at the console, it shows me the state of the object after being mutated by any process in the code, instead of the state it had at the moment of the assignment.

I've also noticed this:

'L.marker(latlng)', by itself, returns us an object with just three child objects in the immediate lower hierarchy: ('options','_initHooksCalled' and '_latlng').

I've tried just deleting the '.bindPopup' chained to 'prueba', on the 'return' statement of 'pointToLayer' method, just to see what I got at the console. After doing this, the object referenced by 'prueba', had lots of children object in addition to those original three ones, although not so many as when '.bindPopup' was chained. One of all of those children objects is 'feature'. These contains all the information of the correspondent "Feature" of the GeoJSON, from which that Layer object is spawned from.

So, my conclusion is this one: the 'L.geoJSON' method (which returns a LayerGroup), while parsing the GeoJSON received as first parameter, automatically loads all the information contained into there into each Layer of the group, one by one. This is so, the expected behavior of 'L.geoJSON': internally generates some objects, which will be mutated afterwards by the method itself. We must have into consideration that the 'pointToLayer' method is just defined in the context of an options object, passed as a second argument to 'L.geoJSON'.

We could even not pass any second argument at all to 'L.geoJSON', and still getting the entire information of every GeoJSON "Feature", loaded into every Layer of the group returned by 'L.geoJSON'. However, in these case, with each Layer showing just a marker, without any binded popup (the same result as deleting 'bindPopup' like tried before, since that is its default behavior).

So, the way of store the state of 'prueba' at the moment of the assignment is by cloning the object just below, like this (we could use the spread operator instead):

var prueba=L.marker(latlng);
var prueba_cop=structuredClone(prueba);

There, prueba_cop will keep the original state of 'prueba' (if not mutated afterwards).

I'll share some observations, commented within the code shared in the original question:

return prueba   //Here, we had 'L.marker(latlng)' before, instead of 'prueba'.
    .bindPopup(

    //EDIT: This runs only when a marker is clicked
    function (layer) {
        console.log(layer);
        console.log(prueba); //EDIT, just to compare objects 'layer' and 'prueba'
        console.log(prueba==layer); //EDIT, returns 'true'
        return layer.feature.properties.map_popup_content;
    }
);

Since 'L.geoJSON' was completely executed once the document has been properly loaded, 'prueba' is loaded then with all the "Feature" information. So, when we click a marker, the callback of 'bindPopup' is executed, and so does its 'console.log's. In there, we can see that the 'layer' param received by the callback, refers to the same object as 'prueba'.

The 'return' statement of the callback, looks for information within the Layer "Feature", for displaying the content of the popup when a marker is clicked (working in a similar way that an event handler).

Not the answer you're looking for? Browse other questions tagged or ask your own question.