Google maps API V3 - multiple markers on exact same spot

Viewed 164444

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?

22 Answers

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.

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.

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);
                } 
            }            
        });

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;
}

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.

I'm using Android's Map Cluster. These are the libs I'm using:

implementation 'com.google.android.gms:play-services-places:16.0.0'   
 
implementation 'com.google.android.gms:play-services-maps:16.0.0'
 
implementation 'com.google.android.gms:play-services-location:16.0.0'
 
implementation 'com.google.maps.android:android-maps-utils:2.0.1'

The problem I was running into is that the Cluster Markers don't separate if two items have the exact same Latitude and Longitudinal points. My fix is to scan through my array of items and if two positions match, I move their positions slightly. Here's my code:

Field Variables:

private ArrayList<Tool> esTools;

When you're done initializing the ArrayList of Tools. From your parsing method, call this:

loopThroughToolsListAndFixOnesThatHaveSameGeoPoint_FixStackingIssue();

Where the magic happens:

private void loopThroughToolsListAndFixOnesThatHaveSameGeoPoint_FixStackingIssue() { 
    DecimalFormat decimalFormatTool = new DecimalFormat("000.0000");
    decimalFormatTool.setRoundingMode(RoundingMode.DOWN);

    for(int backPointer=0; backPointer <= (esTools.size()-1); backPointer++){
        Map<String, Double> compareA = esTools.get(backPointer).getUserChosenGeopoint();
        Double compareA_Latitude = compareA.get("_latitude");
        compareA_Latitude= Double.valueOf(decimalFormatTool.format(compareA_Latitude));
        Double compareA_Longitude = compareA.get("_longitude");
        compareA_Longitude= Double.valueOf(decimalFormatTool.format(compareA_Longitude));
        System.out.println("compareA_Lat= "+ compareA_Latitude+ ", compareA_Long= "+ compareA_Longitude);
        for(int frontPointer=0; frontPointer <= (esTools.size()-1); frontPointer++){
            if(backPointer==frontPointer){
                continue;
            }
            Map<String, Double> compareB = esTools.get(frontPointer).getUserChosenGeopoint();
            Double compareB_Latitude = compareB.get("_latitude");
            compareB_Latitude= Double.valueOf(decimalFormatTool.format(compareB_Latitude));
            Double compareB_Longitude = compareB.get("_longitude");
            compareB_Longitude= Double.valueOf(decimalFormatTool.format(compareB_Longitude));

            if((compareB_Latitude.equals(compareA_Latitude)) && (compareB_Longitude.equals(compareA_Longitude))) {
                System.out.println("these tools match");

                Random randomGen = new Random();
                Double randomNumLat  = randomGen.nextDouble() * 0.00015;
                int addOrSubtractLatitude= ( randomGen.nextBoolean() ? 1 : -1 );
                randomNumLat = randomNumLat*addOrSubtractLatitude;

                Double randomNumLong = randomGen.nextDouble() * 0.00015;
                int addOrSubtractLongitude= ( randomGen.nextBoolean() ? 1 : -1 );
                randomNumLong = randomNumLong*addOrSubtractLongitude;
                System.out.println("Adding Random Latitude="+ randomNumLat + ", Longitude= "+ randomNumLong);

                System.out.println("\n");
                Map<String, Double> latitudeLongitude = new HashMap<>();
                latitudeLongitude.put("_latitude", (compareB_Latitude+ randomNumLat));
                latitudeLongitude.put("_longitude", (compareB_Longitude+ randomNumLong));
                esTools.get(frontPointer).setUserChosenGeopoint(latitudeLongitude);

            }
        }
    }
}

So what the above method does is scan through my ArrayList and see if there are any two Tools have matching points. If the Lat Long points match, move one slightly.

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!

Related