WMTS Overzoom (Google Maps JS API)
HTML
<script
async
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=weekly&callback=initMap"
></script>
<div id="map"></div>
CSS
html,
body,
#map {
height: 100%;
margin: 0;
padding: 0;
}
.credit {
position: absolute;
z-index: 5;
left: 8px;
bottom: 8px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
padding: 6px 8px;
border-radius: 6px;
font:
12px/1.2 system-ui,
-apple-system,
Segoe UI,
Roboto,
sans-serif;
}
.credit a {
color: #9bd;
text-decoration: none;
}
JavaScript
const MAX_NATIVE_ZOOM = 19; // change if your layer supports more
// Build a WMTS URL for a given z/x/y (XYZ in Google Maps tile space)
function wmtsUrl(zoom, x, y) {
return `https://data.geopf.fr/wmts?layer=HR.ORTHOIMAGERY.ORTHOPHOTOS&style=normal&tilematrixset=PM&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fjpeg&TileMatrix=${zoom}&TileCol=${x}&TileRow=${y}`;
}
// Normalize tile coords to the world at a given zoom:
// - wrap X horizontally (world repeats)
// - clamp Y vertically (outside world becomes null)
function normalizeCoord(coord, zoom) {
const tileRange = 1 << zoom; // 2^zoom
const x = ((coord.x % tileRange) + tileRange) % tileRange;
const y = coord.y;
if (y < 0 || y >= tileRange) return null;
return { x, y };
}
class OverzoomWMTSMapType {
constructor() {
this.tileSize = new google.maps.Size(256, 256);
this.maxZoom = 22; // allow users to zoom further than native (even made it possible to zoom past 22)
this.minZoom = 0;
this.name = "Orthophotos";
this.alt = "WMTS Orthophotos with overzoom";
}
getTile(coord, zoom, ownerDocument) {
const n = normalizeCoord(coord, zoom);
if (!n) return null;
const div = ownerDocument.createElement("div");
div.style.width = "256px";
div.style.height = "256px";
div.style.backgroundRepeat = "no-repeat";
// Optional: keep pixels crisp when scaling up tiles
div.style.imageRendering = "pixelated";
if (zoom <= MAX_NATIVE_ZOOM) {
// Normal fetch at this zoom
const url = wmtsUrl(zoom, n.x, n.y);
div.style.backgroundImage = `url("${url}")`;
return div;
}
// Overzoom: use parent tile(s) at MAX_NATIVE_ZOOM, crop & scale
const dz = zoom - MAX_NATIVE_ZOOM;
const scale = 1 << dz; // 2^dz
const parentX = Math.floor(n.x / scale);
const parentY = Math.floor(n.y / scale);
const subX = n.x % scale; // child offset within parent
const subY = n.y %...