georaster icon indicating copy to clipboard operation
georaster copied to clipboard

Support for loading cloud optimized geotiff (COG) onto google maps without leaflet

Open abdul-imran opened this issue 1 year ago • 11 comments

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?

abdul-imran avatar Sep 27 '24 12:09 abdul-imran

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

DanielJDufour avatar Sep 28 '24 13:09 DanielJDufour

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.

DanielJDufour avatar Sep 28 '24 13:09 DanielJDufour

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.

abdul-imran avatar Sep 30 '24 08:09 abdul-imran

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.

abdul-imran avatar Oct 01 '24 09:10 abdul-imran

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>

ashley-mort avatar Oct 06 '24 00:10 ashley-mort

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.

abdul-imran avatar Oct 08 '24 11:10 abdul-imran

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>

abdul-imran avatar Oct 08 '24 11:10 abdul-imran

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

ashley-mort avatar Oct 08 '24 14:10 ashley-mort

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.

abdul-imran avatar Oct 08 '24 15:10 abdul-imran

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>

abdul-imran avatar Oct 10 '24 13:10 abdul-imran

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

ashley-mort avatar Oct 11 '24 22:10 ashley-mort