-
Notifications
You must be signed in to change notification settings - Fork 302
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
feature: Add Cloud Optimized GeoTIFF (COG) sample (#2250)
* feature: Add Cloud Optimized GeoTIFF (COG) sample * fix(cog): Fix geotiff link * refactor(COG): Apply code review * fix(COG): Use readRGB instead readRasters --------- Co-authored-by: Kevin ETOURNEAU <kevin.etourneau@sogelink.com>
1 parent
9761d58
commit f707e26
Showing
5 changed files
with
436 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
/* global itowns, THREE */ | ||
|
||
/** | ||
* @typedef {Object} GeoTIFFLevel | ||
* @property {GeoTIFFImage} image | ||
* @property {number} width | ||
* @property {number} height | ||
* @property {number[]} resolution | ||
*/ | ||
|
||
/** | ||
* Select the best overview level (or the final image) to match the | ||
* requested extent and pixel width and height. | ||
* | ||
* @param {Object} source The COGSource | ||
* @param {Extent} source.extent Source extent | ||
* @param {GeoTIFFLevel[]} source.levels | ||
* @param {THREE.Vector2} source.dimensions | ||
* @param {Extent} requestExtent The node extent. | ||
* @param {number} requestWidth The pixel width of the window. | ||
* @param {number} requestHeight The pixel height of the window. | ||
* @returns {GeoTIFFLevel} The selected zoom level. | ||
*/ | ||
function selectLevel(source, requestExtent, requestWidth, requestHeight) { | ||
// Number of images = original + overviews if any | ||
const cropped = requestExtent.clone().intersect(source.extent); | ||
// Dimensions of the requested extent | ||
const extentDimension = cropped.planarDimensions(); | ||
|
||
const targetResolution = Math.min( | ||
extentDimension.x / requestWidth, | ||
extentDimension.y / requestHeight, | ||
); | ||
|
||
let level; | ||
|
||
// Select the image with the best resolution for our needs | ||
for (let index = source.levels.length - 1; index >= 0; index--) { | ||
level = source.levels[index]; | ||
const sourceResolution = Math.min( | ||
source.dimensions.x / level.width, | ||
source.dimensions.y / level.height, | ||
); | ||
|
||
if (targetResolution >= sourceResolution) { | ||
break; | ||
} | ||
} | ||
|
||
return level; | ||
} | ||
|
||
/** | ||
* Returns a window in the image's coordinates that matches the requested extent. | ||
* | ||
* @param {Object} source The COGSource | ||
* @param {number[]} source.origin Root image origin as an XYZ-vector | ||
* @param {Extent} extent The window extent. | ||
* @param {number[]} resolution The spatial resolution of the window. | ||
* @returns {number[]} The window. | ||
*/ | ||
function makeWindowFromExtent(source, extent, resolution) { | ||
const [oX, oY] = source.origin; | ||
const [imageResX, imageResY] = resolution; | ||
|
||
const wnd = [ | ||
Math.round((extent.west - oX) / imageResX), | ||
Math.round((extent.north - oY) / imageResY), | ||
Math.round((extent.east - oX) / imageResX), | ||
Math.round((extent.south - oY) / imageResY), | ||
]; | ||
|
||
const xMin = Math.min(wnd[0], wnd[2]); | ||
let xMax = Math.max(wnd[0], wnd[2]); | ||
const yMin = Math.min(wnd[1], wnd[3]); | ||
let yMax = Math.max(wnd[1], wnd[3]); | ||
|
||
// prevent zero-sized requests | ||
if (Math.abs(xMax - xMin) === 0) { | ||
xMax += 1; | ||
} | ||
if (Math.abs(yMax - yMin) === 0) { | ||
yMax += 1; | ||
} | ||
|
||
return [xMin, yMin, xMax, yMax]; | ||
} | ||
|
||
/** | ||
* Creates a texture from the pixel buffer(s). | ||
* | ||
* @param {Object} source The COGSource | ||
* @param {THREE.TypedArray | THREE.TypedArray[]} buffers The buffers (one buffer per band) | ||
* @param {number} buffers.width | ||
* @param {number} buffers.height | ||
* @param {number} buffers.byteLength | ||
* @returns {THREE.DataTexture} The generated texture. | ||
*/ | ||
function createTexture(source, buffers) { | ||
const { width, height, byteLength } = buffers; | ||
const pixelCount = width * height; | ||
const targetDataType = source.dataType; | ||
const format = THREE.RGBAFormat; | ||
const channelCount = 4; | ||
let texture; | ||
let data; | ||
|
||
// Check if it's a RGBA buffer | ||
if (pixelCount * channelCount === byteLength) { | ||
data = buffers; | ||
} | ||
|
||
switch (targetDataType) { | ||
case THREE.UnsignedByteType: { | ||
if (!data) { | ||
// We convert RGB buffer to RGBA | ||
const newBuffers = new Uint8ClampedArray(pixelCount * channelCount); | ||
data = convertToRGBA(buffers, newBuffers, source.defaultAlpha); | ||
} | ||
texture = new THREE.DataTexture(data, width, height, format, THREE.UnsignedByteType); | ||
break; | ||
} | ||
case THREE.FloatType: { | ||
if (!data) { | ||
// We convert RGB buffer to RGBA | ||
const newBuffers = new Float32Array(pixelCount * channelCount); | ||
data = convertToRGBA(buffers, newBuffers, source.defaultAlpha / 255); | ||
} | ||
texture = new THREE.DataTexture(data, width, height, format, THREE.FloatType); | ||
break; | ||
} | ||
default: | ||
throw new Error('unsupported data type'); | ||
} | ||
|
||
return texture; | ||
} | ||
|
||
function convertToRGBA(buffers, newBuffers, defaultAlpha) { | ||
const { width, height } = buffers; | ||
|
||
for (let i = 0; i < width * height; i++) { | ||
const oldIndex = i * 3; | ||
const index = i * 4; | ||
// Copy RGB from original buffer | ||
newBuffers[index + 0] = buffers[oldIndex + 0]; // R | ||
newBuffers[index + 1] = buffers[oldIndex + 1]; // G | ||
newBuffers[index + 2] = buffers[oldIndex + 2]; // B | ||
// Add alpha to new buffer | ||
newBuffers[index + 3] = defaultAlpha; // A | ||
} | ||
|
||
return newBuffers; | ||
} | ||
|
||
/** | ||
* The COGParser module provides a [parse]{@link module:COGParser.parse} | ||
* method that takes a COG in and gives a `THREE.DataTexture` that can be | ||
* displayed in the view. | ||
* | ||
* It needs the [geotiff](https://github.com/geotiffjs/geotiff.js/) library to parse the | ||
* COG. | ||
* | ||
* @example | ||
* GeoTIFF.fromUrl('http://image.tif') | ||
* .then(COGParser.parse) | ||
* .then(function _(texture) { | ||
* var source = new itowns.FileSource({ features: texture }); | ||
* var layer = new itowns.ColorLayer('cog', { source }); | ||
* view.addLayer(layer); | ||
* }); | ||
* | ||
* @module COGParser | ||
*/ | ||
const COGParser = (function _() { | ||
if (typeof THREE == 'undefined' && itowns.THREE) { | ||
// eslint-disable-next-line no-global-assign | ||
THREE = itowns.THREE; | ||
} | ||
|
||
return { | ||
/** | ||
* Parse a COG file and return a `THREE.DataTexture`. | ||
* | ||
* @param {Object} data Data passed with the Tile extent | ||
* @param {Extent} data.extent | ||
* @param {Object} options Options (contains source) | ||
* @param {Object} options.in | ||
* @param {COGSource} options.in.source | ||
* @param {number} options.in.tileWidth | ||
* @param {number} options.in.tileHeight | ||
* @return {Promise<THREE.DataTexture>} A promise resolving with a `THREE.DataTexture`. | ||
* | ||
* @memberof module:COGParser | ||
*/ | ||
parse: async function _(data, options) { | ||
const source = options.in; | ||
const nodeExtent = data.extent.as(source.crs); | ||
const level = selectLevel(source, nodeExtent, source.tileWidth, source.tileHeight); | ||
const viewport = makeWindowFromExtent(source, nodeExtent, level.resolution); | ||
|
||
const buffers = await level.image.readRGB({ | ||
window: viewport, | ||
pool: source.pool, | ||
enableAlpha: true, | ||
interleave: true, | ||
}); | ||
|
||
const texture = createTexture(source, buffers); | ||
texture.flipY = true; | ||
texture.extent = data.extent; | ||
texture.needsUpdate = true; | ||
texture.magFilter = THREE.LinearFilter; | ||
texture.minFilter = THREE.LinearFilter; | ||
|
||
return Promise.resolve(texture); | ||
}, | ||
}; | ||
}()); | ||
|
||
if (typeof module != 'undefined' && module.exports) { | ||
module.exports = COGParser; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
/* global itowns, GeoTIFF, COGParser, THREE */ | ||
|
||
/** | ||
* @classdesc | ||
* An object defining the source of resources to get from a [COG]{@link | ||
* https://www.cogeo.org/} file. It | ||
* inherits from {@link Source}. | ||
* | ||
* @extends Source | ||
* | ||
* @property {Object} zoom - Object containing the minimum and maximum values of | ||
* the level, to zoom in the source. | ||
* @property {number} zoom.min - The minimum level of the source. Default value is 0. | ||
* @property {number} zoom.max - The maximum level of the source. Default value is Infinity. | ||
* @property {string} url - The URL of the COG. | ||
* @property {GeoTIFF.Pool} pool - Pool use to decode GeoTiff. | ||
* @property {number} defaultAlpha - Alpha byte value used if no alpha is present in COG. Default value is 255. | ||
* | ||
* @example | ||
* // Create the source | ||
* const cogSource = new itowns.COGSource({ | ||
* url: 'https://cdn.jsdelivr.net/gh/iTowns/iTowns2-sample-data/cog/orvault.tif', | ||
* }); | ||
* | ||
* // Create the layer | ||
* const colorLayer = new itowns.ColorLayer('COG', { | ||
* source: cogSource, | ||
* }); | ||
* | ||
* // Add the layer | ||
* view.addLayer(colorLayer); | ||
*/ | ||
class COGSource extends itowns.Source { | ||
/** | ||
* @param {Object} source - An object that can contain all properties of a | ||
* COGSource and {@link Source}. Only `url` is mandatory. | ||
* @constructor | ||
*/ | ||
constructor(source) { | ||
super(source); | ||
|
||
if (source.zoom) { | ||
this.zoom = source.zoom; | ||
} else { | ||
this.zoom = { min: 0, max: Infinity }; | ||
} | ||
|
||
this.url = source.url; | ||
this.pool = source.pool || new GeoTIFF.Pool(); | ||
// We don't use fetcher, we let geotiff.js manage it | ||
this.fetcher = () => Promise.resolve({}); | ||
this.parser = COGParser.parse; | ||
|
||
this.defaultAlpha = source.defaultAlpha || 255; | ||
|
||
this.whenReady = GeoTIFF.fromUrl(this.url) | ||
.then(async (geotiff) => { | ||
this.geotiff = geotiff; | ||
this.firstImage = await geotiff.getImage(); | ||
this.origin = this.firstImage.getOrigin(); | ||
this.dataType = this.selectDataType(this.firstImage.getSampleFormat(), this.firstImage.getBitsPerSample()); | ||
|
||
this.tileWidth = this.firstImage.getTileWidth(); | ||
this.tileHeight = this.firstImage.getTileHeight(); | ||
|
||
// Compute extent | ||
const [minX, minY, maxX, maxY] = this.firstImage.getBoundingBox(); | ||
this.extent = new itowns.Extent(this.crs, minX, maxX, minY, maxY); | ||
this.dimensions = this.extent.planarDimensions(); | ||
|
||
this.levels = []; | ||
this.levels.push(this.makeLevel(this.firstImage, this.firstImage.getResolution())); | ||
|
||
// Number of images (original + overviews) | ||
const imageCount = await this.geotiff.getImageCount(); | ||
|
||
const promises = []; | ||
for (let index = 1; index < imageCount; index++) { | ||
const promise = this.geotiff.getImage(index) | ||
.then(image => this.makeLevel(image, image.getResolution(this.firstImage))); | ||
promises.push(promise); | ||
} | ||
this.levels.push(await Promise.all(promises)); | ||
}); | ||
} | ||
|
||
/** | ||
* @param {number} format - Format to interpret each data sample in a pixel | ||
* https://www.awaresystems.be/imaging/tiff/tifftags/sampleformat.html | ||
* @param {number} bitsPerSample - Number of bits per component. | ||
* https://www.awaresystems.be/imaging/tiff/tifftags/bitspersample.html | ||
* @return {THREE.AttributeGPUType} | ||
*/ | ||
selectDataType(format, bitsPerSample) { | ||
switch (format) { | ||
case 1: // unsigned integer data | ||
if (bitsPerSample <= 8) { | ||
return THREE.UnsignedByteType; | ||
} | ||
break; | ||
default: | ||
break; | ||
} | ||
return THREE.FloatType; | ||
} | ||
|
||
makeLevel(image, resolution) { | ||
return { | ||
image, | ||
width: image.getWidth(), | ||
height: image.getHeight(), | ||
resolution, | ||
}; | ||
} | ||
|
||
// We don't use UrlFromExtent, we let geotiff.js manage it | ||
urlFromExtent() { | ||
return ''; | ||
} | ||
|
||
extentInsideLimit(extent) { | ||
return this.extent.intersectsExtent(extent); | ||
} | ||
} | ||
|
||
if (typeof module != 'undefined' && module.exports) { | ||
module.exports = COGSource; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<html> | ||
<head> | ||
<title>Cloud Optimized GeoTiff</title> | ||
<meta charset="UTF-8"> | ||
<link rel="stylesheet" type="text/css" href="css/example.css"> | ||
<link rel="stylesheet" type="text/css" href="css/LoadingScreen.css"> | ||
|
||
<style type="text/css"> | ||
#description { | ||
z-index: 2; | ||
left: 10px; | ||
} | ||
</style> | ||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script> | ||
</head> | ||
<body> | ||
<div id="description"> | ||
<div>Specify the URL of a COG to load: | ||
<input type="text" id="cog_url" /> | ||
<button onclick="readCOGURL()">Load</button> | ||
</div> | ||
<button onclick="loadRGBSample()">Load RGB sample</button> | ||
<button onclick="load1BandSample()">Load 1 band sample</button> | ||
</div> | ||
<div id="viewerDiv"></div> | ||
<script src="js/GUI/GuiTools.js"></script> | ||
<script src="../dist/itowns.js"></script> | ||
<script src="../dist/debug.js"></script> | ||
<script src="js/GUI/LoadingScreen.js"></script> | ||
<script src="https://cdn.jsdelivr.net/npm/geotiff"></script> | ||
<script src="js/plugins/COGParser.js"></script> | ||
<script src="js/plugins/COGSource.js"></script> | ||
<script type="text/javascript"> | ||
itowns.proj4.defs('EPSG:2154', '+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'); | ||
|
||
var viewerDiv = document.getElementById('viewerDiv'); | ||
|
||
function readCOGURL() { | ||
var url = document.getElementById('cog_url').value || new URLSearchParams(window.location.search).get('geotiff'); | ||
|
||
if (url) { | ||
loadCOG(url); | ||
document.getElementById('cog_url').value = url; | ||
} | ||
} | ||
|
||
function loadRGBSample() { | ||
document.getElementById('cog_url').value = 'https://cdn.jsdelivr.net/gh/bloc-in-bloc/iTowns2-sample-data@add-cog-sample/cog/orvault.tif'; | ||
readCOGURL(); | ||
} | ||
|
||
function load1BandSample() { | ||
document.getElementById('cog_url').value = 'https://oin-hotosm.s3.amazonaws.com/60fbca155a90f10006fd2fc3/0/60fbca155a90f10006fd2fc4.tif'; | ||
readCOGURL(); | ||
} | ||
|
||
function loadCOG(url, crs) { | ||
// create a source from a Cloud Optimized GeoTiff | ||
var cogSource = new COGSource({ | ||
url: url, | ||
crs: "EPSG:2154" | ||
}); | ||
|
||
cogSource.whenReady.then(() => { | ||
var view = new itowns.PlanarView(viewerDiv, cogSource.extent, { disableSkirt: true, placement: { tilt: 90 } }); | ||
setupLoadingScreen(viewerDiv, view); | ||
new itowns.PlanarControls(view, {}); | ||
var cogLayer = new itowns.ColorLayer('cog', { | ||
source: cogSource, | ||
}); | ||
view.addLayer(cogLayer); | ||
}); | ||
} | ||
|
||
readCOGURL(); | ||
</script> | ||
</body> | ||
</html> |