I have a leaflet map that has zoom levels 2-7 and uses the MarkerCluster plugin, by default I have the L.MarkerClusterGroup disable clustering a zoom level 2 (which means no clustering) and I'm trying to allow the user to click a button that then changes the clustering zoom level to 5. Is this possible?
I know I could do it by making two markercluster groups, one that has no clustering and one that has clustering and remove/add it based on click but that just seems incredibly messy. Really, there's several ways to do it but they are so incredibly clunky.
Code:
Default (2 is the lowest level of zoom):
var markers = new L.MarkerClusterGroup (
{
disableClusteringAtZoom: 2,
maxClusterRadius: 100,
animateAddingMarkers: true
});
What I want to do be able to do:
$('#mcluster').click(function() {
//do some code that sets the disableClusterAtZoom to 5
});
I could not find a way to disable clustering or set a new value for disableClustering at zoom, but I found a less clunky way of achieving this.
var markers = new L.LayerGroup(); //non cluster layer is added to map
markers.addTo(map);
var clusters = new L.MarkerClusterGroup (
{
disableClusteringAtZoom: 5,
maxClusterRadius: 100,
animateAddingMarkers: true
}); //cluster layer is set and waiting to be used
var clusterStatus = 'no'; //since non cluster group is on by default, the status for cluster is set to no
$('#mcluster').click(function( event ) {
if(clusterStatus === 'no'){
clusterStatus = 'yes';
var current1 = markers.getLayers(); //get current layers in markers
map.removeLayer(markers); // remove markers from map
clusters.clearLayers(); // clear any layers in clusters just in case
current1.forEach(function(item) { //loop through the current layers and add them to clusters
clusters.addLayer(item);
});
map.addLayer(clusters);
} else {
clusterStatus = 'no'; //we're turning off clustering here
var current2 = clusters.getLayers(); //same code as before just reversed
map.removeLayer(clusters);
markers.clearLayers();
current2.forEach(function(item) {
markers.addLayer(item);
});
map.addLayer(markers);
}
});
I'm sure there is a more elegant solution but with my still growing knowledge this is what I came up with.
I know you needed a solution a few months ago, but just to let you know that I released recently a sub-plugin for Leaflet.markercluster that can perform exactly what you are looking for (with a few extra code): Leaflet.MarkerCluster.Freezable (demo here).
var mcg = L.markerClusterGroup().addTo(map),
disableClusteringAtZoom = 2;
function changeClustering() {
if (map.getZoom() >= disableClusteringAtZoom) {
mcg.disableClustering(); // New method from sub-plugin.
} else {
mcg.enableClustering(); // New method from sub-plugin.
}
}
map.on("zoomend", changeClustering);
$('#mcluster').click(function () {
disableClusteringAtZoom = (disableClusteringAtZoom === 2) ? 5 : 2;
changeClustering();
});
mcg.addLayers(arrayOfMarkers);
// Initially disabled, as if disableClusteringAtZoom option were at 2.
changeClustering();
Demo: http://jsfiddle.net/fqnbwg3q/3/
Note: in the above demo I used a refinement to make sure the markers merge with animation when clustering is re-enabled. Simply use a timeout before using enableClustering():
// Use a timeout to trigger clustering after the zoom has ended,
// and make sure markers animate.
setTimeout(function () {
mcg.enableClustering();
}, 0);
Related
Similar to Search for markers in a markercluster group Leaflet-MarkerCluster
But i am using a Control group ontop of Marker Cluster so they will be displayed upon a radio button click.
var map = L.map("map"),
parentGroup = L.markerClusterGroup(options), // Could be any other Layer Group type.
// arrayOfMarkers refers to layers to be added under the parent group as sub group feature
mySubGroup = L.featureGroup.subGroup(parentGroup, arrayOfMarkers);
parentGroup.addTo( map );
mySubGroup.addTo( map );
I am attempting to implement Leaflet Search - but as per the documentation says it requires a group layer of markers as the second parameter for it work. Trouble is using L.featureGroup.subGroup requires an array of markers.
Attempted to iterate through mySubGroup at run time to get the layers of markers using Leaflet eachLayer but this will duplicate the amount of markers i have on the map for the search to work.
var markersLayer = new L.LayerGroup().addTo( this.map );
forEach( mySubGroup, layers => {
layers.eachLayer( function (layer ) {
console.log ( layer );
markersLayer.addLayer( layer );
})
});
map.addControl( new L.Control.Search({layer: markersLayer}) );
Solved this issue - though it's quite inefficient. If you can find a more elegant solution to avoid duplication then feel free to contribute it as an answer!
var title = layer.options.title;
// iterate through the cluster points
forEach( mySubGroup, layers => {
layers.eachLayer(function (layer) {
var title = layer.options.title; // match the search title to marker title
marker = new L.Circle(new L.LatLng(layer.getLatLng().lat,layer.getLatLng().lng),
{radius: 0, title: title, fill: 'red', fillOpacity: 0, opacity: 0 }); // Create an invisible L circle marker for each cluseter marker
markersLayer.addLayer(marker);
})
});
You then add the markersLayer to the Leaflet Search
i am working on a react application in which we use google maps. The goal that we want to achieve is to make the map's scrolling as fast and as smooth as possible, and ideally as close to jslancerteam's vector map's scrolling, which can be found here: https://jslancerteam.github.io/crystal-dashboard/#/maps/vector-map . By fast i mean skip some zoom levels per mousewheel scroll as can be seen on the vector map above.
I have tried many things i found online but the closest i got was by disabling the default scrollwheel option of the map and trying to make a custom function called on every mousewheel scroll captured by a listener.
I have tried two different approaches , one by adding an integer value z as increment to the zoom level of the map (i think we need a value between 2 and 3 levels for each scroll.). I wrote the second approach myself and the idea behind it is to give different multipliers to map's zoom level depending on the value of the current zoom level.( For example having faster zoomIn if you are at a low zoom level and slower if you are already pretty zoomed in. The opposite is true for the zoomOut.).
Although i think the second approach has some potential to become smooth, sadly i don't believe the google maps can achieve the same kind of smoothness and fast scrolling as in the vector maps example.
In google maps every layer(tiles) is going to be rendered (grey tiles) even if you try to increase the zoom level by a great value on every scroll because they are served on demand.
When scrolling the mousewheel gently , the results are close to what we would like. When scrolling the mousewheel very fast, the map tiles don't have time to get rendered
On the other hand, we believe that other libraries don't render said layers , and thus they get that smooth feeling.
Unfortunately the application is very dependent on the maps and has a lot of it's basic functionality built around them , so we would like to avoid migrating to another maps library if we are able to for now.
// Need to point out that in some other point in the app
// the scrollwheel map option is set to false and
// the max zoom level is set to 22
//
//1st Approach
//
// Having disabled the default scrollwheel i wanted to
// make the custom scroll follow the mouse
// so i get the coordinates of the mouse through a listener
// and then i set it as the center of the map on every scroll
maps.event.addListener(map, 'mousemove', function (event) {
displayCoordinates(event.latLng);
});
let displayCoordinates=function (pnt) {
var lat = pnt.lat();
lat = lat.toFixed(4);
var lng = pnt.lng();
lng = lng.toFixed(4);
mouselatlng =new LatLng(lat,lng)
}
// the listener used to capture the mousewheel scroll
maps.event.addDomListener(map.getDiv(), 'mousewheel', fx);
// the function called on every scroll
// depending if you zoomIn or zoomOut
// a z is added to the zoom level of the map to replicate
// the zoom functionality
let fx = function(e) {
e.preventDefault();
map.setCenter(mouselatlng); //setting the mouse coords as center
//
// increase by 3 levels on zoomIn : decrease by 3 (-3) on zoomOut
//
var z = e.wheelDelta > 0 || e.detail < 0 ? 3 : -3;
map.setZoom(Math.max(0, Math.min(22, map.getZoom() + z)));
setTimeout(()=>{
map.setZoom(map.getZoom + z);
},80)
return false;
};
//
//2nd Approach
//
maps.event.addListener(map, 'mousemove', function (event) {
displayCoordinates(event.latLng);
});
let displayCoordinates=function (pnt) {
var lat = pnt.lat();
lat = lat.toFixed(4);
var lng = pnt.lng();
lng = lng.toFixed(4);
mouselatlng =new LatLng(lat,lng)
}
maps.event.addDomListener(map.getDiv(), 'mousewheel', fx);
let fx = function(e) {
e.preventDefault();
map.setCenter(mouselatlng);
// Using different multipliers for some zoom levels
// the logic is not finished and the numbers used are not
// final. This was a test to see if the multipliers could
// make a difference.
if(map.getZoom >= 10){
var z = e.wheelDelta > 0 || e.detail < 0 ? 1.1 : 0.91;
}
else if ((map.getZoom >= 7)){
var z = e.wheelDelta > 0 || e.detail < 0 ? 1.2 : 0.7;
}else{
var z = e.wheelDelta > 0 || e.detail < 0 ? 1.4 : 0.6;
}
map.setZoom(Math.max(0, Math.min(22, map.getZoom() * z)));
return false;
};
I am using Leaflet.js for desktop and mobile browser-based maps, and need to support a variety of map tile services. Some of these tile services are defined with coarse zoom levels (like 1, 5, 10, 15), and if I make a request for an unsupported zoom level, the service does not return a tile. (For example, if I request service/tiles/6/x/y when zoom level 6 is not supported).
Leaflet tile layers support minZoom and maxZoom but I'm trying to figure out if there is a best practice for doing coarse zoom levels, and if other folks have encountered this.
I found this post on GitHub that addresses tile scaling for unsupported zoom levels: https://github.com/Leaflet/Leaflet/pull/1802
But I am not sure if this applies. (I'm not sure I want to scale or 'tween' the zoom levels... but if this makes sense and is not too difficult I am willing to try.)
I've started experimenting with this approach which gets messy because zooming can cause more zooming and I have to differentiate user-driven zooms from system-driven zooms:
// layer metadata (assumption: Levels is in ascending order of zoom)
var layerDef = { Title: 'Service lines', Levels: [10, 15, 19] };
// create Leaflet Tile Layer and show on map
var layer = L.tileLayer('://host/service/{z}/{x}/{y}');
layer.minZoom = layerDef.Levels[0];
layer.maxZoom = layerDef.Levels[layerDef.Levels-1];
layer.addTo(map);
// initialize lastZoom for coarse zoom management
var lastZoom = map.getZoom();
var userZoom = true;
// handle supported zoom levels when zoom changes
map.on('zoomend', function (e)
{
// get new zoom level
var z = e.target._zoom || map.getZoom();
if (userZoom) // assume user initiated this zoom
{
// is this zoom level supported?
var zIdx = $.inArray(z, layerDef.Levels);
if (zIdx === -1)
{
// zoom level is not supported; zoom out or in to supported level
// delta: < 0 for zoom out, > 0 for zoom in
var zDelta = z - lastZoom;
var zLastIdx = $.inArray(lastZoom, layerDef.Levels);
var newZoom = -1;
if (zDelta > 0)
{
// user zoomed in to unsupported level.
// zoom in to next supported level (rely on layer.maxZoom)
newZoom = layerDef.Levels[zLastIdx + 1];
}
else
{
// user zoomed out to unsupported level.
// zoom out to next supported level (rely on layer.minZoom)
newZoom = layerDef.Levels[zLastIdx - 1];
}
if (newZoom !== -1)
{
userZoom = false; // set flag
setTimeout(function ()
{
map.setZoom(newZoom); // set zoom -- seems to not work if called from within zoomend handler
}, 100); // delay call to setZoom() to fix issue
}
}
}
else
{
userZoom = true; // clear flag
}
lastZoom = z;
});
(Side note: I hope the reason for coarse zoom levels is obvious: it can get expensive to create and store raster tiles at each zoom level, especially for large geographic areas, and especially when used with offline mobile devices with their own [local] tile servers, limited wireless data plans, limited storage capacity, etc. This is perhaps not something you might encounter with toy apps and Google maps, for example, but rather with domain-specific and mobile applications in which space and bandwidth are at a premium.)
Thanks!
UPDATE: I found that the problem I was having with this code is that map.setZoom(z) does not work right when called from within the zoomEnd handler (it does set the zoom, but causes display issue with gray/nonexistent tiles, perhaps because Leaflet is still in process of scaling / zooming). Fix was to use setTimeout to delay the call to setZoom() a bit. However, I'm still really curious if anybody else has dealt with this, and if there is a 'better way'... (I updated above code to work with setZoom fix)
There is currently a commit under review in Leaflet's repository on GitHub. It adds zoomFactor to the map's options. Maybe that's what you're looking for. At least, i think it will work just as long as your available tileset has zoomlevels which are (don't know if this is the correct technical term) multiples of the lowest available zoomlevel.
See: https://github.com/Leaflet/Leaflet/pull/3285
The following (no guarantees, based on this) should work with Leaflet v1.7.3 but probably not with current master.
It uses a serverZooms option to specify available zoom levels on the tile server as an ordered array.
Overrides L.TileLayer._getZoomForUrl to return a matching or the next lower available server zoom. Also overrides L.TileLayer._getTileSize to increase tile size in order to scale the tiles in between server zooms.
L.TileLayer.Overzoom = L.TileLayer.extend({
options: {
// List of available server zoom levels in ascending order. Empty means all
// client zooms are available (default). Allows to only request tiles at certain
// zooms and resizes tiles on the other zooms.
serverZooms: []
},
// add serverZooms (when maxNativeZoom is not defined)
// #override
_getTileSize: function() {
var map = this._map,
options = this.options,
zoom = map.getZoom() + options.zoomOffset,
zoomN = options.maxNativeZoom || this._getServerZoom(zoom);
// increase tile size when overscaling
return zoomN && zoom !== zoomN ?
Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * options.tileSize) :
options.tileSize;
},
// #override
_getZoomForUrl: function () {
var zoom = L.TileLayer.prototype._getZoomForUrl.call(this);
return this._getServerZoom(zoom);
},
// Returns the appropriate server zoom to request tiles for the current zoom level.
// Next lower or equal server zoom to current zoom, or minimum server zoom if no lower
// (should be restricted by setting minZoom to avoid loading too many tiles).
_getServerZoom: function(zoom) {
var serverZooms = this.options.serverZooms || [],
result = zoom;
// expects serverZooms to be sorted ascending
for (var i = 0, len = serverZooms.length; i < len; i++) {
if (serverZooms[i] <= zoom) {
result = serverZooms[i];
} else {
if (i === 0) {
// zoom < smallest serverZoom
result = serverZooms[0];
}
break;
}
}
return result;
}
});
(function () {
var map = new L.Map('map');
map.setView([50, 10], 5);
new L.TileLayer.Overzoom('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
serverZooms: [0, 1, 2, 3, 6, 9, 12, 15, 17],
attribution : '© <a target="_parent" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
})();
body {
margin: 0;
}
html, body, #map {
width: 100%;
height: 100%;
}
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
<div id="map"></div>
I have a map with various markers and i need to be able to draw a rectangle on the map and select the markers which are within the rectangle bounds.
So far i have found some great info here: How to get markers inside an area selected by mouse drag?
I have implemented the keymapzoom plugin ok. like so
$('#dispatcher').gmap3({action:'get'}).enableKeyDragZoom({
boxStyle: {
border: "dashed black",
//backgroundColor: "red",
opacity: 0.5
},
paneStyle: {
backgroundColor: "gray",
opacity: 0.2
}
});
var dz = $('#dispatcher').gmap3({action:'get'}).getDragZoomObject();
google.maps.event.addListener(dz, 'dragend', function (bnds) {
alert(bnds);
});
This gives me the following
((lat,long),(lat,long)) format from the alert(bnds);
I need to know how i can now check if any markers are within this?
I already have an object that is storing the markers for another reason. like:
markers[name] = {};
markers[name].lat = lati;
markers[name].lng = longi;
which might be useful?
I don't understand how to use the GLatLngBounds and containsLatLng(latlng:GLatLng) as suggested.
Your question is tagged with the v3 version of the Maps API, so I'll assume you are using that version (which you should as v2 is deprecated). Note that some classes and methods are named different than in your question.
Bounds are represented with the LatLngBounds class. You can perform the contains method on an instance of that class to determine if a point lies within those bounds.
If you have an object with all your markers, you can loop through them and check each marker, for example:
var bounds = new google.maps.LatLngBounds(sw, ne);
for (var a in markers) {
if (bounds.contains(new google.maps.LatLng(markers[a].lat, markers[a].lng)) {
// marker is within bounds
}
}
On a side note, I would store the LatLng object in the markers object when creating them. That way you don't have to create them wherever you need.
Box/Rectangle Draw Selection in Google Maps
This was my solution..
google.maps.event.addListener(dz, 'dragend', function(e) { //important listener
for(var i = 0; i < markers.length; i++){ // looping through my Markers Collection
if(e.contains(markers[i].position))
console.log("Marker"+ i +" - matched");
}
});
I'm drawing a series of markers on a map (using v3 of the maps api).
In v2, I had the following code:
bounds = new GLatLngBounds();
... loop thru and put markers on map ...
bounds.extend(point);
... end looping
map.setCenter(bounds.getCenter());
var level = map.getBoundsZoomLevel(bounds);
if ( level == 1 )
level = 5;
map.setZoom(level > 6 ? 6 : level);
And that work fine to ensure that there was always an appropriate level of detail displayed on the map.
I'm trying to duplicate this functionality in v3, but the setZoom and fitBounds don't seem to be cooperating:
... loop thru and put markers on the map
var ll = new google.maps.LatLng(p.lat,p.lng);
bounds.extend(ll);
... end loop
var zoom = map.getZoom();
map.setZoom(zoom > 6 ? 6 : zoom);
map.fitBounds(bounds);
I've tried different permutation (moving the fitBounds before the setZoom, for example) but nothing I do with setZoom seems to affect the map. Am I missing something? Is there a way to do this?
At this discussion on Google Groups I discovered that basically when you do a fitBounds, the zoom happens asynchronously so you need to capture the zoom and bounds change event. The code in the final post worked for me with a small modification... as it stands it stops you zooming greater than 15 completely, so used the idea from the fourth post to have a flag set to only do it the first time.
// Do other stuff to set up map
var map = new google.maps.Map(mapElement, myOptions);
// This is needed to set the zoom after fitbounds,
google.maps.event.addListener(map, 'zoom_changed', function() {
zoomChangeBoundsListener =
google.maps.event.addListener(map, 'bounds_changed', function(event) {
if (this.getZoom() > 15 && this.initialZoom == true) {
// Change max/min zoom here
this.setZoom(15);
this.initialZoom = false;
}
google.maps.event.removeListener(zoomChangeBoundsListener);
});
});
map.initialZoom = true;
map.fitBounds(bounds);
Anthony.
Without trying it, I'd say you should be able to do it just by having fitBounds() before you get the zoom level, i.e.
map.fitBounds(bounds);
var zoom = map.getZoom();
map.setZoom(zoom > 6 ? 6 : zoom);
If you did try that and it didn't work, you can setup your map with minZoom in the MapOptions (api-reference) like this:
var map = new google.maps.Map(document.getElementById("map"), { minZoom: 6 });
This would keep the map from zooming any further out when using fitBounds().
Anthony's solution is very nice. I only needed to fix the zoom for the inital page load (ensuring that you weren't too far zoomed in to start with) and this did the trick for me:
var zoomChangeBoundsListener =
google.maps.event.addListener(map, 'bounds_changed', function(event) {
google.maps.event.removeListener(zoomChangeBoundsListener);
map.setZoom( Math.min( 15, map.getZoom() ) );
});
map.fitBounds( zoomBounds );
You can also set the maxZoom option just before calling fitBounds() and reset the value afterwards:
if(!bounds.isEmpty()) {
var originalMaxZoom = map.maxZoom;
map.setOptions({maxZoom: 18});
map.fitBounds(bounds);
map.setOptions({maxZoom: originalMaxZoom});
}
When you call map.fitBounds() on one item - the map may zoom in too closely. To fix this, simply add 'maxZoom' to mapOptions...
var mapOptions = {
maxZoom: 15
};
In my case, I simply wanted to set the zoom level to one less than what google maps chose for me during fitBounds. The purpose was to use fitBounds, but also ensure no markers were under any map tools, etc.
My map is created early and then a number of other dynamic components of the page have an opportunity to add markers, calling fitBounds after each addition.
This is in the initial block where the map object is originally created...
var mapZoom = null;
Then this is added to each block where a marker is added, right before the map.fitBounds is called...
google.maps.event.addListenerOnce(map, 'bounds_changed', function() {
if (mapZoom != map.getZoom()) {
mapZoom = (map.getZoom() - 1);
map.setZoom(mapZoom);
}
});
When using 'bounds_changed' without the check in place, the map zoomed out once for every marker regardless of whether it needed it or not. Conversely, when I used 'zoom_changed', I would sometimes have markers under map tools because the zoom didn't actually change. Now it is always triggered, but the check ensures that it only zooms out once and only when needed.
Hope this helps.
Since Google Maps V3 is event driven, you can tell the API to set back the zoom to a proper amount when the zoom_changed event triggers:
var initial = true
google.maps.event.addListener(map, "zoom_changed", function() {
if (initial == true){
if (map.getZoom() > 11) {
map.setZoom(11);
initial = false;
}
}
});
I used initial to make the map not zooming too much when the eventual fitBounds is permorfed, but to let the user zoom as much as he/she wants. Without the condition any zoom event over 11 would be possible for the user.
I found the following to work quite nicely. It is a variant on Ryan's answer to https://stackoverflow.com/questions/3334729/.... It guarantees to show an area of at least two times the value of offset in degrees.
const center = bounds.getCenter()
const offset = 0.01
const northEast = new google.maps.LatLng(
center.lat() + offset,
center.lng() + offset
)
const southWest = new google.maps.LatLng(
center.lat() - offset,
center.lng() - offset
)
const minBounds = new google.maps.LatLngBounds(southWest, northEast)
map.fitBounds(bounds.union(minBounds))
I just had the same task to solve and used a simple function to solve it.
it doesn't care how many Markers are in the bounds - if there are a lot and the zoom is already far away, this zooms a little bit out, but when there is only one marker (or a lot very close to each other), then the zoomout is significant (customizable with the extendBy variable):
var extendBounds = function() {
// Extends the Bounds so that the Zoom Level on fitBounds() is a bit farer away
var extendBy = 0.005;
var point1 = new google.maps.LatLng(
bounds.getNorthEast().lat() + extendBy,
bounds.getNorthEast().lng() + extendBy
)
var point2 = new google.maps.LatLng(
bounds.getSouthWest().lat() - extendBy,
bounds.getSouthWest().lng() - extendBy
)
bounds.extend(point1);
bounds.extend(point2);
}
To use it, I use an own function to do the fitBounds():
map is my GoogleMap Object
var refreshBounds = function() {
extendBounds();
map.fitBounds(bounds);
}