Toggle geojson layer interactivity - leaflet

First of all, since it's my first question, I'm sorry if I make any mistakes.
I'd like to "deactivate" a Geojson Layer, composed by a feature collection with many polygons (or lines). I have seen how to create it with the option interactive = yes or interactive = no, but I can't find out how to toggle this dynamically.
Thanks in advance.
EDIT: last thing I tried was this:
var onEachFeature = function(feature, layer) {
function onmouseover(e) {
var layer = e.target;
var activeLayer = ___get_active_layergroup();
if (activeLayer === null){
layer.setStyle({
cursor: 'move'
});
} else if (activeLayer.hasLayer(layer)){
layer.setStyle({
cursor: 'pointer'
});
} else {
layer.setStyle({
cursor: 'move'
});
}
}
function onmouseout(e) {
}
return {
mouseover: onmouseover,
mouseout: onmouseout
}
}
And I put that function in the layer initialization.
___get_active_layergroup is accesible in the context.
EDIT2:
It works if instead of setStyle I use this:
var element = layer.getElement();
if (element.classList.contains("leaflet-interactive")) {
element.classList.remove("leaflet-interactive")
}
But I don't really like how it works. I can see the pointer while I'm moving the mouse blinking (because it takes a few milliseconds process the mouseover I think).

Related

how to hide the leaflet tooltip (visibility already depending on zoom level) when activating a layer?

I am interested in showing the tooltip only for a specific zoom range (>=12) at any time. With the help of this other article I could manage to almost get there but I still have the problem that the tooltip come to appear when I activate the layer in my OverlayMaps-menu, disregarding the current zoom level.
In my leaflet map I have implemented a OverlayMaps-menu, in which almost all layers are deactivated from the start. For one of these deactivated layers, my aim is to show the tooltips for all features only from a zoom level of 12 or bigger. The code is as follows:
Declaration of the variable from a geoJSON object:
var vibro_offshore = L.geoJson (vbc_offshore, {
pointToLayer: function (feature, latlng) {
return new L.Circle (latlng, {
color:'#a13c02',
})
},
onEachFeature: enCadaVBC
});
In the onEachFeature function, beside some other code regarding popup, I also have defined the tooltips:
function enCadaVBC (feature, layer) {
//declaramos un tooltip
var tooltip = L.tooltip({
content: feature.properties.Location,
permanent: true,
direction:'right',
className:'tooltip'
});
//introducimos un tooltip
layer.bindTooltip(tooltip);
Finally, I have managed to govern the visibility of the tooltips depending on the zoom level with the code suggested in the article referred above:
var lastZoom;
map.on('zoomend', function() {
var zoom = map.getZoom();
console.log(zoom);
if (zoom < 12 && (!lastZoom || lastZoom >= 12)) {
map.eachLayer(function(l) {
if (l.getTooltip) {
var toolTip = l.getTooltip();
if (toolTip) {
this.map.closeTooltip(toolTip);
}
}
});
} else if (zoom >= 12 && (!lastZoom || lastZoom < 12)) {
map.eachLayer(function(l) {
if (l.getTooltip) {
var toolTip = l.getTooltip();
if (toolTip) {
console.log(toolTip);
this.map.addLayer(toolTip);
}
}
});
}
lastZoom = zoom;
})
All this works pretty good, but when I activate the layer via mouseclick on the OverlayMaps-menu, lets say when the zoom level is 9, all the tooltips suddenly appear. I want to avoid this.
I have tried to add a closeTooltip() command right after the bindTooltip-command, but this hasnt have any effect. A consol.log for Is Tooltipopen leads to an undefined...(?)
I also tried to relocate the layer.bindTooltip command into the if-loop, but here I do get some other error related to Mercator projection and latlng, which I do not really understand...

In Leaflet, how to know when map.fitBounds actually changed anything?

I have a click event on a map feature that zooms into that feature when clicked by the user
Map starts like this:
When a user clicks on the map feature:
L.geoJson(geoJsonFeatureCollection, {
style,
onEachFeature
}).addTo(map)
function zoomToFeature (e) {
map.fitBounds(e.target.getBounds())
}
function onEachFeature (feature, layer) {
layer.on({
click: zoomToFeature
})
}
On a second click on the same map feature, after being already zoomed in, I'd like to forward the URL to another page (the info HTML page of that feature). But I can't distinguish both situations.
How can I know in map.fitBounds if the map bounds were already fitted, i.e., if the method actually did/zoomed/panned anything?
You can use the same calculation function from leaflet and check if it equals to the current map state:
function zoomToFeature (e) {
const bounds = e.target.getBounds();
const target = map._getBoundsCenterZoom(bounds);
if(target.zoom === map.getZoom() && map.getCenter().equals(target.center)){
// bounds already fitting
} else {
map.fitBounds(bounds);
}
}
Maybe you need to change the margin of equals. Default: .equals(target.center, 1.0E-9)

Leaflet Trigger Event on Clustered Marker by external element

I just starting to learn about Leaflet.js for my upcoming project.
What i am trying to accomplish:
I need to make a list of marker which displayed on the map, and when the list item is being hovered (or mouseover) it will show where the position on the map (for single marker, it should change its color. For Clustered marker, it should display Coverage Line like how it behave when we hover it.. and perhaps change its color too if possible).
The map should not be changed as well as the zoom level, to put it simply, i need to highlight the marker/ Cluster on the map.
What i have accomplished now : I am able to do it on Single Marker.
what i super frustrated about : I failed to find a way to make it happen on Clustered Marker.
I use global var object to store any created marker.
function updateMapMarkerResult(data) {
markers.clearLayers();
for (var i = 0; i < data.length; i++) {
var a = data[i];
var myIcon = L.divIcon({
className: 'prop-div-icon',
html: a.Description
});
var marker = L.marker(new L.LatLng(a.Latitude, a.Longitude), {
icon: myIcon
}, {
title: a.Name
});
marker.bindPopup('<div><div class="row"><h5>Name : ' + a.Name + '</h5></div><div class="row">Lat : ' + a.Latitude + '</div><div class="row">Lng : ' + a.Longitude + '</div>' + '</div>');
marker.on('mouseover', function(e) {
if (this._icon != null) {
this._icon.classList.remove("prop-div-icon");
this._icon.classList.add("prop-div-icon-shadow");
}
});
marker.on('mouseout', function(e) {
if (this._icon != null) {
this._icon.classList.remove("prop-div-icon-shadow");
this._icon.classList.add("prop-div-icon");
}
});
markersRef[a.LocId] = marker; // <-- Store Reference
markers.addLayer(marker);
updateMapListResult(a, i + 1);
}
map.addLayer(markers);
}
But i don't know which object or property to get the Clustered Marker reference.
And i trigger the marker event by my global variable (which only works on single marker).
...
li.addEventListener("mouseover", function(e) {
jQuery(this).addClass("btn-info");
markersRef[this.getAttribute('marker')].fire('mouseover'); // --> Trigger Marker Event "mouseover"
// TODO : Trigger ClusteredMarker Event "mouseover"
});
...
This is my current https://jsfiddle.net/oryza_anggara/2gze75L6/, any lead could be a very big help. Thank you.
Note: the only js lib i'm familiar is JQuery, i have no knowledge for others such as Angular.js
You are probably looking for markers.getVisibleParent(marker) method, to retrieve the containing cluster in case your marker is clustered.
Unfortunately, it is then not enough to fire your event on that cluster. The coverage display functionality is set on the Cluster Group, not on its individual clusters. Therefore you need to fire your event on that group:
function _fireEventOnMarkerOrVisibleParentCluster(marker, eventName) {
var visibleLayer = markers.getVisibleParent(marker);
if (visibleLayer instanceof L.MarkerCluster) {
// In case the marker is hidden in a cluster, have the clusterGroup
// show the regular coverage polygon.
markers.fire(eventName, {
layer: visibleLayer
});
} else {
marker.fire(eventName);
}
}
var marker = markersRef[this.getAttribute('marker')];
_fireEventOnMarkerOrVisibleParentCluster(marker, 'mouseover');
Updated JSFiddle: https://jsfiddle.net/2gze75L6/5/
That being said, I think another interesting UI, instead of showing the regular coverage polygon that you get when "manually" hovering a cluster, would be to spiderfy the cluster and highlight your marker. Not very easy to implement, but the result seems nice to me. Here is a quick try, it would probably need more work to make it bullet proof:
Demo: https://jsfiddle.net/2gze75L6/6/
function _fireEventOnMarkerOrVisibleParentCluster(marker, eventName) {
if (eventName === 'mouseover') {
var visibleLayer = markers.getVisibleParent(marker);
if (visibleLayer instanceof L.MarkerCluster) {
// We want to show a marker that is currently hidden in a cluster.
// Make sure it will get highlighted once revealed.
markers.once('spiderfied', function() {
marker.fire(eventName);
});
// Now spiderfy its containing cluster to reveal it.
// This will automatically unspiderfy other clusters.
visibleLayer.spiderfy();
} else {
// The marker is already visible, unspiderfy other clusters if
// they do not contain the marker.
_unspiderfyPreviousClusterIfNotParentOf(marker);
marker.fire(eventName);
}
} else {
// For mouseout, marker should be unclustered already, unless
// the next mouseover happened before?
marker.fire(eventName);
}
}
function _unspiderfyPreviousClusterIfNotParentOf(marker) {
// Check if there is a currently spiderfied cluster.
// If so and it does not contain the marker, unspiderfy it.
var spiderfiedCluster = markers._spiderfied;
if (
spiderfiedCluster
&& !_clusterContainsMarker(spiderfiedCluster, marker)
) {
spiderfiedCluster.unspiderfy();
}
}
function _clusterContainsMarker(cluster, marker) {
var currentLayer = marker;
while (currentLayer && currentLayer !== cluster) {
currentLayer = currentLayer.__parent;
}
// Say if we found a cluster or nothing.
return !!currentLayer;
}

Mapbox GL JS: Style is not done loading

I have a map wher we can classically switch from one style to another, streets to satellite for example.
I want to be informed that the style is loaded to then add a layer.
According to the doc, I tried to wait that the style being loaded to add a layer based on a GEOJson dataset.
That works perfectly when the page is loaded which fires map.on('load') but I get an error when I just change the style, so when adding layer from map.on('styledataloading'), and I even get memory problems in Firefox.
My code is:
mapboxgl.accessToken = 'pk.token';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v10',
center: [5,45.5],
zoom: 7
});
map.on('load', function () {
loadRegionMask();
});
map.on('styledataloading', function (styledata) {
if (map.isStyleLoaded()) {
loadRegionMask();
}
});
$('#typeMap').on('click', function switchLayer(layer) {
var layerId = layer.target.control.id;
switch (layerId) {
case 'streets':
map.setStyle('mapbox://styles/mapbox/' + layerId + '-v10');
break;
case 'satellite':
map.setStyle('mapbox://styles/mapbox/satellite-streets-v9');
break;
}
});
function loadJSON(callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open('GET', 'regions.json', true);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
};
xobj.send(null);
}
function loadRegionMask() {
loadJSON(function(response) {
var geoPoints_JSON = JSON.parse(response);
map.addSource("region-boundaries", {
'type': 'geojson',
'data': geoPoints_JSON,
});
map.addLayer({
'id': 'region-fill',
'type': 'fill',
'source': "region-boundaries",
'layout': {},
'paint': {
'fill-color': '#C4633F',
'fill-opacity': 0.5
},
"filter": ["==", "$type", "Polygon"]
});
});
}
And the error is:
Uncaught Error: Style is not done loading
at t._checkLoaded (mapbox-gl.js:308)
at t.addSource (mapbox-gl.js:308)
at e.addSource (mapbox-gl.js:390)
at map.js:92 (map.addSource("region-boundaries",...)
at XMLHttpRequest.xobj.onreadystatechange (map.js:63)
Why do I get this error whereas I call loadRegionMask() after testing that the style is loaded?
1. Listen styledata event to solve your problem
You may need to listen styledata event in your project, since this is the only standard event mentioned in mapbox-gl-js documents, see https://docs.mapbox.com/mapbox-gl-js/api/#map.event:styledata.
You can use it in this way:
map.on('styledata', function() {
addLayer();
});
2. Reasons why you shouldn't use other methods mentioned above
setTimeout may work but is not a recommend way to solve the problem, and you would got unexpected result if your render work is heavy;
style.load is a private event in mapbox, as discussed in issue https://github.com/mapbox/mapbox-gl-js/issues/7579, so we shouldn't listen to it apparently;
.isStyleLoaded() works but can't be called all the time until style is full loaded, you need a listener rather than a judgement method;
Ok, this mapbox issue sucks, but I have a solution
myMap.on('styledata', () => {
const waiting = () => {
if (!myMap.isStyleLoaded()) {
setTimeout(waiting, 200);
} else {
loadMyLayers();
}
};
waiting();
});
I mix both solutions.
I was facing a similar issue and ended up with this solution:
I created a small function that would check if the style was done loading:
// Check if the Mapbox-GL style is loaded.
function checkIfMapboxStyleIsLoaded() {
if (map.isStyleLoaded()) {
return true; // When it is safe to manipulate layers
} else {
return false; // When it is not safe to manipulate layers
}
}
Then whenever I swap or otherwise modify layers in the app I use the function like this:
function swapLayer() {
var check = checkIfMapboxStyleIsLoaded();
if (!check) {
// It's not safe to manipulate layers yet, so wait 200ms and then check again
setTimeout(function() {
swapLayer();
}, 200);
return;
}
// Whew, now it's safe to manipulate layers!
the rest of the swapLayer logic goes here...
}
Use the style.load event. It will trigger once each time a new style loads.
map.on('style.load', function() {
addLayer();
});
My working example:
when I change style
map.setStyle()
I get error Uncaught Error: Style is not done loading
This solved my problem
Do not use map.on("load", loadTiles);
instead use
map.on('styledata', function() {
addLayer();
});
when you change style, map.setStyle(), you must wait for setStyle() finished, then to add other layers.
so far map.setStyle('xxx', callback) Does not allowed. To wait until callback, work around is use map.on("styledata"
map.on("load" not work, if you change map.setStyle(). you will get error: Uncaught Error: Style is not done loading
The current style event structure is broken (at least as of Mapbox GL v1.3.0). If you check map.isStyleLoaded() in the styledata event handler, it always resolves to false:
map.on('styledata', function (e) {
if (map.isStyleLoaded()){
// This never happens...
}
}
My solution is to create a new event called "style_finally_loaded" that gets fired only once, and only when the style has actually loaded:
var checking_style_status = false;
map.on('styledata', function (e) {
if (checking_style_status){
// If already checking style status, bail out
// (important because styledata event may fire multiple times)
return;
} else {
checking_style_status = true;
check_style_status();
}
});
function check_style_status() {
if (map.isStyleLoaded()) {
checking_style_status = false;
map._container.trigger('map_style_finally_loaded');
} else {
// If not yet loaded, repeat check after delay:
setTimeout(function() {check_style_status();}, 200);
return;
}
}
I had the same problem, when adding real estate markers to the map. For the first time addding the markers I wait till the map turns idle. After it was added once I save this in realEstateWasInitialLoaded and just add it afterwards without any waiting. But make sure to reset realEstateWasInitialLoaded to false when changing the base map or something similar.
checkIfRealEstateLayerCanBeAddedAndAdd() {
/* The map must exist and real estates must be ready */
if (this.map && this.realEstates) {
this.map.once('idle', () => {
if (!this.realEstateWasInitialLoaded) {
this.addRealEstatesLayer();
this.realEstateWasInitialLoaded = true
}
})
if(this.realEstateWasInitialLoaded) {
this.addRealEstatesLayer();
}
}
},
I ended up with :
map.once("idle", ()=>{ ... some function here});
In case you have a bunch of stuff you want to do , i would do something like this =>
add them to an array which looks like [{func: function, param: params}], then you have another function which does this:
executeActions(actions) {
actions.forEach((action) => {
action.func(action.params);
});
And at the end you have
this.map.once("idle", () => {
this.executeActions(actionsArray);
});
I have created simple solution. Give 1 second for mapbox to load the style after you set the style and you can draw the layer
map.setStyle(styleUrl);
setTimeout(function(){
reDrawMapSourceAndLayer(); /// your function layer
}, 1000);
when you use map.on('styledataloading') it will trigger couple of time when you changes the style
map.on('styledataloading', () => {
const waiting = () => {
if (!myMap.isStyleLoaded()) {
setTimeout(waiting, 200);
} else {
loadMyLayers();
}
};
waiting();
});

Update Leaflet GeoJson layer and maintain selected feature popup

I have a leaflet map which I am refreshing with new data from the server. You can click on the map feature and a popup will show for the point. Every time the refresh happens, I remove the layer using map.removeLayer, add the new data using L.geoJson, but the popup goes away. I want the popup to stay active with the new data. I know this probably won't work the way I'm doing it by removing the layer. Is there another way to do this that will refresh the layer data and maintain the selected feature popup?
This is the refresh function that I call after I get the new data from the server.
function refreshMapLocations() {
map.removeLayer(locationLayer);
locationLayer = L.geoJson(locations, {
onEachFeature: onEachFeature
}).addTo(map);
}
This creates the popup for each feature
function onEachFeature(feature, layer) {
if (feature.properties && feature.properties.UserName) {
layer.bindPopup(feature.properties.UserName);
}
}
This worked, I keep track of an id that I set in the popup content. After the layer is added I store the Popup that has the clickedId and do popupOpen on that.
var popupToOpen;
var clickedId;
function onEachFeature(feature, layer) {
if (feature.properties && feature.properties.UserName) {
if (feature.properties.MarkerId == clickedId) {
layer.bindPopup("div id='markerid'>"+feature.properties.MarkerId+"</div>feature.properties.UserName);
} else {
layer.bindPopup("div id='markerid'>"+feature.properties.MarkerId+"</div>feature.properties.UserName);
}
}
}
function refreshMapLocations() {
map.removeLayer(locationLayer);
locationLayer = L.geoJson(locations, {
onEachFeature: onEachFeature
}).addTo(map);
if (popupToOpen != null) {
popupToOpen.openPopup();
}
}
function initMap() {
...
map.on('popupopen', function (e) {
...
//clickedId = id from event popup
}
}