From c0e52e05c5f55209c408ab96d39152dbfc6c7b6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:25:13 +0000 Subject: [PATCH 01/11] Bump http-proxy from 1.18.0 to 1.18.1 Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.18.0 to 1.18.1. - [Release notes](https://github.com/http-party/node-http-proxy/releases) - [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md) - [Commits](https://github.com/http-party/node-http-proxy/compare/1.18.0...1.18.1) Signed-off-by: dependabot[bot] --- yarn.lock | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index fdf7b9f..382d4bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1618,7 +1618,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@3.2.6, debug@^3.0.0, debug@^3.1.0, debug@^3.2.6: +debug@3.2.6, debug@^3.1.0, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -2092,9 +2092,9 @@ event-emitter@~0.3.5: es5-ext "~0.10.14" eventemitter3@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" - integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.0.0: version "3.0.0" @@ -2306,11 +2306,9 @@ flatted@^2.0.0: integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== follow-redirects@^1.0.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" - integrity sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A== - dependencies: - debug "^3.0.0" + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== for-in@^1.0.2: version "1.0.2" @@ -2595,9 +2593,9 @@ http-errors@1.7.2: toidentifier "1.0.0" http-proxy@^1.13.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" - integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: eventemitter3 "^4.0.0" follow-redirects "^1.0.0" From 481f1f784ef132910a17ee72d89f84a230fd88b7 Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Thu, 24 Sep 2020 16:01:40 -0400 Subject: [PATCH 02/11] Add Contributing section to README --- CHANGELOG | 1 + README.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 429661e..af5cd43 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ ## Upcoming release +- Add information on contributing to the README ## 1.0.0-rc.1 (2020-07-24) - Add loam.rasterize() wrapper for GDALRasterize() diff --git a/README.md b/README.md index 4607c51..feddd61 100644 --- a/README.md +++ b/README.md @@ -145,3 +145,7 @@ After cloning, 2. `yarn dev` and in another session `yarn test:watch` Built assets are placed in `lib`. + +# Contributing + +Contributions are welcomed! Please feel free to work on any of the open issues or open an issue describing the changes you'd like to make. All contributions will be licensed under the Apache License, as per the [GitHub Terms of Service](https://docs.github.com/en/github/site-policy/github-terms-of-service#6-contributions-under-repository-license). From 443f773719ecc3d77a6a31bc8d80ca032253dcc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2020 22:57:19 +0000 Subject: [PATCH 03/11] Bump elliptic from 6.5.1 to 6.5.3 Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.1 to 6.5.3. - [Release notes](https://github.com/indutny/elliptic/releases) - [Commits](https://github.com/indutny/elliptic/compare/v6.5.1...v6.5.3) Signed-off-by: dependabot[bot] --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2590423..fdf7b9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1003,9 +1003,9 @@ bluebird@^3.3.0: integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== body-parser@^1.16.1: version "1.19.0" @@ -1770,9 +1770,9 @@ electron-to-chromium@^1.3.295: integrity sha512-KxlGE9GcZTv7xGwYJGMEABHJq2JuTMNF7jD8NwHk6sBY226mW+Dyp9kZmA2Od9tKHMCS7ltPnqFg+zq3jTWN7Q== elliptic@^6.0.0: - version "6.5.1" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.1.tgz#c380f5f909bf1b9b4428d028cd18d3b0efd6b52b" - integrity sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg== + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" From 51f846b8ca8955581d20d3a295759813462fb934 Mon Sep 17 00:00:00 2001 From: Henry Unite Date: Fri, 31 Jul 2020 22:25:10 -0400 Subject: [PATCH 04/11] Use a utility class for string parameter mem alloc --- src/stringParamAllocator.js | 70 ++++++++++++++++++++++++++++++++ src/wrappers/gdalRasterize.js | 75 +++++++++++++++-------------------- src/wrappers/gdalTranslate.js | 71 ++++++++++++++------------------- src/wrappers/gdalWarp.js | 73 +++++++++++++--------------------- 4 files changed, 159 insertions(+), 130 deletions(-) create mode 100644 src/stringParamAllocator.js diff --git a/src/stringParamAllocator.js b/src/stringParamAllocator.js new file mode 100644 index 0000000..0bc5571 --- /dev/null +++ b/src/stringParamAllocator.js @@ -0,0 +1,70 @@ +import { isArrayAllStrings } from './validation.js'; + +/* global Module */ +export default class ParamParser { + constructor(args) { + let self = this; + + if (!isArrayAllStrings(args)) { + throw new Error('All items in the argument list must be strings'); + } + + self.args = args; + } + + allocate() { + const self = this; + + // So first, we need to allocate Emscripten heap space sufficient to store each string as a + // null-terminated C string. + // Because the C function signature is char **, this array of pointers is going to need to + // get copied into Emscripten heap space eventually, so we're going to prepare by storing + // the pointers as a typed array so that we can more easily copy it into heap space later. + let argPtrsArray = Uint32Array.from( + self.args + .map((argStr) => { + return Module._malloc(Module.lengthBytesUTF8(argStr) + 1); // +1 for the null terminator byte + }) + .concat([0]) + ); + // ^ In addition to each individual argument being null-terminated, the GDAL docs specify that + // GDALTranslateOptionsNew takes its options passed in as a null-terminated array of + // pointers, so we have to add on a null (0) byte at the end. + + // Next, we need to write each string from the JS string array into the Emscripten heap space + // we've allocated for it. + self.args.forEach(function (argStr, i) { + Module.stringToUTF8( + argStr, + argPtrsArray[i], + Module.lengthBytesUTF8(argStr) + 1 + ); + }); + + // Now, as mentioned above, we also need to copy the pointer array itself into heap space. + let argPtrsArrayPtr = Module._malloc( + argPtrsArray.length * argPtrsArray.BYTES_PER_ELEMENT + ); + + Module.HEAPU32.set( + argPtrsArray, + argPtrsArrayPtr / argPtrsArray.BYTES_PER_ELEMENT + ); + + self.argPtrsArray = argPtrsArray; + self.argPtrsArrayPtr = argPtrsArrayPtr; + } + + deallocate() { + const self = this; + + Module._free(self.argPtrsArrayPtr); + // Don't try to free the null terminator byte + self.argPtrsArray + .subarray(0, self.argPtrsArray.length - 1) + .forEach((ptr) => Module._free(ptr)); + delete self.argPtrsArray; + delete self.argPtrsArrayPtr; + } + +} diff --git a/src/wrappers/gdalRasterize.js b/src/wrappers/gdalRasterize.js index ace6f53..7ae83c0 100644 --- a/src/wrappers/gdalRasterize.js +++ b/src/wrappers/gdalRasterize.js @@ -1,65 +1,48 @@ /* global Module, FS, MEMFS */ import randomKey from '../randomKey.js'; import guessFileExtension from '../guessFileExtension.js'; -import { isArrayAllStrings } from '../validation.js'; +import ParamParser from '../stringParamAllocator.js'; export default function (GDALRasterize, errorHandling, rootPath) { return function (geojson, args) { - if (!isArrayAllStrings(args)) { - throw new Error('All items in the argument list must be strings'); - } + let params = new ParamParser(args); + // Make a temporary file location to hold the geojson const geojsonPath = rootPath + randomKey() + '.geojson'; FS.writeFile(geojsonPath, JSON.stringify(geojson)); // Append the geojson path to the args so that it's read as the source. // Open the geojson using GDALOpenEx, which can handle non-raster sources. - const datasetPtr = Module.ccall('GDALOpenEx', 'number', ['string'], [geojsonPath]); - - // TODO: Break this out into a util function; this pattern is showing up in several places now. - // So first, we need to allocate Emscripten heap space sufficient to store each string as a - // null-terminated C string. - // Because the C function signature is char **, this array of pointers is going to need to - // get copied into Emscripten heap space eventually, so we're going to prepare by storing - // the pointers as a typed array so that we can more easily copy it into heap space later. - const argPtrsArray = Uint32Array.from(args.map(argStr => { - return Module._malloc(Module.lengthBytesUTF8(argStr) + 1); // +1 for the null terminator byte - }).concat([0])); - // ^ In addition to each individual argument being null-terminated, the GDAL docs specify that - // GDALRasterizeOptionsNew takes its options passed in as a null-terminated array of - // pointers, so we have to add on a null (0) byte at the end. - - // Next, we need to write each string from the JS string array into the Emscripten heap space - // we've allocated for it. - args.forEach(function (argStr, i) { - Module.stringToUTF8(argStr, argPtrsArray[i], Module.lengthBytesUTF8(argStr) + 1); - }); - - // Now, as mentioned above, we also need to copy the pointer array itself into heap space. - let argPtrsArrayPtr = Module._malloc(argPtrsArray.length * argPtrsArray.BYTES_PER_ELEMENT); - - Module.HEAPU32.set(argPtrsArray, argPtrsArrayPtr / argPtrsArray.BYTES_PER_ELEMENT); + const datasetPtr = Module.ccall( + 'GDALOpenEx', + 'number', + ['string'], + [geojsonPath] + ); + + 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 GDALRasterizeOptionsNew requires. - - let rasterizeOptionsPtr = Module.ccall('GDALRasterizeOptionsNew', 'number', + let rasterizeOptionsPtr = Module.ccall( + 'GDALRasterizeOptionsNew', + 'number', ['number', 'number'], - [argPtrsArrayPtr, null] + [params.argPtrsArrayPtr, null] ); // Validate that the options were correct let optionsErrType = errorHandling.CPLGetLastErrorType(); - if (optionsErrType === errorHandling.CPLErr.CEFailure || - optionsErrType === errorHandling.CPLErr.CEFatal) { + if ( + optionsErrType === errorHandling.CPLErr.CEFailure || + optionsErrType === errorHandling.CPLErr.CEFatal + ) { Module.ccall('GDALClose', 'number', ['number'], datasetPtr); FS.unlink(geojsonPath); - Module._free(argPtrsArrayPtr); - // Don't try to free the null terminator byte - argPtrsArray.subarray(0, argPtrsArray.length - 1).forEach(ptr => Module._free(ptr)); + params.deallocate(); const message = errorHandling.CPLGetLastErrorMsg(); throw new Error(message); @@ -102,16 +85,21 @@ export default function (GDALRasterize, errorHandling, rootPath) { function cleanUp() { Module.ccall('GDALClose', 'number', ['number'], datasetPtr); FS.unlink(geojsonPath); - Module.ccall('GDALRasterizeOptionsFree', null, ['number'], [rasterizeOptionsPtr]); - Module._free(argPtrsArrayPtr); + Module.ccall( + 'GDALRasterizeOptionsFree', + null, + ['number'], + [rasterizeOptionsPtr] + ); Module._free(usageErrPtr); - // Don't try to free the null terminator byte - argPtrsArray.subarray(0, argPtrsArray.length - 1).forEach(ptr => Module._free(ptr)); + params.deallocate(); } // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); @@ -125,7 +113,6 @@ export default function (GDALRasterize, errorHandling, rootPath) { }; cleanUp(); - return result; } }; diff --git a/src/wrappers/gdalTranslate.js b/src/wrappers/gdalTranslate.js index 26d0baf..ebfa4e3 100644 --- a/src/wrappers/gdalTranslate.js +++ b/src/wrappers/gdalTranslate.js @@ -1,54 +1,34 @@ import randomKey from '../randomKey.js'; import guessFileExtension from '../guessFileExtension.js'; -import { isArrayAllStrings } from '../validation.js'; +import ParamParser from '../stringParamAllocator.js'; /* global Module, FS, MEMFS */ export default function (GDALTranslate, errorHandling, rootPath) { // Args is expected to be an array of strings that could function as arguments to gdal_translate return function (dataset, args) { - if (!isArrayAllStrings(args)) { - throw new Error('All items in the argument list must be strings'); - } - // So first, we need to allocate Emscripten heap space sufficient to store each string as a - // null-terminated C string. - // Because the C function signature is char **, this array of pointers is going to need to - // get copied into Emscripten heap space eventually, so we're going to prepare by storing - // the pointers as a typed array so that we can more easily copy it into heap space later. - let argPtrsArray = Uint32Array.from(args.map(argStr => { - return Module._malloc(Module.lengthBytesUTF8(argStr) + 1); // +1 for the null terminator byte - }).concat([0])); - // ^ In addition to each individual argument being null-terminated, the GDAL docs specify that - // GDALTranslateOptionsNew takes its options passed in as a null-terminated array of - // pointers, so we have to add on a null (0) byte at the end. - - // Next, we need to write each string from the JS string array into the Emscripten heap space - // we've allocated for it. - args.forEach(function (argStr, i) { - Module.stringToUTF8(argStr, argPtrsArray[i], Module.lengthBytesUTF8(argStr) + 1); - }); - - // Now, as mentioned above, we also need to copy the pointer array itself into heap space. - let argPtrsArrayPtr = Module._malloc(argPtrsArray.length * argPtrsArray.BYTES_PER_ELEMENT); - - Module.HEAPU32.set(argPtrsArray, argPtrsArrayPtr / argPtrsArray.BYTES_PER_ELEMENT); + let 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 GDALTranslateOptionsNew requires. - let translateOptionsPtr = Module.ccall('GDALTranslateOptionsNew', 'number', + let translateOptionsPtr = Module.ccall( + 'GDALTranslateOptionsNew', + 'number', ['number', 'number'], - [argPtrsArrayPtr, null] + [params.argPtrsArrayPtr, null] ); // Validate that the options were correct let optionsErrType = errorHandling.CPLGetLastErrorType(); - if (optionsErrType === errorHandling.CPLErr.CEFailure || - optionsErrType === errorHandling.CPLErr.CEFatal) { - Module._free(argPtrsArrayPtr); - // Don't try to free the null terminator byte - argPtrsArray.subarray(0, argPtrsArray.length - 1).forEach(ptr => Module._free(ptr)); + if ( + optionsErrType === errorHandling.CPLErr.CEFailure || + optionsErrType === errorHandling.CPLErr.CEFatal + ) { + params.deallocate(); const message = errorHandling.CPLGetLastErrorMsg(); throw new Error(message); @@ -74,7 +54,12 @@ export default function (GDALTranslate, errorHandling, rootPath) { let usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT); Module.setValue(usageErrPtr, 0, 'i32'); - let newDatasetPtr = GDALTranslate(filePath, dataset, translateOptionsPtr, usageErrPtr); + let newDatasetPtr = GDALTranslate( + filePath, + dataset, + translateOptionsPtr, + usageErrPtr + ); let errorType = errorHandling.CPLGetLastErrorType(); // If we ever want to use the usage error pointer: @@ -82,16 +67,21 @@ export default function (GDALTranslate, errorHandling, rootPath) { // The final set of cleanup we need to do, in a function to avoid writing it twice. function cleanUp() { - Module.ccall('GDALTranslateOptionsFree', null, ['number'], [translateOptionsPtr]); - Module._free(argPtrsArrayPtr); + Module.ccall( + 'GDALTranslateOptionsFree', + null, + ['number'], + [translateOptionsPtr] + ); Module._free(usageErrPtr); - // Don't try to free the null terminator byte - argPtrsArray.subarray(0, argPtrsArray.length - 1).forEach(ptr => Module._free(ptr)); + params.deallocate(); } // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); @@ -105,7 +95,6 @@ export default function (GDALTranslate, errorHandling, rootPath) { }; cleanUp(); - return result; } }; diff --git a/src/wrappers/gdalWarp.js b/src/wrappers/gdalWarp.js index e1e5f7e..7e715d1 100644 --- a/src/wrappers/gdalWarp.js +++ b/src/wrappers/gdalWarp.js @@ -1,60 +1,39 @@ import randomKey from '../randomKey.js'; import guessFileExtension from '../guessFileExtension.js'; -import { isArrayAllStrings } from '../validation.js'; +import ParamParser from '../stringParamAllocator.js'; /* global Module, FS, MEMFS */ export default function (GDALWarp, errorHandling, rootPath) { // Args is expected to be an array of strings that could function as arguments to gdal_translate return function (dataset, args) { - if (!isArrayAllStrings(args)) { - throw new Error('All items in the argument list must be strings'); - } - // So first, we need to allocate Emscripten heap space sufficient to store each string as a - // null-terminated C string. - // Because the C function signature is char **, this array of pointers is going to need to - // get copied into Emscripten heap space eventually, so we're going to prepare by storing - // the pointers as a typed array so that we can more easily copy it into heap space later. - let argPtrsArray = Uint32Array.from(args.map(argStr => { - return Module._malloc(Module.lengthBytesUTF8(argStr) + 1); // +1 for the null terminator byte - }).concat([0])); - // ^ In addition to each individual argument being null-terminated, the GDAL docs specify that - // GDALTranslateOptionsNew takes its options passed in as a null-terminated array of - // pointers, so we have to add on a null (0) byte at the end. - - // Next, we need to write each string from the JS string array into the Emscripten heap space - // we've allocated for it. - args.forEach(function (argStr, i) { - Module.stringToUTF8(argStr, argPtrsArray[i], Module.lengthBytesUTF8(argStr) + 1); - }); - - // Now, as mentioned above, we also need to copy the pointer array itself into heap space. - let argPtrsArrayPtr = Module._malloc(argPtrsArray.length * argPtrsArray.BYTES_PER_ELEMENT); - - Module.HEAPU32.set(argPtrsArray, argPtrsArrayPtr / argPtrsArray.BYTES_PER_ELEMENT); + let 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 GDALWarpAppOptionsNew requires. - let warpAppOptionsPtr = Module.ccall('GDALWarpAppOptionsNew', 'number', + let warpAppOptionsPtr = Module.ccall( + 'GDALWarpAppOptionsNew', + 'number', ['number', 'number'], - [argPtrsArrayPtr, null] + [params.argPtrsArrayPtr, null] ); // Validate that the options were correct let optionsErrType = errorHandling.CPLGetLastErrorType(); - if (optionsErrType === errorHandling.CPLErr.CEFailure || - optionsErrType === errorHandling.CPLErr.CEFatal) { - Module._free(argPtrsArrayPtr); - // Don't try to free the null terminator byte - argPtrsArray.subarray(0, argPtrsArray.length - 1).forEach(ptr => Module._free(ptr)); + if ( + optionsErrType === errorHandling.CPLErr.CEFailure || + optionsErrType === errorHandling.CPLErr.CEFatal + ) { + params.deallocate(); const message = errorHandling.CPLGetLastErrorMsg(); throw new Error(message); } - // Now that we have our translate options, we need to make a file location to hold the output. let directory = rootPath + '/' + randomKey(); FS.mkdir(directory); @@ -89,22 +68,27 @@ export default function (GDALWarp, errorHandling, rootPath) { usageErrPtr ); - let 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('GDALWarpAppOptionsFree', null, ['number'], [warpAppOptionsPtr]); - Module._free(argPtrsArrayPtr); + Module.ccall( + 'GDALWarpAppOptionsFree', + null, + ['number'], + [warpAppOptionsPtr] + ); Module._free(usageErrPtr); - // Don't try to free the null terminator byte - argPtrsArray.subarray(0, argPtrsArray.length - 1).forEach(ptr => Module._free(ptr)); + params.deallocate(); } + let errorType = errorHandling.CPLGetLastErrorType(); + // If we ever want to use the usage error pointer: + // let usageErr = Module.getValue(usageErrPtr, 'i32'); + // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); @@ -118,7 +102,6 @@ export default function (GDALWarp, errorHandling, rootPath) { }; cleanUp(); - return result; } }; From 15d4d46151a1ea383db019b7fb37780894e331be Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Wed, 30 Sep 2020 12:58:35 -0400 Subject: [PATCH 05/11] Add prettier to dev dependencies --- .eslintrc | 2 +- .prettierrc | 7 +++++++ package.json | 1 + yarn.lock | 5 +++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .prettierrc diff --git a/.eslintrc b/.eslintrc index 90b5103..8dabeef 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,7 +29,7 @@ "block-scoped-var": 2, "brace-style": [2, "1tbs", { "allowSingleLine": true }], "camelcase": [2, { "properties": "always" }], - "comma-dangle": [2, "never"], + "comma-dangle": [2, "always-multiline"], "comma-spacing": [2, { "before": false, "after": true }], "comma-style": [2, "last"], "complexity": 0, diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f6aa061 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, +} diff --git a/package.json b/package.json index d2f77f0..2bae764 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "karma-chrome-launcher": "^3.1.0", "karma-mocha": "^1.3.0", "mocha": "^6.0.0", + "prettier": "^2.1.2", "webpack": "^3.10.0", "yargs": "^14.0.0" }, diff --git a/yarn.lock b/yarn.lock index fdf7b9f..34c7aa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3947,6 +3947,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== + private@^0.1.6: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" From 7135aa60c5f9b20b91428fd2999218c21cfe0c7f Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Wed, 30 Sep 2020 13:49:13 -0400 Subject: [PATCH 06/11] Apply autoformatting to code --- CHANGELOG | 1 + src/api.js | 21 ++- src/gdalDataset.js | 17 +- src/guessFileExtension.js | 2 +- src/stringParamAllocator.js | 16 +- src/validation.js | 2 +- src/worker.js | 68 +++++--- src/workerCommunication.js | 11 +- src/wrappers/gdalClose.js | 7 +- src/wrappers/gdalGetGeoTransform.js | 6 +- src/wrappers/gdalGetProjectionRef.js | 6 +- src/wrappers/gdalGetRasterCount.js | 6 +- src/wrappers/gdalGetRasterXSize.js | 6 +- src/wrappers/gdalGetRasterYSize.js | 6 +- src/wrappers/gdalOpen.js | 8 +- src/wrappers/gdalRasterize.js | 18 +- src/wrappers/gdalTranslate.js | 20 +-- src/wrappers/gdalWarp.js | 13 +- src/wrappers/reproject.js | 41 +++-- test/loam.spec.js | 242 ++++++++++++++++----------- 20 files changed, 292 insertions(+), 225 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index af5cd43..26eff2e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ ## Upcoming release - Add information on contributing to the README +- Apply code auto-formatting ## 1.0.0-rc.1 (2020-07-24) - Add loam.rasterize() wrapper for GDALRasterize() diff --git a/src/api.js b/src/api.js index 3c79fc8..f8673b7 100644 --- a/src/api.js +++ b/src/api.js @@ -3,21 +3,32 @@ import { GDALDataset } from './gdalDataset.js'; function open(file) { return new Promise((resolve, reject) => { - const ds = new GDALDataset({func: 'GDALOpen', src: file, args: []}); + const ds = new GDALDataset({ func: 'GDALOpen', src: file, args: [] }); - return ds.open().then(() => resolve(ds), (reason) => reject(reason)); + return ds.open().then( + () => resolve(ds), + (reason) => reject(reason) + ); }); } function rasterize(geojson, args) { return new Promise((resolve, reject) => { - resolve(new GDALDataset({func: 'GDALRasterize', src: geojson, args: args})); + resolve(new GDALDataset({ func: 'GDALRasterize', src: geojson, args: args })); }); } function reproject(fromCRS, toCRS, coords) { - var xCoords = new Float64Array(coords.map(function (pair) { return pair[0]; })); - var yCoords = new Float64Array(coords.map(function (pair) { return pair[1]; })); + var xCoords = new Float64Array( + coords.map(function (pair) { + return pair[0]; + }) + ); + var yCoords = new Float64Array( + coords.map(function (pair) { + return pair[1]; + }) + ); return runOnWorker('LoamReproject', [fromCRS, toCRS, xCoords, yCoords]); } diff --git a/src/gdalDataset.js b/src/gdalDataset.js index 0d96165..2c975b6 100644 --- a/src/gdalDataset.js +++ b/src/gdalDataset.js @@ -50,19 +50,30 @@ export class GDALDataset { convert(args) { return new Promise((resolve, reject) => { - resolve(new GDALDataset(this.source, this.operations.concat(new DatasetOperation('GDALTranslate', args)))); + resolve( + new GDALDataset( + this.source, + this.operations.concat(new DatasetOperation('GDALTranslate', args)) + ) + ); }); } warp(args) { return new Promise((resolve, reject) => { - resolve(new GDALDataset(this.source, this.operations.concat(new DatasetOperation('GDALWarp', args)))); + resolve( + new GDALDataset( + this.source, + this.operations.concat(new DatasetOperation('GDALWarp', args)) + ) + ); }); } close() { return new Promise((resolve, reject) => { - const warningMsg = 'It is not necessary to call close() on a Loam dataset. This is a no-op'; + const warningMsg = + 'It is not necessary to call close() on a Loam dataset. This is a no-op'; console.warn(warningMsg); resolve([]); diff --git a/src/guessFileExtension.js b/src/guessFileExtension.js index 309f5d1..4a6a550 100644 --- a/src/guessFileExtension.js +++ b/src/guessFileExtension.js @@ -2,7 +2,7 @@ export default function guessFileExtension(args) { const supportedFormats = { PNG: 'png', JPEG: 'jpg', - GTiff: 'tif' + GTiff: 'tif', }; // Match GDAL 2.1 behavior: if output format is unspecified, the output format is GeoTiff diff --git a/src/stringParamAllocator.js b/src/stringParamAllocator.js index 0bc5571..059a11e 100644 --- a/src/stringParamAllocator.js +++ b/src/stringParamAllocator.js @@ -34,22 +34,13 @@ export default class ParamParser { // Next, we need to write each string from the JS string array into the Emscripten heap space // we've allocated for it. self.args.forEach(function (argStr, i) { - Module.stringToUTF8( - argStr, - argPtrsArray[i], - Module.lengthBytesUTF8(argStr) + 1 - ); + Module.stringToUTF8(argStr, argPtrsArray[i], Module.lengthBytesUTF8(argStr) + 1); }); // Now, as mentioned above, we also need to copy the pointer array itself into heap space. - let argPtrsArrayPtr = Module._malloc( - argPtrsArray.length * argPtrsArray.BYTES_PER_ELEMENT - ); + let argPtrsArrayPtr = Module._malloc(argPtrsArray.length * argPtrsArray.BYTES_PER_ELEMENT); - Module.HEAPU32.set( - argPtrsArray, - argPtrsArrayPtr / argPtrsArray.BYTES_PER_ELEMENT - ); + Module.HEAPU32.set(argPtrsArray, argPtrsArrayPtr / argPtrsArray.BYTES_PER_ELEMENT); self.argPtrsArray = argPtrsArray; self.argPtrsArrayPtr = argPtrsArrayPtr; @@ -66,5 +57,4 @@ export default class ParamParser { delete self.argPtrsArray; delete self.argPtrsArrayPtr; } - } diff --git a/src/validation.js b/src/validation.js index f217f40..9c31223 100644 --- a/src/validation.js +++ b/src/validation.js @@ -1,5 +1,5 @@ function isArrayAllStrings(args) { - return args.every(arg => typeof arg === 'string'); + return args.every((arg) => typeof arg === 'string'); } export { isArrayAllStrings }; diff --git a/src/worker.js b/src/worker.js index d8f535f..3f40d2b 100644 --- a/src/worker.js +++ b/src/worker.js @@ -30,30 +30,42 @@ let errorHandling = { CEDebug: 1, CEWarning: 2, CEFailure: 3, - CEFatal: 4 + CEFatal: 4, }, // These will be populated by onRuntimeInitialized, below CPLErrorReset: null, CPLGetLastErrorMsg: null, CPLGetLastErrorNo: null, - CPLGetLastErrorType: null + CPLGetLastErrorType: null, }; self.Module = { - 'print': function (text) { console.log('stdout: ' + text); }, - 'printErr': function (text) { console.log('stderr: ' + text); }, + 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 () { + onRuntimeInitialized: function () { try { // Initialize GDAL self.Module.ccall('GDALAllRegister', null, [], []); // Set up error handling errorHandling.CPLErrorReset = self.Module.cwrap('CPLErrorReset', null, []); - errorHandling.CPLGetLastErrorMsg = self.Module.cwrap('CPLGetLastErrorMsg', 'string', []); + errorHandling.CPLGetLastErrorMsg = self.Module.cwrap( + 'CPLGetLastErrorMsg', + 'string', + [] + ); errorHandling.CPLGetLastErrorNo = self.Module.cwrap('CPLGetLastErrorNo', 'number', []); - errorHandling.CPLGetLastErrorType = self.Module.cwrap('CPLGetLastErrorType', 'number', []); + errorHandling.CPLGetLastErrorType = self.Module.cwrap( + 'CPLGetLastErrorType', + 'number', + [] + ); // Get a "function pointer" to the built-in quiet error handler so that errors don't // cause tons of console noise. const cplQuietFnPtr = addFunction( @@ -80,7 +92,7 @@ self.Module = { 'number', // GDALDatasetH destination dataset or NULL 'number', // GDALDatasetH source dataset or NULL 'number', // GDALRasterizeOptions * or NULL - 'number' // int * to use for error reporting + 'number', // int * to use for error reporting ]), errorHandling, DATASETPATH @@ -106,9 +118,7 @@ self.Module = { errorHandling ); registry.GDALGetGeoTransform = wGDALGetGeoTransform( - self.Module.cwrap('GDALGetGeoTransform', 'number', [ - 'number', 'number' - ]), + self.Module.cwrap('GDALGetGeoTransform', 'number', ['number', 'number']), errorHandling ); registry.GDALTranslate = wGDALTranslate( @@ -116,7 +126,7 @@ self.Module = { 'string', // Output path 'number', // GDALDatasetH source dataset 'number', // GDALTranslateOptions * - 'number' // int * to use for error reporting + 'number', // int * to use for error reporting ]), errorHandling, DATASETPATH @@ -128,7 +138,7 @@ self.Module = { 'number', // Number of input datasets 'number', // GDALDatasetH * list of source datasets 'number', // GDALWarpAppOptions * - 'number' // int * to use for error reporting + 'number', // int * to use for error reporting ]), errorHandling, DATASETPATH @@ -136,7 +146,7 @@ self.Module = { registry.LoamFlushFS = function () { let datasetFolders = FS.lookupPath(DATASETPATH).node.contents; - Object.values(datasetFolders).forEach(node => { + Object.values(datasetFolders).forEach((node) => { FS.unmount(FS.getPath(node)); FS.rmdir(FS.getPath(node)); }); @@ -144,12 +154,12 @@ self.Module = { }; registry.LoamReproject = wReproject; initialized = true; - postMessage({ready: true}); + postMessage({ ready: true }); } catch (error) { console.error(error); - postMessage({error: error}); + postMessage({ error: error }); } - } + }, }; // Load gdal.js. This will populate the Module object, and then call @@ -168,7 +178,7 @@ function handleDatasetAccess(accessor, dataset) { // 3. Close open dataset, delete files. // 4. If forther operations, back to 2, otherwise pass open dataset along so the data can be // accessed. - for (const {func: op, args: args} of dataset.operations) { + for (const { func: op, args: args } of dataset.operations) { resultDs = registry[op](srcDs.datasetPtr, args); registry.GDALClose(srcDs.datasetPtr, srcDs.directory, srcDs.filePath); srcDs = resultDs; @@ -177,7 +187,12 @@ function handleDatasetAccess(accessor, dataset) { let result; if (accessor === 'LoamReadBytes') { - result = registry.GDALClose(resultDs.datasetPtr, resultDs.directory, resultDs.filePath, true); + result = registry.GDALClose( + resultDs.datasetPtr, + resultDs.directory, + resultDs.filePath, + true + ); } else if (accessor) { result = registry[accessor](resultDs.datasetPtr); registry.GDALClose(resultDs.datasetPtr, resultDs.directory, resultDs.filePath, false); @@ -197,7 +212,11 @@ function handleFunctionCall(func, args) { onmessage = function (msg) { if (!initialized) { - postMessage({success: false, message: 'Runtime not yet initialized', id: msg.data.id}); + postMessage({ + success: false, + message: 'Runtime not yet initialized', + id: msg.data.id, + }); return; } try { @@ -210,21 +229,22 @@ onmessage = function (msg) { } else { postMessage({ success: false, - message: 'Worker could not parse message: either func + args or accessor + dataset is required', - id: msg.data.id + message: + 'Worker could not parse message: either func + args or accessor + dataset is required', + id: msg.data.id, }); return; } postMessage({ success: true, result: result, - id: msg.data.id + id: msg.data.id, }); } catch (error) { postMessage({ success: false, message: error.message, - id: msg.data.id + id: msg.data.id, }); } }; diff --git a/src/workerCommunication.js b/src/workerCommunication.js index f22aa21..086eae5 100644 --- a/src/workerCommunication.js +++ b/src/workerCommunication.js @@ -11,10 +11,7 @@ const THIS_SCRIPT = _scripts[_scripts.length - 1]; // Inspired by Emscripten's method for doing the same thing function getPathPrefix() { - return THIS_SCRIPT.src.substring( - 0, - THIS_SCRIPT.src.lastIndexOf('/') - ) + '/'; + return THIS_SCRIPT.src.substring(0, THIS_SCRIPT.src.lastIndexOf('/')) + '/'; } // Set up a WebWorker and an associated promise that resolves once it's ready @@ -67,11 +64,11 @@ function workerTaskPromise(options) { return initWorker().then((worker) => { return new Promise((resolve, reject) => { let resolverId = addMessageResolver( - workerResult => resolve(workerResult), - reason => reject(reason) + (workerResult) => resolve(workerResult), + (reason) => reject(reason) ); - worker.postMessage({id: resolverId, ...options}); + worker.postMessage({ id: resolverId, ...options }); }); }); } diff --git a/src/wrappers/gdalClose.js b/src/wrappers/gdalClose.js index ccb7fca..a2df75d 100644 --- a/src/wrappers/gdalClose.js +++ b/src/wrappers/gdalClose.js @@ -14,8 +14,10 @@ export default function (GDALClose, errorHandling) { let errorType = errorHandling.CPLGetLastErrorType(); // Check for errors; throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { let message = errorHandling.CPLGetLastErrorMsg(); throw new Error(message); @@ -24,4 +26,3 @@ export default function (GDALClose, errorHandling) { } }; } - diff --git a/src/wrappers/gdalGetGeoTransform.js b/src/wrappers/gdalGetGeoTransform.js index bbeb0f4..0310550 100644 --- a/src/wrappers/gdalGetGeoTransform.js +++ b/src/wrappers/gdalGetGeoTransform.js @@ -26,8 +26,10 @@ export default function (GDALGetGeoTransform, errorHandling) { let errorType = errorHandling.CPLGetLastErrorType(); // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { Module._free(byteOffset); let message = errorHandling.CPLGetLastErrorMsg(); diff --git a/src/wrappers/gdalGetProjectionRef.js b/src/wrappers/gdalGetProjectionRef.js index 7f00e9d..59f2a94 100644 --- a/src/wrappers/gdalGetProjectionRef.js +++ b/src/wrappers/gdalGetProjectionRef.js @@ -5,8 +5,10 @@ export default function (GDALGetProjectionRef, errorHandling) { let errorType = errorHandling.CPLGetLastErrorType(); // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { let message = errorHandling.CPLGetLastErrorMsg(); throw new Error(message); diff --git a/src/wrappers/gdalGetRasterCount.js b/src/wrappers/gdalGetRasterCount.js index 4eaf517..1b63297 100644 --- a/src/wrappers/gdalGetRasterCount.js +++ b/src/wrappers/gdalGetRasterCount.js @@ -5,8 +5,10 @@ export default function (GDALGetRasterCount, errorHandling) { let errorType = errorHandling.CPLGetLastErrorType(); // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { let message = errorHandling.CPLGetLastErrorMsg(); throw new Error(message); diff --git a/src/wrappers/gdalGetRasterXSize.js b/src/wrappers/gdalGetRasterXSize.js index 19ff617..f1d1347 100644 --- a/src/wrappers/gdalGetRasterXSize.js +++ b/src/wrappers/gdalGetRasterXSize.js @@ -5,8 +5,10 @@ export default function (GDALGetRasterXSize, errorHandling) { let errorType = errorHandling.CPLGetLastErrorType(); // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { let message = errorHandling.CPLGetLastErrorMsg(); throw new Error(message); diff --git a/src/wrappers/gdalGetRasterYSize.js b/src/wrappers/gdalGetRasterYSize.js index 8b563b1..fcfa305 100644 --- a/src/wrappers/gdalGetRasterYSize.js +++ b/src/wrappers/gdalGetRasterYSize.js @@ -5,8 +5,10 @@ export default function (GDALGetRasterYSize, errorHandling) { let errorType = errorHandling.CPLGetLastErrorType(); // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { let message = errorHandling.CPLGetLastErrorMsg(); throw Error(message); diff --git a/src/wrappers/gdalOpen.js b/src/wrappers/gdalOpen.js index f2735ee..8533dbc 100644 --- a/src/wrappers/gdalOpen.js +++ b/src/wrappers/gdalOpen.js @@ -23,8 +23,10 @@ export default function (GDALOpen, errorHandling, rootPath) { let errorType = errorHandling.CPLGetLastErrorType(); // Check for errors; clean up and throw if error is detected - if (errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal) { + if ( + errorType === errorHandling.CPLErr.CEFailure || + errorType === errorHandling.CPLErr.CEFatal + ) { FS.unmount(directory); FS.rmdir(directory); let message = errorHandling.CPLGetLastErrorMsg(); @@ -35,7 +37,7 @@ export default function (GDALOpen, errorHandling, rootPath) { datasetPtr: datasetPtr, filePath: filePath, directory: directory, - filename: filename + filename: filename, }; } }; diff --git a/src/wrappers/gdalRasterize.js b/src/wrappers/gdalRasterize.js index 7ae83c0..c9953df 100644 --- a/src/wrappers/gdalRasterize.js +++ b/src/wrappers/gdalRasterize.js @@ -13,12 +13,7 @@ export default function (GDALRasterize, errorHandling, rootPath) { FS.writeFile(geojsonPath, JSON.stringify(geojson)); // Append the geojson path to the args so that it's read as the source. // Open the geojson using GDALOpenEx, which can handle non-raster sources. - const datasetPtr = Module.ccall( - 'GDALOpenEx', - 'number', - ['string'], - [geojsonPath] - ); + const datasetPtr = Module.ccall('GDALOpenEx', 'number', ['string'], [geojsonPath]); params.allocate(); @@ -85,12 +80,7 @@ export default function (GDALRasterize, errorHandling, rootPath) { function cleanUp() { Module.ccall('GDALClose', 'number', ['number'], datasetPtr); FS.unlink(geojsonPath); - Module.ccall( - 'GDALRasterizeOptionsFree', - null, - ['number'], - [rasterizeOptionsPtr] - ); + Module.ccall('GDALRasterizeOptionsFree', null, ['number'], [rasterizeOptionsPtr]); Module._free(usageErrPtr); params.deallocate(); } @@ -98,7 +88,7 @@ export default function (GDALRasterize, errorHandling, rootPath) { // Check for errors; clean up and throw if error is detected if ( errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal + errorType === errorHandling.CPLErr.CEFatal ) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); @@ -109,7 +99,7 @@ export default function (GDALRasterize, errorHandling, rootPath) { datasetPtr: newDatasetPtr, filePath: filePath, directory: directory, - filename: filename + filename: filename, }; cleanUp(); diff --git a/src/wrappers/gdalTranslate.js b/src/wrappers/gdalTranslate.js index ebfa4e3..ab5dca0 100644 --- a/src/wrappers/gdalTranslate.js +++ b/src/wrappers/gdalTranslate.js @@ -26,7 +26,7 @@ export default function (GDALTranslate, errorHandling, rootPath) { if ( optionsErrType === errorHandling.CPLErr.CEFailure || - optionsErrType === errorHandling.CPLErr.CEFatal + optionsErrType === errorHandling.CPLErr.CEFatal ) { params.deallocate(); const message = errorHandling.CPLGetLastErrorMsg(); @@ -54,12 +54,7 @@ export default function (GDALTranslate, errorHandling, rootPath) { let usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT); Module.setValue(usageErrPtr, 0, 'i32'); - let newDatasetPtr = GDALTranslate( - filePath, - dataset, - translateOptionsPtr, - usageErrPtr - ); + let newDatasetPtr = GDALTranslate(filePath, dataset, translateOptionsPtr, usageErrPtr); let errorType = errorHandling.CPLGetLastErrorType(); // If we ever want to use the usage error pointer: @@ -67,12 +62,7 @@ export default function (GDALTranslate, errorHandling, rootPath) { // The final set of cleanup we need to do, in a function to avoid writing it twice. function cleanUp() { - Module.ccall( - 'GDALTranslateOptionsFree', - null, - ['number'], - [translateOptionsPtr] - ); + Module.ccall('GDALTranslateOptionsFree', null, ['number'], [translateOptionsPtr]); Module._free(usageErrPtr); params.deallocate(); } @@ -80,7 +70,7 @@ export default function (GDALTranslate, errorHandling, rootPath) { // Check for errors; clean up and throw if error is detected if ( errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal + errorType === errorHandling.CPLErr.CEFatal ) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); @@ -91,7 +81,7 @@ export default function (GDALTranslate, errorHandling, rootPath) { datasetPtr: newDatasetPtr, filePath: filePath, directory: directory, - filename: filename + filename: filename, }; cleanUp(); diff --git a/src/wrappers/gdalWarp.js b/src/wrappers/gdalWarp.js index 7e715d1..270d536 100644 --- a/src/wrappers/gdalWarp.js +++ b/src/wrappers/gdalWarp.js @@ -26,7 +26,7 @@ export default function (GDALWarp, errorHandling, rootPath) { if ( optionsErrType === errorHandling.CPLErr.CEFailure || - optionsErrType === errorHandling.CPLErr.CEFatal + optionsErrType === errorHandling.CPLErr.CEFatal ) { params.deallocate(); const message = errorHandling.CPLGetLastErrorMsg(); @@ -70,12 +70,7 @@ export default function (GDALWarp, errorHandling, rootPath) { // The final set of cleanup we need to do, in a function to avoid writing it twice. function cleanUp() { - Module.ccall( - 'GDALWarpAppOptionsFree', - null, - ['number'], - [warpAppOptionsPtr] - ); + Module.ccall('GDALWarpAppOptionsFree', null, ['number'], [warpAppOptionsPtr]); Module._free(usageErrPtr); params.deallocate(); } @@ -87,7 +82,7 @@ export default function (GDALWarp, errorHandling, rootPath) { // Check for errors; clean up and throw if error is detected if ( errorType === errorHandling.CPLErr.CEFailure || - errorType === errorHandling.CPLErr.CEFatal + errorType === errorHandling.CPLErr.CEFatal ) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); @@ -98,7 +93,7 @@ export default function (GDALWarp, errorHandling, rootPath) { datasetPtr: newDatasetPtr, filePath: filePath, directory: directory, - filename: filename + filename: filename, }; cleanUp(); diff --git a/src/wrappers/reproject.js b/src/wrappers/reproject.js index 49e6a89..2d91a47 100644 --- a/src/wrappers/reproject.js +++ b/src/wrappers/reproject.js @@ -7,11 +7,10 @@ export default function (srcCRSStr, destCRSStr, xCoords, yCoords) { let OSRNewSpatialReference = Module.cwrap('OSRNewSpatialReference', 'number', ['string']); - let OCTNewCoordinateTransformation = Module.cwrap( - 'OCTNewCoordinateTransformation', + let OCTNewCoordinateTransformation = Module.cwrap('OCTNewCoordinateTransformation', 'number', [ 'number', - ['number', 'number'] - ); + 'number', + ]); // Transform arrays of coordinates in-place // Params are: @@ -20,11 +19,13 @@ export default function (srcCRSStr, destCRSStr, xCoords, yCoords) { // 3. Array of X coordinates to transform // 4. Array of Y coordinates to transform // 5. Array of Z coordinates to transform - let OCTTransform = Module.cwrap( - 'OCTTransform', + let OCTTransform = Module.cwrap('OCTTransform', 'number', [ 'number', - ['number', 'number', 'number', 'number', 'number'] - ); + 'number', + 'number', + 'number', + 'number', + ]); // We need SRSes for the source and destinations of our transformation let sourceSrs = OSRNewSpatialReference(srcCRSStr); @@ -46,18 +47,24 @@ export default function (srcCRSStr, destCRSStr, xCoords, yCoords) { // Z is null in this case. This transforms in place. OCTTransform(coordTransform, xCoords.length, xCoordPtr, yCoordPtr, null); // Pull out the coordinates - let transXCoords = Array.from(Module.HEAPF64.subarray( - xCoordPtr / xCoords.BYTES_PER_ELEMENT, - xCoordPtr / xCoords.BYTES_PER_ELEMENT + xCoords.length - )); + let transXCoords = Array.from( + Module.HEAPF64.subarray( + xCoordPtr / xCoords.BYTES_PER_ELEMENT, + xCoordPtr / xCoords.BYTES_PER_ELEMENT + xCoords.length + ) + ); - let transYCoords = Array.from(Module.HEAPF64.subarray( - yCoordPtr / yCoords.BYTES_PER_ELEMENT, - yCoordPtr / yCoords.BYTES_PER_ELEMENT + yCoords.length - )); + let transYCoords = Array.from( + Module.HEAPF64.subarray( + yCoordPtr / yCoords.BYTES_PER_ELEMENT, + yCoordPtr / yCoords.BYTES_PER_ELEMENT + yCoords.length + ) + ); // Zip it all back up - let returnVal = transXCoords.map(function (x, i) { return [x, transYCoords[i]]; }); + let returnVal = transXCoords.map(function (x, i) { + return [x, transYCoords[i]]; + }); // Clear memory Module._free(xCoordPtr); diff --git a/test/loam.spec.js b/test/loam.spec.js index 3ae128e..d184f03 100644 --- a/test/loam.spec.js +++ b/test/loam.spec.js @@ -1,9 +1,31 @@ /* global describe, it, before, expect, loam */ const tinyTifPath = '/base/test/assets/tiny.tif'; const invalidTifPath = 'base/test/assets/not-a-tiff.bytes'; -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 = 'PROJCS["WGS 84 / Pseudo-Mercator",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"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]'; -const geojson = {type: 'FeatureCollection', features: [{type: 'Feature', properties: {}, geometry: {type: 'Polygon', coordinates: [[[-75.15416622161865, 39.96212240336062], [-75.15519618988037, 39.96115204441345], [-75.15409111976624, 39.96055173071228], [-75.15339374542236, 39.96149742799007], [-75.15416622161865, 39.96212240336062]]]}}]} +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 = + 'PROJCS["WGS 84 / Pseudo-Mercator",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"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]'; +const geojson = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-75.15416622161865, 39.96212240336062], + [-75.15519618988037, 39.96115204441345], + [-75.15409111976624, 39.96055173071228], + [-75.15339374542236, 39.96149742799007], + [-75.15416622161865, 39.96212240336062], + ], + ], + }, + }, + ], +}; function xhrAsPromiseBlob(url) { let xhr = new XMLHttpRequest(); @@ -43,25 +65,29 @@ describe('Given that loam exists', () => { describe('calling reproject()', function () { it('should reproject points from one CRS to another', () => { - return loam.reproject(epsg4326, epsg3857, [ - [-75.1652, 39.9526], - [44.8271, 41.7151], - [-47.9218, -15.8267] - ]).then((coords) => { - expect(coords).to.deep.equal([ - [-8367351.7893745685, 4859056.629543971], - [4990129.945739155, 5118397.8827427635], - [-5334630.373897098, -1784662.322609764] - ]); - }); + return loam + .reproject(epsg4326, epsg3857, [ + [-75.1652, 39.9526], + [44.8271, 41.7151], + [-47.9218, -15.8267], + ]) + .then((coords) => { + expect(coords).to.deep.equal([ + [-8367351.7893745685, 4859056.629543971], + [4990129.945739155, 5118397.8827427635], + [-5334630.373897098, -1784662.322609764], + ]); + }); }); }); describe('calling count()', function () { it('should return the number of bands in the GeoTiff', () => { - return xhrAsPromiseBlob(tinyTifPath).then((tifBlob) => loam.open(tifBlob).then((ds) => { - return ds.count().then((count) => expect(count).to.equal(1)); - })); + return xhrAsPromiseBlob(tinyTifPath).then((tifBlob) => + loam.open(tifBlob).then((ds) => { + return ds.count().then((count) => expect(count).to.equal(1)); + }) + ); }); }); @@ -84,7 +110,7 @@ describe('Given that loam exists', () => { }); describe('calling wkt()', function () { - it('should return the GeoTiff\'s WKT CRS string', () => { + it("should return the GeoTiff's WKT CRS string", () => { return xhrAsPromiseBlob(tinyTifPath) .then((tifBlob) => loam.open(tifBlob)) .then((ds) => ds.wkt()) @@ -92,31 +118,35 @@ describe('Given that loam exists', () => { expect(wkt).to.equal( 'PROJCS["unnamed",' + 'GEOGCS["unnamed ellipse",' + - 'DATUM["unknown",' + - 'SPHEROID["unnamed",6378137,0]],' + - 'PRIMEM["Greenwich",0],' + - 'UNIT["degree",0.0174532925199433]],' + + 'DATUM["unknown",' + + 'SPHEROID["unnamed",6378137,0]],' + + 'PRIMEM["Greenwich",0],' + + 'UNIT["degree",0.0174532925199433]],' + 'PROJECTION["Mercator_1SP"],' + 'PARAMETER["central_meridian",0],' + 'PARAMETER["scale_factor",1],' + 'PARAMETER["false_easting",0],' + 'PARAMETER["false_northing",0],' + 'UNIT["metre",1,' + - 'AUTHORITY["EPSG","9001"]]]' + 'AUTHORITY["EPSG","9001"]]]' ); }); }); }); describe('calling transform()', function () { - it('should return the GeoTiff\'s 6-element GDAL transform array', () => { + it("should return the GeoTiff's 6-element GDAL transform array", () => { return xhrAsPromiseBlob(tinyTifPath) .then((tifBlob) => loam.open(tifBlob)) .then((ds) => ds.transform()) .then((transform) => { expect(transform).to.deep.equal([ - -8380165.213197844, 2416.6666666666665, 0, - 4886134.645645497, 0, -2468.75 + -8380165.213197844, + 2416.6666666666665, + 0, + 4886134.645645497, + 0, + -2468.75, ]); }); }); @@ -124,9 +154,9 @@ describe('Given that loam exists', () => { describe('calling close', function () { it('should succeed and clear the GDALDataset', function () { - return xhrAsPromiseBlob(tinyTifPath).then(tifBlob => { - return loam.open(tifBlob).then(ds => { - return ds.close().then(result => { + return xhrAsPromiseBlob(tinyTifPath).then((tifBlob) => { + return loam.open(tifBlob).then((ds) => { + return ds.close().then((result) => { expect(result).to.deep.equal([]); }); }); @@ -137,49 +167,54 @@ describe('Given that loam exists', () => { describe('calling bytes', function () { it('should succeed and return the file contents', function () { return xhrAsPromiseBlob(tinyTifPath) - .then(tifBlob => loam.open(tifBlob)) - .then(ds => ds.bytes()) - .then(bytes => expect(bytes.length).to.equal(862)); + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.bytes()) + .then((bytes) => expect(bytes.length).to.equal(862)); }); }); describe('calling convert', function () { it('should succeed and return a new Dataset with the transformed values', function () { return xhrAsPromiseBlob(tinyTifPath) - .then(tifBlob => loam.open(tifBlob)) - .then(ds => ds.convert(['-outsize', '200%', '200%'])) - .then(newDs => newDs.width()) - .then(width => expect(width).to.equal(30)); + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.convert(['-outsize', '200%', '200%'])) + .then((newDs) => newDs.width()) + .then((width) => expect(width).to.equal(30)); }); }); describe('calling warp', function () { it('should succeed and return a new Dataset that has been warped', function () { - return xhrAsPromiseBlob(tinyTifPath) - .then(tifBlob => loam.open(tifBlob)) - .then(ds => ds.warp(['-s_srs', 'EPSG:3857', '-t_srs', 'EPSG:4326'])) - .then(newDS => newDS.transform()) - // Determined out-of-band by executing gdalwarp on the command line. - .then(transform => { - expect(transform).to.deep.equal([ - -75.2803049446235, - 0.019340471787624117, - 0.0, - 40.13881222863268, - 0.0, - -0.019340471787624117 - ]); - }); + return ( + xhrAsPromiseBlob(tinyTifPath) + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.warp(['-s_srs', 'EPSG:3857', '-t_srs', 'EPSG:4326'])) + .then((newDS) => newDS.transform()) + // Determined out-of-band by executing gdalwarp on the command line. + .then((transform) => { + expect(transform).to.deep.equal([ + -75.2803049446235, + 0.019340471787624117, + 0.0, + 40.13881222863268, + 0.0, + -0.019340471787624117, + ]); + }) + ); }); }); describe('calling rasterize', function () { it('should succeed and return a rasterized version of the GeoJSON', function () { - return loam.rasterize(geojson, ['-burn', '1', '-of', 'GTiff', '-ts', '10', '10']) - .then(ds => ds.bytes()) - // Byte length was experimentally determined by running gdal_rasterize from the - // command-line - .then(bytes => expect(bytes.length).to.equal(1166)); + return ( + loam + .rasterize(geojson, ['-burn', '1', '-of', 'GTiff', '-ts', '10', '10']) + .then((ds) => ds.bytes()) + // Byte length was experimentally determined by running gdal_rasterize from the + // command-line + .then((bytes) => expect(bytes.length).to.equal(1166)) + ); }); }); @@ -189,14 +224,15 @@ describe('Given that loam exists', () => { describe('calling open() on an invalid file', function () { it('should fail and return an error message', function () { return xhrAsPromiseBlob(invalidTifPath) - .then(garbage => loam.open(garbage)) + .then((garbage) => loam.open(garbage)) .then( () => { throw new Error('GDALOpen promise should have been rejected'); }, - error => expect(error.message).to.include( - 'not recognized as a supported file format' - ) + (error) => + expect(error.message).to.include( + 'not recognized as a supported file format' + ) ); }); }); @@ -204,19 +240,18 @@ describe('Given that loam exists', () => { describe('calling convert with invalid arguments', function () { it('should fail and return an error message', function () { return xhrAsPromiseBlob(tinyTifPath) - .then(tifBlob => loam.open(tifBlob)) - .then(ds => ds.convert(['-notreal', 'xyz%', 'oink%'])) - .then(ds => ds.bytes()) // Need to call an accessor to trigger operation execution + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.convert(['-notreal', 'xyz%', 'oink%'])) + .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution .then( (result) => { throw new Error( 'convert() promise should have been rejected but got ' + - result + ' instead.' + result + + ' instead.' ); }, - error => expect(error.message).to.include( - 'Unknown option name' - ) + (error) => expect(error.message).to.include('Unknown option name') ); }); }); @@ -224,19 +259,18 @@ describe('Given that loam exists', () => { describe('calling warp with invalid arguments', function () { it('should fail and return an error message', function () { return xhrAsPromiseBlob(tinyTifPath) - .then(tifBlob => loam.open(tifBlob)) - .then(ds => ds.warp(['-s_srs', 'EPSG:Fake', '-t_srs', 'EPSG:AlsoFake'])) - .then(ds => ds.bytes()) // Need to call an accessor to trigger operation execution + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.warp(['-s_srs', 'EPSG:Fake', '-t_srs', 'EPSG:AlsoFake'])) + .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution .then( (result) => { throw new Error( 'warp() promise should have been rejected but got ' + - result + ' instead.' + result + + ' instead.' ); }, - error => expect(error.message).to.include( - 'Failed to lookup UOM CODE 0' - ) + (error) => expect(error.message).to.include('Failed to lookup UOM CODE 0') ); }); }); @@ -244,18 +278,19 @@ describe('Given that loam exists', () => { describe('calling rasterize with invalid arguments', function () { it('should fail and return an error message', function () { // The -ts parameter is for output image size, so negative values are nonsensical - return loam.rasterize(geojson, ['-burn', '1', '-of', 'GTiff', '-ts', '-10', '10']) - .then(ds => ds.bytes()) // Need to call an accessor to trigger operation execution + return loam + .rasterize(geojson, ['-burn', '1', '-of', 'GTiff', '-ts', '-10', '10']) + .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution .then( (result) => { throw new Error( 'rasterize() promise should have been rejected but got ' + - result + ' instead.' + result + + ' instead.' ); }, - error => expect(error.message).to.include( - 'Wrong value for -outsize parameter.' - ) + (error) => + expect(error.message).to.include('Wrong value for -outsize parameter.') ); }); }); @@ -263,19 +298,21 @@ describe('Given that loam exists', () => { describe('calling convert with non-string arguments', function () { it('should fail and return an error message', function () { return xhrAsPromiseBlob(tinyTifPath) - .then(tifBlob => loam.open(tifBlob)) - .then(ds => ds.convert(['-outsize', 25])) - .then(ds => ds.bytes()) // Need to call an accessor to trigger operation execution + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.convert(['-outsize', 25])) + .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution .then( (result) => { throw new Error( 'convert() promise should have been rejected but got ' + - result + ' instead.' + result + + ' instead.' ); }, - error => expect(error.message).to.include( - 'All items in the argument list must be strings' - ) + (error) => + expect(error.message).to.include( + 'All items in the argument list must be strings' + ) ); }); }); @@ -283,37 +320,42 @@ describe('Given that loam exists', () => { describe('calling warp with non-string arguments', function () { it('should fail and return an error message', function () { return xhrAsPromiseBlob(tinyTifPath) - .then(tifBlob => loam.open(tifBlob)) - .then(ds => ds.warp(['-order', 2])) - .then(ds => ds.bytes()) // Need to call an accessor to trigger operation execution + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.warp(['-order', 2])) + .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution .then( (result) => { throw new Error( 'warp() promise should have been rejected but got ' + - result + ' instead.' + result + + ' instead.' ); }, - error => expect(error.message).to.include( - 'All items in the argument list must be strings' - ) + (error) => + expect(error.message).to.include( + 'All items in the argument list must be strings' + ) ); }); }); describe('calling rasterize with non-string arguments', function () { it('should fail and return an error message', function () { - return loam.rasterize(geojson, ['-burn', 1, '-of', 'GTiff', '-ts', 10, 10]) - .then(ds => ds.bytes()) // Need to call an accessor to trigger operation execution + return loam + .rasterize(geojson, ['-burn', 1, '-of', 'GTiff', '-ts', 10, 10]) + .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution .then( (result) => { throw new Error( 'rasterize() promise should have been rejected but got ' + - result + ' instead.' + result + + ' instead.' ); }, - error => expect(error.message).to.include( - 'All items in the argument list must be strings' - ) + (error) => + expect(error.message).to.include( + 'All items in the argument list must be strings' + ) ); }); }); From c61bdd4d31fd82d11862fd57ed1d7ca8339f7e70 Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Wed, 30 Sep 2020 13:49:13 -0400 Subject: [PATCH 07/11] Apply autoformatting to code --- .eslintrc | 4 ++-- src/stringParamAllocator.js | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.eslintrc b/.eslintrc index 8dabeef..9771060 100644 --- a/.eslintrc +++ b/.eslintrc @@ -51,7 +51,7 @@ "keyword-spacing": [2, {"before": true, "after": true}], "linebreak-style": 0, "max-depth": 0, - "max-len": [2, 120, 4], + "max-len": [2, 100, 4], "max-nested-callbacks": 0, "max-params": 0, "max-statements": 0, @@ -155,7 +155,7 @@ "operator-linebreak": [2, "after"], "padded-blocks": 0, "quote-props": 0, - "quotes": [2, "single", "avoid-escape"], + "quotes": [2, "single", {"allowTemplateLiterals": true, "avoidEscape": true}], "radix": 2, "semi": [2, "always"], "semi-spacing": 0, diff --git a/src/stringParamAllocator.js b/src/stringParamAllocator.js index 059a11e..9419289 100644 --- a/src/stringParamAllocator.js +++ b/src/stringParamAllocator.js @@ -23,16 +23,17 @@ export default class ParamParser { let argPtrsArray = Uint32Array.from( self.args .map((argStr) => { - return Module._malloc(Module.lengthBytesUTF8(argStr) + 1); // +1 for the null terminator byte + // +1 for the null terminator byte + return Module._malloc(Module.lengthBytesUTF8(argStr) + 1); }) .concat([0]) ); - // ^ In addition to each individual argument being null-terminated, the GDAL docs specify that - // GDALTranslateOptionsNew takes its options passed in as a null-terminated array of + // ^ In addition to each individual argument being null-terminated, the GDAL docs specify + // that GDALTranslateOptionsNew takes its options passed in as a null-terminated array of // pointers, so we have to add on a null (0) byte at the end. - // Next, we need to write each string from the JS string array into the Emscripten heap space - // we've allocated for it. + // Next, we need to write each string from the JS string array into the Emscripten heap + // space we've allocated for it. self.args.forEach(function (argStr, i) { Module.stringToUTF8(argStr, argPtrsArray[i], Module.lengthBytesUTF8(argStr) + 1); }); From 679a56a60c023c4720c11dce639b3e56ee4016c6 Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Thu, 15 Oct 2020 08:55:01 -0400 Subject: [PATCH 08/11] Improve error messages Lazy evaluation has made it more difficult to track down the source of errors because all operations on a dataset are chained together and executed at once. This adds a prefix to any errors emitted by GDAL that explains where the error occurred. --- src/wrappers/gdalClose.js | 5 +++-- src/wrappers/gdalGetGeoTransform.js | 6 +++--- src/wrappers/gdalGetProjectionRef.js | 2 +- src/wrappers/gdalGetRasterCount.js | 2 +- src/wrappers/gdalGetRasterXSize.js | 2 +- src/wrappers/gdalGetRasterYSize.js | 2 +- src/wrappers/gdalOpen.js | 2 +- src/wrappers/gdalRasterize.js | 9 +++++---- src/wrappers/gdalTranslate.js | 9 +++++---- src/wrappers/gdalWarp.js | 9 +++++---- 10 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/wrappers/gdalClose.js b/src/wrappers/gdalClose.js index a2df75d..e7db552 100644 --- a/src/wrappers/gdalClose.js +++ b/src/wrappers/gdalClose.js @@ -14,13 +14,14 @@ export default function (GDALClose, errorHandling) { let errorType = errorHandling.CPLGetLastErrorType(); // Check for errors; throw if error is detected + // Note that due to https://github.com/ddohler/gdal-js/issues/38 this can only check for + // CEFatal errors in order to avoid raising an exception on GDALClose if ( - errorType === errorHandling.CPLErr.CEFailure || errorType === errorHandling.CPLErr.CEFatal ) { let message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALClose: ' + message); } else { return result; } diff --git a/src/wrappers/gdalGetGeoTransform.js b/src/wrappers/gdalGetGeoTransform.js index 0310550..e2c8d37 100644 --- a/src/wrappers/gdalGetGeoTransform.js +++ b/src/wrappers/gdalGetGeoTransform.js @@ -33,10 +33,10 @@ export default function (GDALGetGeoTransform, errorHandling) { Module._free(byteOffset); let message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALGetGeoTransform: ' + message); } else { - // To avoid memory leaks in the Emscripten heap, we need to free up the memory we allocated - // after we've converted it into a Javascript object. + // To avoid memory leaks in the Emscripten heap, we need to free up the memory we + // allocated after we've converted it into a Javascript object. let result = Array.from(geoTransform); Module._free(byteOffset); diff --git a/src/wrappers/gdalGetProjectionRef.js b/src/wrappers/gdalGetProjectionRef.js index 59f2a94..0bcdf37 100644 --- a/src/wrappers/gdalGetProjectionRef.js +++ b/src/wrappers/gdalGetProjectionRef.js @@ -11,7 +11,7 @@ export default function (GDALGetProjectionRef, errorHandling) { ) { let message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALGetProjectionRef: ' + message); } else { return result; } diff --git a/src/wrappers/gdalGetRasterCount.js b/src/wrappers/gdalGetRasterCount.js index 1b63297..601a44c 100644 --- a/src/wrappers/gdalGetRasterCount.js +++ b/src/wrappers/gdalGetRasterCount.js @@ -11,7 +11,7 @@ export default function (GDALGetRasterCount, errorHandling) { ) { let message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALGetRasterCount: ' + message); } else { return result; } diff --git a/src/wrappers/gdalGetRasterXSize.js b/src/wrappers/gdalGetRasterXSize.js index f1d1347..101ecfc 100644 --- a/src/wrappers/gdalGetRasterXSize.js +++ b/src/wrappers/gdalGetRasterXSize.js @@ -11,7 +11,7 @@ export default function (GDALGetRasterXSize, errorHandling) { ) { let message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALGetRasterXSize: ' + message); } else { return result; } diff --git a/src/wrappers/gdalGetRasterYSize.js b/src/wrappers/gdalGetRasterYSize.js index fcfa305..62bfd26 100644 --- a/src/wrappers/gdalGetRasterYSize.js +++ b/src/wrappers/gdalGetRasterYSize.js @@ -11,7 +11,7 @@ export default function (GDALGetRasterYSize, errorHandling) { ) { let message = errorHandling.CPLGetLastErrorMsg(); - throw Error(message); + throw Error('Error in GDALGetRasterYSize: ' + message); } else { return result; } diff --git a/src/wrappers/gdalOpen.js b/src/wrappers/gdalOpen.js index 8533dbc..815336b 100644 --- a/src/wrappers/gdalOpen.js +++ b/src/wrappers/gdalOpen.js @@ -31,7 +31,7 @@ export default function (GDALOpen, errorHandling, rootPath) { FS.rmdir(directory); let message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALOpen: ' + message); } else { return { datasetPtr: datasetPtr, diff --git a/src/wrappers/gdalRasterize.js b/src/wrappers/gdalRasterize.js index c9953df..6e80d79 100644 --- a/src/wrappers/gdalRasterize.js +++ b/src/wrappers/gdalRasterize.js @@ -40,11 +40,12 @@ export default function (GDALRasterize, errorHandling, rootPath) { params.deallocate(); const message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALRasterize: ' + message); } - // Now that we have our translate options, we need to make a file location to hold the output. - let directory = rootPath + '/' + randomKey(); + // Now that we have our translate options, we need to make a file location to hold the + // output. + let directory = rootPath + randomKey(); FS.mkdir(directory); // This makes it easier to remove later because we can just unmount rather than recursing @@ -93,7 +94,7 @@ export default function (GDALRasterize, errorHandling, rootPath) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALRasterize: ' + message); } else { const result = { datasetPtr: newDatasetPtr, diff --git a/src/wrappers/gdalTranslate.js b/src/wrappers/gdalTranslate.js index ab5dca0..316a6b9 100644 --- a/src/wrappers/gdalTranslate.js +++ b/src/wrappers/gdalTranslate.js @@ -31,11 +31,12 @@ export default function (GDALTranslate, errorHandling, rootPath) { params.deallocate(); const message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALTranslate: ' + message); } - // Now that we have our translate options, we need to make a file location to hold the output. - let directory = rootPath + '/' + randomKey(); + // Now that we have our translate options, we need to make a file location to hold the + // output. + let directory = rootPath + randomKey(); FS.mkdir(directory); // This makes it easier to remove later because we can just unmount rather than recursing @@ -75,7 +76,7 @@ export default function (GDALTranslate, errorHandling, rootPath) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALTranslate: ' + message); } else { const result = { datasetPtr: newDatasetPtr, diff --git a/src/wrappers/gdalWarp.js b/src/wrappers/gdalWarp.js index 270d536..4ef9720 100644 --- a/src/wrappers/gdalWarp.js +++ b/src/wrappers/gdalWarp.js @@ -31,10 +31,10 @@ export default function (GDALWarp, errorHandling, rootPath) { params.deallocate(); const message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALWarp: ' + message); } - let directory = rootPath + '/' + randomKey(); + let directory = rootPath + randomKey(); FS.mkdir(directory); // This makes it easier to remove later because we can just unmount rather than recursing @@ -58,7 +58,8 @@ export default function (GDALWarp, errorHandling, rootPath) { // at a time, we don't need to do anything fancy here. let datasetListPtr = Module._malloc(4); // 32-bit pointer - Module.setValue(datasetListPtr, dataset, '*'); // Set datasetListPtr to the address of dataset + // Set datasetListPtr to the address of dataset + Module.setValue(datasetListPtr, dataset, '*'); let newDatasetPtr = GDALWarp( filePath, // Output 0, // NULL because filePath is not NULL @@ -87,7 +88,7 @@ export default function (GDALWarp, errorHandling, rootPath) { cleanUp(); const message = errorHandling.CPLGetLastErrorMsg(); - throw new Error(message); + throw new Error('Error in GDALWarp: ' + message); } else { const result = { datasetPtr: newDatasetPtr, From 951b9d54d7c3231a3415e0eb32d5276d6edd2e4d Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Thu, 15 Oct 2020 08:58:34 -0400 Subject: [PATCH 09/11] Add gdal-dem support via a `render()` function --- src/gdalDataset.js | 18 ++++ src/worker.js | 15 ++++ src/wrappers/gdalDemProcessing.js | 144 ++++++++++++++++++++++++++++++ test/assets/tiny_dem.tif | Bin 0 -> 1820 bytes test/loam.spec.js | 73 +++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 src/wrappers/gdalDemProcessing.js create mode 100644 test/assets/tiny_dem.tif diff --git a/src/gdalDataset.js b/src/gdalDataset.js index 2c975b6..25ffa3a 100644 --- a/src/gdalDataset.js +++ b/src/gdalDataset.js @@ -70,6 +70,24 @@ export class GDALDataset { }); } + render(mode, args, colors) { + return new Promise((resolve, reject) => { + // DEMProcessing requires an auxiliary color definition file in some cases, so the API + // can't be easily represented as an array of strings. This packs the user-friendly + // interface of render() into an array that the worker communication machinery can + // easily make use of. It'll get unpacked inside the worker. Yet another reason to use + // something like comlink (#49) + const cliOrderArgs = [mode, colors].concat(args); + + resolve( + new GDALDataset( + this.source, + this.operations.concat(new DatasetOperation('GDALDEMProcessing', cliOrderArgs)) + ) + ); + }); + } + close() { return new Promise((resolve, reject) => { const warningMsg = diff --git a/src/worker.js b/src/worker.js index 3f40d2b..e7a6031 100644 --- a/src/worker.js +++ b/src/worker.js @@ -6,6 +6,7 @@ import wGDALOpen from './wrappers/gdalOpen.js'; import wGDALRasterize from './wrappers/gdalRasterize.js'; import wGDALClose from './wrappers/gdalClose.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'; @@ -143,6 +144,19 @@ self.Module = { errorHandling, DATASETPATH ); + registry.GDALDEMProcessing = wGDALDEMProcessing( + self.Module.cwrap('GDALDEMProcessing', 'number', [ + 'string', // Destination dataset path or NULL + 'number', // GDALDatasetH destination dataset + // eslint-disable-next-line max-len + 'string', // The processing to apply (one of "hillshade", "slope", "aspect", "color-relief", "TRI", "TPI", "roughness") + 'string', // Color file path (when previous is "hillshade") or NULL (otherwise) + 'number', // GDALDEMProcessingOptions * + 'number', // int * to use for error reporting + ]), + errorHandling, + DATASETPATH + ); registry.LoamFlushFS = function () { let datasetFolders = FS.lookupPath(DATASETPATH).node.contents; @@ -230,6 +244,7 @@ onmessage = function (msg) { postMessage({ success: false, message: + // eslint-disable-next-line max-len 'Worker could not parse message: either func + args or accessor + dataset is required', id: msg.data.id, }); diff --git a/src/wrappers/gdalDemProcessing.js b/src/wrappers/gdalDemProcessing.js new file mode 100644 index 0000000..b971b40 --- /dev/null +++ b/src/wrappers/gdalDemProcessing.js @@ -0,0 +1,144 @@ +/* global Module, FS, MEMFS */ +import randomKey from '../randomKey.js'; +import guessFileExtension from '../guessFileExtension.js'; +import ParamParser from '../stringParamAllocator.js'; + +// TODO: This is another good reason to switch to Typescript #55 +const DEMProcessingModes = Object.freeze({ + hillshade: 'hillshade', + slope: 'slope', + aspect: 'aspect', + 'color-relief': 'color-relief', + TRI: 'TRI', + TPI: 'TPI', + roughness: 'roughness', +}); + +export default function (GDALDEMProcessing, errorHandling, rootPath) { + /* mode: one of the options in DEMProcessingModes + * colors: Array of strings matching the format of the color file defined at + * https://gdal.org/programs/gdaldem.html#color-relief + * args: Array of strings matching the remaining arguments of gdaldem, excluding output filename + */ + return function (dataset, packedArgs) { + // TODO: Make this unnecessary by switching to comlink or similar (#49) + const mode = packedArgs[0]; + const colors = packedArgs[1]; + const args = packedArgs.slice(2); + + if (!mode || !DEMProcessingModes.hasOwnProperty(mode)) { + throw new Error(`mode must be one of {Object.keys(DEMProcessingModes)}`); + } else if (mode === DEMProcessingModes['color-relief'] && !colors) { + throw new Error( + 'A color definition array must be provided if `mode` is "color-relief"' + ); + } else if (mode !== DEMProcessingModes['color-relief'] && colors && colors.length > 0) { + throw new Error( + 'A color definition array should not be provided if `mode` is not "color-relief"' + ); + } + + // If mode is hillshade, we need to create a color file path + let colorFilePath = null; + + if (mode === DEMProcessingModes['color-relief']) { + colorFilePath = rootPath + randomKey() + '.txt'; + + FS.writeFile(colorFilePath, colors.join('\n')); + } + let params = new ParamParser(args); + + params.allocate(); + + // 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 GDALDEMProcessingOptionsNew requires. + const demOptionsPtr = Module.ccall( + 'GDALDEMProcessingOptionsNew', + '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 + ) { + if (colorFilePath) { + FS.unlink(colorFilePath); + } + params.deallocate(); + const message = errorHandling.CPLGetLastErrorMsg(); + + throw new Error('Error in GDALDEMProcessing: ' + message); + } + + // Now that we have our options, we need to make a file location to hold the output. + let 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); + let filename = randomKey(8) + '.' + guessFileExtension(args); + + let filePath = directory + '/' + filename; + + // And then we can kick off the actual processing. + // 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. + // Malloc ourselves an int and set it to 0 (False) + let usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT); + + Module.setValue(usageErrPtr, 0, 'i32'); + + let newDatasetPtr = GDALDEMProcessing( + filePath, // Output + dataset, + mode, + colorFilePath, + demOptionsPtr, + usageErrPtr + ); + + let 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() { + if (colorFilePath) { + FS.unlink(colorFilePath); + } + Module.ccall('GDALDEMProcessingOptionsFree', null, ['number'], [demOptionsPtr]); + Module._free(usageErrPtr); + 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 GDALDEMProcessing: ' + message); + } else { + const result = { + datasetPtr: newDatasetPtr, + filePath: filePath, + directory: directory, + filename: filename, + }; + + cleanUp(); + return result; + } + }; +} diff --git a/test/assets/tiny_dem.tif b/test/assets/tiny_dem.tif new file mode 100644 index 0000000000000000000000000000000000000000..13bdef698e8334c9dd0cd8104b0dec47249a788d GIT binary patch literal 1820 zcmZ`&e{54#6uw=@7+o1dD<};6bQ38^S=WJVaox(kZ7s-vc8iz|1jim5%&uKW2Xq@M80tFLImf#etBN#-BkNOkXk5y28Dk2@BvK|JRzOoSjb}}9j_lGYF8Jk4<8vk1Vr-$5u?MEv=S^{8&-qh6 zESs?fk{P1^ZL%+%;yJWw;k4K#)3`>G#bV@}Pd?@;I~~RHJ#r@CDYCo0a+X8<9PxJp zQr1ZPZ{lMCIm;vdS{7sDfh_i@U?(mNTK>;8uGiNa8)~^mV_h9*-ea|Pb@#|>x~%)U zJ9f7BaGLUR4Yyo?a_;$kT&)Ru^AnuW(yHe4ot>OXaI$lzHhWvQqiu(*dP$M0*i>WY zJ38AcxVD}SUfs$SbH%1MM~A(m%TmE<)S42tMqMt>Qnf}SEMt2QxtErdl&vT!U!hsa zX;xR1l~$B!YSG9Qi}z-Oo@$Z&i+8R6``x&%OfN1zJ^L=wk3`Iz>Q40#IF(1ltnBf? zLcy8ZLc-oDkmQaBa@azaBmR#R&B`4MNZ3r4Ct?oECzO)&brMRw^oyld~7 zrMs8ciNCJBxG2^w_*Wlq+@T=+SAkCCoc6d z=Se?O#Ub#Ehhf?_jQE#fB<6(?Ew_T7SA`a36%6_YD9`SI>I*lb`T%_P5R~7CkzDRa zOCX4Z&JX355%BL1qw-V`+GRs9Y;vGdR)yq69c)L|!k5Rxm-HZUDFB5f48^4gGRH!& zH3gB9A3!=Ui0Ds)FzpQ@xqb+;-u+;#3Z@73Q2b&5<9Nizf^?rDq;7=}eJBLqG7nS= z4>G?EK>1!DR4r~)eiXpOeS_dv4InwA3b9cgw8?rzuWW^KwhMf|a6X-}B?#rke)#Sm zK|DQ#Scw;=++I50g9yb(-1`wLsDk1ZiZ7~#>P!<74|hOz*n)(EbTD>8RyBwOPkCJN zBYx8bXUu{4{BCG3c;NhFHzL2+Abnbgw7G_KSdY}p&6xPEAMu$xC@&k**IY=4N1%#% zp*p`8$weM0oBE+>XohL^COUVksGsSjdZaxYNS}73Vty#{2au?9W5PXv$pSxAg(GOW z<})2C7t|v9<~Bq=chY%280LnMI6ey2?uYYgfa)E= zLkuJBWXW~}aURaCCTRX)=*U9#n=Rh<_4B%X0xtmeAeC zs6P%0JupbU;YI2y^~E_JY?NZ?9?G+n&tb}E@fz3;(S5%e#>B254CADW-UHi8KO&{1 z!74AbQV-%E*pYdpm2}dB{n|oj)x{cQLNZzPH#DPHo>^DH6-brt!8-`;x(rW;*6SPM!%{eY8eFxxcr9G~Csn-JZmfT=t a^cH^HjrilOkfoauf3g+n>z&AaYr#M7!ymu^ literal 0 HcmV?d00001 diff --git a/test/loam.spec.js b/test/loam.spec.js index d184f03..2750df8 100644 --- a/test/loam.spec.js +++ b/test/loam.spec.js @@ -1,5 +1,6 @@ /* global describe, it, before, expect, loam */ 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 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"]]'; @@ -218,6 +219,32 @@ describe('Given that loam exists', () => { }); }); + describe('calling render with color-relief', function () { + it('should succeed and return a rendered version of the GeoTIFF', function () { + return ( + xhrAsPromiseBlob(tinyDEMPath) + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.render('color-relief', ['-of', 'PNG'], ['993.0 255 0 0'])) + .then((ds) => ds.bytes()) + // Determined out-of-band by executing gdaldem on the command line. + .then((bytes) => expect(bytes.length).to.equal(80)) + ); + }); + }); + + describe('calling render with hillshade', function () { + it('should succeed and return a rendered version of the GeoTIFF', function () { + return ( + xhrAsPromiseBlob(tinyDEMPath) + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.render('hillshade', ['-of', 'PNG'])) + .then((ds) => ds.bytes()) + // Determined out-of-band by executing gdaldem on the command line. + .then((bytes) => expect(bytes.length).to.equal(246)) + ); + }); + }); + /** * Failure cases **/ @@ -359,4 +386,50 @@ describe('Given that loam exists', () => { ); }); }); + + describe('calling render with an invalid mode', function () { + it('should fail and return an error message', function () { + return xhrAsPromiseBlob(tinyTifPath) + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.render('gobbledegook', [])) + .then((ds) => ds.bytes()) // Call an accessor to trigger operation execution + .then( + (result) => { + throw new Error('render() promise should have been rejected but got ' + + result + ' instead.' + ); + }, + (error) => expect(error.message).to.include('mode must be one of')); + }); + }); + describe('calling render with color-relief but no colors', function () { + it('should fail and return an error message', function () { + return xhrAsPromiseBlob(tinyTifPath) + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.render('color-relief', [])) + .then((ds) => ds.bytes()) // Call an accessor to trigger operation execution + .then( + (result) => { + throw new Error('render() promise should have been rejected but got ' + + result + ' instead.' + ); + }, + (error) => expect(error.message).to.include('color definition array must be provided')); + }); + }); + describe('calling render with non-color-relief but providing colors', function () { + it('should fail and return an error message', function () { + return xhrAsPromiseBlob(tinyTifPath) + .then((tifBlob) => loam.open(tifBlob)) + .then((ds) => ds.render('hillshade', [], ['0.5 100 100 100'])) + .then((ds) => ds.bytes()) // Call an accessor to trigger operation execution + .then( + (result) => { + throw new Error('render() promise should have been rejected but got ' + + result + ' instead.' + ); + }, + (error) => expect(error.message).to.include('color definition array should not be provided')); + }); + }); }); From e92502fbdc49ac4f37e986bac26954a6a64be305 Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Thu, 15 Oct 2020 09:35:21 -0400 Subject: [PATCH 10/11] Update CHANGELOG and README --- CHANGELOG | 2 ++ README.md | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 26eff2e..fa4f057 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ ## Upcoming release - Add information on contributing to the README - Apply code auto-formatting +- Improve error messages with originating function names +- Add GDALDataset.render() to provide gdaldem functionality ## 1.0.0-rc.1 (2020-07-24) - Add loam.rasterize() wrapper for GDALRasterize() diff --git a/README.md b/README.md index feddd61..0aa74df 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,19 @@ Image reprojection and warping utility. This is the equivalent of the [gdalwarp] #### Return value A promise that resolves to a new `GDALDataset`. +
+ +### `GDALDataset.render(mode, args, colors)` +Utility for rendering and computing DEM metrics. This is the equivalent of the [gdaldem](https://gdal.org/programs/gdaldem.html) command. + +**Note**: This returns a new `GDALDataset` object but does not perform any immediate calculation. Instead, calls to `.render()` are evaluated lazily (as with `convert()` and `warp()`, above). The render operation is only evaluated when necessary in order to access some property of the dataset, such as its size, bytes, or band count. Successive calls to `.warp()` and `.convert()` can be lazily chained onto datasets produced by `.render()`, and vice-versa. +#### Parameters +- `mode`: One of ['hillshade', 'slope','aspect', 'color-relief', 'TRI', 'TPI', 'roughness']. See the [`gdaldem documentation`](https://gdal.org/programs/gdaldem.html#cmdoption-arg-mode) for an explanation of the function of each mode. +- `args`: An array of strings, each representing a single [command-line argument](https://gdal.org/programs/gdaldem.html#synopsis) accepted by the `gdaldem` command. The `inputdem` and `output_xxx_map` parameters should be omitted; these are handled by `GDALDataset`. Example: `ds.render('hillshade', ['-of', 'PNG'])` +- `colors`: If (and only if) `mode` is equal to 'color-relief', an array of strings representing lines in [the color text file](https://gdal.org/programs/gdaldem.html#color-relief). Example: `ds.render('color-relief', ['-of', 'PNG'], ['993.0 255 0 0'])`. See the [`gdaldem documentation`](https://gdal.org/programs/gdaldem.html#cmdoption-arg-color_text_file) for an explanation of the text file syntax. +#### Return value +A promise that resolves to a new `GDALDataset`. + # Developing After cloning, From 88b6da5f53df470f142c149c8d264bac8f0b2079 Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Fri, 30 Oct 2020 09:26:03 -0400 Subject: [PATCH 11/11] Bump version number --- CHANGELOG | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index fa4f057..19643ac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ ## Upcoming release + +## 1.0.0-rc.2 (2020-10-30) - Add information on contributing to the README - Apply code auto-formatting - Improve error messages with originating function names diff --git a/package.json b/package.json index 2bae764..65d0322 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loam", - "version": "1.0.0-rc.1", + "version": "1.0.0-rc.2", "description": "Javascript wrapper for GDAL in the browser", "main": "lib/loam.js", "scripts": {