Styled Google Maps from CMS Collection with Multiple (over 100) Overlapping Markers and Dynamic Info Windows

Hello Everyone!

It took me a while to get this project working, but once I did, I wanted to share it with the community, and I hope someone might find this useful.

The Goal: I am making a personal portfolio website and I have a CMS collection with over 300 images and each of them has a set of coordinates of where the photo was taken. I wanted to create a Map page where I have all the markers for every photo, and that each marker on click displays a thumbnail and some information about that particular photo. On click it take you to the individual CMS page for that image.

The Problem – the initial google maps setup was not too bad, it took some doing but was possible, but I had two issues:

  1. I had a number of images that had exact same coordinates and since the markers were overlaying each other I would only get the top one visible and clickable.
  2. The CMS collection is limited only to 100 items

NOW I present the solution to the problem that involves Google Maps JS API, and Overlapping Marker Spiderfier script (for overlapping markers – MAD PROPS TO jawj), and multiple collections on the same page so that all 300 markers are visible.

(This presumes that you have setup you Google Cloud Console and have the Google Maps API Key ready)

Step 1. SETUP THE MAP CANVAS

Create a page where you intend to use the google maps. I used a div with “fixed full” positioning, 100VH height and 5vh/5vw padding, I named it “google-maps-wrapper”.

Create a child div “google-maps-canvas” with static positioning, and 100% width and height.

Click on the gear icon (next the brush icon) and in the Div Block Settings give the “google-maps-wrapper” ID as “map_wrapper” and to “google-maps-canvas” give an ID “map_element” (of course without the quotes)

Step 2. SETUP THE COLLECTIONS

Create your CMS collection(s), and make sure they are all named the same. (mine were “dynamicGallerySection”. Inside the “galleryItem” div, place an “HTML Embed”.

The HTML Embed should have all the information that you may want to use on the map.
Use the following script:

<div class="imgMap"
<script>
imgs.push ({
  'name'  : ‘[title]’,
  'slug'  : ‘[slug]’,
  'url'   : 'https://[YOUR PATH TO PUBLISHED INDIVIDUAL CMS PAGE]/‘[slug]’,
  'description'  :  ‘[description]’,
  'photo'   : ‘[photo]’,
  'lat'   : ‘[lat]’,
  'lng'   :  ‘[lng]’,
});
</script> 

Replace the items [XXX] with the dynamic fields as needed (click the purple “+Add Field” button in the HTML Embed Code Editor) and don’t forget your path to the CMS collection individual item page - it is formed with the last part being the “slug” of your CMS item - this is important so that the InfoWindow popups link to the CMS items.

If you have multiple CMS Collections on the same page (if you need to have more than 100 markers on the map) then make sure that each CMS collection is named the same and has an identical HTML embed in the “galleryItem” div.

Set the collection “layout” as hidden.

Step 3. SETUP THE CUSTOM CODE (THE BIG STEP :slight_smile: )

Open the custom code section of your page. In the “inside tag” add the following code:

<script>
var imgs = [];
</script>

<style>
#map_element .imgMap {
  min-height: 0;
  height: 100px;
  margin-bottom: 5px;
}
 </style>

In the “Before tag” add the following code:

<script async defer src="https://maps.google.com/maps/api/js?v=3&callback=mapLibReadyHandler&key=[YOUR API KEY] "></script>
<script async defer src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier/1.0.3/oms.min.js?spiderfier_callback=mapLibReadyHandler"></script>

<div id="map_element"></div>

<script>
    var isIE = false;

            var mapLibsReady = 0;
    
    function mapLibReadyHandler() {
        if (++mapLibsReady < 2) return;
        var mapElement = document.getElementById('map_element');
        var map = new google.maps.Map(mapElement, {
            center: new google.maps.LatLng(0, 0),
            zoom: 4,
            minZoom: 3,
            mapTypeId: 'terrain',
            mapTypeControl: true,
            mapTypeControlOptions: {
                style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
                mapTypeIds: ['terrain', 'satellite'],  
                },
            streetViewControl: false,
        });

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

        function iwClose() {
            iw.close();
        }
        google.maps.event.addListener(map, 'click', iwClose);
        var oms = new OverlappingMarkerSpiderfier(map, {
            markersWontMove: true,
            markersWontHide: true
        });
        oms.addListener('format', function(marker, status) {
            var iconURL = status == OverlappingMarkerSpiderfier.markerStatus.SPIDERFIED ? 'https://uploads-ssl.webflow.com/5d38e6641d65b655aa18d423/5d629dcd45d893fadf546bd7_marker-highlight%20N.svg' :
                          status == OverlappingMarkerSpiderfier.markerStatus.SPIDERFIABLE ? 'https://uploads-ssl.webflow.com/5d38e6641d65b655aa18d423/5d629dcda93ad9effccff365_marker-plus%20N.svg' :
                          status == OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIABLE ? 'https://uploads-ssl.webflow.com/5d38e6641d65b655aa18d423/5d629dcd45d893fadf546bd7_marker-highlight%20N.svg' :                    null;
            var iconSize = new google.maps.Size(23, 32);
            marker.setIcon({
                url: iconURL,
                size: iconSize,
                scaledSize: iconSize // makes SVG icons work in IE
            });
        });

        for (var i = 0, len = window.mapData.length; i < len; i++) {
            (function() { // make a closure over the marker and marker data
                var markerData = window.mapData[i]; // e.g. { lat: 50.123, lng: 0.123, text: 'XYZ' }
                var marker = new google.maps.Marker({
                    position: markerData,
                    optimized: !isIE // makes SVG icons work in IE
                });
                google.maps.event.addListener(marker, 'click', iwClose);
                oms.addMarker(marker, function(e) {
                    iw.setContent(markerData.text);
                    iw.open(map, marker);
                });
            })();
        }
        window.map = map;
        window.oms = oms;
    }

    var data = [];
      for (i = 0; i < imgs.length; i++) {
        var imgMap = imgs[i];
        data.push({
	lng: parseFloat(imgMap.lng),
            lat: parseFloat(imgMap.lat),
            text: '<a href="' + imgMap.url + '"><div class="imgMap" style="background:url(' + imgMap.photo + ') center/contain no-repeat"></div></a>' + '<h3>' + imgMap.name + '</h3> <i><small>' + imgMap.classification + '</small></i><br/><b>' + imgMap.description + ' </b>'
        });
    }
    window.mapData = data;
</script>

Please pay attention to the following sections of the script:

  1. <div id="map_element"></div>
    This connects to the map_element div ID setup in Step 1.

  2. var mapElement = document.getElementById('map_element');
    This part connects the google maps API to the div ID you made in Step 1

  3. var map = new google.maps.Map(mapElement, {
             center: new google.maps.LatLng(0, 0),
             zoom: 4,
             minZoom: 3,
             mapTypeId: 'terrain',
             mapTypeControl: true,
             mapTypeControlOptions: {
                 style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
                 mapTypeIds: ['terrain', 'satellite'],  
                 },
             streetViewControl: false,
         });
    

    This part styles the map how you like it – e.g. if you want to have street view button visible, or at what zoom level it starts and the max/min zoom you can use, type of map, etc… for more details read this article

  4.             var iconURL = status == OverlappingMarkerSpiderfier.markerStatus.SPIDERFIED ? 'https://uploads-ssl.webflow.com/5d38e6641d65b655aa18d423/5d629dcd45d893fadf546bd7_marker-highlight%20N.svg' :
                           status == OverlappingMarkerSpiderfier.markerStatus.SPIDERFIABLE ? 'https://uploads-ssl.webflow.com/5d38e6641d65b655aa18d423/5d629dcda93ad9effccff365_marker-plus%20N.svg' :
                           status == OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIABLE ? 'https://uploads-ssl.webflow.com/5d38e6641d65b655aa18d423/5d629dcd45d893fadf546bd7_marker-highlight%20N.svg'
    

those are links to marker icons – I have uploaded SVG’ of markers to my webflow site and got the links to them and stuck them in the script.

  1. lng: parseFloat(imgMap.lng),
    lat: parseFloat(imgMap.lat),
    

    I have stored the coordinates as a text field in my CMS collection so in that case I had to turn them into numbers, hence the parseFloat command – if yours are in number format you do not need to use that.

  2.  text: '<a href="' + imgMap.url + '"><div class="imgMap" style="background:url(' + imgMap.photo + ') center/contain no-repeat"></div></a>' + '<h3>' + imgMap.name + '</h3> <i><small>' + imgMap.classification + '</small></i><br/><b>' + imgMap.description + ' </b>'
         });
    

    This is HTML styling for the info window that will show up when a marker is clicked on the map – style as you need – it references the dynamic information that you set up in the HTML Embed in Step 2

That’s it folks, publish and enjoy your google map with CMS custom markers!

Here is the link to my map

4 Likes

Super cool! :slight_smile:

2 Likes

I seem to be having trouble re-creating the same thing! I have the map working but don’t see anything on the map. Could you clarify what to do when you set up the collections? Is the galleryItem Div just a div inside of the collection item. and what exactly is called “dynamicGallerySection” and what is hidden (the CMS wrapper, list, or item). Is there something I need to name the collection itself? This is what my page looks like


and here’s the link to my site:
catalist-network.webflow.io/map

The page i have this on is not a collection page, to clarify.

Thank you in advanced for your help and I hope you can help me get this to work!

Your cms items have to have latitude and longitude information in them - then the map will display the icons of their location. Also please share a read only link to your page and a better description of what you are trying to achieve. I will try and have a look at it :slight_smile:

Here’s the link:
https://preview.webflow.com/preview/catalist-network?utm_medium=preview_link&utm_source=dashboard&utm_content=catalist-network&preview=911ef67926481620e528f5ea636865e2&mode=preview

“Map” is the page that I’m trying to do this on…

the project is quite complicated but on the top header i put a link to the map page…which will eventually be a map to share locations of projects for people who are using the site.

Thank you so much for your help!

Also i put in some example coordinates, are they not in the same format as you?

So, you have a number of issues.

  1. your script in the embed on line one is missing a closing “>” so make sure to add that
  2. I would try to add the coordinates in the format that google uses (the one I used in my example)
  3. in the embed the ‘slug’ does not need to be in quotes… it has to look like this:
    url' : 'https://ivgphoto-fin.webflow.io/dbwth/slug'

let me know if it works after that…

oh and you don’t need to have your collection inside the google maps element - you can have it as a separate element and hidden - like this:

Ahhh okay, thank you!! yeah this picture helps a lot that was a little unclear.

  1. So i took the embed outside of the google maps element. Fixed the code issue by changing:
<div class="imgMap" (i took this right from your code so maybe update this since webflow doesn't have a code verifier) to
or should it be
  1. made all the coordinates XX.XXX like in your example (like 54.234)
  2. in the embed i fixed the slug part

Maybe it will take some time but as of now it’s still not working… same url.

I appreciate your help, I’m pretty certain i followed everything else you said exactly so I’m not sure if inspecting the html will give us any hints as to why it’s not working?

Ok, so it look like you still have an issue inside the embed… if you will look closely you are not using consistent “quote” simbols on all the parts of the script, and as a result if you look even closer you will see that the text of the ‘name’ is green but the following line its yellow - that means that there is an error - replace all the quote (single or double) marks with same one - it must be all consistent throughout. all text elements (name, discription lat lang etc…) must be green.

Also, you copied the actual links to the icons that represent the markers from my script - i think it might be an issue - so you should try finding markers that you want to use - any svg will do - then upload them to your website (the same way you upload any other assets). and then copy the link to your asset in the relevant place of the script - I talked about this in my tutorial…

try those steps see if it starts working…

You’re awesome. Seriously… It works!!! I’m so excited. Thank you so much. Don’t hesitate to hit me up if you ever need any sort of design or business help! I would love to return the favor.

Glad I could help! So it’s all working as expected?

Yeah it connects with the cms and displays the info. The next steps are seeing if I can style the popup a bit, and then the final thing I’d like to do is set up some filters that limit the pins that show up… or would you reccomend just having multiple CMSs to do this? Let you know if I break it trying to do that lol.

@IVG is awesome!!! Thank you so much!

1 Like

I’ve got it working too! Awesome!

Some bugs indeed appeared when copy-pasting the code for the html-embed.
The ’ (quotes) transformed into opening/closing quotes, not regular ones.

Also i had to close the first div with an > and also close it in the end with < /div >, to properly get it to work.

<div class="imgMap">
<script>
imgs.push ({
  'name'  : '[name]',
  'slug'  : '{{slug}',
  'url'   : 'https://website.url/[slug]',
  'description'  :  '[description]',
  'photo'   : '[photo]',
  'lat'   : '[latitude#]',
  'lng'   :  '[longitude#]',
});
</script>
</div>

And even though i’m using numbers in my Collection, i still do need to use the lat/lon conversion with:

lng: parseFloat(imgMap.lng),
lat: parseFloat(imgMap.lat),

I do have a question: How can i remove the spider-combine thing properly, if i just want to show all the icons?

Please clarify what do you mean by removing the spiderfier? as a script alltogether? and what do you mean by showing all the icons? - you mean that you don’t have any overlapping icons? which icons you want to show - the original google one’s or the custom ones?

Hi IVG,

I’d like to use the custom icons.
I would even like a second set of icons, because i have 2 types of locations that ideally should have 2 different icons.

However, when you zoom in and out, with your script, the icons ‘pop’ (suddenly increase/decrease in size) - i’d like that removed and the combining of icons.

But i do like your approach of being able to show more than 100 items, which is what i need.

There’s another tutorial on this that does have smooth icon transitions (well rather, the icons simply stay the same size) - check: Google Maps JavaScript API - CMS map with multiple locations

I hope i can use your script, but i need your help removing the spider thing.

And one more question: Is there a way to change the minimum distance 2 (or more) icons should be apart before “spiderfing”? I’ve increased the size of the icons, and now that feels weird.

Also - i think i’d like to have 2 options instead of 3.
Actual icons or the spiderfied + icons (to expand a location).
Currently icons that are close together, also show as special icons, and when clicked they move apart so you can more easily select each. Nice idea, but I don’t like this unfortunately.

BTW: If you would like to style a map on https://mapstyle.withgoogle.com/
You’ll get a JSON script like:

[
  {
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#f5f5f5"
      }
    ]
  },
  {
    "elementType": "labels.icon",
    "stylers": [
      {
        "visibility": "off"
      }
    ]
  },
  {
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#616161"
      }
    ]
  },
  {
    "elementType": "labels.text.stroke",
    "stylers": [
      {
        "color": "#f5f5f5"
      }
    ]
  },
]

If you put this in the var map = new google.maps.Map, make sure to disable the mapTypeId: like //mapTypeId: ‘satellite’.

Then this does also work :smiley:

1 Like

Firstly, if you like the other script why not use it? - its perfectly workable…

The trick for having more than 100 items is to have multiple CMS collection on the page and having them display only part of your CMS collection each (see Step 2) – you just have to make sure that they have same names. Also, you have to set the collection up so that first displays you cms items 1-100 second one 101-200 and so on…

if you chose to use Siderfy script then you should look at its api to get an idea on how to further customize it.

Finally, I am not sure why your icons pop… try testing different browsers and in incognito mode - I don’t have this behavior…

P.S. you can also clone this page and I think it has a working script like you wanted…

Hi IVG,

The other script says nothing about multiple collections (like you do with 1-100, 101-200 etc). I need that because i have over 400 pins to put on the map :). Perhaps i can do the same trick with the other script.

Spiderfy is nice, and i would like to keep it, but that poppin is something that is there. I’m on a clean Chrome install (windows) and also Firefox and Microsoft browsers have this. (The fancy demo on the Spiderfy website is the only one that does it right).

So i think i should tinker with the spiderfy script and/or remove it somehow with the other script as a simpler example.

I’ll let you know if it works out !