Clip Google Maps JS API ImageMapType to a polygon

JavascriptGoogle MapsGoogle Maps-Api-3

Javascript Problem Overview


How can I clip a MapType in Google Maps to an arbitrary polygon. For example, if I have a custom ImageMapType that covers a large area (i.e. all the world), but I want to show it only inside a given polygon (i.e. one country).

Is there a way to clip the ImageMapType to a given polygon, or to implement a custom MapType to achieve this behaviour? It should allow for zooming and panning normally.

The rest of the map should stay the same, and there would be a MapType covering only a specific area. Therefore, it is not possible to simply overlay a polygon to cover the areas outside the polygon to display just what is needed.

Like so:

Australia map with a clipped overlay on South Australia

Server-side clipping is not an option.

Javascript Solutions


Solution 1 - Javascript

I have written the code for an overlay map type that does what you want. Be sure to test in your target browsers. Fiddle

function ClipMapType(polygon, map) {
  this.tileSize = new google.maps.Size(256, 256);
  this.polygon = polygon;
  this.map = map;
}

ClipMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
  var map = this.map;
  var scale = Math.pow(2, zoom);
  if (coord.y < 0 || coord.y >= scale) return ownerDocument.createElement('div');
  var tileX = ((coord.x % scale) + scale) % scale;
  var tileY = coord.y;
  // Your url pattern below
  var url = "https://khms0.google.com/kh/v=694&x=" + tileX + "&y=" + tileY + "&z=" + zoom;
  var image = new Image();
  image.src = url;

  var canvas = ownerDocument.createElement('canvas');
  canvas.width = this.tileSize.width;
  canvas.height = this.tileSize.height;
  var context = canvas.getContext('2d');

  var xdif = coord.x * this.tileSize.width;
  var ydif = coord.y * this.tileSize.height;

  var ring = this.polygon.getArray()[0];
  var points = ring.getArray().map(function(x) {
    var worldPoint = map.getProjection().fromLatLngToPoint(x);
    return new google.maps.Point((worldPoint.x) * scale - xdif, (worldPoint.y) * scale - ydif);
  });

  image.onload = function() {
    context.beginPath();
    context.moveTo(points[0].x, points[0].y);
    var count = points.length;
    for (var i = 0; i < count; i++) {
      context.lineTo(points[i].x, points[i].y);
    }
    context.lineTo(points[count - 1].x, points[count - 1].y);

    context.clip();
    context.drawImage(image, 0, 0);
    context.closePath();
  };

  return canvas;
};

function initMap() {
  var map = new google.maps.Map(document.getElementById('map'), {
    zoom: 4,
    center: {
      lat: 15,
      lng: 15
    }
  });
  var polygon = new google.maps.Data.Polygon([
    [{
      lat: 0,
      lng: 0
    }, {
      lat: 30,
      lng: 30
    }, {
      lat: 0,
      lng: 30
    }]
  ]);
  var mapType = new ClipMapType(polygon, map);
  map.overlayMapTypes.insertAt(0, mapType);
}

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
}
#map {
  height: 100%;
}

<div id="map"></div>
<script async defer src="https://maps.googleapis.com/maps/api/js?callback=initMap">
</script>

How it works

Basically ClipMapType class is a MapType interface. getTile method of this interface is called with tile coordinates and zoom level to get tile for every tile. ClipMapType creates a canvas element to act as a tile and draws the tile image clipped to inside of the polygon. If performance is important, it can be optimized to work faster.

Disclaimer

Usage of Google tile servers by hacking the URL, probably violates Google Maps Terms of Service. I used it for demonstration and don't recommend using it in production. My answer is an attempt to give you an insight for you to create your own solution.

Solution 2 - Javascript

Do you require Google Maps perse? I know Openlayers 3 provides better support for this kind of stuff. For example, take a look at this.

If you really must use Google Maps, I suggest implementing your own MapType and generate the tiles needed to cover your polygon area yourself using MapTiler. (MapTiler also generates an example Google Maps implementation for you, so that shouldn't be too hard.)

Solution 3 - Javascript

I see that you can't use normal masking strategies because you need to be able to see the lower layer. May I suggest SVG's more complete clipping suite? See here.

The browser compatibility is good but not great, but you can absolutely accomplish what you're trying here (unless you need to pan/zoom the Map, then you're screwed until Maps implements such a thing).

Solution 4 - Javascript

You can use the canvas.toDataURI() option in HTML5 to obtain the url that is required for getTileUrl() of ImageMapType.

getTileUrl: function(coord, zoom) {
	var normalizedCoord = getNormalizedCoord(coord, zoom);
	if (!normalizedCoord) {
	  return null;
	}
	var bound = Math.pow(2, zoom);
	// reset and clip the preloaded image in a hidden canvas to your required height and width
	
	clippedImage = canvas.toDataURL();
	return clippedImage;
    }

Solution 5 - Javascript

You could use an svg clippath, together with the foreignobject svg tag to put a html document within the svg then clip it to the desired shape like this code taken from codepen.io/yoksel/pen/oggRwR:

@import url(http://fonts.googleapis.com/css?family=Arvo:700);

.svg {
  display: block;
  width: 853px;
  height: 480px;
  margin: 2em auto;
	}

text {
  font: bold 5.3em/1 Arvo, Arial sans-serif;
	}

<svg class="svg">
    <clippath id="cp-circle">
      <circle r="180" cx="50%" cy="42%"></circle>
      <text
            text-anchor="middle"
            x="50%"
            y="98%"
            >Soldier Of Fortune</text>
    </clippath>
  
    <g clip-path="url(#cp-circle)">   
   	<foreignObject width="853" x="0"
                y="0" height="480">
      <body xmlns="http://www.w3.org/1999/xhtml">
        <iframe width="853" height="480" src="//www.youtube.com/embed/RKrNdxiBW3Y" frameborder="0" allowfullscreen></iframe>
      </body>
    </foreignObject>
   </g>	
</svg>

http://codepen.io/yoksel/pen/oggRwR

Solution 6 - Javascript

You could place a DIV above your map, with absolute positioning and high z-index. then, apply a polygon mask to that DIV like this: -webkit-clip-path: polygon(0 0, 0 100%, 100% 0);

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
QuestionNicolasView Question on Stackoverflow
Solution 1 - JavascriptGokhan KurtView Answer on Stackoverflow
Solution 2 - JavascriptDanny HoekView Answer on Stackoverflow
Solution 3 - JavascriptReed SpoolView Answer on Stackoverflow
Solution 4 - JavascriptcnvzmxcvmcxView Answer on Stackoverflow
Solution 5 - JavascriptPaul HumphreysView Answer on Stackoverflow
Solution 6 - JavascriptManuel AlejandroView Answer on Stackoverflow