Let's say we have the following map:
https://jsfiddle.net/fcumj09w/5/
In the above example, we have 2 marker cluster groups (clustRed and clustYellow) and a single marker outside of these groups.
I want the red marker cluster group to be on top (higher z-index) of the yellow marker cluster group, when zooming out.
I have created 3 custom panes to attach each cluster group to a different pane but it seems like panes don't work with cluster groups (or I haven't find the way to make them work).
What I tried:
var clustRed = L.markerClusterGroup({pane:'hilevel'});
var clustYellow = L.markerClusterGroup({pane:'lowlevel'});
I can only make panes work with the single marker:
L.circleMarker([45,5],{pane:"midlevel"}).addTo(map);
How can I make Leaflet.markercluster use the pane that I specify?
Note: this functionality is now available as clusterPane option. Added since version 1.1.0.
var clustRed = L.markerClusterGroup({clusterPane: 'hilevel'});
Although Layer Groups in Leaflet (including the MarkerClusterGroup from Leaflet.markercluster plugin) inherit from the Layer base class, which indeed provide the pane option, any child layer added to them still use their own specified pane, if any, or use the default one (i.e. overlayPane).
It is still undecided whether that behaviour should be changed or not (see Leaflet issue #4279).
In the case of a MarkerClusterGroup, the latter even actually generates markers on its own, using the L.MarkerCluster class, which represent a cluster of individual markers.
From your description, you would like those generated markers to be inserted in specific panes.
In that case, you could very simply override the initialize method of L.MarkerCluster class so that it uses whichever pane you want. In your case, you would read the MarkerClusterGroup' option pane member:
L.MarkerCluster.include({
initialize: function(group, zoom, a, b) {
var latLng = a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0),
options = {
icon: this
},
pane = group.options.pane; // Read the MarkerClusterGroup's pane, if any.
// If a pane is specified, add it to the MarkerCluster's options.
if (pane) {
options.pane = pane;
}
L.Marker.prototype.initialize.call(this, latLng, options);
// Remaining code is unchanged compared to original method.
this._group = group;
this._zoom = zoom;
this._markers = [];
this._childClusters = [];
this._childCount = 0;
this._iconNeedsUpdate = true;
this._boundsNeedUpdate = true;
this._bounds = new L.LatLngBounds();
if (a) {
this._addChild(a);
}
if (b) {
this._addChild(b);
}
}
});
Once this is patched, the generated marker clusters will use the pane that you specify when instantiating the MarkerClusterGroup, as shown in your question:
var clustRed = L.markerClusterGroup({pane:'hilevel'});
var clustYellow = L.markerClusterGroup({pane:'lowlevel'});
Updated JSFiddle: https://jsfiddle.net/fcumj09w/9/
Related
I've got a list of positions that I retrieve from an ms endpoint like so:
https://atlas.microsoft.com/search/fuzzy/json?top=100&typeahead=true&subscription-key=subscription-key&api-version=1&query=Leeds
The user then selects one of the proposed addresses and the position provided by this endpoint is then used to be displayed on a map that uses clusters. So for Leeds for example, I have the following:
-1.548567, 53.801277
However, when I create the clusters in the clusterRenderCallback function I provide upon creating the HtmlMarkerLayer, I get positions that are close to the ones I've provided, but different and I have no idea how or why.
so the code would look something like:
First I create the dataSource
dataSource = new atlas.source.DataSource(null, {
//Tell the data source to cluster point data.
cluster: true
});
map.sources.add(dataSource);
Then I manage the cluster creation inside the HtmlMarkerLayer creation:
clusterRenderCallback: function (id, position, properties) {
var cluster = new atlas.HtmlMarker({
position: position, // different position to that which I have provided
htmlContent: `<div>${properties.point_count_abbreviated}</div>`,
values: properties,
});
map.events.add('click', cluster, clusterClicked);
return cluster;
}
And here I create points to add to my data source:
let features = list.map(x => new atlas.data.Feature(new atlas.data.Point(new atlas.data.Position(x.lon, x.lat)), x));
dataSource.add(features);
The position I receive for the Leeds cluster for example is -1.549072265625, 53.80065082633024, even though I had 2 poisitions in Leeds, both made out of the same coordinates: -1.548567, 53.801277
It seems like there's some sort of mechanism inside the atlas code that "fixes" the provided coordinates; anyone knows how to stop this or what am I doing wrong here?
==EDIT 02/05==
ok, so following #rbrundritt answer, here the last bit of code I should have added, showing what we do once the cluster is clicked:
function clusterClicked(e) {
var cluster = e.target;
datasource.getClusterExpansionZoom(cluster.properties.cluster_id).then(function (zoom) {
map.setCamera({
center: cluster.getOptions().position,
zoom: zoom
});
});
}
And this is where we have our problem with this discrepancy- clicking on the cluster zooms in to the zoom level where the cluster breaks; however, since we centre the map to the cluster position, the pin position, being different to the cluster one, is not being seen in the map on that zoom level (ie 3 afair). On top of that we have no way of knowing inside the context of this function to what pin does the cluster corresponds, this leaves us with a buggy behaviour.
Clusters should rarely have the same coordinates as any points that they contain. Clusters group overlapping points together and an average position is used to represent that group on the map. When a cluster breaks apart into its individual points then that marker will have the original position value.
Ok, so I wasn't aware of what the datasource.getClusterLeaves method was returning (I've simply mistaken the leaves for the verb leaving). This is what I was looking for, so my code now looks like this:
function inSamePosition(pos1, pos2) {
return pos1.data.geometry.coordinates[0] == pos2.data.geometry.coordinates[0]
&& pos1.data.geometry.coordinates[1] == pos2.data.geometry.coordinates[1];
}
function clusterClicked(e) {
var cluster = e.target;
(cluster.properties.cluster_id, Number.POSITIVE_INFINITY, 0).then(pins => {
let position = pins.every(p => inSamePosition(p, pins[0])) ? pins[0]['data'].geometry.coordinates : null;
datasource.getClusterExpansionZoom(cluster.properties.cluster_id).then(function (zoom) {
map.setCamera({
center: position ? position : cluster.getOptions().position,
zoom: zoom
});
});
})
}
I'm working on an SPA project (ionic, so it's angular with ui-router) where I need to display two different maps on two different pages/controllers.
The first map is a general map (let's call it the main map)where a few locations are marked and the second one (let's call it the edit map)is a focus on a specific location where the user can edit the location by dragging the marker.
The general implementation scheme I'm using is that I'm calling a initMap method from mappingService that instanciates a google map from each controller.
$rootScope.markers = [];
this.initMap = function initMap(mapTarget, mapCenter) {
// the initMap function initialize the map it takes two arguments:
// + mapTaget which defines the html element the map is bound to.
// + mapCenter that defines the center of the map and whether to display the center
var markup = "<div id='" + mapTarget.mapId + "'></div>";
document.getElementById(mapTarget.containerId).innerHTML = markup;
var centerPos = new google.maps.LatLng(mapCenter.lat, mapCenter.lng);
$rootScope.map = new google.maps.Map(document.getElementById(mapTarget.mapId), {
center: centerPos,
zoom: 18,
disableDefaultUI: true
});
// eventually place a person marker for the user's position
if (mapCenter.display) {
console.log('placing the position marker');
$rootScope.markers[0] = new google.maps.Marker({
position: {lat: mapCenter.lat, lng: mapCenter.lng},
map: $rootScope.map,
title: 'userLocation',
icon: './img/person_icon.png'
});
}
};
The only difference is that on the main map I'm first calling a geolocate service the return a promise and I'm using the returned location coordinates to then call the mapping service:
geolocateService.getLocalPosition()
.then(function(coords) {
mappingService.initMap(
{containerId: $scope.mapContainer, mapId: $scope.mapId},
{lat: coords.lat, lng: coords.lng, display: true}
);
While on the edit map I'm calling directly the mapping service.
mappingService.initMap(
{containerId: $scope.mapContainer, mapId: $scope.mapId},
{lat: $scope.location.lat, lng: $scope.location.lng, display: false}
);
I am able to render both maps without problem and even to add markers and some event listeners.
However, I run into the problem that after some sequence of actions, for example going from the main map to the edit map two times, one of the map would suddenly become blank (white to be exactly, so it doesn't seems to be something that I could solve by resizing the map). I'm receiving the Ag object and the only difference is that I don't get the mapDataProviders property on the broken map.
When it works, I get:
Ag object when it works
While when it doesn't, I get:
Ag object when it doesn't work
The code snippet above is my last implementation attempt. I've been trying to implement those maps from a lot of different ways to no avail. Among those attempts, I tried:
totally separates both instanciation, dividing the initMap methods into an initMainMap and an initEditMap.
using one instance for both maps and replacing the DOM element> This is what you see above with the following additional method that is called when leaving the view:
this.removeMap = function removeMap(containerId) {
var container = document.getElementById(containerId);
$rootScope.markers = [];
container.innerHTML = '';
// important part:
var old_element = container;
var new_element = old_element.cloneNode(true);
old_element.parentNode.replaceChild(new_element, old_element);
delete $rootScope.map;
}
knowing that on both views I have either:
or
<div id="edit-map-container"></div>
I'm trying to understand what would make the google map API return a map without mapDataProvider (which I believe means that the map works and even starts to render except that it lacks the tiles to display).
P.S. it looks like there is a memory leak, which is apparently a well known issue.
If anyone has the answer to this, I'm a bit lost right here!
My objective here is to remove all the markers added to leaflet using layer group for a paginated listing page.When navigated to other page i am able to remove layer group of previous page, but on adding new layer group of markers(markers of next page) i am getting this error in browser and markers are not added to the map
error is - Uncaught TypeError: layer.onAdd is not a function;
code is
var leaflet_factory = {
//initializing map container
initialize: function() {
var map = L.map('mapresults');
var googleLayer = new L.Google('ROADMAP');
map.addLayer(googleLayer);
},
//set view of mao
setview: function(lat, long, zoom) {
map.setView([lat, long], zoom);
},
//add list of markers to maps
addMarkersList: function(marker_array) {
var markerArray = [];
$.each(marker_array, function(key, data) {
var marker_pointer = L.marker([data.lat, data.long]).bindPopup('<strong>' + $(this).attr('data-vendor').capitalize() + '</strong><br>' + $(this).attr('data-location').capitalize());
markerArray.push(marker_pointer);
});
window.page_makers_layer = L.layerGroup(markerArray);
window.page_makers_layer.addTo(map);
},
//remove the current marker layer group
removeMarkerLayer: function() {
map.removeLayer(window.page_makers_layer);
}
}
The problem with the above code is when calling addMarkersList for first time after initializing the map container it works.
But when i call addMarkerList with new list of markers(lat long pair) after calling removeMarkerLayer to remove existing marker layer it gives me following error which i am trying to debug.
Uncaught TypeError: layer.onAdd is not a function
Please point where i am doing wrong.
You're doing a few things wrong. Not catastrophically wrong, just antipattern wrong, e.g.:
map.removeLayer(window.page_makers_layer);
Do NOT use window globals to store references to your data (unless you're really, really, really sure of what you're doing). If you're wrapping map creation in a factory or a module, store your data in that scope.
var leaflet_factory = {
Do not name something a factory if it doesn't follow the factory pattern. It's very confusing. Just name it differently, make it a CJS module instead, or skip it entirely.
Research into common programming patterns. Do you have something that appears only once in the webpage? Consider singletons.
gives me following error which I am trying to debug.
How are you trying to debug it? Learn to use your browser debugging capabilities, and provide a complete stack trace.
You should be able to easily keep track of the value of the problematic variable, and see if it is an instance of L.LayerGroup when the call is made.
I am working with the lealflet api, where user can draw shapes to
map(image)...
Initially the layer control(handling 1 layer) is added for base map
using imageoverlay......
I have added a button of id 'newLyer' to page where click event
handles the creation of new layer.....i.e user can create new layer
and update layer control(which is now handling 2 layers)....
I have used several methods to create the layers and adding to control
but failed....
Adding new layer to layerGroup
var layerGroup = new L.LayerGroup(),
imageOverlayUrl = 'aa.jpg',
// 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);
LayerControl where i needed to add the control(if i am correct)
var layerControl = new L.control.layers({
'Main': layerGroup,
//here i need to add new layer control
}, null, { collapsed: false }).addTo(map);
OnClick function with so far static code, this will be executed on click
$('#newLayer').click(function addNewLayer() {
// Second layergroup not added to the map yet
var layerGroupNew = new L.LayerGroup(),
imageOverlayUrlNew = 'bb.png',
// New imageoverlay added to the second layergroup
imageOverlayNew = new L.imageOverlay(imageOverlayUrlNew, bounds).addTo(layerGroup2),
// New featuregroup added to the second layergroup
featureGroupNew = new L.FeatureGroup().addTo(layerGroupNew);
});
In Short
Initially, i have one layer with its control, now onclick function
creates the new layer which will be added to the map but how i can add
this layer into layerControl....
If someone has idea about how to do this sort of thing, please do help,,,, any kind of help or reference will be appreciated....
Thanks for your time
If you look at the documentation for L.Control.Layers:
http://leafletjs.com/reference.html#control-layers
You'll see that L.Control.Layers has a addBaseLayer method:
http://leafletjs.com/reference.html#control-layers-addbaselayer
Adds a base layer (radio button entry) with the given name to the control.
Thus you can do:
layerControl.addBaseLayer(newBaseLayer, 'My New BaseLayer');
And you're good to go. As you see, you could have spared yourself the trouble of posting this question if you would have taken a look at the reference. Leaflet is very well documented. I've personally learned most that i know about Leaflet by reading the docs completely once of twice. Good luck with your project, cheers!
I'd like to list all of the elements from a Mapbox gridLayer that are visible to a user in the viewport.
There is an excellent example on the Mapbox site called 'Listing markers in view'. It shows all the markers on the featureLayer that are in the view of the browser.
I'd like to create something similar, but using using a gridLayer instead. (the gridLayer was created in TileMill)
Here is an example fiddle of the data with a version of non-working 'in-bounds' code:
http://jsfiddle.net/bfab/uSLVw/1/
For reference, the gridLayer has a variable in it (passed from TileMill in the UTFGrid) called '{{{Magnitude}}}' I'd like to list each of the instances of earthquakes that appear to the user, and add it to a list on the bottom left of the example. The function I'm trying to use is gridLayer.getData(latlng,callback).
Here is the snippet of code that is not working:
map.on('move', function() {
// Construct an empty list to fill with gridLayer elements.
var inBounds = [],
// Get the map bounds - the top-left and bottom-right locations.
bounds = map.getBounds();
// For each part of the grid, consider whether
// it is currently visible by comparing with the current map bounds.
// This is what fails....
myGridLayer.getData(bounds,function(earthquake){
inBounds.push(earthquake.Magnitude);
});
// Display a list of markers.
document.getElementById('coordinates').innerHTML = inBounds.join('\n');
});
Apologies if I am making a simple error...
Actually you can't (not easily at least)
1/ You misunderstand the getData function on the gridlayer
first parm is not a boundary but a location latlng
when you use this in a callback (onclick for example) you will get data only if you click on one of your circles, if not you get undefined). The data is not a list of markers but the information of your earthquake (e.g. {DateTime: "2014-04-26T16:59:15.710+00:00", Magnitude: 3.9})
myGridLayer.getData(event.latlng, function(data) {
console.log(data);
// if clicked on a marker: data is defined
// e.g. Object {DateTime: "2014-04-26T16:59:15.710+00:00", Magnitude: 3.9}
});
http://jsfiddle.net/FranceImage/uSLVw/11/
2/ It is not easy to think in terms of markers when using gridlayer as the markers are already part of the image when you load tiles:
http://a.tiles.mapbox.com/v3/bfab.i4nm0adm/3/0/2.png
The interaction is described in some javascript loaded at the same time
http://a.tiles.mapbox.com/v3/bfab.i4nm0adm/3/0/2.grid.json