58

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.

Nicolas
  • 2,297
  • 3
  • 28
  • 40
  • 1
    One way could be to create a custom ImageMapType that covers the whole world, but is transparent everywhere except the area you want, and apply it as an [overlay map type](https://developers.google.com/maps/documentation/javascript/maptypes?hl=fr#OverlayMapTypes) – Gabriele Petrioli Jun 28 '14 at 12:33
  • 1
    Like this? [maptype-image-overlay](https://developers.google.com/maps/documentation/javascript/examples/maptype-image-overlay) – Jonas Jun 28 '14 at 20:35
  • 1
    @GabyakaG.Petrioli, for that you would need access to modify the tiles, so it is not a client side solution. – Nicolas Jun 29 '14 at 05:12
  • 1
    @JonasHartmann that example looks like what I need, but, again, it considers server side clipping of the selected area – Nicolas Jun 29 '14 at 05:13
  • @Nicolas the only server side part of it that I see is where they store the image. But you cannot avoid storing an image in a server. – Jonas Jun 29 '14 at 10:02
  • 2
    @Nikolas Is maybe [this](http://www.gewiss-saarland.de/) or [this](http://gis.stackexchange.com/questions/18867/is-it-possible-to-gray-out-a-google-maps-map-except-for-some-area) what you're looking for? Furthermore, you can clip an image on client-side using something like [this](http://www.useragentman.com/blog/2011/10/29/clipping-jpeg-images-into-non-rectangular-polygons-using-polyclip-js/) but I'm not sure if it is a solution or possible in this situation. Finally, maybe you could create a mask to polygon using [this](https://github.com/adammck/gmaps-v3-mask) – hex494D49 Jun 29 '14 at 10:04
  • I just tried the last solution and I see that masking would't be a problem. Check [this jsbin](http://jsbin.com/sacazibi/1/) – hex494D49 Jun 29 '14 at 10:12
  • @JonasHartmann of course you cannot avoid storing the image on a server. The thing is that I don't have access to modifying the image on the server, so it has to be clipped on the client side... – Nicolas Jun 29 '14 at 16:25
  • @hex494D49 thank you for your solution. The problem is that I can't see how you could show a base map with that approach. What you seem to be doing is covering everything outside the polygon with a mask, and that necessarily will cover the base map... – Nicolas Jun 29 '14 at 16:26
  • I guess this question is old...but if @JonasHartmann solution is what you need/needed you could try converting the overlay image to a data URI and using that on the client side: - https://css-tricks.com/data-uris/ – Ben Jul 17 '15 at 14:34
  • Hey Ben. I don't see how that would work; why would using a data URI instead of a normal help with the clipping? BTW, the official answer from google is that this it no currently possible https://code.google.com/p/gmaps-api-issues/issues/detail?id=8162 – Nicolas Jul 19 '15 at 23:06

6 Answers6

13

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.

Gokhan Kurt
  • 8,239
  • 1
  • 27
  • 51
  • Are you able to update Fiddle to work again? I need the same thing on my Angular Google Maps. User draw a polygon and I wont to inject tile image which I'm getting from weather API. – Wolf87 Jun 02 '20 at 17:50
2

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.)

Emzor
  • 1,380
  • 17
  • 28
Danny Hoek
  • 420
  • 4
  • 14
  • Do you know of a way of doing it in OpenLayers that would not involve WebGL? – Nicolas May 04 '16 at 17:06
  • When you instantiate a new OpenLayers map you can set the preferred renderer in the options object you pass in ol.Map. Just set renderer: 'canvas' – Danny Hoek May 11 '16 at 09:40
  • Right, but the clipping method used in this example would break down (as it is relies heavily on WebGL), wouldn't it? That is my point. – Nicolas May 12 '16 at 20:14
  • Ah, yeah you're totally right. If you can't use WebGL I'd go for a custom tileset which covers just the area you need. MapTiler would be the way to go if you have a high-res version of your map. – Danny Hoek May 13 '16 at 12:51
1

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).

Reed Spool
  • 843
  • 7
  • 15
1

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;
    }
Community
  • 1
  • 1
cnvzmxcvmcx
  • 1,061
  • 2
  • 15
  • 32
1

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

Paul Humphreys
  • 338
  • 2
  • 9
0

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

Manuel Alejandro
  • 588
  • 4
  • 13