Leaflet clearLayers is freezing browser - leaflet

I have a function that adds many circle markers to a layerGroup
const renderCatalogQuery = (catalog, catalogLayer) => {
catalogLayer.clearLayers();
catalogLayerControl.removeLayer(catalogLayer);
catalogLayerControl.addOverlay(catalogLayer, catalog.name);
for (let [name,lon,lat] of catalog.currentQuery) {
let coordinates = L.latLng(lat,lon)
let myMarker = L.circle(coordinates, {
radius: catalog.markerSize,
color: catalog.markerColor,
weight: 1})
myMarker.bindTooltip(`${name} (${catalog.name})`)
myMarker.on('click', () => displayObjectInformation(catalog, name));
myMarker.addTo(catalogLayer);
}
}
catalogLayerControl is a control.layers , and catalogLayer is a layerGroup. The first time this run it's no problem. It will populate the layerGroup with circle markers and add the layer group to the layer control. The issue I am having is if the number of circle markers is quite large, the browser freezes up due to the clearLayers() method. It works fine for smaller markers. I can't figure out why adding the markers happens no problem, but removing them doesn't work. Is there a better approach I can take here?

I am guessing clear layers is not the culprit, but adding circles one by one while the Layer Group is still on map is more likely to give performance issue.
Try removing the Layer Group from the map before your loop, then add it back once you are done, so that the heavy computation happens only once.
const renderCatalogQuery = (catalog, catalogLayer) => {
catalogLayer.remove(); // First remove the Layer Group from map
catalogLayer.clearLayers();
for (let [name,lon,lat] of catalog.currentQuery) {
// ...
myMarker.addTo(catalogLayer);
}
catalogLayer.addTo(map); // Finally re-add to map
}

Related

leaftletjs-adding points dynamically and draw line string

I am trying to draw the path of a flight using leafletjs and geojson. I'll be getting the geometry from a stream.
this is what I have done so far:
let index = 0;
let geoJsonLayer;
let intervalFn = setInterval(function () {
let point = trackData.features[index++];
if(point) {
let coords = point.geometry.coordinates;
coords.pop();
coords.reverse();
geoFeature.geometry.coordinates.push(coords);
if(map.hasLayer(geoJsonLayer)) map.removeLayer(geoJsonLayer);
geoJsonLayer = L.geoJson(geoFeature, {
onEachFeature: (feature, layer) => {
const content = feature.properties.title;
layer.bindPopup(content);
}
});
geoJsonLayer.addTo(map);
// console.log(coords);
} else {
clearInterval(intervalFn);
}
}, 100);
setInterval is to simulate the part whereby I get the geometry from a stream.
now when a user clicks on the path I need to show some properties of the path, and I am trying to use the onEachFeature for that, but its not working correctly.
I suspect its because I am removing the layers (I did this to improve the performance)
Is there any other better ways to do what I am trying to achieve ?
You should probably try addLatLng()
Adds a given point to the polyline.
Your geoFeature sounds to be a single Feature, so your geoJsonLayer will contain a single layer (polyline):
let myPolyline;
geoJsonLayer.eachLayer(function (layer) {
myPolyline = layer; // Will be done only once actually.
});
// When you receive a new point…
myPolyline.addLatLng([lat, lng]);
With this you should not have to remove your layers every time.
The popup should therefore stay open, if it is shown.
Demo: https://jsfiddle.net/3v7hd2vx/265/ (click on the button to add new points)

How to set the zIndex layer order for geoJson layers?

I would like to have certain layers to be always on top of others, no matter in which order they are added to the map.
I am aware of bringToFront(), but it does not meet my requirements. I would like to set the zIndex dynamically based on properties.
Leaflet has the method setZIndex(), but this apparently does not work for geoJson layers:
https://jsfiddle.net/jw2srhwn/
Any ideas?
Cannot be done for vector geometries.
zIndex is a property of HTMLElements, and vector geometries (lines and polygons) are rendered as SVG elements, or programatically as <canvas> draw calls. Those two methods have no concept of zIndex, so the only thing that works is pushing elements to the top (or bottom) of the SVG group or <canvas> draw sequence.
Also, remind that L.GeoJSON is just a specific type of L.LayerGroup, in your case containing instances of L.Polygon. Furthermore, if you read Leaflet's documentation about the setZIndex() method on L.LayerGroup:
Calls setZIndex on every layer contained in this group, passing the z-index.
So, do L.Polygons have a setZIndex() method? No. So calling that in their containing group does nothing. It will have an effect on any L.GridLayers contained in that group, though.
Coming back to your problem:
I would like to have certain layers to be always on top of others, no matter in which order they are added to the map.
Looks like the thing you're looking for is map panes. Do read the map panes tutorial.
This is one of the reason for the implementation of user defined "panes" in Leaflet 1.0 (compared to versions 0.x).
Create panes: var myPane = map.createPane("myPaneName")
If necessary, set the class / z-index of the pane element: myPane.style.zIndex = 450 (refer to z-index values of built-in panes)
When creating your layers, specify their target pane option: L.rectangle(corners, { pane: "myPaneName" })
When building through the L.geoJSON factory, you can loop through your features with the onEachFeature option to clone your layers with specified target pane.
Demo: https://jsfiddle.net/3v7hd2vx/90/
For peoples who are searching about Z-Index
All path layers (so all except for markers) have no z-index because svg layers have a fix order. The first element is painted first. So the last element is painted on top.
#IvanSanchez described good why zIndex not working.
You can control the order with layer.bringToBack() or layer.bringToFront().
With that code you have more options to control the order of the layers.
L.Path.include({
getZIndex: function() {
var node = this._path;
var index = 0;
while ( (node = node.previousElementSibling) ) {
index++;
}
return index;
},
setZIndex: function(idx) {
var obj1 = this._path;
var parent = obj1.parentNode;
if(parent.childNodes.length < idx){
idx = parent.childNodes.length-1;
}
var obj2 = parent.childNodes[idx];
if(obj2 === undefined || obj2 === null){
return;
}
var next2 = obj2.nextSibling;
if (next2 === obj1) {
parent.insertBefore(obj1, obj2);
} else {
parent.insertBefore(obj2, obj1);
if (next2) {
parent.insertBefore(obj1, next2);
} else {
parent.appendChild(obj1);
}
}
},
oneUp: function(){
this.setZIndex(this.getZIndex()+1)
},
oneDown: function(){
this.setZIndex(this.getZIndex()-1)
}
});
Then you can call
polygon.oneUp()
polygon.oneDown()
polygon.setZIndex(2)
polygon.getZIndex()
And now layergroup.setZIndex(2) are working

Updating layers in Leaflet / Mapbox

I'm trying to make a mapping visualization in realtime, where I keep getting new points via websockets. The initial plotting these markers on the map seems simple, but I'm not sure what's the right way of updating a layer on Mapbox.
As of now, whenever I get a new point, I remove the old layer, create a new one and then add it on the map. The problem with this approach is that it is slow and for high number of points (>5000) it starts lagging.
// remove layer
if (this.pointsLayer != null) {
map.removeLayer(this.pointsLayer);
}
// build geoJSON
var geoJSON = { "type": "FeatureCollection", "features": [] };
geoJSON["features"] = tweets.map(function(tweet) {
return this.getGeoPoint(tweet);
}.bind(this));
// add geoJSON to layer
this.pointsLayer = L.mapbox.featureLayer(geoJSON, {
pointToLayer: function(feature, latlon) {
return L.circleMarker(latlon, {
fillColor: '#AA5042',
fillOpacity: 0.7,
radius: 3,
stroke: false
});
}
}).addTo(map);
Is there a better way?
You can create an empty GeoJSON layer by passing it a false instead of real data:
//create empty layer
this.pointsLayer = L.mapbox.featureLayer(false, {
pointToLayer: function(feature, latlon) {
return L.circleMarker(latlon, {
fillColor: '#AA5042',
fillOpacity: 0.7,
radius: 3,
stroke: false
});
}
}).addTo(map);
then use .addData to update it as new tweets come in. Something like:
// build geoJSON
var geoJSON = { "type": "FeatureCollection", "features": [] };
geoJSON["features"] = /**whatever function you use to build a single tweet's geoJSON**/
// add geoJSON to layer
this.pointsLayer.addData(geoJSON);
For a single tweet, I guess you could just create a Feature instead of a FeatureCollection, though I don't know whether that extra layer of abstraction would make any difference in terms of performance.
EDIT: Here is an example fiddle showing the .addData method at work:
http://jsfiddle.net/nathansnider/4mwrwo0t/
It does slow down noticeably if you add 10,000 points, and for 15,000 points, it's really sluggish, but I suspect that has less to do with how the points are added that the demands of rendering so many circleMarkers.
If you aren't already, you may want to try using the new Leaflet 1.0 beta, which redraws vector layers faster and is generally much more responsive with large datasets. Compare this 15,000-point example using Leaflet 0.7.5 to the same code using Leaflet 1.0.0b2. Not everything is fixed (popups take a long time to open in both), but the difference in lag time when trying to drag the map is pretty dramatic.
There's no reason to go through the intermediate step of construction a GeoJSON object just so you can add it to the map. Depending on your exact needs, you can do something like this:
tweets.forEach(function(t) {
L.marker(this.getGeoPoint(t)).addTo(map);
}, this);
You should manage the tweets object so it only contains points that are not already visible on the map, though. Deleting all the old markers, just so you can add them again, is of course going to be very slow.
I would take a look at Leaflet Realtime:
Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything.
https://github.com/perliedman/leaflet-realtime

Leaflet: Removing markers from map

I load some lat / lon info, then use it to build a polyline.
I then want to add a marker at each of the polyline vertices that will show when the polyline is clicked.
The vertices should disappear if a different (or the same) polyline is clicked.
The code below creates the polyline and the vertex markers.
But the vertex markers do not ever disappear.
I've tried to do this several ways with the same result. I've tried storing the vertex markers in an array and adding them directly to the map, then map.removeLayer'ing them. That doesn't work either. Nor does it work if I use an L.featureGroup instead of a layerGroup.
Clearly I've missed the point somewhere as to how markers can be removed. Could someone point me at the error in my methodology?
// trackMarkersVisible is a global L.layerGroup
// success is a callback from an ajax that fetches trackData, an array f lat/lon pairs
success: function (trackData) {
// build an array of latLng's
points = buildTrackPointSet(trackData, marker.deviceID);
var newTrack = L.polyline(
points, {
color: colors[colorIndex],
weight: 6,
clickable: true
}
);
$(newTrack).on("click", function () {
trackMarkersVisible.clearLayers();
$.each(points, function(idx, val) {
var tm = new L.Marker(val);
trackMarkersVisible.addLayer(tm);
});
map.addLayer(trackMarkersVisible);
});
}
Without a JSFiddle or Plunker it's hard to say because i'm not sure what behaviour your getting but using the clearLayers() method of L.LayerGroup should remove all layers from that group. I would check in the onclick handler if the group already has layers: group.getLayers().length === 0 If the group is empty, add the markers, if the group has layers use clearLayers. Example in code:
polyline.on('click', function (e) {
if (group.getLayers().length === 0) {
e.target._latlngs.forEach(function (latlng) {
group.addLayer(L.marker(latlng));
});
} else {
group.clearLayers();
}
});
This works for me, see the example on Plunker: http://plnkr.co/edit/7IPHrO?p=preview
FYI: an instance of L.Polyline is always clickable by default so you can leave out the clickable: true

Layer order changing when turning layer on/off

I have two geoJson layers being loaded - both layers are the same data for testing purposes, but being drawn from two different json files. When I turn the layers on and off in the layer controller, the draw order of the layers change.
Any ideas why this is happening?
I have put my code into a JSFiddle: http://jsfiddle.net/lprashad/ph5y9/10/ and the JS is below:
//styling for watersheds_copy
var Orange = {
"color": "#ff7800",
"weight": 5,
"opacity": 0.65
};
var Water_Orange = L.geoJson(watersheds_copy, {
style: Orange
});
Water_Orange.addData(watersheds_copy);
//these are blue
var Water_blue = L.geoJson(watersheds, {});
Water_blue.addData(watersheds);
//This sets the inital order - last in layer list being on top. Except minimal - tile layer is always on bottom
var map = L.map('map', {
center: [41.609, -74.028],
zoom: 8,
layers: [minimal, Water_Orange, Water_blue]
});
var baseLayers = {
"Minimal": minimal,
"Night View": midnight
};
//This controls the order in the layer switcher. This does not change draw order
var overlays = {
"Water_Orange": Water_Orange,
"Water_blue": Water_blue
};
L.control.layers(baseLayers, overlays).addTo(map);
LP
While searching I happened upon this site that shows some of the Leaflet code:
http://ruby-doc.org/gems/docs/l/leaflet-js-0.7.0.3/lib/leaflet/src/control/Control_Layers_js.html
In it I found this condition for the application of autoZIndex:
if (this.options.autoZIndex && layer.setZIndex) {
this._lastZIndex++;
layer.setZIndex(this._lastZIndex);
}
TileLayer is the only layer type that has a setZIndex function, so apparently autoZIndex only works there.
I'm not sure which annoys me more. This incredible limitation or the fact that Leafet documentation doesn't point it out.
At least on 0.7.2, I had to use bringToFront in the callback of map.on('overlayadd'). autoZIndex: false did not work in my case neither. A comment on this issue may explain the reason.
It's not specific to L.GeoJson layers. As far as I can tell, it's true of all Leaflet layers with layer control. The last layer turned on is simply on top. I don't think this is a bug either. It's predictable behavior which I use and depend on when I'm designing maps with layer control...