Visualizing unlimited Cloud Optimized GeoTiffs(COG) with single FlatGeobuf(FGB)
Partial set of CONUS 30m DEM's, 514 Cloud Optimized GeoTiffs, sourced from FGB file, zoom level 8+
...or just STAC killer. That's what I've been calling it.
So, here it is. Let's create an FGB file with unlimited features. Each feature has 2 attributes:
We do this with our old friend, gdaltindex. We pass a whole slew of COG's and it returns them as an FGB:
gdaltindex -lyr_name dem30 -tileindex file dem30m.fgb /path/to/cogs/*.tif
This leads to a very interesting advantage. FGB is a cloud native or streaming format. Using the built in indexing we can get byte ranges of data out of the file, without reading the entire file, whether it's on a web server or cheap cloud storage like S3. This doesn't require any intermediate servers or services. No API's, just a web browser and the data. Using our highly efficient streaming format, we can get a bounding box of just the COG's we need, from an otherwise unlimited number of COG's!
See where I'm going with this?
By specifying a bounding box, we can get a list of COG's that intersect that bounding box. In the example, that bounding box is a web browser view port that's running LeafletJS. I'm also using Georaster-for-leaflet and flatgeobuf-geojson with Leaflet. This also works with Open Layers
Time to throw a bit of code at you. The following is the core piece of JavaScript that runs in your browser each time you pan/move/zoom your map:
// get browser viewport bbox, spatial extent let b = o.map.getBounds(); // read only the features intersecting the bbox from the FGB file for await (let feature of flatgeobuf.deserialize(o.opts.fgbUrl, {minX: b.getWest() ,maxX: b.getEast() ,minY: b.getSouth() ,maxY: b.getNorth()})) { // skip COG if already loaded if(feature.properties.file in o.loadedCogs) continue; // add COG layer to map parseGeoraster(o.opts.cogUrl + feature.properties.file, {readOnDemand: true}).then(georaster => { o.georasterOpts.georaster = georaster; o.loadedCogs[feature.properties.file] = new GeoRasterLayer(o.georasterOpts); this.loadedCogs[feature.properties.file].addTo(o.map); }); }
That's the basic premise. But so much more can be done. Here's a feature from our FGB file:
OGRFeature(dem30):1 file (String) = n30w082.tif POLYGON ((-82.00166666667 30.0016666666641,-80.9983333333359 30.0016666666641,-80.9983333333359 28.99833333333,-82.00166666667 28.99833333333,-82.00166666667 30.0016666666641))
The feature has only 2 attributes. That's all we need to make it work. So much more information can be added, such as meta data. Perhaps we added some pre-calculated data as well. A feature may look something like this:
OGRFeature(dem30):1 file (String) = n30w082.tif group (String) = CONUS desc (String) = 30 meter raster minElev (real) = 123.456 maxElev (real) = 654.321 avgElev (real) = 777.777 POLYGON ((-82.00166666667 30.0016666666641,-80.9983333333359 30.0016666666641,-80.9983333333359 28.99833333333,-82.00166666667 28.99833333333,-82.00166666667 30.0016666666641))
Once we add more attributes to the feature, we are able to filter in a whole new way. Initially, we want to get our COG's within a specific area. With the bounding box we've done that. In the browser using JavaScript, we can filter by any attribute. On the server side, being that this is just OGR, we can use SQL queries in our GDAL ogr* commands. Further, you could add JSON with any free from type of data as an attribute.
Further, we have put only COG's in the FGB. There's no reason we couldn't put other file types that have a known spatial extent. JSON, CSV, GPKG, whatever. Don't expect those formats to perform like COG. As long as your browser JavaScript has a handler for the format, it will work.
If you like the structure that STAC brings, but don't want to interact with an API, the strict structure or the complexity, this might be an alternative.
Apr 10 2024 - Scott Parks