where each element has the shape `{name: string, data: Blob}`.
+
#### Return value
A promise that resolves with an instance of `GDALDataset`.
@@ -116,9 +118,16 @@ A promise that resolves immediately with an empty list (for historical reasons).
### `GDALDataset.count()`
-Get the number of bands in the dataset.
+Get the number of raster bands in the dataset.
+#### Return value
+A promise which resolves to the number of raster bands in the dataset.
+
+
+
+### `GDALDataset.layerCount()`
+Get the number of vector layers in the dataset.
#### Return value
-A promise which resolves to the number of bands in the dataset.
+A promise which resolves to the number of vector layers in the dataset.
@@ -150,6 +159,59 @@ A promise which resolves to the affine transform.
+### `GDALDataset.bandMinimum(bandNum)`
+Get the actual minimum value or the minimum possible value of a band (depending on format).
+#### Parameters
+- `bandNum`: The number of the band for which to get the minimum value. Band numbering starts at 1.
+#### Return value
+A promise which resolves to a minimum value for the specified band.
+
+
+
+### `GDALDataset.bandMaximum(bandNum)`
+Get the actual maximum value or the maximum possible value of a band (depending on format).
+#### Parameters
+- `bandNum`: The number of the band for which to get the maximum value. Band numbering starts at 1.
+#### Return value
+A promise which resolves to a maximum value for the specified band.
+
+
+
+### `GDALDataset.bandStatistics(bandNum)`
+Get statistics about the values in a band.
+#### Parameters
+- `bandNum`: The number of the band for which to get statistics. Band numbering starts at 1.
+#### Return value
+A promise which resolves to an object containing statistics. The shape of the object will be:
+```javascript
+{
+ minimum: The calculated minimum value of the band
+ maximum: The calculated minimum value of the band
+ median: The calculated median value of the band
+ stdDev: The calculated standard deviation of the band
+}
+```
+
+
+
+### `GDALDataset.bandNoDataValue(bandNum)`
+Get the value representing "no data" within the band.
+#### Parameters
+- `bandNum`: The number of the band for which to get the no-data value. Band numbering starts at 1.
+#### Return value
+A promise which resolves to the no-data value for the specified band.
+
+
+
+### `GDALDataset.bandDataType(bandNum)`
+Get the data type of the band (Byte, UInt16, Float32, etc.)
+#### Parameters
+- `bandNum`: The number of the band for which to get the data type. Band numbering starts at 1.
+#### Return value
+A promise which resolves to a string containing the name of the data type for the specified band. For example, 'Byte', 'Float32', etc.
+
+
+
### `GDALDataset.bytes()`
Get the on-disk representation of the dataset, as an array of bytes.
#### Return value
@@ -168,6 +230,17 @@ A promise that resolves to a new `GDALDataset`.
+### `GDALDataset.vectorConvert(args)`
+Converts vector data between different formats. This is the equivalent of the [ogr2ogr](https://gdal.org/programs/ogr2ogr.html) command.
+
+**Note**: This returns a new `GDALDataset` object but does not perform any immediate calculation. Instead, calls to `.vectorConvert()` are evaluated lazily. Each successive call to `.vectorConvert()` is stored in a list of operations on the dataset object. These operations are only evaluated when necessary in order to access some property of the dataset, such as its size, bytes, or layer count.
+#### Parameters
+- `args`: An array of strings, each representing a single command-line argument accepted by the `ogr2ogr` command. The `dst_datasource_name` and `src_datasource_name` parameters should be omitted; these are handled by `GDALDataset`. Example: `ds.vectorConvert(['-f', 'GeoJSON'])`.
+#### Return value
+A promise that resolves to a new `GDALDataset`.
+
+
+
### `GDALDataset.warp(args)`
Image reprojection and warping utility. This is the equivalent of the [gdalwarp](https://gdal.org/programs/gdalwarp.html) command.
diff --git a/demo/index.html b/demo/index.html
index e5b2612..ddbce80 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -4,14 +4,25 @@
Loam Test
+
-
- Select a GeoTIFF using the Browse... button. Information about the file will be
- displayed below.
+ Select a file using the Source file input and then click "Display metadata". Metadata
+ about the file will be displayed below. GeoTIFFs work best, but GeoJSON and Shapefiles
+ will work to some extent as well. Make sure to select sidecar files when relevant (e.g.
+ .prj, .dbf, .shx, etc. for Shapefiles).
+
diff --git a/demo/index.js b/demo/index.js
index 5e01004..4353b5a 100644
--- a/demo/index.js
+++ b/demo/index.js
@@ -1,60 +1,76 @@
/* global loam */
// Use the locally built version of loam, with a CDN copy of GDAL from unpkg.
-loam.initialize('/', 'https://unpkg.com/gdal-js@2.0.0/');
+loam.initialize('/', 'https://unpkg.com/gdal-js@2.1.0/');
const EPSG4326 =
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]';
function displayInfo() {
- const file = document.querySelector('#geotiff-file').files[0];
+ const sourceFile = document.querySelector('#source-file').files[0];
+ const sidecars = Array.from(document.querySelector('#sidecar-files').files);
+
const displayElem = document.getElementById('gdalinfo');
// Clear display text
displayElem.innerText = '';
// Use Loam to get GeoTIFF metadata
- loam.open(file).then((ds) => {
- return Promise.all([ds.width(), ds.height(), ds.count(), ds.wkt(), ds.transform()]).then(
- ([width, height, count, wkt, geoTransform]) => {
- displayElem.innerText +=
- 'Size: ' + width.toString() + ', ' + height.toString() + '\n';
- displayElem.innerText += 'Band count: ' + count.toString() + '\n';
- displayElem.innerText += 'Coordinate system:\n' + wkt + '\n';
-
- const cornersPx = [
- [0, 0],
- [width, 0],
- [width, height],
- [0, height],
+ loam.open(sourceFile, sidecars).then((ds) => {
+ return Promise.all([
+ ds.width(),
+ ds.height(),
+ ds.count(),
+ ds.layerCount(),
+ ds.wkt(),
+ ds.transform(),
+ ]).then(([width, height, count, layerCount, wkt, geoTransform]) => {
+ displayElem.innerText += 'Size: ' + width.toString() + ', ' + height.toString() + '\n';
+ displayElem.innerText += 'Raster band count: ' + count.toString() + '\n';
+ displayElem.innerText += 'Vector layer count: ' + layerCount.toString() + '\n';
+ displayElem.innerText += 'Coordinate system:\n' + wkt + '\n';
+
+ const cornersPx = [
+ [0, 0],
+ [width, 0],
+ [width, height],
+ [0, height],
+ ];
+ const cornersGeo = cornersPx.map(([x, y]) => {
+ return [
+ // http://www.gdal.org/gdal_datamodel.html
+ geoTransform[0] + geoTransform[1] * x + geoTransform[2] * y,
+ geoTransform[3] + geoTransform[4] * x + geoTransform[5] * y,
];
- const cornersGeo = cornersPx.map(([x, y]) => {
- return [
- // http://www.gdal.org/gdal_datamodel.html
- geoTransform[0] + geoTransform[1] * x + geoTransform[2] * y,
- geoTransform[3] + geoTransform[4] * x + geoTransform[5] * y,
- ];
+ });
+
+ loam.reproject(wkt, EPSG4326, cornersGeo).then((cornersLngLat) => {
+ displayElem.innerText += 'Corner Coordinates:\n';
+ cornersLngLat.forEach(([lng, lat], i) => {
+ displayElem.innerText +=
+ '(' +
+ cornersGeo[i][0].toString() +
+ ', ' +
+ cornersGeo[i][1].toString() +
+ ') (' +
+ lng.toString() +
+ ', ' +
+ lat.toString() +
+ ')\n';
});
+ });
- loam.reproject(wkt, EPSG4326, cornersGeo).then((cornersLngLat) => {
- displayElem.innerText += 'Corner Coordinates:\n';
- cornersLngLat.forEach(([lng, lat], i) => {
- displayElem.innerText +=
- '(' +
- cornersGeo[i][0].toString() +
- ', ' +
- cornersGeo[i][1].toString() +
- ') (' +
- lng.toString() +
- ', ' +
- lat.toString() +
- ')\n';
- });
+ if (count > 0) {
+ ds.bandStatistics(1).then((stats) => {
+ displayElem.innerText += 'Band 1 min: ' + stats.minimum + '\n';
+ displayElem.innerText += 'Band 1 max: ' + stats.maximum + '\n';
+ displayElem.innerText += 'Band 1 median: ' + stats.median + '\n';
+ displayElem.innerText += 'Band 1 standard deviation: ' + stats.stdDev + '\n';
});
}
- );
+ });
});
}
-document.getElementById('geotiff-file').onchange = function () {
+document.getElementById('display-metadata-button').onclick = function () {
displayInfo();
};
diff --git a/package.json b/package.json
index c106e71..a0830d8 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
{
"name": "loam",
- "version": "1.1.2",
+ "version": "1.2.0",
"description": "Javascript wrapper for GDAL in the browser",
"main": "lib/loam.js",
"scripts": {
"build": "webpack --config=webpack.dev.js && webpack --config=webpack.prod.js",
"dev": "webpack --progress --color --watch --config=webpack.dev.js",
"demo": "webpack serve --config=webpack.dev.js",
- "format": "prettier --write ./src",
+ "format": "prettier --write ./src ./test ./demo",
"test": "karma start --single-run --browser ChromeHeadless karma.conf.js",
"test:watch": "karma start --auto-watch --browser ChromeHeadless karma.conf.js",
"test:ci": "prettier --check src/**/*.js && webpack --config=webpack.dev.js && webpack --config=webpack.prod.js && karma start --single-run --browser ChromeHeadless karma.conf.js"
@@ -58,7 +58,7 @@
"yargs": "^17.5.1"
},
"dependencies": {
- "gdal-js": "2.0.0"
+ "gdal-js": "2.1.0"
},
"packageManager": "yarn@3.2.3"
}
diff --git a/src/api.js b/src/api.js
index ad63fe3..7fba577 100644
--- a/src/api.js
+++ b/src/api.js
@@ -1,9 +1,9 @@
import { initWorker, clearWorker, runOnWorker } from './workerCommunication.js';
-import { GDALDataset } from './gdalDataset.js';
+import { DatasetSource, GDALDataset } from './gdalDataset.js';
-function open(file) {
+function open(file, sidecars = []) {
return new Promise((resolve, reject) => {
- const ds = new GDALDataset({ func: 'GDALOpen', src: file, args: [] });
+ const ds = new GDALDataset(new DatasetSource('GDALOpen', file, sidecars));
return ds.open().then(
() => resolve(ds),
@@ -14,7 +14,7 @@ function open(file) {
function rasterize(geojson, args) {
return new Promise((resolve, reject) => {
- resolve(new GDALDataset({ func: 'GDALRasterize', src: geojson, args: args }));
+ resolve(new GDALDataset(new DatasetSource('GDALRasterize', geojson, [], args)));
});
}
diff --git a/src/gdalDataType.js b/src/gdalDataType.js
new file mode 100644
index 0000000..ac7598c
--- /dev/null
+++ b/src/gdalDataType.js
@@ -0,0 +1,19 @@
+// In order to make enums available from JS it's necessary to use embind, which seems like
+// overkill for something this small. This replicates the GDALDataType enum:
+// https://gdal.org/api/raster_c_api.html#_CPPv412GDALDataType and will need to be changed
+// if that enum changes. There is a smoke test that should warn us if it changes upstream.
+export const GDALDataTypes = [
+ 'Unknown',
+ 'Byte',
+ 'UInt16',
+ 'Int16',
+ 'UInt32',
+ 'Int32',
+ 'Float32',
+ 'Float64',
+ 'CInt16',
+ 'CInt32',
+ 'CFloat32',
+ 'CFloat64',
+ 'TypeCount',
+];
diff --git a/src/gdalDataset.js b/src/gdalDataset.js
index 25ffa3a..ce6fb89 100644
--- a/src/gdalDataset.js
+++ b/src/gdalDataset.js
@@ -8,6 +8,22 @@ export class DatasetOperation {
}
}
+// The starting point of a dataset within a GDAL webworker context. Outputs a dataset.
+// - functionName is the name of a GDAL function that can generate a dataset
+// Currently, this is either GDALOpen or GDALRasterize
+// - file is a File, Blob, or {name: string, data: Blob}
+// - sidecars are any other files that need to get loaded into the worker filesystem alongside file
+// Must match the type of file
+// - args are any additional arguments that should be passed to functionName.
+export class DatasetSource {
+ constructor(functionName, file, sidecars = [], args = []) {
+ this.func = functionName;
+ this.src = file;
+ this.sidecars = sidecars;
+ this.args = args;
+ }
+}
+
export class GDALDataset {
constructor(source, operations) {
this.source = source;
@@ -32,6 +48,10 @@ export class GDALDataset {
return accessFromDataset('GDALGetRasterCount', this);
}
+ layerCount() {
+ return accessFromDataset('GDALDatasetGetLayerCount', this);
+ }
+
width() {
return accessFromDataset('GDALGetRasterXSize', this);
}
@@ -48,6 +68,26 @@ export class GDALDataset {
return accessFromDataset('GDALGetGeoTransform', this);
}
+ bandMinimum(bandNum) {
+ return accessFromDataset('GDALGetRasterMinimum', this, bandNum);
+ }
+
+ bandMaximum(bandNum) {
+ return accessFromDataset('GDALGetRasterMaximum', this, bandNum);
+ }
+
+ bandStatistics(bandNum) {
+ return accessFromDataset('GDALGetRasterStatistics', this, bandNum);
+ }
+
+ bandDataType(bandNum) {
+ return accessFromDataset('GDALGetRasterDataType', this, bandNum);
+ }
+
+ bandNoDataValue(bandNum) {
+ return accessFromDataset('GDALGetRasterNoDataValue', this, bandNum);
+ }
+
convert(args) {
return new Promise((resolve, reject) => {
resolve(
@@ -59,6 +99,17 @@ export class GDALDataset {
});
}
+ vectorConvert(args) {
+ return new Promise((resolve, reject) => {
+ resolve(
+ new GDALDataset(
+ this.source,
+ this.operations.concat(new DatasetOperation('GDALVectorTranslate', args))
+ )
+ );
+ });
+ }
+
warp(args) {
return new Promise((resolve, reject) => {
resolve(
diff --git a/src/worker.js b/src/worker.js
index 6043af7..77059c4 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -6,13 +6,20 @@
import wGDALOpen from './wrappers/gdalOpen.js';
import wGDALRasterize from './wrappers/gdalRasterize.js';
import wGDALClose from './wrappers/gdalClose.js';
+import wGDALDatasetGetLayerCount from './wrappers/gdalDatasetGetLayerCount.js';
import wGDALDEMProcessing from './wrappers/gdalDemProcessing.js';
import wGDALGetRasterCount from './wrappers/gdalGetRasterCount.js';
import wGDALGetRasterXSize from './wrappers/gdalGetRasterXSize.js';
import wGDALGetRasterYSize from './wrappers/gdalGetRasterYSize.js';
+import wGDALGetRasterMinimum from './wrappers/gdalGetRasterMinimum.js';
+import wGDALGetRasterMaximum from './wrappers/gdalGetRasterMaximum.js';
+import wGDALGetRasterNoDataValue from './wrappers/gdalGetRasterNoDataValue.js';
+import wGDALGetRasterDataType from './wrappers/gdalGetRasterDataType.js';
+import wGDALGetRasterStatistics from './wrappers/gdalGetRasterStatistics.js';
import wGDALGetProjectionRef from './wrappers/gdalGetProjectionRef.js';
import wGDALGetGeoTransform from './wrappers/gdalGetGeoTransform.js';
import wGDALTranslate from './wrappers/gdalTranslate.js';
+import wGDALVectorTranslate from './wrappers/gdalVectorTranslate.js';
import wGDALWarp from './wrappers/gdalWarp.js';
import wReproject from './wrappers/reproject.js';
@@ -74,7 +81,13 @@ self.Module = {
// C returns a pointer to a GDALDataset, we need to use 'number'.
//
registry.GDALOpen = wGDALOpen(
- self.Module.cwrap('GDALOpen', 'number', ['string']),
+ self.Module.cwrap('GDALOpenEx', 'number', [
+ 'string', // Filename
+ 'number', // nOpenFlags
+ 'number', // NULL-terminated list of drivers to limit to when opening file
+ 'number', // NULL-terminated list of option flags passed to drivers
+ 'number', // Paths to sibling files to avoid file system searches
+ ]),
errorHandling,
DATASETPATH
);
@@ -97,6 +110,10 @@ self.Module = {
self.Module.cwrap('GDALGetRasterCount', 'number', ['number']),
errorHandling
);
+ registry.GDALDatasetGetLayerCount = wGDALDatasetGetLayerCount(
+ self.Module.cwrap('GDALDatasetGetLayerCount', 'number', ['number']),
+ errorHandling
+ );
registry.GDALGetRasterXSize = wGDALGetRasterXSize(
self.Module.cwrap('GDALGetRasterXSize', 'number', ['number']),
errorHandling
@@ -105,6 +122,26 @@ self.Module = {
self.Module.cwrap('GDALGetRasterYSize', 'number', ['number']),
errorHandling
);
+ registry.GDALGetRasterMinimum = wGDALGetRasterMinimum(
+ self.Module.cwrap('GDALGetRasterMinimum', 'number', ['number']),
+ errorHandling
+ );
+ registry.GDALGetRasterMaximum = wGDALGetRasterMaximum(
+ self.Module.cwrap('GDALGetRasterMaximum', 'number', ['number']),
+ errorHandling
+ );
+ registry.GDALGetRasterNoDataValue = wGDALGetRasterNoDataValue(
+ self.Module.cwrap('GDALGetRasterNoDataValue', 'number', ['number']),
+ errorHandling
+ );
+ registry.GDALGetRasterDataType = wGDALGetRasterDataType(
+ self.Module.cwrap('GDALGetRasterDataType', 'number', ['number']),
+ errorHandling
+ );
+ registry.GDALGetRasterStatistics = wGDALGetRasterStatistics(
+ self.Module.cwrap('GDALGetRasterStatistics', 'number', ['number']),
+ errorHandling
+ );
registry.GDALGetProjectionRef = wGDALGetProjectionRef(
self.Module.cwrap('GDALGetProjectionRef', 'string', ['number']),
errorHandling
@@ -123,6 +160,19 @@ self.Module = {
errorHandling,
DATASETPATH
);
+ // Equivalent to ogr2ogr
+ registry.GDALVectorTranslate = wGDALVectorTranslate(
+ self.Module.cwrap('GDALVectorTranslate', 'number', [
+ 'string', // Output path or NULL
+ 'number', // Destination dataset or NULL
+ 'number', // Number of input datasets (only 1 is supported)
+ 'number', // GDALDatasetH * list of source datasets
+ 'number', // GDALVectorTranslateOptions *
+ 'number', // int * to use for error reporting
+ ]),
+ errorHandling,
+ DATASETPATH
+ );
registry.GDALWarp = wGDALWarp(
self.Module.cwrap('GDALWarp', 'number', [
'string', // Destination dataset path or NULL
@@ -168,9 +218,13 @@ self.Module = {
},
};
-function handleDatasetAccess(accessor, dataset) {
+function handleDatasetAccess(accessor, dataset, args) {
// 1: Open the source.
- let srcDs = registry[dataset.source.func](dataset.source.src, dataset.source.args);
+ let srcDs = registry[dataset.source.func](
+ dataset.source.src,
+ dataset.source.args,
+ dataset.source.sidecars
+ );
let resultDs = srcDs;
@@ -196,7 +250,7 @@ function handleDatasetAccess(accessor, dataset) {
true
);
} else if (accessor) {
- result = registry[accessor](resultDs.datasetPtr);
+ result = registry[accessor](resultDs.datasetPtr, ...args);
registry.GDALClose(resultDs.datasetPtr, resultDs.directory, resultDs.filePath, false);
} else {
registry.GDALClose(resultDs.datasetPtr, resultDs.directory, resultDs.filePath, false);
@@ -227,7 +281,7 @@ onmessage = function (msg) {
if ('func' in msg.data && 'args' in msg.data) {
result = handleFunctionCall(msg.data.func, msg.data.args);
} else if ('accessor' in msg.data && 'dataset' in msg.data) {
- result = handleDatasetAccess(msg.data.accessor, msg.data.dataset);
+ result = handleDatasetAccess(msg.data.accessor, msg.data.dataset, msg.data.args);
} else {
postMessage({
success: false,
diff --git a/src/workerCommunication.js b/src/workerCommunication.js
index 2f76656..95d065b 100644
--- a/src/workerCommunication.js
+++ b/src/workerCommunication.js
@@ -172,8 +172,8 @@ function workerTaskPromise(options) {
}
// Accessors is a list of accessors operations to run on the dataset defined by dataset.
-function accessFromDataset(accessor, dataset) {
- return workerTaskPromise({ accessor: accessor, dataset: dataset });
+function accessFromDataset(accessor, dataset, ...otherArgs) {
+ return workerTaskPromise({ accessor: accessor, dataset: dataset, args: otherArgs });
}
// Run a single function on the worker.
diff --git a/src/wrappers/gdalDatasetGetLayerCount.js b/src/wrappers/gdalDatasetGetLayerCount.js
new file mode 100644
index 0000000..e850f11
--- /dev/null
+++ b/src/wrappers/gdalDatasetGetLayerCount.js
@@ -0,0 +1,19 @@
+export default function (GDALDatasetGetLayerCount, errorHandling) {
+ return function (datasetPtr) {
+ const result = GDALDatasetGetLayerCount(datasetPtr);
+
+ const errorType = errorHandling.CPLGetLastErrorType();
+
+ // Check for errors; clean up and throw if error is detected
+ if (
+ errorType === errorHandling.CPLErr.CEFailure ||
+ errorType === errorHandling.CPLErr.CEFatal
+ ) {
+ const message = errorHandling.CPLGetLastErrorMsg();
+
+ throw new Error('Error in GDALDatasetGetLayerCount: ' + message);
+ } else {
+ return result;
+ }
+ };
+}
diff --git a/src/wrappers/gdalGetRasterDataType.js b/src/wrappers/gdalGetRasterDataType.js
new file mode 100644
index 0000000..cb68004
--- /dev/null
+++ b/src/wrappers/gdalGetRasterDataType.js
@@ -0,0 +1,31 @@
+import { GDALDataTypes } from '../gdalDataType.js';
+
+/* global Module */
+export default function (GDALGetRasterDataType, errorHandling) {
+ return function (datasetPtr, bandNum) {
+ const bandPtr = Module.ccall(
+ 'GDALGetRasterBand',
+ 'number',
+ ['number', 'number'],
+ [datasetPtr, bandNum]
+ );
+ // GDALGetRasterDataType will provide an integer because it's pulling from an enum
+ // So we use that to index into an array of the corresponding type strings so that it's
+ // easier to work with from Javascript land.
+ const result = GDALDataTypes[GDALGetRasterDataType(bandPtr)];
+
+ const errorType = errorHandling.CPLGetLastErrorType();
+
+ // Check for errors; clean up and throw if error is detected
+ if (
+ errorType === errorHandling.CPLErr.CEFailure ||
+ errorType === errorHandling.CPLErr.CEFatal
+ ) {
+ throw new Error(
+ 'Error in GDALGetRasterDataType: ' + errorHandling.CPLGetLastErrorMsg()
+ );
+ } else {
+ return result;
+ }
+ };
+}
diff --git a/src/wrappers/gdalGetRasterMaximum.js b/src/wrappers/gdalGetRasterMaximum.js
new file mode 100644
index 0000000..2b4e2a5
--- /dev/null
+++ b/src/wrappers/gdalGetRasterMaximum.js
@@ -0,0 +1,24 @@
+/* global Module */
+export default function (GDALGetRasterMaximum, errorHandling) {
+ return function (datasetPtr, bandNum) {
+ const bandPtr = Module.ccall(
+ 'GDALGetRasterBand',
+ 'number',
+ ['number', 'number'],
+ [datasetPtr, bandNum]
+ );
+ const result = GDALGetRasterMaximum(bandPtr);
+
+ const errorType = errorHandling.CPLGetLastErrorType();
+
+ // Check for errors; clean up and throw if error is detected
+ if (
+ errorType === errorHandling.CPLErr.CEFailure ||
+ errorType === errorHandling.CPLErr.CEFatal
+ ) {
+ throw new Error('Error in GDALGetRasterMaximum: ' + errorHandling.CPLGetLastErrorMsg());
+ } else {
+ return result;
+ }
+ };
+}
diff --git a/src/wrappers/gdalGetRasterMinimum.js b/src/wrappers/gdalGetRasterMinimum.js
new file mode 100644
index 0000000..3e1cabe
--- /dev/null
+++ b/src/wrappers/gdalGetRasterMinimum.js
@@ -0,0 +1,24 @@
+/* global Module */
+export default function (GDALGetRasterMinimum, errorHandling) {
+ return function (datasetPtr, bandNum) {
+ const bandPtr = Module.ccall(
+ 'GDALGetRasterBand',
+ 'number',
+ ['number', 'number'],
+ [datasetPtr, bandNum]
+ );
+ const result = GDALGetRasterMinimum(bandPtr);
+
+ const errorType = errorHandling.CPLGetLastErrorType();
+
+ // Check for errors; clean up and throw if error is detected
+ if (
+ errorType === errorHandling.CPLErr.CEFailure ||
+ errorType === errorHandling.CPLErr.CEFatal
+ ) {
+ throw new Error('Error in GDALGetRasterMinimum: ' + errorHandling.CPLGetLastErrorMsg());
+ } else {
+ return result;
+ }
+ };
+}
diff --git a/src/wrappers/gdalGetRasterNoDataValue.js b/src/wrappers/gdalGetRasterNoDataValue.js
new file mode 100644
index 0000000..49a3257
--- /dev/null
+++ b/src/wrappers/gdalGetRasterNoDataValue.js
@@ -0,0 +1,26 @@
+/* global Module */
+export default function (GDALGetRasterNoDataValue, errorHandling) {
+ return function (datasetPtr, bandNum) {
+ const bandPtr = Module.ccall(
+ 'GDALGetRasterBand',
+ 'number',
+ ['number', 'number'],
+ [datasetPtr, bandNum]
+ );
+ const result = GDALGetRasterNoDataValue(bandPtr);
+
+ const errorType = errorHandling.CPLGetLastErrorType();
+
+ // Check for errors; clean up and throw if error is detected
+ if (
+ errorType === errorHandling.CPLErr.CEFailure ||
+ errorType === errorHandling.CPLErr.CEFatal
+ ) {
+ throw new Error(
+ 'Error in GDALGetRasterNoDataValue: ' + errorHandling.CPLGetLastErrorMsg()
+ );
+ } else {
+ return result;
+ }
+ };
+}
diff --git a/src/wrappers/gdalGetRasterStatistics.js b/src/wrappers/gdalGetRasterStatistics.js
new file mode 100644
index 0000000..f4b6f76
--- /dev/null
+++ b/src/wrappers/gdalGetRasterStatistics.js
@@ -0,0 +1,56 @@
+/* global Module */
+export default function (GDALGetRasterStatistics, errorHandling) {
+ return function (datasetPtr, bandNum) {
+ const bandPtr = Module.ccall(
+ 'GDALGetRasterBand',
+ 'number',
+ ['number', 'number'],
+ [datasetPtr, bandNum]
+ );
+ // We need to allocate pointers to store statistics into which will get passed into
+ // GDALGetRasterStatistics(). They're all doubles, so allocate 8 bytes each.
+ const minPtr = Module._malloc(8);
+ const maxPtr = Module._malloc(8);
+ const meanPtr = Module._malloc(8);
+ const sdPtr = Module._malloc(8);
+ const returnErr = GDALGetRasterStatistics(
+ bandPtr,
+ 0, // Approximate statistics flag -- set to false
+ 1, // Force flag -- will always return statistics even if image must be rescanned
+ minPtr,
+ maxPtr,
+ meanPtr,
+ sdPtr
+ );
+
+ const errorType = errorHandling.CPLGetLastErrorType();
+
+ // Check for errors; throw if error is detected
+ // GDALGetRasterStatistics returns CE_Failure if an error occurs.
+ try {
+ if (
+ errorType === errorHandling.CPLErr.CEFailure ||
+ errorType === errorHandling.CPLErr.CEFatal ||
+ returnErr === errorHandling.CPLErr.CEFailure
+ ) {
+ throw new Error(
+ 'Error in GDALGetRasterStatistics: ' + errorHandling.CPLGetLastErrorMsg()
+ );
+ } else {
+ // At this point the values at each pointer should have been written with statistics
+ // so we can read them out and send them back.
+ return {
+ minimum: Module.getValue(minPtr, 'double'),
+ maximum: Module.getValue(maxPtr, 'double'),
+ median: Module.getValue(meanPtr, 'double'),
+ stdDev: Module.getValue(sdPtr, 'double'),
+ };
+ }
+ } finally {
+ Module._free(minPtr);
+ Module._free(maxPtr);
+ Module._free(meanPtr);
+ Module._free(sdPtr);
+ }
+ };
+}
diff --git a/src/wrappers/gdalOpen.js b/src/wrappers/gdalOpen.js
index 815336b..74a7505 100644
--- a/src/wrappers/gdalOpen.js
+++ b/src/wrappers/gdalOpen.js
@@ -1,26 +1,47 @@
import randomKey from '../randomKey.js';
+// Redefine constants from https://github.com/OSGeo/gdal/blob/v2.4.4/gdal/gcore/gdal.h
+// Constants are hard to get Emscripten to output in a way that we can usefully reference from
+// Javascript.
+const GDAL_OF_UPDATE = 0x01;
+const GDAL_OF_VERBOSE_ERROR = 0x40;
+
/* global FS WORKERFS */
-export default function (GDALOpen, errorHandling, rootPath) {
- return function (file) {
+export default function (GDALOpenEx, errorHandling, rootPath) {
+ return function (file, args = [], sidecars = []) {
let filename;
- let directory = rootPath + randomKey();
+ const directory = rootPath + randomKey();
FS.mkdir(directory);
if (file instanceof File) {
filename = file.name;
- FS.mount(WORKERFS, { files: [file] }, directory);
+ FS.mount(WORKERFS, { files: [file, ...sidecars] }, directory);
} else if (file instanceof Blob) {
- filename = 'geotiff.tif';
- FS.mount(WORKERFS, { blobs: [{ name: filename, data: file }] }, directory);
+ filename = 'dataset';
+ FS.mount(WORKERFS, { blobs: [{ name: filename, data: file }, ...sidecars] }, directory);
+ } else if (file instanceof Object && 'name' in file && 'data' in file) {
+ filename = file.name;
+ FS.mount(
+ WORKERFS,
+ { blobs: [{ name: filename, data: file.data }, ...sidecars] },
+ directory
+ );
}
- let filePath = directory + '/' + filename;
+ const filePath = directory + '/' + filename;
- let datasetPtr = GDALOpen(filePath);
+ const datasetPtr = GDALOpenEx(
+ filePath,
+ // Open for update by default. We don't currently provide users a way to control this
+ // externally and the default is read-only.
+ GDAL_OF_UPDATE | GDAL_OF_VERBOSE_ERROR,
+ null,
+ null,
+ null
+ );
- let errorType = errorHandling.CPLGetLastErrorType();
+ const errorType = errorHandling.CPLGetLastErrorType();
// Check for errors; clean up and throw if error is detected
if (
@@ -29,7 +50,7 @@ export default function (GDALOpen, errorHandling, rootPath) {
) {
FS.unmount(directory);
FS.rmdir(directory);
- let message = errorHandling.CPLGetLastErrorMsg();
+ const message = errorHandling.CPLGetLastErrorMsg();
throw new Error('Error in GDALOpen: ' + message);
} else {
diff --git a/src/wrappers/gdalRasterize.js b/src/wrappers/gdalRasterize.js
index 6e80d79..0598813 100644
--- a/src/wrappers/gdalRasterize.js
+++ b/src/wrappers/gdalRasterize.js
@@ -4,7 +4,7 @@ import guessFileExtension from '../guessFileExtension.js';
import ParamParser from '../stringParamAllocator.js';
export default function (GDALRasterize, errorHandling, rootPath) {
- return function (geojson, args) {
+ return function (geojson, args = [], sidecars = []) {
let params = new ParamParser(args);
// Make a temporary file location to hold the geojson
diff --git a/src/wrappers/gdalTranslate.js b/src/wrappers/gdalTranslate.js
index 316a6b9..b0a6bdf 100644
--- a/src/wrappers/gdalTranslate.js
+++ b/src/wrappers/gdalTranslate.js
@@ -28,6 +28,7 @@ export default function (GDALTranslate, errorHandling, rootPath) {
optionsErrType === errorHandling.CPLErr.CEFailure ||
optionsErrType === errorHandling.CPLErr.CEFatal
) {
+ Module.ccall('GDALTranslateOptionsFree', null, ['number'], [translateOptionsPtr]);
params.deallocate();
const message = errorHandling.CPLGetLastErrorMsg();
diff --git a/src/wrappers/gdalVectorTranslate.js b/src/wrappers/gdalVectorTranslate.js
new file mode 100644
index 0000000..6828fa5
--- /dev/null
+++ b/src/wrappers/gdalVectorTranslate.js
@@ -0,0 +1,110 @@
+import randomKey from '../randomKey.js';
+import guessFileExtension from '../guessFileExtension.js';
+import ParamParser from '../stringParamAllocator.js';
+
+/* global Module, FS, MEMFS */
+export default function (GDALVectorTranslate, errorHandling, rootPath) {
+ // Args is expected to be an array of strings that could function as arguments to ogr2ogr
+ return function (dataset, args) {
+ const params = new ParamParser(args);
+
+ params.allocate();
+
+ // Whew, all finished. argPtrsArrayPtr is now the address of the start of the list of
+ // pointers in Emscripten heap space. Each pointer identifies the address of the start of a
+ // parameter string, also stored in heap space. This is the direct equivalent of a char **,
+ // which is what GDALVectorTranslateOptionsNew requires.
+ const translateOptionsPtr = Module.ccall(
+ 'GDALVectorTranslateOptionsNew',
+ 'number',
+ ['number', 'number'],
+ [params.argPtrsArrayPtr, null]
+ );
+
+ // Validate that the options were correct
+ const optionsErrType = errorHandling.CPLGetLastErrorType();
+
+ if (
+ optionsErrType === errorHandling.CPLErr.CEFailure ||
+ optionsErrType === errorHandling.CPLErr.CEFatal
+ ) {
+ Module.ccall('GDALVectorTranslateOptionsFree', null, ['number'], [translateOptionsPtr]);
+ params.deallocate();
+ const message = errorHandling.CPLGetLastErrorMsg();
+
+ throw new Error('Error in GDALVectorTranslate: ' + message);
+ }
+
+ // Now that we have our translate options, we need to make a file location to hold the
+ // output.
+ const directory = rootPath + randomKey();
+
+ FS.mkdir(directory);
+ // This makes it easier to remove later because we can just unmount rather than recursing
+ // through the whole directory structure.
+ FS.mount(MEMFS, {}, directory);
+ const filename = randomKey(8) + '.' + guessFileExtension(args);
+
+ const filePath = directory + '/' + filename;
+
+ // GDALVectorTranslate takes a list of input datasets, even though it can only ever have one
+ // dataset in that list, so we need to allocate space for that list and then store the
+ // dataset pointer in that list.
+ const dsPtrsArray = Uint32Array.from([dataset]);
+ const dsPtrsArrayPtr = Module._malloc(dsPtrsArray.length * dsPtrsArray.BYTES_PER_ELEMENT);
+
+ Module.HEAPU32.set(dsPtrsArray, dsPtrsArrayPtr / dsPtrsArray.BYTES_PER_ELEMENT);
+
+ // TODO: The last parameter is an int* that can be used to detect certain kinds of errors,
+ // but I'm not sure how it works yet and whether it gives the same or different information
+ // than CPLGetLastErrorType
+ // We can get some error information out of the final pbUsageError parameter, which is an
+ // int*, so malloc ourselves an int and set it to 0 (False)
+ const usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT);
+
+ Module.setValue(usageErrPtr, 0, 'i32');
+
+ // And then we can kick off the actual translation process.
+ const newDatasetPtr = GDALVectorTranslate(
+ filePath,
+ 0, // Destination dataset, which we don't use, so pass NULL
+ 1, // nSrcCount, which must always be 1 https://gdal.org/api/gdal_utils.html
+ dsPtrsArrayPtr, // This needs to be a list of input datasets
+ translateOptionsPtr,
+ usageErrPtr
+ );
+
+ const errorType = errorHandling.CPLGetLastErrorType();
+ // If we ever want to use the usage error pointer:
+ // let usageErr = Module.getValue(usageErrPtr, 'i32');
+
+ // The final set of cleanup we need to do, in a function to avoid writing it twice.
+ function cleanUp() {
+ Module.ccall('GDALVectorTranslateOptionsFree', null, ['number'], [translateOptionsPtr]);
+ Module._free(usageErrPtr);
+ Module._free(dsPtrsArrayPtr);
+ params.deallocate();
+ }
+
+ // Check for errors; clean up and throw if error is detected
+ if (
+ errorType === errorHandling.CPLErr.CEFailure ||
+ errorType === errorHandling.CPLErr.CEFatal
+ ) {
+ cleanUp();
+ const message = errorHandling.CPLGetLastErrorMsg();
+
+ throw new Error('Error in GDALVectorTranslate: ' + message);
+ } else {
+ const result = {
+ datasetPtr: newDatasetPtr,
+ filePath: filePath,
+ directory: directory,
+ filename: filename,
+ };
+
+ cleanUp();
+ return result;
+ }
+ };
+}
diff --git a/test/assets/geom.geojson b/test/assets/geom.geojson
new file mode 100644
index 0000000..3194cb5
--- /dev/null
+++ b/test/assets/geom.geojson
@@ -0,0 +1,10 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": { "type": "Point", "coordinates": [-75.16384363174437, 39.952533336963306] }
+ }
+ ]
+}
diff --git a/test/assets/point.dbf b/test/assets/point.dbf
new file mode 100644
index 0000000..982f2c3
Binary files /dev/null and b/test/assets/point.dbf differ
diff --git a/test/assets/point.prj b/test/assets/point.prj
new file mode 100644
index 0000000..a30c00a
--- /dev/null
+++ b/test/assets/point.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file
diff --git a/test/assets/point.shp b/test/assets/point.shp
new file mode 100644
index 0000000..995cb30
Binary files /dev/null and b/test/assets/point.shp differ
diff --git a/test/assets/point.shx b/test/assets/point.shx
new file mode 100644
index 0000000..39c837c
Binary files /dev/null and b/test/assets/point.shx differ
diff --git a/test/loam.spec.js b/test/loam.spec.js
index 917e747..07c9926 100644
--- a/test/loam.spec.js
+++ b/test/loam.spec.js
@@ -2,6 +2,11 @@
const tinyTifPath = '/base/test/assets/tiny.tif';
const tinyDEMPath = '/base/test/assets/tiny_dem.tif';
const invalidTifPath = 'base/test/assets/not-a-tiff.bytes';
+const geojsonPath = '/base/test/assets/geom.geojson';
+const shpPath = '/base/test/assets/point.shp';
+const shxPath = '/base/test/assets/point.shx';
+const dbfPath = '/base/test/assets/point.dbf';
+const prjPath = '/base/test/assets/point.prj';
const epsg4326 =
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]';
const epsg3857 =
@@ -107,7 +112,45 @@ describe('Given that loam exists', () => {
});
describe('calling open with a File', function () {
- it('should return a GDALDataset');
+ it('should return a GDALDataset', () => {
+ return xhrAsPromiseBlob(tinyTifPath)
+ .then((tifBlob) => new File([tifBlob], 'tinyTif.tif'))
+ .then((tinyTifFile) => loam.open(tinyTifFile))
+ .then((ds) => {
+ expect(ds).to.be.an.instanceof(loam.GDALDataset);
+ });
+ });
+ });
+
+ describe('calling open with a vector', function () {
+ it('should return a GDALDataset', () => {
+ return xhrAsPromiseBlob(geojsonPath)
+ .then((geojsonBlob) => loam.open(geojsonBlob))
+ .then((ds) => {
+ expect(ds).to.be.an.instanceof(loam.GDALDataset);
+ });
+ });
+ });
+
+ describe('calling open with a multi-file format', function () {
+ it('should return a GDALDataset', () => {
+ return Promise.all([
+ xhrAsPromiseBlob(shpPath),
+ xhrAsPromiseBlob(shxPath),
+ xhrAsPromiseBlob(dbfPath),
+ xhrAsPromiseBlob(prjPath),
+ ])
+ .then(([shpBlob, shxBlob, dbfBlob, prjBlob]) =>
+ loam.open({ name: 'shp.shp', data: shpBlob }, [
+ { name: 'shp.shx', data: shxBlob },
+ { name: 'shp.dbf', data: dbfBlob },
+ { name: 'shp.prj', data: prjBlob },
+ ])
+ )
+ .then((ds) => {
+ expect(ds).to.be.an.instanceof(loam.GDALDataset);
+ });
+ });
});
describe('calling reproject()', function () {
@@ -128,7 +171,7 @@ describe('Given that loam exists', () => {
});
});
- describe('calling count()', function () {
+ describe('calling count() on a raster', function () {
it('should return the number of bands in the GeoTiff', () => {
return xhrAsPromiseBlob(tinyTifPath).then((tifBlob) =>
loam.open(tifBlob).then((ds) => {
@@ -138,6 +181,36 @@ describe('Given that loam exists', () => {
});
});
+ describe('calling count() on a vector dataset', function () {
+ it('should return 0', () => {
+ return xhrAsPromiseBlob(geojsonPath).then((geojsonBlob) =>
+ loam.open(geojsonBlob).then((ds) => {
+ return ds.count().then((count) => expect(count).to.equal(0));
+ })
+ );
+ });
+ });
+
+ describe('calling layerCount() on a raster', function () {
+ it('should return 0', () => {
+ return xhrAsPromiseBlob(tinyTifPath).then((tifBlob) =>
+ loam.open(tifBlob).then((ds) => {
+ return ds.layerCount().then((count) => expect(count).to.equal(0));
+ })
+ );
+ });
+ });
+
+ describe('calling layerCount() on a vector dataset', function () {
+ it('should return 1', () => {
+ return xhrAsPromiseBlob(geojsonPath).then((geojsonBlob) =>
+ loam.open(geojsonBlob).then((ds) => {
+ return ds.layerCount().then((count) => expect(count).to.equal(1));
+ })
+ );
+ });
+ });
+
describe('calling width()', function () {
it('should return the x-size of the GeoTiff', () => {
return xhrAsPromiseBlob(tinyTifPath)
@@ -156,6 +229,80 @@ describe('Given that loam exists', () => {
});
});
+ describe('calling bandMinimum()', function () {
+ it('should return the minimum possible value of the raster band', () => {
+ return xhrAsPromiseBlob(tinyTifPath)
+ .then((tifBlob) => loam.open(tifBlob))
+ .then((ds) => ds.bandMinimum(1))
+ .then((min) => expect(min).to.equal(0));
+ });
+ });
+
+ describe('calling bandMaximum()', function () {
+ it('should return the maximum possible value of the raster band', () => {
+ return xhrAsPromiseBlob(tinyTifPath)
+ .then((tifBlob) => loam.open(tifBlob))
+ .then((ds) => ds.bandMaximum(1))
+ .then((max) => expect(max).to.equal(255)); // Determined with gdalinfo
+ });
+ });
+
+ describe('calling bandStatistics()', function () {
+ it('should return the statistics for the raster band', () => {
+ return xhrAsPromiseBlob(tinyTifPath)
+ .then((tifBlob) => loam.open(tifBlob))
+ .then((ds) => ds.bandStatistics(1))
+ .then((stats) => {
+ expect(stats.minimum).to.equal(15);
+ expect(stats.maximum).to.equal(255);
+ expect(stats.median).to.be.approximately(246.52, 0.01);
+ expect(stats.stdDev).to.be.approximately(39.941, 0.01);
+ });
+ });
+ });
+
+ describe('calling bandNoDataValue()', function () {
+ it('should return the no-data value of the raster band', () => {
+ return xhrAsPromiseBlob(tinyTifPath)
+ .then((tifBlob) => loam.open(tifBlob))
+ .then((ds) => ds.bandNoDataValue(1))
+ .then((ndValue) => expect(ndValue).to.equal(0)); // Determined with gdalinfo
+ });
+ });
+
+ describe('calling bandDataType()', function () {
+ it('should return the data type of the raster band for all band types', () => {
+ const validDataTypes = [
+ 'Byte',
+ 'UInt16',
+ 'Int16',
+ 'UInt32',
+ 'Int32',
+ 'Float32',
+ 'Float64',
+ 'CInt16',
+ 'CInt32',
+ 'CFloat32',
+ 'CFloat64',
+ ];
+ return (
+ xhrAsPromiseBlob(tinyTifPath)
+ .then((tifBlob) => loam.open(tifBlob))
+ // Create an array of datasources that each has been converted to one of the different
+ // valid data types using gdal_translate
+ .then((ds) => Promise.all(validDataTypes.map((dt) => ds.convert(['-ot', dt]))))
+ // Then pull the data types back out of the converted datasources...
+ .then((everyDataTypeDataset) =>
+ Promise.all(everyDataTypeDataset.map((dtDs) => dtDs.bandDataType(1)))
+ )
+ // ...and expect that we get the same set of data types as we put in.
+ .then((everyDataTypeResult) =>
+ expect(everyDataTypeResult).to.deep.equal(validDataTypes)
+ )
+ );
+ });
+ });
+
describe('calling wkt()', function () {
it("should return the GeoTiff's WKT CRS string", () => {
return xhrAsPromiseBlob(tinyTifPath)
@@ -225,6 +372,30 @@ describe('Given that loam exists', () => {
});
});
+ describe('calling vectorConvert', function () {
+ it('should succeed and return a new Dataset in the new format', function () {
+ return Promise.all([
+ xhrAsPromiseBlob(shpPath),
+ xhrAsPromiseBlob(shxPath),
+ xhrAsPromiseBlob(dbfPath),
+ xhrAsPromiseBlob(prjPath),
+ ])
+ .then(([shpBlob, shxBlob, dbfBlob, prjBlob]) =>
+ loam.open({ name: 'shp.shp', data: shpBlob }, [
+ { name: 'shp.shx', data: shxBlob },
+ { name: 'shp.dbf', data: dbfBlob },
+ { name: 'shp.prj', data: prjBlob },
+ ])
+ )
+ .then((ds) => ds.vectorConvert(['-f', 'GeoJSON']))
+ .then((newDs) => newDs.bytes())
+ .then((jsonBytes) => {
+ const utf8Decoder = new TextDecoder();
+ expect(utf8Decoder.decode(jsonBytes)).to.include('FeatureCollection');
+ });
+ });
+ });
+
describe('calling warp', function () {
it('should succeed and return a new Dataset that has been warped', function () {
return (
@@ -301,6 +472,20 @@ describe('Given that loam exists', () => {
});
});
+ describe('calling bandDataType() with incorrect band number', function () {
+ it('should fail and return an error message', function () {
+ return xhrAsPromiseBlob(tinyTifPath)
+ .then((tinyTif) => loam.open(tinyTif))
+ .then((ds) => ds.bandDataType(2))
+ .then(
+ () => {
+ throw new Error('bandDataType promise should have been rejected');
+ },
+ (error) => expect(error.message).to.include("Pointer 'hBand' is NULL")
+ );
+ });
+ });
+
describe('calling convert with invalid arguments', function () {
it('should fail and return an error message', function () {
return xhrAsPromiseBlob(tinyTifPath)
@@ -334,7 +519,7 @@ describe('Given that loam exists', () => {
' instead.'
);
},
- (error) => expect(error.message).to.include('Failed to lookup UOM CODE 0')
+ (error) => expect(error.message).to.include('source or target SRS failed')
);
});
});
@@ -482,4 +667,49 @@ describe('Given that loam exists', () => {
);
});
});
+
+ describe('calling render() with a vector dataset', function () {
+ it('should fail and return a useful error message', function () {
+ return xhrAsPromiseBlob(geojsonPath)
+ .then((geojsonBlob) => loam.open(geojsonBlob))
+ .then((ds) => ds.render('hillshade', ['-of', 'PNG']))
+ .then((ds) => ds.bytes())
+ .then(
+ (result) => {
+ throw new Error(
+ `render() should have failed for a vector dataset but got ${result}`
+ );
+ },
+ (error) => expect(error.message).to.include('Error in GDALDEMProcessing')
+ );
+ });
+ });
+
+ describe('calling convert with a vector dataset', function () {
+ it('should fail because the vector dataset has no raster bands', function () {
+ return Promise.all([
+ xhrAsPromiseBlob(shpPath),
+ xhrAsPromiseBlob(shxPath),
+ xhrAsPromiseBlob(dbfPath),
+ xhrAsPromiseBlob(prjPath),
+ ])
+ .then(([shpBlob, shxBlob, dbfBlob, prjBlob]) =>
+ loam.open({ name: 'shp.shp', data: shpBlob }, [
+ { name: 'shp.shx', data: shxBlob },
+ { name: 'shp.dbf', data: dbfBlob },
+ { name: 'shp.prj', data: prjBlob },
+ ])
+ )
+ .then((ds) => ds.convert(['-outsize', '200%', '200%']))
+ .then((newDs) => newDs.width())
+ .then(
+ (result) => {
+ throw new Error(
+ `convert() should have failed for vector dataset but got ${result}`
+ );
+ },
+ (error) => expect(error.message).to.include('Input file has no bands')
+ );
+ });
+ });
});
diff --git a/yarn.lock b/yarn.lock
index 969fc6d..366748d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3935,10 +3935,10 @@ __metadata:
languageName: node
linkType: hard
-"gdal-js@npm:2.0.0":
- version: 2.0.0
- resolution: "gdal-js@npm:2.0.0"
- checksum: b2ca359b21ff9e462621f0c76cff89970b57837a0d02c1fc333864b1142a0fa59596de860875d8bc77b9f655a7e4b0e85b01f3c58bacb1790bedfca2d57b23d5
+"gdal-js@npm:2.1.0":
+ version: 2.1.0
+ resolution: "gdal-js@npm:2.1.0"
+ checksum: 7bab71d3621c7f10cd6f0c956ea194a0b979055c333920a56849e1a7a5c17755add590bf92536ef64b5df8f39bf3e3798dffd30066999d4b9c0ff906c705b916
languageName: node
linkType: hard
@@ -4792,7 +4792,7 @@ __metadata:
chai-as-promised: ^7.1.1
eslint: ^7.31.0
eslint-webpack-plugin: ^3.0.1
- gdal-js: 2.0.0
+ gdal-js: 2.1.0
karma: ^6.4.1
karma-babel-preprocessor: ^8.0.2
karma-chai: ^0.1.0