Leandro Caplan
Leandro Caplan

Reputation: 121

Leaflet - Layer-type object spawned by method 'L.marker' it contains information, which wasn't passed to that method

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 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

Upvotes: 0

Views: 482

Answers (1)

Leandro Caplan
Leandro Caplan

Reputation: 121

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).

Upvotes: 1

Related Questions