Skip to content

Commit

Permalink
Export functions for inspecting GeoTIFFs
Browse files Browse the repository at this point in the history
Also adds an example for using gdal.js with WebWorkers.
  • Loading branch information
ddohler committed Nov 6, 2017
1 parent e7377ce commit 2f4d24d
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.swp
*.mem
24 changes: 21 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
PROJ4 = proj4
GDAL = gdal
EMMAKE ?= emmake
EMCC ?= emcc
EMCONFIGURE ?= emconfigure
export EMCONFIGURE_JS ?= 0
export EMCC_CFLAGS += -msse
EMCONFIGURE_JS ?= 0
EMCC_CFLAGS := -msse
EXPORTED_FUNCTIONS = "[\
'_GDALAllRegister',\
'_GDALOpen',\
'_GDALGetRasterXSize',\
'_GDALGetRasterYSize',\
'_GDALGetRasterCount',\
'_GDALGetProjectionRef',\
'_GDALGetGeoTransform'\
]"

export EMCONFIGURE_JS
export EMCC_CFLAGS

include gdal-configure.opt

########
# GDAL #
########
gdal: gdal.js

gdal.js: gdal-lib
$(EMCC) $(GDAL)/libgdal.a -o gdal.js -O3 -s EXPORTED_FUNCTIONS=$(EXPORTED_FUNCTIONS)

gdal-lib: $(GDAL)/libgdal.a

$(GDAL)/libgdal.a: $(GDAL)/config.status proj4
cd $(GDAL) && EMCC_FLAGS="-msse" $(EMMAKE) make lib-target
cd $(GDAL) && $(EMMAKE) make lib-target

# TODO: Pass the configure params more elegantly so that this uses the
# EMCONFIGURE variable
Expand Down
27 changes: 12 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,19 @@ python2: No such file or directory`
./emsdk install sdk-incoming-64bit --build=Release
./emsdk activate sdk-incoming-64bit
```
3. From the project directory, run `make gdal-lib`
3. From the project directory, run `make gdal`

Usage
---------------
There's not really much you can easily do right now; making a more ergonomic interface to the GDAL C
API is high on my to-do list.
This library exports the following GDAL functions:
- GDALOpen
- GDALGetRasterCount
- GDALGetRasterXSize
- GDALGetRasterYSize
- GDALGetProjectionRef
- GDALGetGeoTransform

If you're feeling adventurous, Emscripten provides a debugging interface for command-line programs
which I've used successfully with `gdalinfo`. You should be able to compile `gdalinfo`, for example,
by doing something like this from the `gdal` directory:
```
emmake make apps-target
cd apps
em++ -s ALLOW_MEMORY_GROWTH=1 gdalinfo_bin.o ../libgdal.a
-L../../proj4/src/.libs -lproj -lpthread -ldl -o gdalinfo.html
```
Note that you will need to preload a useful file (a GeoTiff) into the Emscripten file system
and pass it to `gdalinfo` as a command line argument in order for this to do anything useful. The
Emscripten docs contain information on how to do both of those things.
To see a full-fledged example using all of these functions from within a WebWorker, check out the
`examples/inspect_geotiff` directory.

In order to limit Javascript build size, GDAL is currently built with support for GeoTIFFs only.
13 changes: 13 additions & 0 deletions examples/inspect_geotiff/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
This shows an example of how to use the GDAL API from within a web browser to
inspect certain aspects of a GeoTIFF. It roughly mirrors the functionality
found in http://www.gdal.org/gdal_tutorial.html.

To use, first make sure that `gdal.js` and `gdal.js.mem` are available in this
directory. There are some symlinks provided that will do this automatically if
you build the project from source. Alternatively, you can
[download a release](https://github.com/ddohler/gdal-js/releases)
and place the files in this directory manually.

Next, start up an HTTP server to serve this folder. For example,
`python -m SimpleHTTPServer`. Navigate to whatever port your server is listening
at, and follow the instructions on the page.
1 change: 1 addition & 0 deletions examples/inspect_geotiff/gdal.js
1 change: 1 addition & 0 deletions examples/inspect_geotiff/gdal.js.mem
13 changes: 13 additions & 0 deletions examples/inspect_geotiff/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<html>
<head>
<script src="index.js"></script>
<title>Test GDAL.js</title>
</head>
<body>
<input type="file" id="geotiff-select">
<button onclick="inspectFiles()">Inspect files</button>
<p>Select a GeoTIFF using the Browse... button. Next, open the developer console.
Click the "Inspect Files" button and you will see information about the file's
bands, projection, and footprint written to the console.</p>
</body>
</html>
10 changes: 10 additions & 0 deletions examples/inspect_geotiff/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var tiffInspector = new Worker('worker.js');

function inspectFiles() {
var files = document.querySelector('#geotiff-select').files;
tiffInspector.postMessage(files);
}

tiffInspector.onmessage = function(evt) {
console.log(evt.data);
};
131 changes: 131 additions & 0 deletions examples/inspect_geotiff/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Setup
*/
var TIFFPATH = '/tiffs';

var initialized = false;

var GDALOpen,
GDALGetRasterCount,
GDALGetRasterXSize,
GDALGetRasterYSize,
GDALGetProjectionRef,
GDALGetGeoTransform;

// Set up Module object for gdal.js to populate. Emscripten sets up its compiled
// code to look for a Module object in the global scope. If found, it reads runtime
// configuration from the existing object, and then further populates that object
// with other helpful functionality (e.g. ccall() and cwrap(), which are used in
// the onRuntimeInitialized callback, below).
var Module = {
'print': function(text) { console.log('stdout: ' + text); },
'printErr': function(text) { console.log('stderr: ' + text); },
// Optimized builds contain a .js.mem file which is loaded asynchronously;
// this waits until that has finished before performing further setup.
'onRuntimeInitialized': function() {
// Initialize GDAL
Module.ccall('GDALAllRegister', null, [], []);

// Set up JS proxy functions
// Note that JS Number types are used to represent pointers, which means that
// any time we want to pass a pointer to an object, such as in GDALOpen, which in
// C returns a pointer to a GDALDataset, we need to use 'number'.
GDALOpen = Module.cwrap('GDALOpen', 'number', ['string']);
GDALGetRasterCount = Module.cwrap('GDALGetRasterCount', 'number', ['number']);
GDALGetRasterXSize = Module.cwrap('GDALGetRasterXSize', 'number', ['number']);
GDALGetRasterYSize = Module.cwrap('GDALGetRasterYSize', 'number', ['number']);
GDALGetProjectionRef = Module.cwrap('GDALGetProjectionRef', 'string', ['number']);
// Returns an affine transform from geographic coordinate space to geographic coordinate space.
// Applying this transform to (0,0), (0, maxY), (maxX, maxY), and (maxX, 0) gives us the raster's
// georeferenced footprint. See http://www.gdal.org/gdal_datamodel.html
GDALGetGeoTransform = Module.cwrap('GDALGetGeoTransform', 'number', ['number', 'number']);

// Create a "directory" where user-selected files will be placed
FS.mkdir(TIFFPATH);
initialized = true;
}
};

// Load gdal.js. This will populate the Module object, and then call
// Module.onRuntimeInitialized() when it is ready for user code to interact with it.
importScripts('gdal.js');

/*
* Logic
*/
// Use GDAL functions to provide information about a list of files.
// @param files a FileList object as returned by a file input's .files field
function inspectTiff(files) {
// Make GeoTiffs available to GDAL in the virtual filesystem that it lives inside
FS.mount(WORKERFS, {
files: files
}, TIFFPATH);

var results = [];
for(var i = 0; i < files.length; i++) {
// Create a GDAL Dataset
var dataset = GDALOpen(TIFFPATH + '/' + files[i].name);
var bandCount = GDALGetRasterCount(dataset);
var maxX = GDALGetRasterXSize(dataset);
var maxY = GDALGetRasterYSize(dataset);
wktStr = GDALGetProjectionRef(dataset);
// This is where things get a bit hairy; the C function follows a common C pattern where an array to
// store the results is allocated and passed into the function, which populates the array with the
// results. Emscripten supports passing arrays to functions, but it always creates a *copy* of the
// array, which means that the original JS array remains unchanged, which isn't what we want in this
// case. So first, we have to malloc an array inside the Emscripten heap with the correct size. In this
// case that is 6 because the GDAL affine transform array has six elements.
var byteOffset = Module._malloc(6 * Float64Array.BYTES_PER_ELEMENT);
// byteOffset is now a pointer to the start of the double array in Emscripten heap space
// GDALGetGeoTransform dumps 6 values into the passed double array.
GDALGetGeoTransform(dataset, byteOffset);
// Module.HEAPF64 provides a view into the Emscripten heap, as an array of doubles. Therefore, our byte offset
// from _malloc needs to be converted into a double offset, so we divide it by the number of bytes per double,
// and then get a subarray of those six elements off the Emscripten heap.
var geoTransform = Module.HEAPF64.subarray(
byteOffset/Float64Array.BYTES_PER_ELEMENT,
byteOffset/Float64Array.BYTES_PER_ELEMENT + 6
);
// Finally, we can apply the affine transform to convert from pixel coordinates into geographic coordinates
// If you wanted to display these on a map, you'd further need to transform to lat/lon, since these
// are in the raster's CRS.
var corners = [
[0, 0],
[maxX, 0],
[maxX, maxY],
[0, maxY]
];
var geoCorners = corners.map(function(coords) {
var x = coords[0];
var y = coords[1];
return [
// http://www.gdal.org/gdal_datamodel.html
geoTransform[0] + geoTransform[1]*x + geoTransform[2]*y,
geoTransform[3] + geoTransform[4]*x + geoTransform[5]*y
];
});
results.push({
bandCount: bandCount,
xSize: maxX,
ySize: maxY,
projectionWkt: wktStr,
coordinateTransform: geoTransform,
corners: geoCorners
});
}

// Now pass this back to the calling thread, which is presumably where we'd want to handle it:
postMessage(results);

// And cleanup
FS.unmount(TIFFPATH);
}

// Assume that all incoming messages are FileLists of GeoTiffs and inspect them.
onmessage = function(msg) {
if (!initialized) {
console.log('Runtime not initialized yet, try again');
return;
}
inspectTiff(msg.data);
}

0 comments on commit 2f4d24d

Please sign in to comment.