georaster
georaster copied to clipboard
Support for loading cloud optimized geotiff (COG) onto google maps without leaflet
I am trying to load a COG using Google Maps JavaScript API. I have tried this using georaster-layer-for-leaflet but due to google terms & conditions, I'd like to achieve this using Google Maps JavaScript API. I have generated COG using GDal & can see CRS=WGS 84, Layout=COG & Compression=LZW. Is it possible to achieve this functionality? Can you please help?
Hi, @abdul-imran . I would review the code on the refactor-2023-winter branch https://github.com/GeoTIFF/georaster-layer-for-leaflet/pull/138/files. Yes, it's definitely possible, but will require porting georaster-layer-for-leaflet to georaster-layer-for-google-maps (ie writing a bunch of code).
Unfortunately, I don't have the time or resources to do this. Fun fact, because you converted your data to WGS 84 you won't have to reproject/warp/rotate your data. All you have to do is pull the values for the tile area and display the as an img tag.
This library may also be of interest as it helps pull data from georasters: https://github.com/geotiff/georaster-stack
If you want to optimize things even further, I would look at converting your geotiffs to web mercator and converting to Cloud optimize geotiffs where each internal tile aligns with a map web tile.
Hi @DanielJDufour Thank you for your reply & assistance. What we actually tried was using a WEBP compressed COG in Google Maps. But couldn't get it working using georaster-layer-for-leaflet. So tried using another library "cogeotiff". We could get the tiles loaded, but its very slow. Hence, would love to get it working using the georaster library. We will try & see what we could do from our end. Thanks again.
Hey @DanielJDufour - I looked the georaster-stack & found something very useful for me, the toCanvas method. But the issue I've got is, I still can't render the layer onto Google Maps.
Please note, I've already successfully implemented this using Leaflet, Google Mutant & COG's. But struggling with plain google maps. Can you please have a look when you find some time ? Thank you.
I've attached the working leaflet code & the google-maps code. You'll only need a ALLOW-CORS extension to run this. If you need, I can send out the COG too. It might be a very simpler issue for you :) cog-canvas.zip
In the leaflet example, I've commented out different COG URL's with compressions like Deflate, LZW, Jpeg & WebP.
This will get you part of the way there:
<!DOCTYPE html>
<html>
<head>
<title>COG on Google Maps</title>
<style>
#map {
height: 100%;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<!-- Add Google Maps script with your API key -->
<script defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBXqGueu-KXcNwHvmtiC1WmTN7e1o-8Hzc&callback=initMap"></script>
<!-- GeoRaster Library -->
<script src="https://unpkg.com/georaster"></script>
<!-- Proj4js to convert coordinates -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.7.5/proj4.js"></script>
<script>
let map;
function initMap() {
//So we can project image (3857) to Google Maps WGS84
proj4.defs("EPSG:3857", "+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs");
proj4.defs("EPSG:4326", "+proj=longlat +datum=WGS84 +no_defs");
// Initialize Google Maps
map = new google.maps.Map(document.getElementById('map'), {
center: { lat: 51.5072, lng: -0.1276 }, // Center on San Francisco (or customize)
zoom: 12
});
// Load the GeoTIFF (COG) using GeoRaster
fetch('https://cog-imran.s3.us-west-1.amazonaws.com/lzw_cog_5G_3400_2100_0700_05Sep2024.tif')
.then(response => response.arrayBuffer())
.then(parseGeoraster)
.then(georaster => {
const canvas = georaster.toCanvas({ height: georaster.height, width: georaster.width });
console.log('after to canvas ');
// Log the data URL
const dataUrl = canvas.toDataURL();
//console.log('Data URL:', dataUrl);
// Log the bounds
console.log('Bounds:', {
northEast: { lat: georaster.ymax, lng: georaster.xmax },
southWest: { lat: georaster.ymin, lng: georaster.xmin }
});
const upperLeft = proj4("EPSG:3857", "EPSG:4326", [georaster.xmin, georaster.ymax]);
const lowerRight = proj4("EPSG:3857", "EPSG:4326", [georaster.xmax, georaster.ymin]);
const myImageBounds = {
north: upperLeft[1],
south: lowerRight[1],
east: lowerRight[0],
west: upperLeft[0]
};
// Convert canvas to an image and overlay on Google Maps
const imageOverlay = new google.maps.GroundOverlay(
dataUrl,
myImageBounds
);
console.log('imageOverlay: ', imageOverlay);
// Set the image overlay on the map
imageOverlay.setMap(map);
console.log('Overlay set on map:', imageOverlay);
})
.catch(err => {
console.error('Error loading GeoTIFF:', err);
});
}
</script>
</body>
</html>
Hey @ashley-mort - Thanks a lot for this. This is a lot of improvement to what I had. I can see the cog loaded on the map now, but its 1 big request which fetches the entire cog. This is really slow & I would like to get the range requests working. That would speed up the process I think. I need to find out how to do it using georaster though.
Hi @ashley-mort - I tried using the parseGeoraster method which takes in the cog as an input. but there are other issues now.
Error loading GeoTIFF: TypeError: Failed to execute 'putImageData' on 'CanvasRenderingContext2D': parameter 1 is not of type 'ImageData'.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>COG on Google Maps with Range Requests</title>
<style>
#map {
height: 100vh;
width: 100%;
}
</style>
<!-- Add Google Maps script with your API key -->
<script defer
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBXqGueu-KXcNwHvmtiC1WmTN7e1o-8Hzc&callback=initMap&loading=async"></script>
<!-- GeoRaster Library -->
<script src="https://unpkg.com/georaster"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.7.5/proj4.js"></script>
</head>
<body>
<div id="map"></div>
<canvas id="georasterCanvas" style="display:none;"></canvas>
<script>
let map;
function initMap() {
//So we can project image (3857) to Google Maps WGS84
proj4.defs("EPSG:3857", "+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs");
proj4.defs("EPSG:4326", "+proj=longlat +datum=WGS84 +no_defs");
// Initialize the map
const map = new google.maps.Map(document.getElementById("map"), {
zoom: 12,
center: { lat: 51.507383, lng: -0.127666 }
});
// Load the GeoTIFF (COG) using GeoRaster
parseGeoraster('https://cog-imran.s3.us-west-1.amazonaws.com/lzw_cog_5G_3400_2100_0700_05Sep2024.tif')
.then(georaster => {
const canvas = georaster.toCanvas({ height: georaster.height, width: georaster.width });
console.log('after to canvas ');
// Log the data URL
const dataUrl = canvas.toDataURL();
//console.log('Data URL:', dataUrl);
// Log the bounds
console.log('Bounds:', {
northEast: { lat: georaster.ymax, lng: georaster.xmax },
southWest: { lat: georaster.ymin, lng: georaster.xmin }
});
const upperLeft = proj4("EPSG:3857", "EPSG:4326", [georaster.xmin, georaster.ymax]);
const lowerRight = proj4("EPSG:3857", "EPSG:4326", [georaster.xmax, georaster.ymin]);
const myImageBounds = {
north: upperLeft[1],
south: lowerRight[1],
east: lowerRight[0],
west: upperLeft[0]
};
// Convert canvas to an image and overlay on Google Maps
const imageOverlay = new google.maps.GroundOverlay(
dataUrl,
myImageBounds
);
console.log('imageOverlay: ', imageOverlay);
// Set the image overlay on the map
imageOverlay.setMap(map);
console.log('Overlay set on map:', imageOverlay);
})
.catch(err => {
console.error('Error loading GeoTIFF:', err);
});
}
</script>
</body>
</html>
It's probably because your georaster.values is null. I would build georaster yourself so you can debug through it without dealing with the minified code from unpkg. For example, see https://github.com/GeoTIFF/georaster-to-canvas/blob/master/index.js
Personally, I would try to use something like OpenLayers to do what you're trying to do. That integrates Google Maps and has COG support (via geotiff.js). I think you're going to find fully implementing what you want with georaster may not be as simple as you'd hope.
I don't have any direct experience with georaster, only geotiff.js & OpenLayers. https://openlayers.org/en/latest/examples/google.html https://openlayers.org/en/latest/examples/cog.html
Thank you @ashley-mort . I already have a working solution using Leaflet & Google mutant plugin. We are just trying to avoid any issues when it comes to google T&C's. I'll have a look at the links you've provided.
Hey @ashley-mort I saw you mentioned about geotiff.js. I've been trying to do the same using this library. But I can see the requested tile range is not within the actual tile size :)
Anything you can do to help here?
<!DOCTYPE html>
<html>
<head>
<title>Load COG on Google Maps</title>
<style>
#map {
height: 100vh;
width: 100%;
}
</style>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBXqGueu-KXcNwHvmtiC1WmTN7e1o-8Hzc&loading=async"></script>
<script src="https://unpkg.com/[email protected]/dist-browser/geotiff.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.7.5/proj4.js"></script>
</head>
<body>
<div id="map"></div>
<script>
let map;
const tileSize = 256; // Size of each tile in pixels
const cogUrl = 'https://cog-imran.s3.us-west-1.amazonaws.com/lzw_cog_5G_3400_2100_0700_05Sep2024.tif';
function initMap() {
proj4.defs("EPSG:3857", "+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs");
proj4.defs("EPSG:4326", "+proj=longlat +datum=WGS84 +no_defs");
map = new google.maps.Map(document.getElementById("map"), {
center: { lat: 51.507383, lng: -0.127666 },
zoom: 5
});
// Load COG metadata using HEAD request
fetch(cogUrl, { method: 'HEAD' })
.then(response => {
if (!response.ok) {
throw new Error("Failed to load COG metadata");
}
return response;
})
.then(() => {
// Load tiles after map is idle
//google.maps.event.addListenerOnce(map, 'idle', loadVisibleTiles);
//map.addListener('bounds_changed', loadVisibleTiles);
loadVisibleTiles();
map.addListener('bounds_changed', loadVisibleTiles);
});
}
function loadVisibleTiles() {
const bounds = map.getBounds();
const sw = bounds.getSouthWest();
const ne = bounds.getNorthEast();
const visibleTiles = calculateVisibleTiles(sw, ne, map.getZoom());
visibleTiles.forEach(tile => {
fetchTileData(tile.x, tile.y);
});
}
function calculateVisibleTiles(sw, ne, zoom) {
const tileCountX = Math.pow(2, zoom);
const tileCountY = Math.pow(2, zoom);
const tileStartX = Math.floor(((sw.lng() + 180) / 360) * tileCountX);
const tileEndX = Math.ceil(((ne.lng() + 180) / 360) * tileCountX);
const tileStartY = Math.floor(((1 - Math.log(Math.tan(sw.lat() * Math.PI / 180) + 1 / Math.cos(sw.lat() * Math.PI / 180)) / Math.PI) / 2) * tileCountY);
const tileEndY = Math.ceil(((1 - Math.log(Math.tan(ne.lat() * Math.PI / 180) + 1 / Math.cos(ne.lat() * Math.PI / 180)) / Math.PI) / 2) * tileCountY);
const tiles = [];
for (let x = tileStartX; x <= tileEndX; x++) {
for (let y = tileStartY; y <= tileEndY; y++) {
tiles.push({ x, y });
}
}
return tiles;
}
async function fetchTileData(x, y) {
const byteRange = calculateByteRange(x, y);
console.log('byteRange: ', byteRange);
const response = await fetch(cogUrl, {
method: 'GET',
headers: {
'Range': `bytes=${byteRange.start}-${byteRange.end}`
}
});
if (!response.ok) {
console.error(`Failed to fetch tile ${x}, ${y}:`, response.status);
return;
}
const arrayBuffer = await response.arrayBuffer();
const tiff = await GeoTIFF.fromArrayBuffer(arrayBuffer);
const image = await tiff.getImage();
renderTile(image, x, y);
}
function calculateByteRange(x, y) {
const tileSizeInBytes = tileSize * tileSize * 4; // 4 bytes per pixel (RGBA)
const start = (y * Math.ceil(tileSizeInBytes / 256) + x) * tileSizeInBytes;
const end = start + tileSizeInBytes - 1;
return { start, end };
}
function renderTile(image, x, y) {
const canvas = document.createElement('canvas');
canvas.width = tileSize;
canvas.height = tileSize;
const ctx = canvas.getContext('2d');
const raster = image.readRasters();
const data = ctx.createImageData(tileSize, tileSize);
for (let i = 0; i < raster[0].length; i++) {
data.data[i * 4] = raster[0][i]; // Red
data.data[i * 4 + 1] = raster[1][i]; // Green
data.data[i * 4 + 2] = raster[2][i]; // Blue
data.data[i * 4 + 3] = 255; // Alpha
}
ctx.putImageData(data, 0, 0);
const dataUrl = canvas.toDataURL();
const bounds = map.getBounds();
const tileSizeInDegrees = 360 / Math.pow(2, map.getZoom());
const myImageBounds = {
north: bounds.getNorthEast().lat(),
south: bounds.getSouthWest().lat(),
east: bounds.getNorthEast().lng(),
west: bounds.getSouthWest().lng()
};
const imageOverlay = new google.maps.GroundOverlay(dataUrl, myImageBounds);
imageOverlay.setMap(map);
}
window.onload = initMap;
</script>
</body>
</html>
I would take a look at the cog-explorer project as it's doing basically what you are trying to do. See if you can run it and debug through this file (mapview.jsx) specifically. That will show you all the things you need to consider and handle.
https://github.com/geotiffjs/cog-explorer/blob/master/src/components/mapview.jsx