Google Maps: Dealing with Multiple Markers vs Single Markers use cases and how to do it?

Saurav Prakash
8 min readAug 6, 2017

--

I am working on few projects where there is always a need to have map based interactivity. One of the recent app I launched is tdcautoauction.com which shows live auctions on the index page with a map and cluster together auctions which are at the same place. When you click on a marker, it will spread out and allow you to choose one from them, when you double click on any, it will take you to that auction. I am using a Javascript library to help me out with cases of overlapping markers. Its a great work by George MacKerron

Now, personally I am a big fan of map based interactivity and I am learning various ways of tackling various complex use cases. I started with map based interface a while ago and was hooked after I saw the zillow map interface and I loved it. I wanted to have it in all my apps…lol. Luckily, I am working on a lot of projects where entities have latitude and longitude so its always good exposures and practice sessions.

Lets get to business now. Google maps provides great documentation for maps

but personally I felt it could have been a little more lucid, also they don’t handle the overlapping marker cases and leave you in the ocean with a paddle board. In the start I was feeling a bit confused but I figured it out and felt there should be concrete ways to follow for anyone new and so felt I should write about it.

A single marker
Use cases: On a product show page, on a shop home page, on a car rental show page, mainly when you will need to deal with just one entity at a time.

I work with rails MVC, so I will follow the rails strategy, names and DSL used but its almost the same with any framework.

Suppose I have the entity named product. I generated product through scaffolding and I already have the new, show, _form etc files:

class CreateProducts < ActiveRecord::Migration[5.1]
def change
create_table :products do |t|
t.string :title
t.string :description
t.string :year
t.float :latitude
t.float :longitude
t.string :city
t.string :image
t.timestamps
end
end
end

Run rake rb:migrate

Suppose, I want to show a product in a map. First thing first, in the controller show action, make the variable available:

@product = Product.find_by_id(params[:id])

Keep in mind this will give you one entity, not an array. Now, to show a map on the page we will need a div with id map:

#map {
height: 800px; /* Choose the height you want, it can be percentage for responsiveness, it can be em for adjusting thoroughly or just simple old px*/
width: 100%; /* I will use the map in a bootstrap div so lets set a width 100%*/

overflow: hidden; /*This is important else your map will overflow the div its in*/
}

Now, the way I do it is generate a partial and reuse the partial anywhere I want to passing the variable while rendering. So, lets generate a partial first in your entity folder (product folder here). I will name it as

_productmap.html.erb

Inside the file:

Step 1: First generate the div map

<div id=”map”></div>

Till now its all rainbows and unicorns but now we are entering a much dreaded domain of external APIs. Google maps one of them. We want everything to work as fast as possible and we want our DOM to be loaded as soon as possible because most of the time the maps will be present at the bottom of the page and we want to show at least something on the page (I would like to show my products on the page as html elements at least).

We know we have to use the google map src and I assume you already have the key and the setup ready. The two main questions: where to call it and how to call the script? If we place the script src in the partial it will interfere with the other html elements, also it will be repetitive. To solve this problem lets think and analyze how we can have non blocking calls. For such calls, we have two options, either use async or use defer to call it.

As per a post on stack overflow regarding this question, an author mentioned:

Scripts loaded with ASYNC are parsed and executed immediately when the resource is done downloading. Whereas DEFER scripts don’t execute until the HTML document is done being parsed (AKA, DOM Interactive or performance.timing.domInteractive).

Comparing the ASYNC and DEFER waterfalls, we see that using DEFER makes DOM Interactive fire sooner and allows rendering to proceed more quickly.

HOWEVER:

Even though ASYNC and DEFER don’t block the HTML parser, they can block rendering. This happens when they’re parsed and executed before rendering is complete and take over the browser main thread. There’s nothing in the spec that says they have to wait until rendering is complete.

I thought why not doing my own analysis to see what works and rule out the rest.

1. Calling

<script async
src=”https://maps.googleapis.com/maps/api/js?key=mykeyjsahsasaisuiajsijhello&callback=initMap">
</script>

with async in the head before the DOM element will give you an error because map is not loaded but the script starts running as soon as it is downloaded and will show initmap isn’t a function, so one thing for sure, for async you need to have the script placed at the end before body closing tag so that your map div and functions are loaded.

2. Calling

<script defer
src=”https://maps.googleapis.com/maps/api/js?key=mykeyjsahsasaisuiajsijhello&callback=initMap">
</script>

in the head will work as defer works only when DOM is loaded.

Now, I also did bench-marking of performance myself by using page load time analyzer for one of the show page for the three cases: async at bottom, defer at top, defer at bottom

Running the experiment multiple times, in lots of cases I found defer is faster but there was unsubstantial difference to stand on the theory of using defer over async every time. For most of the cases the timings were almost equal, and for few defer won but that can be correlated to other aspects as well.

For the cases of comparison between defer in the head and body, I did’t see a lot of difference but I chose to go ahead with calling the external script at one place following the DRY principle in the application.html.erb file after yield and before body closing tag.

Now, lets go straight with a simple marker script:

I used the function

function initMap() {}

(The script explained below will all be inside this function)

For Javascript parsing, I used JSON and converted what I am getting from the controller in to raw JSON array object.

var json = <%= raw @product.to_json %>;

I Initialized the map with center as the product’s latitude and longitude which are float types as in the schema and I am using geocoder gem and calling geocoded by city for the product.

var mycenter = {lat: json.latitude, lng: json.longitude};

Now, lets call google map api to generate a new map, set zoom levels, set the product location as center and customize the map according to our needs. A great collection of ready to use map styles is offered by Snazzy Maps

var map = new google.maps.Map(document.getElementById(‘map’), {
zoom: 6,
center: mycenter,

//adding style to the map
styles: [{“featureType”:”water”,”elementType”:”all”,”stylers”:[{“hue”:”#7fc8ed”},{“saturation”:55},{“lightness”:-6},{“visibility”:”on”}]},{“featureType”:”water”,”elementType”:”labels”,”stylers”:[{“hue”:”#7fc8ed”},{“saturation”:55},{“lightness”:-6},{“visibility”:”off”}]},{“featureType”:”poi.park”,”elementType”:”geometry”,”stylers”:[{“hue”:”#83cead”},{“saturation”:1},{“lightness”:-15},{“visibility”:”on”}]},{“featureType”:”landscape”,”elementType”:”geometry”,”stylers”:[{“hue”:”#f3f4f4"},{“saturation”:-84},{“lightness”:59},{“visibility”:”on”}]},{“featureType”:”landscape”,”elementType”:”labels”,”stylers”:[{“hue”:”#ffffff”},{“saturation”:-100},{“lightness”:100},{“visibility”:”off”}]},{“featureType”:”road”,”elementType”:”geometry”,”stylers”:[{“hue”:”#ffffff”},{“saturation”:-100},{“lightness”:100},{“visibility”:”on”}]},{“featureType”:”road”,”elementType”:”labels”,”stylers”:[{“hue”:”#bbbbbb”},{“saturation”:-100},{“lightness”:26},{“visibility”:”on”}]},{“featureType”:”road.arterial”,”elementType”:”geometry”,”stylers”:[{“hue”:”#ffcc00"},{“saturation”:100},{“lightness”:-35},{“visibility”:”simplified”}]},{“featureType”:”road.highway”,”elementType”:”geometry”,”stylers”:[{“hue”:”#ffcc00"},{“saturation”:100},{“lightness”:-22},{“visibility”:”on”}]},{“featureType”:”poi.school”,”elementType”:”all”,”stylers”:[{“hue”:”#d7e4e4"},{“saturation”:-60},{“lightness”:23},{“visibility”:”on”}]}]
});

//Add the markers from json data. Lets first generate a new LatLng object, give the latitude and longitude of the product, generate a marker by calling google.maps.Marker, give a custom icon (I got an icon from flaticon and stored it in my amazon S3), give the marker a title.

latLng = new google.maps.LatLng(json.latitude, json.longitude);

var marker = new google.maps.Marker({
position: latLng,
map: map,
icon: “//s3-us-west-5.amazonaws.com/placeholder/mymarker.png”,
title: json.title
});

This is an added step but if you need to add a click functionality to the marker you need to have a closure function and use google.maps.event.addListener and pass the location href, and finally call the function through pasisng the marker generated and the json data you have.

(function(marker, data) {

google.maps.event.addListener(marker, “click”, function(e) {
var go_to = “/products/” + data.id;
location.href = go_to;
});

})(marker, json);

Finally, use the partial file anywhere (I use it in bootstrap divs as per my sizes to make it responsive)

<%= render “products/productmap” %>

Multiple Overlapping markers:
Use cases: On a homepage, on search page, on live auctions page, mainly when you will need to deal with multiple entities at a time which may even overlap.

This was one of the cases which was challenging for me in the start. But I solved the problem thanks to an external Javascript library by George MacKerronand, customized it to use in my apps. One such app I launched is TDCAUTOAUCTION

Requirements: When few products overlap, I should have the cluster expand when single clicked and on double click, it should go to the product show page

Lets, get started. Suppose we have few products in our database, wherever we wanna show the map, we need the controller action to have those products available as an array to be parsed as raw json.

@products = Product.all

The map, the div, the external script src will all be the same as before. The things which will vary will be whats inside the initmap() function.

Lets first add the external javascript file needed (if you are sure your server is more distributed and faster, have the file downloaded and stored in the assets/javascript folder and make it available to be used in asset pipeline).

I was using it as external dependency in my application.html.erb and calling it in head but when I moved it to after the yield before the body closing tag I got a good speed bump. I will recommend doing the same, place it after the yield, in the body before body closing tag

<script src=”https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier/1.0.3/oms.min.js"></script>

Initialize a div map as before and call it in the partial say _productsallmap.html.erb and place the partial in the corresponding folder (products folder)

<div id=”map”></div>

The function initMap() {} is the one where magic happens!! lol

Get the instance variable from the controller and convert into raw json

var json = <%= raw @products.to_json %>;

Initialize map with center Washington DC

var mycenter = {lat: 38.247535, lng: -98.713875};

Generate a new map, set the zoom level, set the center, set customization and add styles

var map = new google.maps.Map(document.getElementById(‘map’), {
zoom: 3,
center: mycenter,

//adding style to the map
styles: [{“featureType”:”water”,”elementType”:”all”,”stylers”:[{“hue”:”#7fc8ed”},{“saturation”:55},{“lightness”:-6},{“visibility”:”on”}]},{“featureType”:”water”,”elementType”:”labels”,”stylers”:[{“hue”:”#7fc8ed”},{“saturation”:55},{“lightness”:-6},{“visibility”:”off”}]},{“featureType”:”poi.park”,”elementType”:”geometry”,”stylers”:[{“hue”:”#83cead”},{“saturation”:1},{“lightness”:-15},{“visibility”:”on”}]},{“featureType”:”landscape”,”elementType”:”geometry”,”stylers”:[{“hue”:”#f3f4f4"},{“saturation”:-84},{“lightness”:59},{“visibility”:”on”}]},{“featureType”:”landscape”,”elementType”:”labels”,”stylers”:[{“hue”:”#ffffff”},{“saturation”:-100},{“lightness”:100},{“visibility”:”off”}]},{“featureType”:”road”,”elementType”:”geometry”,”stylers”:[{“hue”:”#ffffff”},{“saturation”:-100},{“lightness”:100},{“visibility”:”on”}]},{“featureType”:”road”,”elementType”:”labels”,”stylers”:[{“hue”:”#bbbbbb”},{“saturation”:-100},{“lightness”:26},{“visibility”:”on”}]},{“featureType”:”road.arterial”,”elementType”:”geometry”,”stylers”:[{“hue”:”#ffcc00"},{“saturation”:100},{“lightness”:-35},{“visibility”:”simplified”}]},{“featureType”:”road.highway”,”elementType”:”geometry”,”stylers”:[{“hue”:”#ffcc00"},{“saturation”:100},{“lightness”:-22},{“visibility”:”on”}]},{“featureType”:”poi.school”,”elementType”:”all”,”stylers”:[{“hue”:”#d7e4e4"},{“saturation”:-60},{“lightness”:23},{“visibility”:”on”}]}]
});

From here, the difference starts. First make a new InfoWindow by using google.maps.InfoWindow() function

var iw = new google.maps.InfoWindow();

Call OverlappingMarkerSpiderfier on the map and pass the ways you want the markers to behave

var oms = new OverlappingMarkerSpiderfier(map, {
markersWontMove: true,
markersWontHide: true,
basicFormatEvents: true
});

In a loop, call a function to produce spidifier markers

for (var i=0; i<json.length; i++){
(function() {
var markerData = json[i];
latLng = new google.maps.LatLng(markerData.latitude, markerData.longitude);
var marker = new google.maps.Marker({ position: latLng, icon: “//s3-us-west-5.amazonaws.com/secretdirectory/placeholder.png”, title: markerData.title });

// markerData works here as a LatLngLiteral

//Add event listener to each markers, and use callback function, it sets the clicked marker as the new marker

google.maps.event.addListener(marker, ‘spider_click’, function(e) {
iw.setContent(markerData.title);
iw.open(map, marker);
});

// adds the marker to the spiderfier and the map

oms.addMarker(marker);

(function(marker, markerData) {

// Attaching a double click event to the current marker to go to the product, same logic but using dblclick in addeventListener

google.maps.event.addListener(marker, “dblclick”, function(e) {
var go_to = “/products/” + markerData.id;
location.href = go_to;
});

})(marker, markerData);

})();
}

}

</script>

And finally use this partial anywhere you want by passing the variable.

I am still learning cooler ways of interactivity and more complex use cases but I hope what I learned will help someone out with google maps and markers! Thank you :)

--

--