Leaflet event: how to propagate to overlapping layers - javascript

Let's say I have some overlapping layers and each layer has a click event. When I click on the map, I'd like to know which layers are clicked on, though the click event stops after the first layer and does not propagate to its underlying layers. How can I achieve this?
Here's a sample fiddle and its code: https://jsfiddle.net/r0r0xLoc/
<div id="mapid" style="width: 600px; height: 400px;"></div>
<script>
var mymap = L.map('mapid').setView([51.505, -0.09], 13);
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
maxZoom: 18,
attribution: 'Map data © OpenStreetMap contributors, ' +
'CC-BY-SA, ' +
'Imagery © Mapbox',
id: 'mapbox.streets'
}).addTo(mymap);
L.polygon([
[51.509, -0.08],
[51.503, -0.06],
[51.51, -0.047]
]).addTo(mymap).on('click', function() {
console.log('clicked on 1st polygon')
});
L.polygon([
[51.609, -0.1],
[51.503, -0.06],
[51.51, -0.047]
]).addTo(mymap).on('click', function() {
console.log('clicked on 2nd polygon')
});
</script>
If you click on each polygon, you see its related message. If you click on the overlapping part, you only see the message for the second polygon.

You have to listen directly to the map "click" event and to "manually" determine which layers contain the clicked position.
You can use leaflet-pip plugin (point in polygon) for example for this determination:
map.on("click", function (event) {
var clickedLayers = leafletPip.pointInLayer(event.latlng, geoJSONlayerGroup);
// Do something with clickedLayers
});
Demo: https://jsfiddle.net/ve2huzxw/526/ (listening to "mousemove" instead of "click")

There is a leaflet plugin for propagating events to the underlying layers: https://github.com/danwild/leaflet-event-forwarder
You can use it in your javascript to enable event-forwarding, e.g.:
const myEventForwarder = new L.eventForwarder({
map: map,
events: {click: true, mousemove: false}
});

The problems is the order in which the layers (geometries) are added. In my case I had an array of geometries and what I did was just sort the geometries array using its bounds L.LatLngBounds#contains, so if a geometry contains another it should be added later.
var geometries = [layerOne, layerTwo, ...];
geometries
.sort((a, b) => {
// in my case a separate function is required because the geometry could be a Circle, Rectangle, Polygon or Marker
// and the methods to get the corresponding bounds are different.
var boundsA = this.getBoundsFromGeometry(a);
var boundsB = this.getBoundsFromGeometry(b);
// if the second geometry contains the first, the order must be change so the layers don't overlap
return boundsB.contains(boundsA) ? 1 : -1;
})
.forEach(l => this.map.addLayer(l));

For me the solution was to use the option interactive: false when creating the upper layer:
var options = {
style: feature => this.__determineStyle(feature),
interactive: false,
};
var overlay = L.geoJson(geoJsonData, options);
overlay.addTo(this.map);
https://leafletjs.com/reference-1.0.3.html#interactive-layer

You can listen on the click event on the map. Then, getting the coordinates (lat, lng) of the clicked point, you can filter your features by checking which of them contain it.
For this, you can use booleanPointInPolygon function of turf.js
The following (sample code and fiddle) use polygons as an example.
map.on('click', e => {
const { lat, lng } = e.latlng;
const point = turf.point([lng, lat]);
/* polygons is an array where all your polygon layers are stored */
polygons.forEach((p, i) => {
const polygon= p.toGeoJSON();
if (turf.booleanPointInPolygon(point, polygon)) {
/* do whatever you want with your clicked polygon */
}
});
});
Check fiddle: https://jsfiddle.net/yfg7mdkx/

Related

Remove custom geojson markers (loaded with Leaflet) after zoom

I'm adding some points to a map using the code below and they look great. I'm also adding some json polygons without issue.
When a certain zoom level is reached I would like the points and polygons to turn off. Using the map.removeLayer(name of polygon) turns off the polygon perfectly and then zoom back out I use map.addLayer(name of polygon) and they come back (using 'zoomend' and if else statement).
The point features do not react to the removeLayer function like the polygons do. I've also tried to harvestPoints.setOpacity(0) which does not work either. What code should I use to turn these geojson markers "on" and "off" like the polygon features?
function onEachPoint(feature, layer) {
layer.bindPopup(feature.properties.MGNT_AREA.toString());
layer.on('click', function (e) { layer.openPopup(); });
layer.bindLabel(feature.properties.MGNT_AREA.toString(), {
noHide: true,
className: "my-label",
offset: [-2, -25]
}).addTo(map);
};
var areaIcon = {
icon: L.icon({
iconUrl: 'labels/MonitoringIcon.png',
iconAnchor: [20, 24]
})
};
var harvestPoints = new L.GeoJSON.AJAX('labels/dfo_areas_point.json', {
onEachFeature: onEachPoint,
pointToLayer: function (feature, latlng) {
return L.marker(latlng, areaIcon);
}
});
Not sure exactly what is the root cause for your issue, as we are missing how exactly you reference yours points (markers) when you try to remove them from the map.
Normally, there should be no difference between polygons and points (markers) to achieve what you described (removing the layers from the map at certain zoom level, and add them back at other zooms).
Note that setOpacity is a method for L.Markers, whereas you apply it to harvestPoints which is your geoJson layer group.
What may happen is that you add individual points (markers) to your map (last instruction in your onEachPoint function), but try to remove the layer group harvestPoints from map. Because it seems to be never added to the map, nothing happens.
If you want to turn on/off ALL the points in your harvestPoints layer group at the same time, then you would simply add / remove that layer group to / from the map, instead of adding individual markers:
var harvestPoints = L.geoJson.ajax('labels/dfo_areas_point.json', {
onEachFeature: onEachPoint,
pointToLayer: function (feature, latlng) {
// make sure `areaIcon` is an OPTIONS objects
return L.marker(latlng, areaIcon);
}
}).addTo(map); // Adding the entire geoJson layer to the map.
map.on("zoomend", function () {
var newMapZoom = map.getZoom();
if (newMapZoom >= 13) {
map.addLayer(harvestPoints);
} else {
// Removing entire geoJson layer that contains the points.
map.removeLayer(harvestPoints);
}
});
Side note: popups open on click by default, you should not need to add an on click listener for that?
Demo: http://jsfiddle.net/ve2huzxw/62/

Binding drawn shapes/markers with specific toggling layer : Leaflet

I am working with the application Beta_Here which uses leaflet plugins, all libraries are local except for few(css related)
Usage of application live
First View:This application get input from user and set the distance
calculation formula accordingly....
Second View : After entering input e.g 9, second view will be loaded
where we can draw shapes....
Introduction
I have setup the script which will load two imageoverlays(layers) and
we can toggle them from top right and we can draw or measure from
bottom left....
Problem
When we draw shapes or put markers on an image, controls work nearly
perfect but when we toggle the layers, there starts the problem....
all shapes go to the background or (it seems they disappeared)
Main Question
How can we bind the drawings and marker to the specific
layer(imageoverlay) if there is a way as we can see the drawing are
not bind with the images but the map container..... (Pardon me if you
feel i am doing something stupid because i have limited knowledge
about layers so i came up with my question here....
If someone has idea about how to solve this problem, please do help or
any kind of reference will be appreciated... Thanks for your time
Working Script
var map = L.map('map', {
minZoom: 1,
maxZoom: 4,
center: [0, 0],
zoom: 0,
crs: L.CRS.Simple
});
// dimensions of the image
var w = 3200,
h = 1900,
mainurl = 'assets/img/isbimg.jpg';
childurl = 'assets/img/fjmap.png';
// calculate the edges of the image, in coordinate space
var southWest = map.unproject([0, h], map.getMaxZoom() - 1);
var northEast = map.unproject([w, 0], map.getMaxZoom() - 1);
var bounds = new L.LatLngBounds(southWest, northEast);
var featureGroup = L.featureGroup().addTo(map);
var drawControl = new L.Control.Draw({
edit: {
featureGroup: featureGroup
},
draw: {
polygon: true,
polyline: true,
rectangle: true,
circle: true,
marker: true
}
}).addTo(map);
map.on('draw:created', showPolygonArea);
map.on('draw:edited', showPolygonAreaEdited);
// add the image overlay,so that it covers the entire map
L.control.layers({
Main: L.imageOverlay(mainurl, bounds),
Child: L.imageOverlay(childurl, bounds)
}, null, { collapsed: false }).addTo(map);
L.control.nanomeasure({ nanometersPerPixel: 10000 }).addTo(map);
// tell leaflet that the map is exactly as big as the image
map.setMaxBounds(bounds);
L.tileLayer({
attribution: 'SmartMinds',
maxZoom: 18
}).addTo(map);
//polygon area customization
function showPolygonAreaEdited(e) {
e.layers.eachLayer(function (layer) {
showPolygonArea({ layer: layer });
});
}
function showPolygonArea(e) {
var userInputCustom = prompt("Please enter image name : choose between a to f");
featureGroup.addLayer(e.layer);
e.layer.bindPopup("<div style='width:200px;height:200px;background-image: url(assets/img/" + userInputCustom + ".png);background-size: 195px 195px;;background-repeat: no-repeat;'></div>");
e.layer.openPopup();
}
});
I would contain those FeatureGroup and ImageOverlay pairs into L.LayerGroup's. Then you can switch between those groups. Then you can keep track of the currently selected group, and add your features to the featurelayer of that group. I can explain it better with code through comments:
Basic map, nothing special:
var map = L.map('map', {
'center': [0, 0],
'zoom': 1,
'layers': [
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
'attribution': 'Map data © OpenStreetMap contributors'
})
]
});
// Bounds for the map and imageoverlays
var bounds = L.latLngBounds([[40.712216, -74.22655],[40.773941, -74.12544]]);
// Set bounds on the map
map.fitBounds(bounds);
The grouping part:
// New layergroup, note it's not added to the map yet
var layerGroup = new L.LayerGroup(),
imageOverlayUrl = 'https://placeholdit.imgix.net/~text?txtsize=33&txt=Overlay 1&w=294&h=238',
// New imageoverlay added to the layergroup
imageOverlay = new L.ImageOverlay(imageOverlayUrl, bounds).addTo(layerGroup),
// New featuregroup added to the layergroup
featureGroup = new L.FeatureGroup().addTo(layerGroup);
// Second layergroup not added to the map yet
var layerGroup2 = new L.LayerGroup(),
imageOverlayUrl2 = 'https://placeholdit.imgix.net/~text?txtsize=33&txt=Overlay 2&w=294&h=238',
// New imageoverlay added to the second layergroup
imageOverlay2 = new L.imageOverlay(imageOverlayUrl2, bounds).addTo(layerGroup2),
// New featuregroup added to the second layergroup
featureGroup2 = new L.FeatureGroup().addTo(layerGroup2);
Default drawcontrol and layercontrol with both layergroups added as baselayers:
var layerControl = new L.control.layers({
'Group 1': layerGroup,
'Group 2': layerGroup2
}).addTo(map);
var drawControl = new L.Control.Draw().addTo(map);
Here's where the magic happens ;) :
// Variable to hold the selected layergroup's featuregroup.
var currentFeatureGroup;
// Catch the layer change event
map.on('baselayerchange', function (layersControlEvent) {
// Loop over the layers contained in the current group
layersControlEvent.layer.eachLayer(function (layer) {
// If it's the imageoverlay make sure it's in the background
if (layer instanceof L.ImageOverlay) {
layer.bringToBack();
// If not then it's the featuregroup, reference with variable.
} else {
currentFeatureGroup = layer;
}
});
});
// Catch draw created event
map.on('draw:created', function (e) {
// Store created feature into the current featuregroup
currentFeatureGroup.addLayer(e.layer);
});
That's it. Pretty basic just meant as an example but it does what you want it to do. A real implementation would look different, with errorhandling because for instance when you draw and have no baselayer/overlay selected it fail etc. Here's a working example on Plunker to play with: http://plnkr.co/edit/6cGceX?p=preview

Google Maps API v3 binding events to multiple maps

I've got a page with two Google maps on, using the v3 API. Currently they have one pin each, with the same lat & long set for each pin, although one of the maps will have other pins added at a later date (when I can get this to work!) The maps are generated by looping through an object, so further maps can be added simply if needed.
What I am trying to do is bind the bounds_changed event once to both maps, to run map.setZoom() after map.fitBounds() has been run. The event, however, only binds to the last map to be set up, so does not reset the zoom on the first map.
Link to JSFiddle replicating the issue: http://jsfiddle.net/pkhb8mvz/7/
(For a more clear example of what the event is being bound to, change the event to listen on click rather than bounds_changed then try clicking on the first map and watch the zoom level change on the second map)
Any help greatly appreciated!
The problem is that the map variable is being redefined on each iteration of your loop, so by the time your event listener callback runs it will operate on the second google.maps.Map object. The simplest solution is to capture the value of the map variable on each iteration using a closure, like so:
(function (map) {
var listener = new google.maps.event.addListenerOnce(map, "bounds_changed", function () {
if (!opts.center) {
map.setZoom(opts.zoom);
};
});
}) (map);
I forked your JSFiddle to demonstrate the idea: https://jsfiddle.net/e8qbr8qL/
Created this fiddle that works based on this answer https://stackoverflow.com/a/5839041/2321666.
Apart from the function you need to add, your map variable should be an array so that there is one instance of each map.
map[i] = new google.maps.Map($(maps[i].mapElement)[0], opts);
If you need any explanation of the code please ask, but i think there is enough info about javascript closures in the post added.
You can't add a listener to multiple maps unless you keep references to all of them, currently you are only keeping a reference to the last map created.
// use function closure to associate the map with its bounds listener
addBoundsListener(map, opts, bounds);
map.fitBounds(bounds);
}
function addBoundsListener(map, opts, bounds) {
// If no center option has been specified, center the map to
// contain all pins and reset the zoom
var listener = new google.maps.event.addListenerOnce(map, "bounds_changed", function () {
if (!opts.center) {
map.setZoom(opts.zoom);
}
});
}
updated fiddle
code snippet:
var maps = {
"propertyLocation": {
"mapElement": "#map1",
"options": {
"zoom": 10,
"streetViewControl": false
},
"pins": [{
"title": "Test 1",
"lat": "52.1975331616",
"long": "0.9771180153"
}]
},
"localArea": {
"mapElement": "#map2",
"options": {
"zoom": 14,
"streetViewControl": false
},
"pins": [{
"title": "Test 2",
"lat": "52.1975331616",
"long": "0.9771180153"
}]
}
};
// Sensible defaults
var defaults = {
zoom: 9,
mapTypeId: google.maps.MapTypeId.ROADMAP,
draggable: true
};
for (var i in maps) {
// Extend the options
var opts = $.extend(true, defaults, maps[i].options),
marker,
map,
bounds = new google.maps.LatLngBounds();
// Create the map
map = new google.maps.Map($(maps[i].mapElement)[0], opts);
// Create all pins
for (var p = 0; p < maps[i].pins.length; p++) {
var pin = maps[i].pins[p];
marker = new google.maps.Marker({
position: new google.maps.LatLng(pin.lat, pin.long),
map: map,
title: pin.title,
icon: pin.icon
});
// Extend the bounds of the map to contain the marker
bounds.extend(marker.position);
}
// use function closure to associate the map with its bounds listener
addBoundsListener(map, opts, bounds);
map.fitBounds(bounds);
}
function addBoundsListener(map, opts, bounds) {
// If no center option has been specified, center the map to
// contain all pins and reset the zoom
var listener = new google.maps.event.addListenerOnce(map, "bounds_changed", function() {
if (!opts.center) {
map.setZoom(opts.zoom);
}
});
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://maps.googleapis.com/maps/api/js?libraries=places&key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk"></script>
<div style="width: 500px; height: 500px;" id="map1"></div>
<div style="width: 500px; height: 500px;" id="map2"></div>

Update Leaflet GeoJSON layer with data inside bounding box

I'm using leaflet/JavaScript for the first time and I want to display a map, with a GeoJSON layer which change on every move… To only show points on the area.
This is my code source:
// Function to refresh points to display
function actualiseGeoJSON() {
// Default icon for my points
var defaultIcon = L.icon({
iconUrl: '../images/icones/cabane.png',
iconSize: [16, 16],
iconAnchor: [8, 8],
popupAnchor: [0, -8]
});
// We create each point with its style (from GeoJSON file)
function onEachFeature(feature, layer) {
var popupContent = '' + feature.properties.nom + "";
layer.bindPopup(popupContent);
var cabaneIcon = L.icon({
iconUrl: '../images/icones/' + feature.properties.type + '.png',
iconSize: [16, 16],
iconAnchor: [8, 8],
popupAnchor: [0, -8]
});
layer.setIcon(cabaneIcon);
}
// We download the GeoJSON file (by using ajax plugin)
var GeoJSONlayer = L.geoJson.ajax('../exportations/exportations.php?format=geojson&bbox=' + map.getBounds().toBBoxString() + '',{
onEachFeature: onEachFeature,
pointToLayer: function (feature, latlng) {
return L.marker(latlng, {icon: defaultIcon});
}
}).addTo(map);
}
// We create the map
var map = L.map('map');
L.tileLayer('http://maps.refuges.info/hiking/{z}/{x}/{y}.png', {
attribution: '© Contributeurs d\'OpenStreetMap',
maxZoom: 18
}).addTo(map);
// An empty base layer
var GeoJSONlayer = L.geoJson().addTo(map);
// Used to only show your area
function onLocationFound(e) {
var radius = e.accuracy / 2;
L.marker(e.latlng).addTo(map);
actualiseGeoJSON();
}
function onLocationError(e) {
alert(e.message);
actualiseGeoJSON();
}
function onMove() {
// map.removeLayer(GeoJSONlayer);
actualiseGeoJSON();
}
map.locate({setView: true, maxZoom: 14});
// Datas are modified if
map.on('locationerror', onLocationError);
map.on('locationfound', onLocationFound);
map.on('moveend', onMove);
I have tried to remove the layer in my first function but GeoJSONlayer is not defined
I have tried to remove the layer in onMove() but nothing appears
I have tried to remove the layer in moveend event but I have an syntax error…
If somebody can help me…
Sorry for my bad English, French guy ith french function names
I see you are using the leaflet ajax plugin.
The simplest way to get your map to work is to download all available data right at the start, providing a giant bounding box, and add it to the map just once. This will probably work just fine, unless there's insanely many cabins and stuff to download.
But if you wish to refresh the data regularly, based on the bounding box, you can use the refresh method in the leaflet-ajax plugin:
you can also add an array of urls instead of just one, bear in mind
that "addUrl" adds the new url(s) to the list of current ones, but if
you want to replace them use refresh e.g.:
var geojsonLayer = L.geoJson.ajax("data.json");
geojsonLayer.addUrl("data2.json");//we now have 2 layers
geojsonLayer.refresh();//redownload those two layers
geojsonLayer.refresh(["new1.json","new2.json"]);//add two new layer replacing the current ones
So initially:
var defaultIcon = ...
function onEachFeature(feature, layer) ...
// Do this in the same scope as the actualiseGeoJSON function,
// so it can read the variable
var GeoJSONlayer = L.geoJson.ajax(
'../exportations/exportations.php?format=geojson&bbox='
+ map.getBounds().toBBoxString() + '',{
onEachFeature: onEachFeature,
pointToLayer: function (feature, latlng) {
return L.marker(latlng, {icon: defaultIcon});
}
}).addTo(map);
And then for each update:
function actualiseGeoJSON() {
// GeoJSONLayer refers to the variable declared
// when the layer initially got added
GeoJSONlayer.refresh(
['../exportations/exportations.php?format=geojson&bbox='
+ map.getBounds().toBBoxString() + '']);
}
Alternatively, you could set the layer as a property of the map, instead of as a var:
map.geoJsonLayer = L.geoJson.ajax(...)
And later refer to it as follows:
map.geoJsonLayer.refresh(...)
this leaflet plugin is more suitable for your purpose, manage map events and zoom.
Caching remote requests and much more.
http://labs.easyblog.it/maps/leaflet-layerjson/
var ajaxUrl = "search.php?lat1={minlat}&lat2={maxlat}&lon1={minlon}&lon2={maxlon}";
map.addLayer( new L.LayerJSON({url: ajaxUrl }) );
Extend L.GeoJSON with more features and support ajax or jsonp request. See the source code comments for more documentation.

How to change the map center in Leaflet.js

The following code initializes a leaflet map. The initialize function centers the map based on user location. How do I change the center of the map to a new position after calling the initialize function?
function initialize() {
map = L.map('map');
L.tileLayer('http://{s}.tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © CloudMade'
}).addTo(map);
map.locate({setView: true, maxZoom: 8});
}
For example:
map.panTo(new L.LatLng(40.737, -73.923));
You can also use:
map.setView(new L.LatLng(40.737, -73.923), 8);
It just depends on what behavior you want. map.panTo() will pan to the location with zoom/pan animation, while map.setView() immediately set the new view to the desired location/zoom level.
Use map.panTo(); does not do anything if the point is in the current view. Use map.setView() instead.
I had a polyline and I had to center map to a new point in polyline at every second. Check the code :
GOOD: https://jsfiddle.net/nstudor/xcmdwfjk/
mymap.setView(point, 11, { animation: true });
BAD: https://jsfiddle.net/nstudor/Lgahv905/
mymap.panTo(point);
mymap.setZoom(11);
You could also use:
var latLon = L.latLng(40.737, -73.923);
var bounds = latLon.toBounds(500); // 500 = metres
map.panTo(latLon).fitBounds(bounds);
This will set the view level to fit the bounds in the map leaflet.
I was looking for a way to change the bounds but without the animation. This worked for me:
var bounds = L.latLng(40.737, -73.923).toBounds();
map.fitBounds(bounds, {animation: false});
In the case of map-centering problem despite using panTo(),flyTo() or setView() try to adjust the map with map.invalidateSize():
setTimeout(function () {
map.invalidateSize(true);
}, 100);
jQuery sample:
$(document).ready(function () {
mymap.invalidateSize();
});

Categories