Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
abdul-imran opened this issue Sep 27, 2024 · 11 comments

Comments

@abdul-imran
Copy link

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?

@DanielJDufour
Copy link
Member

DanielJDufour commented Sep 28, 2024

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
Copy link
Member

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.

@abdul-imran
Copy link
Author

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
Copy link
Author

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.

@ashley-mort
Copy link

ashley-mort commented Oct 6, 2024

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>

@abdul-imran
Copy link
Author

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
Copy link
Author

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>

@ashley-mort
Copy link

ashley-mort commented Oct 8, 2024

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

@abdul-imran
Copy link
Author

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
Copy link
Author

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>

@ashley-mort
Copy link

ashley-mort commented Oct 11, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants