Google maps API V3 - multiple markers on exact same spot

JavascriptGoogle MapsGoogle Maps-Api-3

Javascript Problem Overview


Bit stuck on this one. I am retrieving a list of geo coords via JSON and popping them onto a google map. All is working well except in the instance when I have two or more markers on the exact same spot. The API only displays 1 marker - the top one. This is fair enough I suppose but would like to find a way to display them all somehow.

I've searched google and found a few solutions but they mostly seem to be for V2 of the API or just not that great. Ideally I'd like a solution where you click some sort of group marker and that then shows the markers clustered around the spot they are all in.

Anybody had this problem or similar and would care to share a solution?

Javascript Solutions


Solution 1 - Javascript

Take a look at OverlappingMarkerSpiderfier.
There's a demo page, but they don't show markers which are exactly on the same spot, only some which are very close together.

But a real life example with markers on the exact same spot can be seen on http://www.ejw.de/ejw-vor-ort/ (scroll down for the map and click on a few markers to see the spider-effect).

That seems to be the perfect solution for your problem.

Solution 2 - Javascript

Offsetting the markers isn't a real solution if they're located in the same building. What you might want to do is modify the markerclusterer.js like so:

  1. Add a prototype click method in the MarkerClusterer class, like so - we will override this later in the map initialize() function:

     MarkerClusterer.prototype.onClick = function() { 
         return true; 
     };
    
  2. In the ClusterIcon class, add the following code AFTER the clusterclick trigger:

     // Trigger the clusterclick event.
     google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_);
     
     var zoom = this.map_.getZoom();
     var maxZoom = markerClusterer.getMaxZoom();
     // if we have reached the maxZoom and there is more than 1 marker in this cluster
     // use our onClick method to popup a list of options
     if (zoom >= maxZoom && this.cluster_.markers_.length > 1) {
        return markerClusterer.onClickZoom(this);
     }
    
  3. Then, in your initialize() function where you initialize the map and declare your MarkerClusterer object:

     markerCluster = new MarkerClusterer(map, markers);
     // onClickZoom OVERRIDE
     markerCluster.onClickZoom = function() { return multiChoice(markerCluster); }
    

    Where multiChoice() is YOUR (yet to be written) function to popup an InfoWindow with a list of options to select from. Note that the markerClusterer object is passed to your function, because you will need this to determine how many markers there are in that cluster. For example:

     function multiChoice(mc) {
          var cluster = mc.clusters_;
          // if more than 1 point shares the same lat/long
          // the size of the cluster array will be 1 AND
          // the number of markers in the cluster will be > 1
          // REMEMBER: maxZoom was already reached and we can't zoom in anymore
          if (cluster.length == 1 && cluster[0].markers_.length > 1)
          {
               var markers = cluster[0].markers_;
               for (var i=0; i < markers.length; i++)
               {
                   // you'll probably want to generate your list of options here...
               }
    
               return false;
          }
    
          return true;
     }
    

Solution 3 - Javascript

I used this alongside jQuery and it does the job:

var map;
var markers = [];
var infoWindow;

function initialize() {
    var center = new google.maps.LatLng(-29.6833300, 152.9333300);

    var mapOptions = {
        zoom: 5,
        center: center,
        panControl: false,
        zoomControl: false,
        mapTypeControl: false,
        scaleControl: false,
        streetViewControl: false,
        overviewMapControl: false,
        mapTypeId: google.maps.MapTypeId.ROADMAP
      }

    
    map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);

    $.getJSON('jsonbackend.php', function(data) {
        infoWindow = new google.maps.InfoWindow();
    
        $.each(data, function(key, val) {
            if(val['LATITUDE']!='' && val['LONGITUDE']!='')
            {                
                // Set the coordonates of the new point
                var latLng = new google.maps.LatLng(val['LATITUDE'],val['LONGITUDE']);
            
                //Check Markers array for duplicate position and offset a little
                if(markers.length != 0) {
                    for (i=0; i < markers.length; i++) {
                        var existingMarker = markers[i];
                        var pos = existingMarker.getPosition();
                        if (latLng.equals(pos)) {
                            var a = 360.0 / markers.length;
                            var newLat = pos.lat() + -.00004 * Math.cos((+a*i) / 180 * Math.PI);  //x
                            var newLng = pos.lng() + -.00004 * Math.sin((+a*i) / 180 * Math.PI);  //Y
                            var latLng = new google.maps.LatLng(newLat,newLng);
                        }
                    }
                }
                        
                // Initialize the new marker
                var marker = new google.maps.Marker({map: map, position: latLng, title: val['TITLE']});
            
                // The HTML that is shown in the window of each item (when the icon it's clicked)
                var html = "<div id='iwcontent'><h3>"+val['TITLE']+"</h3>"+
                "<strong>Address: </strong>"+val['ADDRESS']+", "+val['SUBURB']+", "+val['STATE']+", "+val['POSTCODE']+"<br>"+
                "</div>";
            
                // Binds the infoWindow to the point
                bindInfoWindow(marker, map, infoWindow, html);
            
                // Add the marker to the array
                markers.push(marker);
            }
        });

        // Make a cluster with the markers from the array
        var markerCluster = new MarkerClusterer(map, markers, { zoomOnClick: true, maxZoom: 15, gridSize: 20 });
    });
}

function markerOpen(markerid) {
    map.setZoom(22);
    map.panTo(markers[markerid].getPosition());
    google.maps.event.trigger(markers[markerid],'click');
    switchView('map');
}

google.maps.event.addDomListener(window, 'load', initialize);

Solution 4 - Javascript

Expanding on Chaoley's answer, I implemented a function that, given a list of locations (objects with lng and lat properties) whose coordinates are exactly the same, moves them away from their original location a little bit (modifying objects in place). They then form a nice circle around the center point.

I found that, for my latitude (52deg North), 0.0003 degrees of circle radius work best, and that you have to make up for the difference between latitude and longitude degrees when converted to kilometres. You can find approximate conversions for your latitude here.

var correctLocList = function (loclist) {
	var lng_radius = 0.0003,         // degrees of longitude separation
	    lat_to_lng = 111.23 / 71.7,  // lat to long proportion in Warsaw
	    angle = 0.5,                 // starting angle, in radians
	    loclen = loclist.length,
	    step = 2 * Math.PI / loclen,
	    i,
	    loc,
	    lat_radius = lng_radius / lat_to_lng;
	for (i = 0; i < loclen; ++i) {
		loc = loclist[i];
		loc.lng = loc.lng + (Math.cos(angle) * lng_radius);
		loc.lat = loc.lat + (Math.sin(angle) * lat_radius);
		angle += step;
	}
};

Solution 5 - Javascript

@Ignatius most excellent answer, updated to work with v2.0.7 of MarkerClustererPlus.

  1. Add a prototype click method in the MarkerClusterer class, like so - we will override this later in the map initialize() function:

     // BEGIN MODIFICATION (around line 715)
     MarkerClusterer.prototype.onClick = function() { 
         return true; 
     };
     // END MODIFICATION
    
  2. In the ClusterIcon class, add the following code AFTER the click/clusterclick trigger:

     // EXISTING CODE (around line 143)
     google.maps.event.trigger(mc, "click", cClusterIcon.cluster_);
     google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name
    
     // BEGIN MODIFICATION
     var zoom = mc.getMap().getZoom();
     // Trying to pull this dynamically made the more zoomed in clusters not render
     // when then kind of made this useless. -NNC @ BNB
     // var maxZoom = mc.getMaxZoom();
     var maxZoom = 15;
     // if we have reached the maxZoom and there is more than 1 marker in this cluster
     // use our onClick method to popup a list of options
     if (zoom >= maxZoom && cClusterIcon.cluster_.markers_.length > 1) {
         return mc.onClick(cClusterIcon);
     }
     // END MODIFICATION
    
  3. Then, in your initialize() function where you initialize the map and declare your MarkerClusterer object:

     markerCluster = new MarkerClusterer(map, markers);
     // onClick OVERRIDE
     markerCluster.onClick = function(clickedClusterIcon) { 
       return multiChoice(clickedClusterIcon.cluster_); 
     }
    

    Where multiChoice() is YOUR (yet to be written) function to popup an InfoWindow with a list of options to select from. Note that the markerClusterer object is passed to your function, because you will need this to determine how many markers there are in that cluster. For example:

     function multiChoice(clickedCluster) {
       if (clickedCluster.getMarkers().length > 1)
       {
         // var markers = clickedCluster.getMarkers();
         // do something creative!
         return false;
       }
       return true;
     };
    

Solution 6 - Javascript

This is more of a stopgap 'quick and dirty' solution similar to the one Matthew Fox suggests, this time using JavaScript.

In JavaScript you can just offset the lat and long of all of your locations by adding a small random offset to both e.g.

myLocation[i].Latitude+ = (Math.random() / 25000)

(I found that dividing by 25000 gives enough separation but doesn't move the marker significantly from the exact location e.g. a specific address)

This makes a reasonably good job of offsetting them from one another, but only after you've zoomed in closely. When zoomed out, it still won't be clear that there are multiple options for the location.

Solution 7 - Javascript

The answers above are more elegant, but I found a quick and dirty way that actually works really really incredibly well. You can see it in action at www.buildinglit.com

All I did was add a random offset to the latitude and longditude to my genxml.php page so it returns slightly different results each time with offset each time the map is created with markers. This sounds like a hack, but in reality you only need the markers to move a slight nudge in a random direction for them to be clickable on the map if they are overlapping. It actually works really well, I would say better than the spider method because who wants to deal with that complexity and have them spring everywhere. You just want to be able to select the marker. Nudging it randomly works perfect.

Here is an example of the while statement iteration node creation in my php_genxml.php

while ($row = @mysql_fetch_assoc($result)){ $offset = rand(0,1000)/10000000;
$offset2 = rand(0, 1000)/10000000;
$node = $dom->createElement("marker");
$newnode = $parnode->appendChild($node);
$newnode->setAttribute("name", $row['name']);
$newnode->setAttribute("address", $row['address']);
$newnode->setAttribute("lat", $row['lat'] + $offset);
$newnode->setAttribute("lng", $row['lng'] + $offset2);
$newnode->setAttribute("distance", $row['distance']);
$newnode->setAttribute("type", $row['type']);
$newnode->setAttribute("date", $row['date']);
$newnode->setAttribute("service", $row['service']);
$newnode->setAttribute("cost", $row['cost']);
$newnode->setAttribute("company", $company);

Notice under lat and long there is the +offset. from the 2 variables above. I had to divide random by 0,1000 by 10000000 in order to get a decimal that was randomly small enough to just barely move the markers around. Feel free to tinker with that variable to get one that is more precise for your needs.

Solution 8 - Javascript

I like simple solutions so here's mine. Instead of modifying the lib, which would make it harder to mantain. you can simply watch the event like this

google.maps.event.addListener(mc, "clusterclick", onClusterClick);

then you can manage it on

function onClusterClick(cluster){
	var ms = cluster.getMarkers();

i, ie, used bootstrap to show a panel with a list. which i find much more confortable and usable than spiderfying on "crowded" places. (if you are using a clusterer chances are you will end up with collisions once you spiderfy). you can check the zoom there too.

btw. i just found leaflet and it seems to work much better, the cluster AND spiderfy works very fluidly http://leaflet.github.io/Leaflet.markercluster/example/marker-clustering-realworld.10000.html and it's open-source.

Solution 9 - Javascript

For situations where there are multiple services in the same building you could offset the markers just a little, (say by .001 degree), in a radius from the actual point. This should also produce a nice visual effect.

Solution 10 - Javascript

Check out http://google-maps-utility-library-v3.googlecode.com/svn/tags/markerclusterer/1.0/">Marker Clusterer for V3 - this library clusters nearby points into a group marker. The map zooms in when the clusters are clicked. I'd imagine when zoomed right in you'd still have the same problem with markers on the same spot though.

Solution 11 - Javascript

Updated to work with MarkerClustererPlus.

  google.maps.event.trigger(mc, "click", cClusterIcon.cluster_);
  google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name

  // BEGIN MODIFICATION
  var zoom = mc.getMap().getZoom();
  // Trying to pull this dynamically made the more zoomed in clusters not render
  // when then kind of made this useless. -NNC @ BNB
  // var maxZoom = mc.getMaxZoom();
  var maxZoom = 15;
  // if we have reached the maxZoom and there is more than 1 marker in this cluster
  // use our onClick method to popup a list of options
  if (zoom >= maxZoom && cClusterIcon.cluster_.markers_.length > 1) {
	var markers = cClusterIcon.cluster_.markers_;
	var a = 360.0 / markers.length;
    for (var i=0; i < markers.length; i++)
    {
    	var pos = markers[i].getPosition();
		var newLat = pos.lat() + -.00004 * Math.cos((+a*i) / 180 * Math.PI);  // x
		var newLng = pos.lng() + -.00004 * Math.sin((+a*i) / 180 * Math.PI);  // Y
		var finalLatLng = new google.maps.LatLng(newLat,newLng);
		markers[i].setPosition(finalLatLng);
    	markers[i].setMap(cClusterIcon.cluster_.map_);
    }
    cClusterIcon.hide();
	return ;
  }
  // END MODIFICATION

Solution 12 - Javascript

I used markerclustererplus, and for me this works:

//Code
google.maps.event.addListener(cMarkerClusterer, "clusterclick", function (c) {
            var markers = c.getMarkers();
            
            //Check Markers array for duplicate position and offset a little
            if (markers .length > 1) {
                //Check if all markers are in the same position (with 4 significant digits)
                if (markers .every((val, index, arr) => (val.getPosition().lat().toFixed(4) == arr[0].getPosition().lat().toFixed(4)) && (val.getPosition().lng().toFixed(4) == arr[0].getPosition().lng().toFixed(4)))) { /
                    //Don't modify first element
                    for (i = 1; i < markers.length; i++) {
                        var existingMarker = markers[i];
                        var pos = existingMarker.getPosition();                        
                        var quot = 360.0 / markers.length;
                        var newLat = pos.lat() + -.00008 * Math.cos(+quot * i); //+ -.00008 * Math.cos((+quot * i) / 180 * Math.PI);  //x                        
                        var newLng = pos.lng() + -.00008 * Math.sin(+quot * i);  //+ -.0008 * Math.sin((+quot * i) / 180 * Math.PI);  //Y
                        existingMarker.setPosition(new google.maps.LatLng(newLat, newLng));                        
                    }
                    let cZoom = map.getZoom();
                    map.setZoom(cZoom-1);
                    map.setZoom(cZoom+1);
                } 
            }            
        });

Solution 13 - Javascript

Check this: https://github.com/plank/MarkerClusterer

This is the MarkerCluster modified to have a infoWindow in a cluster marker, when you have several markers in the same position.

You can see how it works here: http://culturedays.ca/en/2013-activities

Solution 14 - Javascript

Giving offset will make the markers faraway when the user zoom in to max. So i found a way to achieve that. this may not be a proper way but it worked very well.

// This code is in swift
for loop markers
{
//create marker
let mapMarker = GMSMarker()
mapMarker.groundAnchor = CGPosition(0.5, 0.5)
mapMarker.position = //set the CLLocation
//instead of setting marker.icon set the iconView
let image:UIIMage = UIIMage:init(named:"filename")
let imageView:UIImageView = UIImageView.init(frame:rect(0,0, ((image.width/2 * markerIndex) + image.width), image.height))
imageView.contentMode = .Right
imageView.image = image
mapMarker.iconView = imageView
mapMarker.map = mapView
}

set the zIndex of the marker so that you will see the marker icon which you want to see on top, otherwise it will animate the markers like auto swapping. when the user tap the marker handle the zIndex to bring the marker on top using zIndex Swap.

Solution 15 - Javascript

How to get away with it.. [Swift]

    var clusterArray = [String]()
    var pinOffSet : Double = 0
    var pinLat = yourLat
    var pinLong = yourLong
    var location = pinLat + pinLong

A new marker is about to be created? check clusterArray and manipulate it's offset

 if(!clusterArray.contains(location)){
        clusterArray.append(location)
    } else {
        
        pinOffSet += 1
        let offWithIt = 0.00025 // reasonable offset with zoomLvl(14-16)
        switch pinOffSet {
        case 1 : pinLong = pinLong + offWithIt ; pinLat = pinLat + offWithIt
        case 2 : pinLong = pinLong + offWithIt ; pinLat = pinLat - offWithIt
        case 3 : pinLong = pinLong - offWithIt ; pinLat = pinLat - offWithIt
        case 4 : pinLong = pinLong - offWithIt ; pinLat = pinLat + offWithIt
        default : print(1)
        }
        

    }

result

enter image description here

Solution 16 - Javascript

Adding to Matthew Fox's sneaky genius answer, I have added a small random offset to each lat and lng when setting the marker object. For example:

new LatLng(getLat()+getMarkerOffset(), getLng()+getMarkerOffset()),

private static double getMarkerOffset(){
    //add tiny random offset to keep markers from dropping on top of themselves
    double offset =Math.random()/4000;
    boolean isEven = ((int)(offset *400000)) %2 ==0;
    if (isEven) return  offset;
    else        return -offset;
}

Solution 17 - Javascript

I used this http://leaflet.github.io/Leaflet.markercluster/ and perfectly works for me. added full solution.

<html lang="en">
  <head>
    <script src="https://code.jquery.com/jquery-3.2.1.js" integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE=" crossorigin="anonymous"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.js"></script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/leaflet.markercluster.js"></script>
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css" />
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/MarkerCluster.Default.css" />
 </head>
<body>
   <div id="map"></div>

    <script>
        var addressData = [
            {id: 9, name: "Ankita", title: "Manager", latitude: "33.1128019", longitude: "-96.6958939"},
            {id: 1, name: "Aarti", title: "CEO", latitude: "33.1128019", longitude: "-96.6958939"},
            {id: 2, name: "Payal", title: "Employee", latitude: "33.0460488", longitude: "-96.9983386"}];

        var addressPoints = [];
        for (i = 0; i < addressData.length; i++) {
            var marker = {
                latitude: addressData[i].latitude,
                longitude: addressData[i].longitude,
                coverage: addressData[i]
            };
            addressPoints.push(marker);
        }
        var map = L.map('map').setView(["32.9602172", "-96.7036844"], 5);
        var basemap = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="http://cartodb.com/attributions">CartoDB</a>', subdomains: 'abcd'});
        basemap.addTo(map);
        
        var markers = L.markerClusterGroup();
        for (var i = 0; i < addressPoints.length; i++) {
           // var icon1 = "app/common_assest/images/pin/redPin.png"; // set ehere you own marker pin whatever you want to set
            var currentMarker = addressPoints[i];
            console.log(currentMarker);
            var contentString = '<div class="mapinfoWindowContent">' +
                    '<div class="mapInfoTitle">Name: ' + currentMarker.coverage.name + '</div>' +
                    '<div class="mapInfoSubText">Licence: ' + currentMarker.coverage.title + '</div>' +
                    '</div>';
   // var myIcon = L.icon({// set ehere you own marker pin whatever you want to set
     // iconUrl: icon1,
    //  iconRetinaUrl: icon1,
   //                });
            var marker = L.marker(new L.LatLng(currentMarker['latitude'], currentMarker['longitude']), {
                title: currentMarker.coverage.name
            });
            marker.bindPopup(contentString);
            markers.addLayer(marker);
        }
        markers.addTo(map);
    </script>
</body>

Hope fully it will help to you easily.

Solution 18 - Javascript

The solution I've used is pretty simple. Just use @googlemaps/markerclusterer library in combination with the Maps JavaScript API.

Than you will need just one line after the map is filled out with your markers:

new MarkerClusterer({ map, markers });

All information can be found here https://developers.google.com/maps/documentation/javascript/marker-clustering

Solution 19 - Javascript

Expanding on the answers given above, just ensure you set maxZoom option when initializing the map object.

Solution 20 - Javascript

Adding to above answers but offering an alternative quick solution in php and wordpress. For this example I am storing the location field via ACF and looping through the posts to grab that data.

I found that storing the lat / lng in an array and check the value of that array to see if the loop matches, we can then update the value within that array with the amount we want to shift our pips by.

//This is the value to shift the pips by. I found this worked best with markerClusterer
$add_to_latlng = 0.00003;

while ($query->have_posts()) {
	$query->the_post();
	$meta = get_post_meta(get_the_ID(), "postcode", true); //uses an acf field to store location
	$lat = $meta["lat"];
	$lng = $meta["lng"];

	if(in_array($meta["lat"],$lat_checker)){ //check if this meta matches
        
        //if matches then update the array to a new value (current value + shift value)
        // This is using the lng value for a horizontal line of pips, use lat for vertical, or both for a diagonal
		if(isset($latlng_storer[$meta["lng"]])){
			$latlng_storer[$meta["lng"]] = $latlng_storer[$meta["lng"]] + $add_to_latlng;
			$lng = $latlng_storer[$meta["lng"]];
		} else {
			$latlng_storer[$meta["lng"]] = $meta["lng"];
			$lng = $latlng_storer[$meta["lng"]];
		}

	} else {
		$lat_checker[] = $meta["lat"]; //just for simple checking of data
		$latlng_storer[$meta["lat"]] = floatval($meta["lat"]);
		$latlng_storer[$meta["lng"]] =  floatval($meta["lng"]);
	}

	$entry[] = [
		"lat" => $lat,
		"lng" => $lng,
	//...Add all the other post data here and use this array for the pips
	];
} // end query

Once I've grabbed these locations I json encode the $entry variable and use that within my JS.

let locations = <?=json_encode($entry)?>;

I know this is a rather specific situation but I hope this helps someone along the line!

Solution 21 - Javascript

Extending answers above, when you got joined strings, not added/subtracted position (e.g. "37.12340-0.00069"), convert your original lat/longitude to floats, e.g. using parseFloat(), then add or subtract corrections.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLouzoidView Question on Stackoverflow
Solution 1 - JavascriptTad WohlrappView Answer on Stackoverflow
Solution 2 - JavascriptIgnatiusView Answer on Stackoverflow
Solution 3 - JavascriptSteve GrahamView Answer on Stackoverflow
Solution 4 - JavascriptDzinXView Answer on Stackoverflow
Solution 5 - JavascriptNathan ColgateView Answer on Stackoverflow
Solution 6 - JavascriptChris HalcrowView Answer on Stackoverflow
Solution 7 - JavascriptMatthew FoxView Answer on Stackoverflow
Solution 8 - JavascriptNandeView Answer on Stackoverflow
Solution 9 - JavascriptChaoleyView Answer on Stackoverflow
Solution 10 - JavascriptBudgieView Answer on Stackoverflow
Solution 11 - JavascriptthoshinoView Answer on Stackoverflow
Solution 12 - JavascriptCarmine CheckerView Answer on Stackoverflow
Solution 13 - JavascriptMarc Canals GirautView Answer on Stackoverflow
Solution 14 - JavascriptSatheesh GView Answer on Stackoverflow
Solution 15 - JavascriptAbu AlfadlView Answer on Stackoverflow
Solution 16 - JavascriptseekingStillnessView Answer on Stackoverflow
Solution 17 - JavascriptAnkita DobariyaView Answer on Stackoverflow
Solution 18 - JavascriptitradoRDView Answer on Stackoverflow
Solution 19 - JavascriptHosseinSafyView Answer on Stackoverflow
Solution 20 - JavascriptBenjamin VaughanView Answer on Stackoverflow
Solution 21 - JavascriptvipView Answer on Stackoverflow