Leaflet: Layer check box state is reset every time the map moves or zooms - leaflet

I have the following code which fetches some remote GeoJSON from an API and displays the results on a Leaflet map:
<script>
// Center the map
var map = L.map('map').setView([54.233669, -4.406027], 6);
// Attribution
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=REMOVED', {
attribution: 'Map © OpenStreetMap',
id: 'mapbox.streets'
}).addTo(map);
// Create an empty layergroup for the data
var LayerUTMGroundHazards = L.layerGroup();
var LayerUTMAirspace = L.layerGroup();
// Style the features
function setStyle(feature) {
return {
fillColor: feature.properties.fillColor,
color: feature.properties.strokeColor,
fillOpacity: feature.properties.fillOpacity,
opacity: feature.properties.strokeOpacity
};
}
// Build Guardian UTM
function getGuardianUTMdata() {
// Clear the current Layer content
LayerUTMGroundHazards.clearLayers();
LayerUTMAirspace.clearLayers();
// Define what we want to include
function FuncGroundHazards(feature) {
if (feature.properties.category === "groundHazard") return true
}
function FuncAirspace(feature) {
if (
(feature.properties.category === "airspace" || feature.properties.category === "airport")
&& feature.properties.detailedCategory !== "uk:frz"
) return true
}
// Build the layers
fetch("https://example.com/json?n=" + map.getBounds().getNorth() + "&e=" + map.getBounds().getEast() + "&s=" + map.getBounds().getSouth() + "&w=" + map.getBounds().getWest(), { headers: { 'Authorization': 'REMOVED', 'X-AA-DeviceId': 'mySite' } })
.then(function (responseGuardianUTM) { return responseGuardianUTM.json() })
.then(function (dataGuardianUTM) {
// Create Layer: Ground Hazards
var featuresUTMGroundHazards = L.geoJson(dataGuardianUTM, {
filter: FuncGroundHazards,
style: setStyle,
pointToLayer: function (feature, latlng) { return L.marker(latlng, { icon: L.icon({ iconUrl: feature.properties.iconUrl, iconSize: [25, 25], }), }) },
onEachFeature: function (feature, layer) { layer.bindPopup(feature.properties.name); },
});
// Add the L.GeoJSON instance to the empty layergroup
LayerUTMGroundHazards.addLayer(featuresUTMGroundHazards).addTo(map);
});
// other layers are here (removed from this example)
}
// Update the Guardian UTM layer if the map moves
map.on('dragend', function () { getGuardianUTMdata(); });
map.on('zoomend', function () { getGuardianUTMdata(); });
// Layer controls
var layerControl = new L.Control.Layers(null, {
'Airspace Restrictions': LayerUTMAirspace,
'Ground Hazards': LayerUTMGroundHazards
// other layers are here (removed from this example)
}).addTo(map);
</script>
The problem is that every time the map is moved or zoomed, all of the Layer checkboxes are reset to Checked again, regardless of how many were checked before the map moved. They do not honour / remember their state when the map moves.
Given my code above, how can I store or preserve the checkbox state for each of the multiple Layers that I have so they are not reset every time the map is moved?
EDIT:
Here is a working fiddle. Remove the checkbox from the 'Ground Hazards', then move or zoom the map, you will see how it puts a tick back in the box again
https://jsfiddle.net/hdwz1b6t/1/

You're (re-)adding LayerUTMGroundHazards every time. This line here...
// Add the L.GeoJSON instance to the empty layergroup
LayerUTMGroundHazards.addLayer(featuresUTMGroundHazards).addTo(map);
...is not only adding featureUTMGroundHazards to layerUTMGroundHazards, it's also (re-)adding layerUTMGroundHazards to the map.
And quoting from https://leafletjs.com/examples/layers-control/ :
The layers control is smart enough to detect what layers we’ve already added and have corresponding checkboxes and radioboxes set.
So when you do LayerUTMGroundHazards.addTo(map);, the checkboxes reset.

Related

How does leaflet determine teh position of bound markers, and can I change this w/o using anchor for each feature?

PLease see this jsfiddle (scroll down in the JS window to below the geoJSON)
https://jsfiddle.net/n3zerj2q/1/
var prevLayer = null;
var map = L.map("map").setView([30, 0], 3);
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
var noStyle = {
fillColor: "#fff",
weight: 1,
opacity: 1,
color: 'red',
dashArray: '3',
fillOpacity: 0
};
var myGeojSonLayerGroup = L.geoJson(world, {
onEachFeature: onEachFeature2
}).addTo(map);
function onEachFeature2(feature, layer) {
if (feature.properties && feature.properties.name) {
layer.bindPopup('<span class="pcA">' + feature.properties.name + '</span>')
}
layer.setStyle(noStyle);
layer.on({
mouseover: function (e) {
e.target.openPopup();
if (prevLayer !== null) {
// Reset style
prevLayer.setStyle(styleA());
}
var thisLayer = e.target;
thisLayer.setStyle(styleX())
prevLayer = thisLayer;
}
});
}
function styleA() {
return {
fillColor: "#fff", fillOpacity: 0.0
};
}
function styleX() {
return {
fillColor: "pink", fillOpacity: 0.4
};
}
It simply displays a world map and as the user rolls over country a popup appears with the country name (as well as some color styling) - mostly this is positioned "sensibly" more or less in the middle of the country, but for some (esp bigger ones) it's way off - eg the USA, China, Russia, Argentina...
I'm not setting the position of these - leaflet is, somehow... Other than editing the (large) geoJSON to include anchor points for each country, I wonder if there's some way of tweaking whatever leaflet is doing? Why, for example, does it place Antarctica's where it does? Or China's? etc....
So when a Popup is about to be shown, it will check where, in this specific bit of code:
if (!latlng) {
if (source.getCenter) {
latlng = source.getCenter();
} else if (source.getLatLng) {
latlng = source.getLatLng();
} else if (source.getBounds) {
latlng = source.getBounds().getCenter();
} else {
throw new Error('Unable to get source layer LatLng.');
}
}
So for popups without an explicit latlng, the position will dpend on themethods available on the source layer - this means that for Rectangles and ImageOverlays that'll run getBounds().getCenter(), for Markers and Circles that'll run getLatLng(), and for Polylines and Polygons that'll run getCenter().
The getCenter() method from Polygon just delegates the work elsewhere:
return PolyUtil.polygonCenter(this._defaultShape(), this._map.options.crs);
...and, finally, the polygonCenter() function will calculate the polygon's centroid.
Note that the implementation only takes into account the first ring of the polygon:
if (!LineUtil.isFlat(latlngs)) {
console.warn('latlngs are not flat! Only the first ring will be used');
latlngs = latlngs[0];
}
This is due to the centroid algorithm only being able to handle "simple" polygons, with just an outer ring. Remember to read the OGC SFA specification for a refresher on (multi)polygon rings.

Leaflet current position multiple markers

Hello everyone I have some problems its about the current positionmarker in my leaflet its supposed to update every 3 second and it does but it everytime it puts a new "position" marker on the map and the old one stays how can i fix this?
L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: '© Leaflet 2021',
tileSize: 512,
zoomOffset: -1,
id: 'mapbox/streets-v11',
accessToken: '######'
}).addTo(map);
var greenIcon = L.icon({
iconUrl: 'person.png',
iconSize: [35, 35], // size of the icon // the same for the shadow
popupAnchor: [0, -20] // point from which the popup should open relative to the iconAnchor
});
// placeholders for the L.marker and L.circle representing user's current position and accuracy
var current_position, current_accuracy;
function onLocationFound(e) {
var radius = e.accuracy / 2;
var marker;
L.marker(e.latlng, {icon: greenIcon}).addTo(map)
}
// wrap map.locate in a function
function locate() {
map.locate({setView: true, maxZoom: 15});
}
map.on('locationfound', onLocationFound);
// call locate every 3 seconds... forever
setInterval(locate, 3000);
An efficient way to fix this is to keep a reference to the marker you create, so that you can update its position rather than creating a new marker each time you get a location update. The reference needs to be held in a variable that is outside your callback function, but in scope when the callback is created.
For instance, your callback can check whether the marker already exists, and either create it and attach it to the map object for easy re-use, or just update its coordinates if it is already there:
function onLocationFound(e) {
var radius = e.accuracy / 2;
if (map._here_marker) {
// Update the marker if it already exists.
map._here_marker.setLatLng(e.latlng);
} else {
// Create a new marker and add it to the map
map._here_marker = L.marker(e.latlng, {icon: greenIcon}).addTo(map);
}
}
Having this reference will also let you edit the marker from other functions, e.g. to change the icon or popup, hide it from view, etc.
You can do it, for example, in the following way.
Add an ID (customId) to the marker:
const marker = L.marker([lng, lat], {
id: customId
});
And when you add a new marker remove the existing one with the code below:
map.eachLayer(function(layer) {
if (layer.options && layer.options.pane === "markerPane") {
if (layer.options.id !== customId) {
map.removeLayer(layer);
}
}
});

how to bring layer on top of another layer in leaflet map

my leaflet map has two layer: 1. temperature, 2. Wind-global
i want to bring layer 2 to top but my below code in javascript didn't work
var layerControl = L.control.layers(null, {
"temperature": bandsWindLayer,
}).addTo(map);
$.getJSON('wind-global.json', function (data) {
var velocityLayer = L.velocityLayer({
displayValues: true,
displayOptions: {
velocityType: 'Global Wind',
displayPosition: 'bottomleft',
displayEmptyString: 'No wind data'
},
data: data,
maxVelocity: 15
});
layerControl.addOverlay(velocityLayer, 'Wind-Global');
})
map.on("overlayadd", function (event) {
velocityLayer.bringToFront()
bandsWindLayer.bringToBack()
});
my error message is "velocityLayer is not defined"

Changing overlay layers when switching base layer

I have built a leaflet map with two base layers, and each of these base layers will have their own unique points of interest. The points of interest are being stored as geojson that I loop over to create multiple overlays for different categories. So when viewing the default base layer you would see layers for Show All, Cat1, Cat2 etc.
I need a way to be able to attach overlay layers to a base layer, or remove all overlay layers and then load the relevant ones when the base layer changes.
I tried using the following, which worked to switch categories, with the baselayerchange event, but the overlay layers were still displaying when I switched base layers.
layerControl._layers.forEach(function(layer){
if(layer.overlay){
map.removeLayer(layer.layer)
}
});
I've been searching for a couple of days now for an answer to this without any luck, any help is greatly appreciated.
EDIT
Posting additional code for context. This is not the entirety of the code, there are some plugins that I'm not including code for and have excluded definitions for a several variables, but this should provide better insight into how things are working.
//Initialize the map
var map = new L.Map('map', {
maxZoom: mapMaxZoom,
minZoom: mapMinZoom,
crs: crs1848,
attributionControl: false,
layers: [pano1848]
});
//add controls to the map
var layerControl = L.control.layers(null, null, {position: 'bottomleft'}).addTo(map);
//building category layers from geojson
var types = ['African Americans', 'Art Architecture Culture', 'Education Religion Reform', 'Everyday Life', 'Immigrants', 'Science Inventions', 'Transportation Industry Commerce'];
types.forEach(function(type){
var catType = type.replace(/\s/g,"");
var catPoints = L.geoJson(mapData, {
filter: function(feature, layer){
var cat = feature.properties['category'];
return cat.indexOf(catType) >= 0;
},
onEachFeature: function (feature, layer) {
layer.bindTooltip(feature.properties.name);
(function(layer, properties){
//Create Numeric markers
var numericMarker = L.ExtraMarkers.icon({
icon: 'fa-number',
markerColor: 'yellow',
number: feature.properties['id']
});
layer.setIcon(numericMarker);
layer.on('click', function() {
$.ajax({
url:feature.properties['url'],
dataType:'html',
success: function(result){
$('#detailContainer').html(result);
$('#overlay').fadeIn(300);
}
});
});
})(layer, feature.properties);
}
});
layerControl.addOverlay(catPoints, catType);
});
//Base Layer Change Event
map.on('baselayerchange', function(base){
var layerName;
layerControl._layers.forEach(function(layer){
if(layer.overlay){
map.removeLayer(layer.layer)
}
});
if(base._url.indexOf('1848') >= 0){
map.options.crs = crs1848;
map.fitBounds([
crs1848.unproject(L.point(mapExtent1848[2], mapExtent1848[3])),
crs1848.unproject(L.point(mapExtent1848[0], mapExtent1848[1]))
]);
var southWest = map.unproject([0, 8192], map.getMaxZoom());
var northEast = map.unproject([90112, 0], map.getMaxZoom());
map.setMaxBounds(new L.LatLngBounds(southWest, northEast));
map.addLayer(allPoints);
layerName = '1848 Panorama';
}
else if(base._url.indexOf('2018') >= 0){
map.options.crs = crs2018;
map.fitBounds([
crs2018.unproject(L.point(mapExtent2018[2], mapExtent2018[3])),
crs2018.unproject(L.point(mapExtent2018[0], mapExtent2018[1]))
]);
var southWest = map.unproject([0, 8192], map.getMaxZoom());
var northEast = map.unproject([49152, 0], map.getMaxZoom());
map.setMaxBounds(new L.LatLngBounds(southWest, northEast));
layerName = '2018 Panorama'
}
miniMap.changeLayer(minimapLayers[layerName]);
//map.setView(map.getCenter(), map.getZoom());
});
You may create global variable call "overlays", and remove it like an example below.
Here is the similar example to illustrate your problem jsFiddle
var overlays = {
'Name 1': catPoints,
'Name 2': catType
};
L.control.layers(null, overlays).addTo(map);
// Whenever you want to remove all overlays:
for (var name in overlays) {
map.removeLayer(overlays[name]);
}

In Leaflet, how to make a style the default style from now on

I have geoJson map regions rendered on a map with an initial opacity. I have a slider to change that opacity on the fly.
Here is the bit where I set the opacity on the fly (typescript), which I perform on the input change event within my custom leaflet control:
this.layers.forEach((r: L.GeoJSON<any>) => {
r.eachLayer((layer: any) => {
layer.setStyle({ fillOpacity: vm.mapOptions.heatmapOpacity });
});
});
setTimeout(() => this.map.invalidateSize());
I also have the ability to hover over the regions, in which case I lower the opacity and put a border on the active region when they hover.
When they leave the region, it currently uses resetStyles on that region to reset it back to the previous style. I set this up in the options onFeature callback. The region is highlighted in the mouseover event, and reset in the mouseout event, as seen below.
options = {
style: { stroke: false,
fillColor: regionColor,
fillOpacity: mapOptions.heatmapOpacity },
onEachFeature: (feature, layer) => {
layer.on({
mouseover: (e)=> {
const lr = e.target;
lr.setStyle({
weight: 5,
color: "#666",
dashArray: "",
fillOpacity: 0.7,
stroke: true
});
if (!L.Browser.ie && !L.Browser.opera12 && !L.Browser.edge) {
lr.bringToFront();
}
},
mouseout: (e) => {
prop.featureGeoJson.resetStyle(e.target);
}
});
}
};
The problem is, if I have used setStyle to set the opacity to a different value, then I go into a region, then I leave the region again, calling resetStyle resets the style back to the original default style, before the change to the opacity was made.
Is it possible to set the default style on the layer, so that calling resetStyle will set the styles to my value with the new opacity, and not to the original opacity set when the region was created? How would I do that?
The resetStyle method of a Leaflet GeoJSON Layer Group re-applies the style that was applied at the time of creation of that group, or the default one if you did not provide style:
Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events.
If you later change the style of one or all of the vector layers in that group, it will be therefore overridden when you use resetStyle, and the initial style will be applied.
An easy workaround is simply to modify the GeoJSON Layer Group's style option as well. However, it will affect all its child layers:
group.options.style = newStyle;
(that is what is suggested in #GabyakaGPetrioli's answer, but you have to apply it on the group, not on individual features)
Another solution would be to record the new style of each vector layer, and use that recorded value when you want to restore the previous state, instead of using the group's resetStyle method.
var map = L.map("map").setView([48.85, 2.35], 12);
var geojson = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [2.35, 48.85]
}
};
var point;
var startStyle = {
color: "red"
};
var newStyle = {
color: 'green'
};
var group = L.geoJSON(geojson, {
style: startStyle,
pointToLayer: function(feature, latlng) {
point = L.circleMarker(latlng);
assignStyle(point, startStyle);
return point;
}
}).addTo(map);
// Record the style to the individual vector layer if necessary.
function assignStyle(leafletLayer, newStyle) {
leafletLayer.setStyle(newStyle);
leafletLayer._recordedStyle = newStyle;
}
// When desired, apply the style that has been previously recorded.
function reassignStyle(leafletLayer) {
leafletLayer.setStyle(leafletLayer._recordedStyle);
}
document.getElementById('button').addEventListener('click', function(event) {
event.preventDefault();
// Either use the wrapper function that records the new style…
//assignStyle(point, newStyle);
// Or simply modify the group's style option, if it is acceptable affecting all child layers.
point.setStyle(newStyle);
group.options.style = newStyle;
});
group.on({
mouseover: function(e) {
e.target.setStyle({
color: 'blue'
});
},
mouseout: function(e) {
//reassignStyle(e.layer);
group.resetStyle(e.layer);
}
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.0.3/dist/leaflet.css">
<script src="https://unpkg.com/leaflet#1.0.3/dist/leaflet-src.js"></script>
<div id="map" style="height: 100px"></div>
<button id="button">Change color to Green…</button>
<p>…then mouseover and mouseout</p>
Use L.Util.setOptions
so instead of
layer.setStyle({ fillOpacity: vm.mapOptions.heatmapOpacity });
use
L.Util.setOptions(layer, { style: { fillOpacity: vm.mapOptions.heatmapOpacity } });