Clip Google Maps JS API ImageMapType to a polygon
Solution 1:
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:
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:
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;
}
- To set and resize the image to correct dimension, use
canvas.drawImage()
- To clip the image from canvas to any non-rectangular dimension, use the
canvas clip()
Sample code for canvas clipping.
Solution 4:
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 5:
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