I am trying to use marker clustering because I have over 2000 markers being mapped currently, but I am not sure how to implement it with the way that I am loading the markers. Do I have to use a GeoJSON in order to use marker clustering? I would prefer to not use GeoJSON if possible, I am using a JSON file at the moment and looping through the data stored on Firebase. I would like to use the supercluster library if possible (https://github.com/mapbox/supercluster) but I am not sure what to load into index.load(points). Is clustering possible the way I have my code at the moment?
var markers = [];
allMarkers();
function allMarkers() {
for (var i = 0; i < data.length; i++) {
var marker = document.createElement("div");
marker.className = "marker";
marker.style.backgroundImage = "url(./icons/all.png)";
marker.style.backgroundSize = "100%";
marker.style.backgroundRepeat = "no-repeat";
marker.style.width = "25px";
marker.style.height = "25px";
marker.style.filter = "drop-shadow(0px 5px 6px #000000)";
new mapboxgl.Marker(marker)
.setLngLat([data[i].Long, data[i].Lat])
.setPopup(
new mapboxgl.Popup()
.setHTML(
""
).on("close", function() {
}).on("open", function() {
zoom = map.getZoom();
if (zoom < 17) {
map.flyTo({
center: [this._lngLat.lng, this._lngLat.lat],
zoom: 17
});
} else {
map.flyTo({
center: [this._lngLat.lng, this._lngLat.lat]
});
}
}).setMaxWidth("400px")
)
.addTo(map);
markers.push(marker);
}
}
The easiest way to do clustering in Mapbox-GL-JS is using a GeoJSON source with cluster: true as in this example. It doesn't matter that your data is being sent to the browser as some other format, you can convert it into GeoJSON easily and then add it.
Currently you are representing your points using Markers, rather than a layer within the map such as circle or symbol. That would be more complex to combine with clustering, as you would need to create and destroy markers on demand, as the user zooms in and out. It will be much simpler to manage with a map layer, although it will limit your ability to style the marker symbols.
Related
I have large amount of markers I want to display based on user viewpoint. I am looking for most efficient way I can quickly update my map when user changes a viewpoint. In other words display only these markers which falls into bounds (lg, ln).
Currently my solution is following:
On page load pull all data about all places from server and store it in array.
Update map
Every time user drags map remove old markers and place new which falls into bound
Current solutions works but slow and inefficient, to complexity to refresh a map O(N*M) where N old markers which needs to be removed and M new markers which needs to be placed on the map.
I wonder if someone has idea how to make it as faster than this?
How about detecting only the markers which needs to be updated (removed/added)?
I am generally looking for any optimization suggestions - that might be algorithmic improvement, technology or architectural (processing on backend?).
Current code:
var places = function(){} // Some function that pulls a lot of places on page load
function updatePlaces(places){
google.maps.Map.prototype.clearMarkers();
if(places != null){
for(var i = 0; i < places.length; i++){
var lat = places[i].lat;
var lng = places[i].lng;
var position = new google.maps.LatLng(lat, lng);
if(map.getBounds().contains(position) && placedMarkers < markerLimitNumber) {
var marker = new MarkerWithLabel({
position: position,
draggable: false,
raiseOnDrag: true,
map: map,
labelContent: '',
labelAnchor: new google.maps.Point(-10, 15),
labelClass: "labels", // the CSS class for the label
labelStyle: {opacity: 1.0},
icon: 'images/markers/grey_circle_small.png'
});
marker.addListener('click', function(){});
markers.push(marker);
placedMarkers = placedMarkers + 1;
}
}
}
}
google.maps.Map.prototype.clearMarkers = function() {
for(var i=0; i < markers.length; i++){
markers[i].setMap(null);
}
markers = new Array();
placedMarkers = 0;
};
You could try using Google Map Marker Clusterer. It hides the markers at low zoom levels and displays them at higher zoom levels and allows addition of a large number of markers.
Here is a good article on this
https://www.mapbox.com/blog/supercluster/?utm_source=newsletter_april&utm_medium=email&utm_content=supercluster&utm_campaign=newsletter_april
Please take a look
I have a basic markerclusterer example which works very well.
var center = new google.maps.LatLng(37.4419, -122.1419);
var options = {
'zoom': 13,
'center': center,
'mapTypeId': google.maps.MapTypeId.ROADMAP
};
var map = new google.maps.Map(document.getElementById("map"), options);
var markers = [];
for (var i = 0; i < 100; i++) {
var latLng = new google.maps.LatLng(data.photos[i].latitude,
data.photos[i].longitude);
var marker = new google.maps.Marker({'position': latLng});
markers.push(marker);
}
var markerCluster = new MarkerClusterer(map, markers);
What I would like to do is cluster the markers by country and then once you click on it they are still clustered until on3 further click. Currently they are clustered until you are down to one result. I have thousands of markers and would like them visible after one country click and then one more click.
I looked for a solution online and found this http://google-maps-utility-library-v3.googlecode.com/svn/tags/markermanager/1.0/examples/google_northamerica_offices.html
which is produced using this
var officeLayer = [
{
"zoom": [0, 3],
"places": [
{ "name": "US Offices", "icon": ["us", "flag-shadow"], "posn": [40, -97] },
{ "name": "Canadian Offices", "icon": ["ca", "flag-shadow"], "posn": [58, -101] }
]
},
...
};
function setupOfficeMarkers() {
allmarkers.length = 0;
for (var i in officeLayer) {
if (officeLayer.hasOwnProperty(i)) {
var layer = officeLayer[i];
var markers = [];
for (var j in layer["places"]) {
if (layer["places"].hasOwnProperty(j)) {
var place = layer["places"][j];
var icon = getIcon(place["icon"]);
var title = place["name"];
var posn = new google.maps.LatLng(place["posn"][0], place["posn"][1]);
var marker = createMarker(posn, title, getIcon(place["icon"]));
markers.push(marker);
allmarkers.push(marker);
}
}
mgr.addMarkers(markers, layer["zoom"][0], layer["zoom"][1]);
}
}
mgr.refresh();
updateStatus(mgr.getMarkerCount(map.getZoom()));
}
I'm not sure how to implement this into what I've currently got and if i need to include any other scripts/ libraries also.
You are looking at two totally different libraries, there. Your question is about the MarkerClusterer library, but your example solution is about the MarkerManager library.
The MarkerClusterer library automatically clumps markers together based on an algorithm that tries to decide when too markers would be so close together that you can't visibly distinguish one from another. You don't really have a lot of control over when and how it decides to merge markers together this way, so this library is idea when it doesn't matter to you how they get merged, as long as merging happens. Since you want to merge markers together by political boundaries (countries) and not by proximity to each other, this is not the library for you.
The MarkerManager library does not automatically merge markers together at all. What it does do is to selectively hide and reveal markers based on the zoom level of the current map viewport. What you would need to do is do your own merging, and then add to the MarkerManager all of the merged markers, as well as the detail markers, and the zoom levels where you want each marker to be visible. Doing your own merging means you will need an alternate way of determining which country each marker point falls within. Hopefully, you already know (or can get) that information, because it's not automatically provided by any of these libraries.
tl;dr - use the MarkerManager library and not the MarkerClusterer library for grouping by countries, and it's up to you to identify the location for each country and which marker goes with which one.
Edit:
Question = "is there a way to loop through the array and check if each location (long/lat) falls within the current viewport directly" (failing that get all markers within the viewport)
Background:
I have an array of locations (lat, long, id).
I want to:
On a Google Map, use the location array to display markers.
The user can scroll/zoom the map.
Have a button underneath the map, so when the user has decided on an area, he can click the button, and the code will return the ids (from the location array) that are contained within the viewport / map bounds.
There is a .contains for Google, so I guess you could potentially use that with something like
map.getBounds().contains and somehow reference each marker.getPosition()
but I wonder if there's a way to loop through the array and check if each location (long/lat) falls within the current viewport directly
You mean something like this (not tested), map is the google.maps.Map object and needs to be in scope. markersArray is the array of markers.
for (var i=0; i< markersArray.length; i++) {
if (map.getBounds().contains(markersArray[i].getPosition())) {
// the marker is in view
} else {
// the marker is not in view
}
}
http://jsfiddle.net/UA2g2/1/
Thanks geocodezip, you gave me the idea on how to solve it via looping through the array. I don't know if this is the most efficient way, but I put together some code that seems to do what I want - if you check the jsfiddle above and view console you can see that it logs when and which points are in the viewport.
$(document).ready(function(){
var myOptions = {
center: new google.maps.LatLng(51, -2),
zoom: 9,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
var storeArray = new Array(["51.38254", "-2.362804", "ID1"], ["51.235249", "-2.297804","ID2"], ["51.086126", "-2.910767","ID3"]);
google.maps.event.addListener(map, 'idle', function() {
for (i = 0; i < storeArray.length; i++) {
marker = new google.maps.Marker({
position: new google.maps.LatLng(storeArray[i][0], storeArray[i][1]),
map: map
});
}
for (var i=0; i<storeArray.length; i++) {
if (map.getBounds().contains(new google.maps.LatLng(storeArray[i][0], storeArray[i][1]))) {
console.log("marker: " + storeArray[i][2]);
}
}
});
});
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);
}