├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .jshintrc ├── .nvmrc ├── .prettierrc ├── .yarn └── releases │ └── yarn-3.2.3.cjs ├── .yarnrc.yml ├── CHANGELOG ├── LICENSE ├── README.md ├── demo ├── index.html └── index.js ├── karma.conf.js ├── package.json ├── src ├── api.js ├── gdalDataType.js ├── gdalDataset.js ├── guessFileExtension.js ├── index.js ├── randomKey.js ├── stringParamAllocator.js ├── validation.js ├── worker.js ├── workerCommunication.js └── wrappers │ ├── gdalClose.js │ ├── gdalDatasetGetLayerCount.js │ ├── gdalDemProcessing.js │ ├── gdalGetGeoTransform.js │ ├── gdalGetProjectionRef.js │ ├── gdalGetRasterCount.js │ ├── gdalGetRasterDataType.js │ ├── gdalGetRasterMaximum.js │ ├── gdalGetRasterMinimum.js │ ├── gdalGetRasterNoDataValue.js │ ├── gdalGetRasterStatistics.js │ ├── gdalGetRasterXSize.js │ ├── gdalGetRasterYSize.js │ ├── gdalOpen.js │ ├── gdalRasterize.js │ ├── gdalTranslate.js │ ├── gdalVectorTranslate.js │ ├── gdalWarp.js │ └── reproject.js ├── test ├── assets │ ├── geom.geojson │ ├── not-a-tiff.bytes │ ├── point.dbf │ ├── point.prj │ ├── point.shp │ ├── point.shx │ ├── tiny.tif │ └── tiny_dem.tif └── loam.spec.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/env", { "modules": "commonjs" }]], 3 | "plugins": ["add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | 9 | "globals": { 10 | "document": false, 11 | "escape": false, 12 | "navigator": false, 13 | "unescape": false, 14 | "window": false, 15 | "describe": true, 16 | "before": true, 17 | "it": true, 18 | "expect": true, 19 | "sinon": true 20 | }, 21 | 22 | "parser": "@babel/eslint-parser", 23 | 24 | "plugins": [ 25 | 26 | ], 27 | 28 | "rules": { 29 | "block-scoped-var": 2, 30 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 31 | "camelcase": [2, { "properties": "always" }], 32 | "comma-dangle": [2, "always-multiline"], 33 | "comma-spacing": [2, { "before": false, "after": true }], 34 | "comma-style": [2, "last"], 35 | "complexity": 0, 36 | "consistent-return": 2, 37 | "consistent-this": 0, 38 | "curly": [2, "multi-line"], 39 | "default-case": 0, 40 | "dot-location": [2, "property"], 41 | "dot-notation": 0, 42 | "eol-last": 2, 43 | "eqeqeq": [2, "allow-null"], 44 | "func-names": 0, 45 | "func-style": 0, 46 | "generator-star-spacing": [2, "both"], 47 | "guard-for-in": 0, 48 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 49 | "indent": [2, 4, { "SwitchCase": 1 }], 50 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 51 | "keyword-spacing": [2, {"before": true, "after": true}], 52 | "linebreak-style": 0, 53 | "max-depth": 0, 54 | "max-len": [2, 100, 4], 55 | "max-nested-callbacks": 0, 56 | "max-params": 0, 57 | "max-statements": 0, 58 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 59 | "newline-after-var": [2, "always"], 60 | "new-parens": 2, 61 | "no-alert": 0, 62 | "no-array-constructor": 2, 63 | "no-bitwise": 0, 64 | "no-caller": 2, 65 | "no-catch-shadow": 0, 66 | "no-cond-assign": 2, 67 | "no-console": 0, 68 | "no-constant-condition": 0, 69 | "no-continue": 0, 70 | "no-control-regex": 2, 71 | "no-debugger": 2, 72 | "no-delete-var": 2, 73 | "no-div-regex": 0, 74 | "no-dupe-args": 2, 75 | "no-dupe-keys": 2, 76 | "no-duplicate-case": 2, 77 | "no-else-return": 2, 78 | "no-empty": 0, 79 | "no-empty-character-class": 2, 80 | "no-eq-null": 0, 81 | "no-eval": 2, 82 | "no-ex-assign": 2, 83 | "no-extend-native": 2, 84 | "no-extra-bind": 2, 85 | "no-extra-boolean-cast": 2, 86 | "no-extra-parens": 0, 87 | "no-extra-semi": 0, 88 | "no-extra-strict": 0, 89 | "no-fallthrough": 2, 90 | "no-floating-decimal": 2, 91 | "no-func-assign": 2, 92 | "no-implied-eval": 2, 93 | "no-inline-comments": 0, 94 | "no-inner-declarations": [2, "functions"], 95 | "no-invalid-regexp": 2, 96 | "no-irregular-whitespace": 2, 97 | "no-iterator": 2, 98 | "no-label-var": 2, 99 | "no-labels": 2, 100 | "no-lone-blocks": 0, 101 | "no-lonely-if": 0, 102 | "no-loop-func": 0, 103 | "no-mixed-requires": 0, 104 | "no-mixed-spaces-and-tabs": [2, false], 105 | "no-multi-spaces": 2, 106 | "no-multi-str": 2, 107 | "no-multiple-empty-lines": [2, { "max": 1 }], 108 | "no-native-reassign": 2, 109 | "no-negated-in-lhs": 2, 110 | "no-nested-ternary": 0, 111 | "no-new": 2, 112 | "no-new-func": 2, 113 | "no-new-object": 2, 114 | "no-new-require": 2, 115 | "no-new-wrappers": 2, 116 | "no-obj-calls": 2, 117 | "no-octal": 2, 118 | "no-octal-escape": 2, 119 | "no-path-concat": 0, 120 | "no-plusplus": 0, 121 | "no-process-env": 0, 122 | "no-process-exit": 0, 123 | "no-proto": 2, 124 | "no-redeclare": 2, 125 | "no-regex-spaces": 2, 126 | "no-reserved-keys": 0, 127 | "no-restricted-modules": 0, 128 | "no-return-assign": 2, 129 | "no-script-url": 0, 130 | "no-self-compare": 2, 131 | "no-sequences": 2, 132 | "no-shadow": 0, 133 | "no-shadow-restricted-names": 2, 134 | "no-spaced-func": 2, 135 | "no-sparse-arrays": 2, 136 | "no-sync": 0, 137 | "no-ternary": 0, 138 | "no-throw-literal": 2, 139 | "no-trailing-spaces": 2, 140 | "no-undef": 2, 141 | "no-undef-init": 2, 142 | "no-undefined": 0, 143 | "no-underscore-dangle": 0, 144 | "no-unneeded-ternary": 2, 145 | "no-unreachable": 2, 146 | "no-unused-expressions": 0, 147 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 148 | "no-use-before-define": 2, 149 | "no-var": 0, 150 | "no-void": 0, 151 | "no-warning-comments": 0, 152 | "no-with": 2, 153 | "one-var": 0, 154 | "operator-assignment": 0, 155 | "operator-linebreak": [2, "after"], 156 | "padded-blocks": 0, 157 | "quote-props": 0, 158 | "quotes": [2, "single", {"allowTemplateLiterals": true, "avoidEscape": true}], 159 | "radix": 2, 160 | "semi": [2, "always"], 161 | "semi-spacing": 0, 162 | "sort-vars": 0, 163 | "space-before-blocks": [2, "always"], 164 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 165 | "space-in-brackets": 0, 166 | "space-in-parens": [2, "never"], 167 | "space-infix-ops": 2, 168 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 169 | "spaced-comment": [2, "always"], 170 | "strict": 0, 171 | "use-isnan": 2, 172 | "valid-jsdoc": 0, 173 | "valid-typeof": 2, 174 | "vars-on-top": 2, 175 | "wrap-iife": [2, "any"], 176 | "wrap-regex": 0, 177 | "yoda": [2, "never"] 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Brief description of what this PR does and why it is needed. 4 | 5 | 6 | ### Demo 7 | 8 | Optional. Screenshots, `curl` examples, etc. 9 | 10 | 11 | ### Notes 12 | 13 | Optional. Ancillary topics, caveats, alternative strategies that didn't work out, anything else. 14 | 15 | 16 | ## Testing Instructions 17 | 18 | * How to test this PR 19 | * Prefer bulleted description 20 | * Start after checking out this branch 21 | * Include any setup required, such as bundling scripts, restarting services, etc. 22 | * Include test case and expected output 23 | 24 | 25 | ## Checklist 26 | 27 | - [ ] Add entry to CHANGELOG.md 28 | - [ ] Update the README with any function signature changes 29 | 30 | Resolves #XXX 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - '[0-9]+.[0-9]+.[0-9]+*' # Push events to tags starting with numbers 8 | 9 | jobs: 10 | build: 11 | name: Create Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out commit 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node.js 18 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 18.x 21 | registry-url: 'https://registry.npmjs.org' 22 | 23 | - name: Install dependencies 24 | run: yarn install --frozen-lockfile 25 | 26 | - name: Build project 27 | run: yarn build 28 | 29 | - name: Create release 30 | id: create_release 31 | uses: actions/create-release@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 34 | with: 35 | tag_name: ${{ github.ref }} 36 | release_name: Release ${{ github.ref }} 37 | body: | 38 | Changes in this Release 39 | - TODO 40 | draft: true 41 | prerelease: false 42 | 43 | - name: Publish to NPM 44 | run: npm publish 45 | env: 46 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | # Triggers the workflow on push or pull request 4 | # events but only for the master, develop branches 5 | on: 6 | push: 7 | branches: [ master, develop ] 8 | pull_request: 9 | branches: [ master, develop ] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x, 16.x, 18.x] 18 | 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 21 | - name: Check out commit 22 | uses: actions/checkout@v2 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install dependencies 30 | run: yarn install --frozen-lockfile 31 | 32 | - name: Run tests 33 | run: 'yarn test:ci' 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | lib/* 3 | 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | # Remove some common IDE working directories 32 | .idea 33 | .vscode 34 | 35 | .DS_Store 36 | 37 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 38 | .yarn/* 39 | !.yarn/patches 40 | !.yarn/plugins 41 | !.yarn/releases 42 | !.yarn/versions 43 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | } 8 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.2.3.cjs 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ## UPCOMING 2 | 3 | ## 1.2.0 (2022-12-12) 4 | - Add support for opening vector datasets 5 | - Add GDALDataset.layerCount() to wrap GDALDatasetGetLayerCount() 6 | - Add GDALDataset.vectorConvert() to wrap GDALVectorTranslate() 7 | - Add wrappers for GDALGetRasterMinimum, GDALGetRasterMaximum, GDALGetRasterDataType, GDALGetRasterStatistics, and GDALGetRasterNoDataValue 8 | 9 | ## 1.1.2 (2022-10-12) 10 | - Allow users to specify a valid absolute URL when the default URL is invalid 11 | 12 | ## 1.1.1 (2022-09-30) 13 | - Document bytes() method 14 | - Fix production bundle not including unminified assets. 15 | - Add Node versions 16 and 18 to testing matrix (this library currently targets browser environments only) 16 | - Drop testing on Node 12 (this library currently targets browser environments only) 17 | - Update to latest stable version of Yarn 18 | 19 | ## 1.1.0 (2021-09-29) 20 | - Improve documentation for integrating with build tools 21 | - Add a reset() function that allows tearing down the Loam worker 22 | - Allow initializing Loam from a CDN 23 | - Add a demo page for local development 24 | 25 | ## 1.0.0 (2021-03-17) 26 | - No changes from RC.2 except dependency updates 27 | 28 | ## 1.0.0-rc.2 (2020-10-30) 29 | - Add information on contributing to the README 30 | - Apply code auto-formatting 31 | - Improve error messages with originating function names 32 | - Add GDALDataset.render() to provide gdaldem functionality 33 | 34 | ## 1.0.0-rc.1 (2020-07-24) 35 | - Add loam.rasterize() wrapper for GDALRasterize() 36 | - Add pathPrefix parameter to loam.initialize() 37 | - Remove closeAndReadBytes(), flushFS(), and convert close() to a no-op. 38 | - Set up CI and automatic releases via GitHub Actions 39 | - Package updates to resolve security alerts 40 | - Validate that parameters to `warp()` and `convert()` are strings 41 | - Add CHANGELOG, PR template 42 | 43 | ## 1.0.0-alpha.10 (2019-02-13) 44 | - Add `reproject()` utility function 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Azavea, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A wrapper for running GDAL in the browser using [gdal-js](https://github.com/ddohler/gdal-js/) 2 | 3 | ![](https://github.com/azavea/loam/workflows/Tests/badge.svg) 4 | 5 | # Installation 6 | ``` 7 | npm install loam 8 | ``` 9 | 10 | Assuming you are using a build system, the main `loam` library should integrate into your build the same as any other library might. However, in order to correctly initialize the Emscripten environment for running GDAL, there are other assets that need to be accessible via HTTP request at runtime, but which should _not_ be included in the main application bundle. Specifically, these are: 11 | 12 | - `loam-worker.js`: This is the "backend" of the library; it initializes the Web Worker and translates between the Loam "frontend" and GDAL. 13 | - [`gdal.js`](https://www.npmjs.com/package/gdal-js): This initializes the Emscripten runtime and loads the GDAL WebAssembly. 14 | - [`gdal.wasm`](https://www.npmjs.com/package/gdal-js): The GDAL binary, compiled to WebAssembly. 15 | - [`gdal.data`](https://www.npmjs.com/package/gdal-js): Contains configuration files that GDAL expects to find on the host filesystem. 16 | 17 | All of these files will be included in the `node_modules` folder after running `npm install loam`, but it is up to you to integrate them into your development environment and deployment processes. Unfortunately, support for WebAssembly and Web Workers is still relatively young, so many build tools do not yet have a straightforward out-of-the-box solution that will work. However, in general, treating the four files above similarly to static assets (e.g. images, videos, or PDFs) tends to work fairly well. An example for Create React App is given below. 18 | 19 | ## Create React App 20 | When integrating Loam with a React app that was initialized using Create React App, the simplest thing to do is probably to copy the assets above into [the `/public` folder](https://create-react-app.dev/docs/using-the-public-folder#adding-assets-outside-of-the-module-system), like so: 21 | 22 | ``` 23 | cp node_modules/gdal-js/gdal.* node_modules/loam/lib/loam-worker.js public/ 24 | ``` 25 | 26 | This will cause the CRA build system to copy these files into the build folder untouched, where they can then be accessed by URL (e.g. `http://localhost:3000/gdal.wasm`). 27 | However, this has the disadvantage that you will need to commit the copied files to source control, and they won't be updated if you update Loam. A way to work around this is to put symlinks in `/public` instead: 28 | 29 | ``` 30 | ln -s ../node_modules/loam/lib/loam-worker.js public/loam-worker.js 31 | ln -s ../node_modules/gdal-js/gdal.wasm public/gdal.wasm 32 | ln -s ../node_modules/gdal-js/gdal.data public/gdal.data 33 | ln -s ../node_modules/gdal-js/gdal.js public/gdal.js 34 | 35 | ``` 36 | 37 | # API Documentation 38 | ## Basic usage 39 | 40 | ```javascript 41 | import loam from "loam"; 42 | 43 | // Load WebAssembly and data files asynchronously. Will be called automatically by loam.open() 44 | // but it is often helpful for responsiveness to pre-initialize because these files are fairly large. Returns a promise. 45 | loam.initialize(); 46 | 47 | // Assuming you have a `Blob` object from somewhere. `File` objects also work. 48 | loam.open(blob).then((dataset) => { 49 | dataset.width() 50 | .then((width) => /* do stuff with width */); 51 | ``` 52 | 53 | ## Functions 54 | ### `loam.initialize(pathPrefix, gdalPrefix)` 55 | Manually set up web worker and initialize Emscripten runtime. This function is called automatically by other functions on `loam`. Returns a promise that is resolved when Loam is fully initialized. 56 | 57 | Although this function is called automatically by other functions, such as `loam.open()`, it is often beneficial for user experience to manually call `loam.initialize()`, because it allows pre-fetching Loam's WebAssembly assets (which are several megabytes uncompressed) at a time when the latency required to download them will be least perceptible by the user. For example, `loam.initialize()` could be called when the user clicks a button to open a file-selection dialog, allowing the WebAssembly to load in the background while the user selects a file. 58 | 59 | This function is safe to call multiple times. 60 | #### Parameters 61 | - `pathPrefix` (optional): The path or URL that Loam should use as a prefix when fetching its Web Worker. If left undefined, Loam will make a best guess based on the source path of its own ` 6 | 7 | 8 | 9 |

10 | Select a file using the Source file input and then click "Display metadata". Metadata 11 | about the file will be displayed below. GeoTIFFs work best, but GeoJSON and Shapefiles 12 | will work to some extent as well. Make sure to select sidecar files when relevant (e.g. 13 | .prj, .dbf, .shx, etc. for Shapefiles). 14 |

15 |
16 | 20 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /* global loam */ 2 | 3 | // Use the locally built version of loam, with a CDN copy of GDAL from unpkg. 4 | loam.initialize('/', 'https://unpkg.com/gdal-js@2.1.0/'); 5 | 6 | const EPSG4326 = 7 | '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"]]'; 8 | 9 | function displayInfo() { 10 | const sourceFile = document.querySelector('#source-file').files[0]; 11 | const sidecars = Array.from(document.querySelector('#sidecar-files').files); 12 | 13 | const displayElem = document.getElementById('gdalinfo'); 14 | 15 | // Clear display text 16 | displayElem.innerText = ''; 17 | // Use Loam to get GeoTIFF metadata 18 | loam.open(sourceFile, sidecars).then((ds) => { 19 | return Promise.all([ 20 | ds.width(), 21 | ds.height(), 22 | ds.count(), 23 | ds.layerCount(), 24 | ds.wkt(), 25 | ds.transform(), 26 | ]).then(([width, height, count, layerCount, wkt, geoTransform]) => { 27 | displayElem.innerText += 'Size: ' + width.toString() + ', ' + height.toString() + '\n'; 28 | displayElem.innerText += 'Raster band count: ' + count.toString() + '\n'; 29 | displayElem.innerText += 'Vector layer count: ' + layerCount.toString() + '\n'; 30 | displayElem.innerText += 'Coordinate system:\n' + wkt + '\n'; 31 | 32 | const cornersPx = [ 33 | [0, 0], 34 | [width, 0], 35 | [width, height], 36 | [0, height], 37 | ]; 38 | const cornersGeo = cornersPx.map(([x, y]) => { 39 | return [ 40 | // http://www.gdal.org/gdal_datamodel.html 41 | geoTransform[0] + geoTransform[1] * x + geoTransform[2] * y, 42 | geoTransform[3] + geoTransform[4] * x + geoTransform[5] * y, 43 | ]; 44 | }); 45 | 46 | loam.reproject(wkt, EPSG4326, cornersGeo).then((cornersLngLat) => { 47 | displayElem.innerText += 'Corner Coordinates:\n'; 48 | cornersLngLat.forEach(([lng, lat], i) => { 49 | displayElem.innerText += 50 | '(' + 51 | cornersGeo[i][0].toString() + 52 | ', ' + 53 | cornersGeo[i][1].toString() + 54 | ') (' + 55 | lng.toString() + 56 | ', ' + 57 | lat.toString() + 58 | ')\n'; 59 | }); 60 | }); 61 | 62 | if (count > 0) { 63 | ds.bandStatistics(1).then((stats) => { 64 | displayElem.innerText += 'Band 1 min: ' + stats.minimum + '\n'; 65 | displayElem.innerText += 'Band 1 max: ' + stats.maximum + '\n'; 66 | displayElem.innerText += 'Band 1 median: ' + stats.median + '\n'; 67 | displayElem.innerText += 'Band 1 standard deviation: ' + stats.stdDev + '\n'; 68 | }); 69 | } 70 | }); 71 | }); 72 | } 73 | 74 | document.getElementById('display-metadata-button').onclick = function () { 75 | displayInfo(); 76 | }; 77 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | frameworks: ['mocha', 'chai'], 4 | preprocessors: { 5 | 'test/**/*.spec.js': ['babel'] 6 | }, 7 | babelPreprocessor: { 8 | filename: function (file) { 9 | return file.originalPath.replace(/\.js$/, '.es5.js'); 10 | }, 11 | sourceFileName: function (file) { 12 | return file.originalPath; 13 | } 14 | }, 15 | files: [ 16 | 'lib/loam.js', 17 | { 18 | pattern: 'lib/loam-worker.js', 19 | watched: true, 20 | included: false, 21 | served: true 22 | }, 23 | { 24 | pattern: 'lib/*.js.map', 25 | watched: false, 26 | included: false, 27 | served: true 28 | }, 29 | 'test/**/*.spec.js', 30 | { 31 | pattern: 'node_modules/gdal-js/gdal.*', 32 | watched: false, 33 | included: false, 34 | served: true 35 | }, 36 | { 37 | pattern: 'test/assets/*', 38 | watched: false, 39 | included: false, 40 | served: true 41 | } 42 | ], 43 | proxies: { 44 | '/base/lib/gdal.js': '/base/node_modules/gdal-js/gdal.js', 45 | '/base/lib/gdal.wasm': '/base/node_modules/gdal-js/gdal.wasm', 46 | '/base/lib/gdal.data': '/base/node_modules/gdal-js/gdal.data' 47 | }, 48 | // WebAssembly takes a while to parse 49 | browserDisconnectTimeout: 4000, 50 | reporters: ['progress'], 51 | port: 9876, // karma web server port 52 | colors: true, 53 | logLevel: config.LOG_INFO, 54 | browsers: ['ChromeHeadless'], 55 | concurrency: Infinity 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loam", 3 | "version": "1.2.0", 4 | "description": "Javascript wrapper for GDAL in the browser", 5 | "main": "lib/loam.js", 6 | "scripts": { 7 | "build": "webpack --config=webpack.dev.js && webpack --config=webpack.prod.js", 8 | "dev": "webpack --progress --color --watch --config=webpack.dev.js", 9 | "demo": "webpack serve --config=webpack.dev.js", 10 | "format": "prettier --write ./src ./test ./demo", 11 | "test": "karma start --single-run --browser ChromeHeadless karma.conf.js", 12 | "test:watch": "karma start --auto-watch --browser ChromeHeadless karma.conf.js", 13 | "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" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/azavea/loam.git" 18 | }, 19 | "keywords": [ 20 | "gdal", 21 | "emscripten", 22 | "geospatial", 23 | "raster", 24 | "geotiff" 25 | ], 26 | "author": "Derek Dohler", 27 | "license": "Apache-2.0", 28 | "bugs": { 29 | "url": "https://github.com/azavea/loam/issues" 30 | }, 31 | "files": [ 32 | "lib/" 33 | ], 34 | "homepage": "https://github.com/azavea/loam", 35 | "devDependencies": { 36 | "@babel/cli": "^7.18.10", 37 | "@babel/core": "^7.19.1", 38 | "@babel/eslint-parser": "^7.19.1", 39 | "@babel/preset-env": "^7.19.1", 40 | "babel-loader": "^8.2.5", 41 | "babel-plugin-add-module-exports": "^1.0.4", 42 | "chai": "^4.3.6", 43 | "chai-as-promised": "^7.1.1", 44 | "eslint": "^7.31.0", 45 | "eslint-webpack-plugin": "^3.0.1", 46 | "karma": "^6.4.1", 47 | "karma-babel-preprocessor": "^8.0.2", 48 | "karma-chai": "^0.1.0", 49 | "karma-chai-as-promised": "^0.1.2", 50 | "karma-chrome-launcher": "^3.1.1", 51 | "karma-mocha": "^2.0.1", 52 | "mocha": "^10.0.0", 53 | "prettier": "^2.7.1", 54 | "uglifyjs-webpack-plugin": "^2.2.0", 55 | "webpack": "^5.74.0", 56 | "webpack-cli": "^4.7.2", 57 | "webpack-dev-server": "^4.11.1", 58 | "yargs": "^17.5.1" 59 | }, 60 | "dependencies": { 61 | "gdal-js": "2.1.0" 62 | }, 63 | "packageManager": "yarn@3.2.3" 64 | } 65 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import { initWorker, clearWorker, runOnWorker } from './workerCommunication.js'; 2 | import { DatasetSource, GDALDataset } from './gdalDataset.js'; 3 | 4 | function open(file, sidecars = []) { 5 | return new Promise((resolve, reject) => { 6 | const ds = new GDALDataset(new DatasetSource('GDALOpen', file, sidecars)); 7 | 8 | return ds.open().then( 9 | () => resolve(ds), 10 | (reason) => reject(reason) 11 | ); 12 | }); 13 | } 14 | 15 | function rasterize(geojson, args) { 16 | return new Promise((resolve, reject) => { 17 | resolve(new GDALDataset(new DatasetSource('GDALRasterize', geojson, [], args))); 18 | }); 19 | } 20 | 21 | function reproject(fromCRS, toCRS, coords) { 22 | var xCoords = new Float64Array( 23 | coords.map(function (pair) { 24 | return pair[0]; 25 | }) 26 | ); 27 | var yCoords = new Float64Array( 28 | coords.map(function (pair) { 29 | return pair[1]; 30 | }) 31 | ); 32 | 33 | return runOnWorker('LoamReproject', [fromCRS, toCRS, xCoords, yCoords]); 34 | } 35 | 36 | function initialize(loamPrefix, gdalPrefix) { 37 | return initWorker(loamPrefix, gdalPrefix); 38 | } 39 | 40 | function reset() { 41 | return clearWorker(); 42 | } 43 | 44 | export { open, rasterize, initialize, reset, reproject }; 45 | -------------------------------------------------------------------------------- /src/gdalDataType.js: -------------------------------------------------------------------------------- 1 | // In order to make enums available from JS it's necessary to use embind, which seems like 2 | // overkill for something this small. This replicates the GDALDataType enum: 3 | // https://gdal.org/api/raster_c_api.html#_CPPv412GDALDataType and will need to be changed 4 | // if that enum changes. There is a smoke test that should warn us if it changes upstream. 5 | export const GDALDataTypes = [ 6 | 'Unknown', 7 | 'Byte', 8 | 'UInt16', 9 | 'Int16', 10 | 'UInt32', 11 | 'Int32', 12 | 'Float32', 13 | 'Float64', 14 | 'CInt16', 15 | 'CInt32', 16 | 'CFloat32', 17 | 'CFloat64', 18 | 'TypeCount', 19 | ]; 20 | -------------------------------------------------------------------------------- /src/gdalDataset.js: -------------------------------------------------------------------------------- 1 | import { accessFromDataset } from './workerCommunication.js'; 2 | 3 | // A function, to be executed within the GDAL webworker context, that outputs a dataset. 4 | export class DatasetOperation { 5 | constructor(functionName, args) { 6 | this.func = functionName; 7 | this.args = args; 8 | } 9 | } 10 | 11 | // The starting point of a dataset within a GDAL webworker context. Outputs a dataset. 12 | // - functionName is the name of a GDAL function that can generate a dataset 13 | // Currently, this is either GDALOpen or GDALRasterize 14 | // - file is a File, Blob, or {name: string, data: Blob} 15 | // - sidecars are any other files that need to get loaded into the worker filesystem alongside file 16 | // Must match the type of file 17 | // - args are any additional arguments that should be passed to functionName. 18 | export class DatasetSource { 19 | constructor(functionName, file, sidecars = [], args = []) { 20 | this.func = functionName; 21 | this.src = file; 22 | this.sidecars = sidecars; 23 | this.args = args; 24 | } 25 | } 26 | 27 | export class GDALDataset { 28 | constructor(source, operations) { 29 | this.source = source; 30 | if (operations && operations.length > 0) { 31 | this.operations = operations; 32 | } else { 33 | this.operations = []; 34 | } 35 | } 36 | 37 | // Does "nothing", but triggers the dataset to be opened and immediately closed with GDAL, which 38 | // will fail if the file is not a recognized format. 39 | open() { 40 | return accessFromDataset(undefined, this); 41 | } 42 | 43 | bytes() { 44 | return accessFromDataset('LoamReadBytes', this); 45 | } 46 | 47 | count() { 48 | return accessFromDataset('GDALGetRasterCount', this); 49 | } 50 | 51 | layerCount() { 52 | return accessFromDataset('GDALDatasetGetLayerCount', this); 53 | } 54 | 55 | width() { 56 | return accessFromDataset('GDALGetRasterXSize', this); 57 | } 58 | 59 | height() { 60 | return accessFromDataset('GDALGetRasterYSize', this); 61 | } 62 | 63 | wkt() { 64 | return accessFromDataset('GDALGetProjectionRef', this); 65 | } 66 | 67 | transform() { 68 | return accessFromDataset('GDALGetGeoTransform', this); 69 | } 70 | 71 | bandMinimum(bandNum) { 72 | return accessFromDataset('GDALGetRasterMinimum', this, bandNum); 73 | } 74 | 75 | bandMaximum(bandNum) { 76 | return accessFromDataset('GDALGetRasterMaximum', this, bandNum); 77 | } 78 | 79 | bandStatistics(bandNum) { 80 | return accessFromDataset('GDALGetRasterStatistics', this, bandNum); 81 | } 82 | 83 | bandDataType(bandNum) { 84 | return accessFromDataset('GDALGetRasterDataType', this, bandNum); 85 | } 86 | 87 | bandNoDataValue(bandNum) { 88 | return accessFromDataset('GDALGetRasterNoDataValue', this, bandNum); 89 | } 90 | 91 | convert(args) { 92 | return new Promise((resolve, reject) => { 93 | resolve( 94 | new GDALDataset( 95 | this.source, 96 | this.operations.concat(new DatasetOperation('GDALTranslate', args)) 97 | ) 98 | ); 99 | }); 100 | } 101 | 102 | vectorConvert(args) { 103 | return new Promise((resolve, reject) => { 104 | resolve( 105 | new GDALDataset( 106 | this.source, 107 | this.operations.concat(new DatasetOperation('GDALVectorTranslate', args)) 108 | ) 109 | ); 110 | }); 111 | } 112 | 113 | warp(args) { 114 | return new Promise((resolve, reject) => { 115 | resolve( 116 | new GDALDataset( 117 | this.source, 118 | this.operations.concat(new DatasetOperation('GDALWarp', args)) 119 | ) 120 | ); 121 | }); 122 | } 123 | 124 | render(mode, args, colors) { 125 | return new Promise((resolve, reject) => { 126 | // DEMProcessing requires an auxiliary color definition file in some cases, so the API 127 | // can't be easily represented as an array of strings. This packs the user-friendly 128 | // interface of render() into an array that the worker communication machinery can 129 | // easily make use of. It'll get unpacked inside the worker. Yet another reason to use 130 | // something like comlink (#49) 131 | const cliOrderArgs = [mode, colors].concat(args); 132 | 133 | resolve( 134 | new GDALDataset( 135 | this.source, 136 | this.operations.concat(new DatasetOperation('GDALDEMProcessing', cliOrderArgs)) 137 | ) 138 | ); 139 | }); 140 | } 141 | 142 | close() { 143 | return new Promise((resolve, reject) => { 144 | const warningMsg = 145 | 'It is not necessary to call close() on a Loam dataset. This is a no-op'; 146 | 147 | console.warn(warningMsg); 148 | resolve([]); 149 | }); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/guessFileExtension.js: -------------------------------------------------------------------------------- 1 | export default function guessFileExtension(args) { 2 | const supportedFormats = { 3 | PNG: 'png', 4 | JPEG: 'jpg', 5 | GTiff: 'tif', 6 | }; 7 | 8 | // Match GDAL 2.1 behavior: if output format is unspecified, the output format is GeoTiff 9 | // This changes to auto-detection based on extension in GDAL 2.3, so if/when we upgrade to that, 10 | // this will need to be changed. 11 | if (!args.includes('-of')) { 12 | return 'tif'; 13 | } 14 | // Otherwise, try to guess the format from the arguments; this isn't meant for validation, just 15 | // to provide a reasonable filename if it ever ends up getting exposed to the user. 16 | let formatStr = args[args.indexOf('-of') + 1]; 17 | 18 | if (Object.keys(supportedFormats).includes(formatStr)) { 19 | return supportedFormats[formatStr]; 20 | } 21 | // If the next parameter after `-of` isn't in our supported formats, then the user is trying 22 | // to specify a format that's not supported by gdal-js, or their gdal_translate arguments 23 | // array is malformed. Either way, it's not really this function's business to validate 24 | // that, so just return the best guess as to what the user might have intended. Any errors 25 | // will be handled by the main function's error handling code. 26 | return formatStr; 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { open, rasterize, initialize, reset, reproject } from './api.js'; 2 | import { GDALDataset } from './gdalDataset.js'; 3 | 4 | export default { open, rasterize, GDALDataset, initialize, reset, reproject }; 5 | -------------------------------------------------------------------------------- /src/randomKey.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/10726909/random-alpha-numeric-string-in-javascript 2 | export default function randomKey(length = 32, chars = '0123456789abcdefghijklmnopqrstuvwxyz') { 3 | let result = ''; 4 | 5 | for (let i = length; i > 0; i--) { 6 | result += chars[Math.floor(Math.random() * chars.length)]; 7 | } 8 | return result; 9 | } 10 | -------------------------------------------------------------------------------- /src/stringParamAllocator.js: -------------------------------------------------------------------------------- 1 | import { isArrayAllStrings } from './validation.js'; 2 | 3 | /* global Module */ 4 | export default class ParamParser { 5 | constructor(args) { 6 | let self = this; 7 | 8 | if (!isArrayAllStrings(args)) { 9 | throw new Error('All items in the argument list must be strings'); 10 | } 11 | 12 | self.args = args; 13 | } 14 | 15 | allocate() { 16 | const self = this; 17 | 18 | // So first, we need to allocate Emscripten heap space sufficient to store each string as a 19 | // null-terminated C string. 20 | // Because the C function signature is char **, this array of pointers is going to need to 21 | // get copied into Emscripten heap space eventually, so we're going to prepare by storing 22 | // the pointers as a typed array so that we can more easily copy it into heap space later. 23 | let argPtrsArray = Uint32Array.from( 24 | self.args 25 | .map((argStr) => { 26 | // +1 for the null terminator byte 27 | return Module._malloc(Module.lengthBytesUTF8(argStr) + 1); 28 | }) 29 | .concat([0]) 30 | ); 31 | // ^ In addition to each individual argument being null-terminated, the GDAL docs specify 32 | // that GDALTranslateOptionsNew takes its options passed in as a null-terminated array of 33 | // pointers, so we have to add on a null (0) byte at the end. 34 | 35 | // Next, we need to write each string from the JS string array into the Emscripten heap 36 | // space we've allocated for it. 37 | self.args.forEach(function (argStr, i) { 38 | Module.stringToUTF8(argStr, argPtrsArray[i], Module.lengthBytesUTF8(argStr) + 1); 39 | }); 40 | 41 | // Now, as mentioned above, we also need to copy the pointer array itself into heap space. 42 | let argPtrsArrayPtr = Module._malloc(argPtrsArray.length * argPtrsArray.BYTES_PER_ELEMENT); 43 | 44 | Module.HEAPU32.set(argPtrsArray, argPtrsArrayPtr / argPtrsArray.BYTES_PER_ELEMENT); 45 | 46 | self.argPtrsArray = argPtrsArray; 47 | self.argPtrsArrayPtr = argPtrsArrayPtr; 48 | } 49 | 50 | deallocate() { 51 | const self = this; 52 | 53 | Module._free(self.argPtrsArrayPtr); 54 | // Don't try to free the null terminator byte 55 | self.argPtrsArray 56 | .subarray(0, self.argPtrsArray.length - 1) 57 | .forEach((ptr) => Module._free(ptr)); 58 | delete self.argPtrsArray; 59 | delete self.argPtrsArrayPtr; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/validation.js: -------------------------------------------------------------------------------- 1 | function isArrayAllStrings(args) { 2 | return args.every((arg) => typeof arg === 'string'); 3 | } 4 | 5 | export { isArrayAllStrings }; 6 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | /* global FS, addFunction */ 2 | 3 | // w is for wrap 4 | // The wrappers are factories that return functions which perform the necessary setup and 5 | // teardown for interacting with GDAL inside Emscripten world. 6 | import wGDALOpen from './wrappers/gdalOpen.js'; 7 | import wGDALRasterize from './wrappers/gdalRasterize.js'; 8 | import wGDALClose from './wrappers/gdalClose.js'; 9 | import wGDALDatasetGetLayerCount from './wrappers/gdalDatasetGetLayerCount.js'; 10 | import wGDALDEMProcessing from './wrappers/gdalDemProcessing.js'; 11 | import wGDALGetRasterCount from './wrappers/gdalGetRasterCount.js'; 12 | import wGDALGetRasterXSize from './wrappers/gdalGetRasterXSize.js'; 13 | import wGDALGetRasterYSize from './wrappers/gdalGetRasterYSize.js'; 14 | import wGDALGetRasterMinimum from './wrappers/gdalGetRasterMinimum.js'; 15 | import wGDALGetRasterMaximum from './wrappers/gdalGetRasterMaximum.js'; 16 | import wGDALGetRasterNoDataValue from './wrappers/gdalGetRasterNoDataValue.js'; 17 | import wGDALGetRasterDataType from './wrappers/gdalGetRasterDataType.js'; 18 | import wGDALGetRasterStatistics from './wrappers/gdalGetRasterStatistics.js'; 19 | import wGDALGetProjectionRef from './wrappers/gdalGetProjectionRef.js'; 20 | import wGDALGetGeoTransform from './wrappers/gdalGetGeoTransform.js'; 21 | import wGDALTranslate from './wrappers/gdalTranslate.js'; 22 | import wGDALVectorTranslate from './wrappers/gdalVectorTranslate.js'; 23 | import wGDALWarp from './wrappers/gdalWarp.js'; 24 | import wReproject from './wrappers/reproject.js'; 25 | 26 | const DATASETPATH = '/'; 27 | 28 | let initialized = false; 29 | 30 | let registry = {}; 31 | 32 | let errorHandling = { 33 | // In order to make enums available from JS it's necessary to use embind, which seems like 34 | // overkill for something this small. But this is a replication of the CPLErr enum in 35 | // cpl_error.h 36 | CPLErr: { 37 | CENone: 0, 38 | CEDebug: 1, 39 | CEWarning: 2, 40 | CEFailure: 3, 41 | CEFatal: 4, 42 | }, 43 | // These will be populated by onRuntimeInitialized, below 44 | CPLErrorReset: null, 45 | CPLGetLastErrorMsg: null, 46 | CPLGetLastErrorNo: null, 47 | CPLGetLastErrorType: null, 48 | }; 49 | 50 | self.Module = { 51 | print: function (text) { 52 | console.log('stdout: ' + text); 53 | }, 54 | printErr: function (text) { 55 | console.log('stderr: ' + text); 56 | }, 57 | // Optimized builds contain a .js.mem file which is loaded asynchronously; 58 | // this waits until that has finished before performing further setup. 59 | onRuntimeInitialized: function () { 60 | // Initialize GDAL 61 | self.Module.ccall('GDALAllRegister', null, [], []); 62 | 63 | // Set up error handling 64 | errorHandling.CPLErrorReset = self.Module.cwrap('CPLErrorReset', null, []); 65 | errorHandling.CPLGetLastErrorMsg = self.Module.cwrap('CPLGetLastErrorMsg', 'string', []); 66 | errorHandling.CPLGetLastErrorNo = self.Module.cwrap('CPLGetLastErrorNo', 'number', []); 67 | errorHandling.CPLGetLastErrorType = self.Module.cwrap('CPLGetLastErrorType', 'number', []); 68 | // Get a "function pointer" to the built-in quiet error handler so that errors don't 69 | // cause tons of console noise. 70 | const cplQuietFnPtr = addFunction( 71 | self.Module.cwrap('CPLQuietErrorHandler', null, ['number', 'number', 'string']), 72 | 'viii' 73 | ); 74 | 75 | // Then set the error handler to the quiet handler. 76 | self.Module.ccall('CPLSetErrorHandler', 'number', ['number'], [cplQuietFnPtr]); 77 | 78 | // Set up JS proxy functions 79 | // Note that JS Number types are used to represent pointers, which means that 80 | // any time we want to pass a pointer to an object, such as in GDALOpen, which in 81 | // C returns a pointer to a GDALDataset, we need to use 'number'. 82 | // 83 | registry.GDALOpen = wGDALOpen( 84 | self.Module.cwrap('GDALOpenEx', 'number', [ 85 | 'string', // Filename 86 | 'number', // nOpenFlags 87 | 'number', // NULL-terminated list of drivers to limit to when opening file 88 | 'number', // NULL-terminated list of option flags passed to drivers 89 | 'number', // Paths to sibling files to avoid file system searches 90 | ]), 91 | errorHandling, 92 | DATASETPATH 93 | ); 94 | registry.GDALRasterize = wGDALRasterize( 95 | self.Module.cwrap('GDALRasterize', 'number', [ 96 | 'string', // Destination dataset path or NULL 97 | 'number', // GDALDatasetH destination dataset or NULL 98 | 'number', // GDALDatasetH source dataset or NULL 99 | 'number', // GDALRasterizeOptions * or NULL 100 | 'number', // int * to use for error reporting 101 | ]), 102 | errorHandling, 103 | DATASETPATH 104 | ); 105 | registry.GDALClose = wGDALClose( 106 | self.Module.cwrap('GDALClose', 'number', ['number']), 107 | errorHandling 108 | ); 109 | registry.GDALGetRasterCount = wGDALGetRasterCount( 110 | self.Module.cwrap('GDALGetRasterCount', 'number', ['number']), 111 | errorHandling 112 | ); 113 | registry.GDALDatasetGetLayerCount = wGDALDatasetGetLayerCount( 114 | self.Module.cwrap('GDALDatasetGetLayerCount', 'number', ['number']), 115 | errorHandling 116 | ); 117 | registry.GDALGetRasterXSize = wGDALGetRasterXSize( 118 | self.Module.cwrap('GDALGetRasterXSize', 'number', ['number']), 119 | errorHandling 120 | ); 121 | registry.GDALGetRasterYSize = wGDALGetRasterYSize( 122 | self.Module.cwrap('GDALGetRasterYSize', 'number', ['number']), 123 | errorHandling 124 | ); 125 | registry.GDALGetRasterMinimum = wGDALGetRasterMinimum( 126 | self.Module.cwrap('GDALGetRasterMinimum', 'number', ['number']), 127 | errorHandling 128 | ); 129 | registry.GDALGetRasterMaximum = wGDALGetRasterMaximum( 130 | self.Module.cwrap('GDALGetRasterMaximum', 'number', ['number']), 131 | errorHandling 132 | ); 133 | registry.GDALGetRasterNoDataValue = wGDALGetRasterNoDataValue( 134 | self.Module.cwrap('GDALGetRasterNoDataValue', 'number', ['number']), 135 | errorHandling 136 | ); 137 | registry.GDALGetRasterDataType = wGDALGetRasterDataType( 138 | self.Module.cwrap('GDALGetRasterDataType', 'number', ['number']), 139 | errorHandling 140 | ); 141 | registry.GDALGetRasterStatistics = wGDALGetRasterStatistics( 142 | self.Module.cwrap('GDALGetRasterStatistics', 'number', ['number']), 143 | errorHandling 144 | ); 145 | registry.GDALGetProjectionRef = wGDALGetProjectionRef( 146 | self.Module.cwrap('GDALGetProjectionRef', 'string', ['number']), 147 | errorHandling 148 | ); 149 | registry.GDALGetGeoTransform = wGDALGetGeoTransform( 150 | self.Module.cwrap('GDALGetGeoTransform', 'number', ['number', 'number']), 151 | errorHandling 152 | ); 153 | registry.GDALTranslate = wGDALTranslate( 154 | self.Module.cwrap('GDALTranslate', 'number', [ 155 | 'string', // Output path 156 | 'number', // GDALDatasetH source dataset 157 | 'number', // GDALTranslateOptions * 158 | 'number', // int * to use for error reporting 159 | ]), 160 | errorHandling, 161 | DATASETPATH 162 | ); 163 | // Equivalent to ogr2ogr 164 | registry.GDALVectorTranslate = wGDALVectorTranslate( 165 | self.Module.cwrap('GDALVectorTranslate', 'number', [ 166 | 'string', // Output path or NULL 167 | 'number', // Destination dataset or NULL 168 | 'number', // Number of input datasets (only 1 is supported) 169 | 'number', // GDALDatasetH * list of source datasets 170 | 'number', // GDALVectorTranslateOptions * 171 | 'number', // int * to use for error reporting 172 | ]), 173 | errorHandling, 174 | DATASETPATH 175 | ); 176 | registry.GDALWarp = wGDALWarp( 177 | self.Module.cwrap('GDALWarp', 'number', [ 178 | 'string', // Destination dataset path or NULL 179 | 'number', // GDALDatasetH destination dataset or NULL 180 | 'number', // Number of input datasets 181 | 'number', // GDALDatasetH * list of source datasets 182 | 'number', // GDALWarpAppOptions * 183 | 'number', // int * to use for error reporting 184 | ]), 185 | errorHandling, 186 | DATASETPATH 187 | ); 188 | registry.GDALDEMProcessing = wGDALDEMProcessing( 189 | self.Module.cwrap('GDALDEMProcessing', 'number', [ 190 | 'string', // Destination dataset path or NULL 191 | 'number', // GDALDatasetH destination dataset 192 | // eslint-disable-next-line max-len 193 | 'string', // The processing to apply (one of "hillshade", "slope", "aspect", "color-relief", "TRI", "TPI", "roughness") 194 | 'string', // Color file path (when previous is "hillshade") or NULL (otherwise) 195 | 'number', // GDALDEMProcessingOptions * 196 | 'number', // int * to use for error reporting 197 | ]), 198 | errorHandling, 199 | DATASETPATH 200 | ); 201 | registry.LoamFlushFS = function () { 202 | let datasetFolders = FS.lookupPath(DATASETPATH).node.contents; 203 | 204 | Object.values(datasetFolders).forEach((node) => { 205 | FS.unmount(FS.getPath(node)); 206 | FS.rmdir(FS.getPath(node)); 207 | }); 208 | return true; 209 | }; 210 | registry.LoamReproject = wReproject; 211 | 212 | // Errors in this function will result in onerror() being called in the main thread, which 213 | // will reject the initialization promise and tear down the worker, so there's no need to do 214 | // separate error handling here -- if something goes wrong prior to this point, it's 215 | // presumably fatal. 216 | initialized = true; 217 | postMessage({ ready: true }); 218 | }, 219 | }; 220 | 221 | function handleDatasetAccess(accessor, dataset, args) { 222 | // 1: Open the source. 223 | let srcDs = registry[dataset.source.func]( 224 | dataset.source.src, 225 | dataset.source.args, 226 | dataset.source.sidecars 227 | ); 228 | 229 | let resultDs = srcDs; 230 | 231 | // Run the operations (transformations) encapsulated in the dataset. This is a list of GDALWarp 232 | // and/or GDALTranslate calls. 233 | // 2. Run first operation on the open dataset. 234 | // 3. Close open dataset, delete files. 235 | // 4. If forther operations, back to 2, otherwise pass open dataset along so the data can be 236 | // accessed. 237 | for (const { func: op, args: args } of dataset.operations) { 238 | resultDs = registry[op](srcDs.datasetPtr, args); 239 | registry.GDALClose(srcDs.datasetPtr, srcDs.directory, srcDs.filePath); 240 | srcDs = resultDs; 241 | } 242 | 243 | let result; 244 | 245 | if (accessor === 'LoamReadBytes') { 246 | result = registry.GDALClose( 247 | resultDs.datasetPtr, 248 | resultDs.directory, 249 | resultDs.filePath, 250 | true 251 | ); 252 | } else if (accessor) { 253 | result = registry[accessor](resultDs.datasetPtr, ...args); 254 | registry.GDALClose(resultDs.datasetPtr, resultDs.directory, resultDs.filePath, false); 255 | } else { 256 | registry.GDALClose(resultDs.datasetPtr, resultDs.directory, resultDs.filePath, false); 257 | } 258 | return result; 259 | } 260 | 261 | // Handle function call 262 | function handleFunctionCall(func, args) { 263 | if (func in registry) { 264 | return registry[func](...args); 265 | } 266 | throw new Error(`Function ${func} was not found`); 267 | } 268 | 269 | onmessage = function (msg) { 270 | if (!initialized) { 271 | postMessage({ 272 | success: false, 273 | message: 'Runtime not yet initialized', 274 | id: msg.data.id, 275 | }); 276 | return; 277 | } 278 | try { 279 | let result; 280 | 281 | if ('func' in msg.data && 'args' in msg.data) { 282 | result = handleFunctionCall(msg.data.func, msg.data.args); 283 | } else if ('accessor' in msg.data && 'dataset' in msg.data) { 284 | result = handleDatasetAccess(msg.data.accessor, msg.data.dataset, msg.data.args); 285 | } else { 286 | postMessage({ 287 | success: false, 288 | message: 289 | // eslint-disable-next-line max-len 290 | 'Worker could not parse message: either func + args or accessor + dataset is required', 291 | id: msg.data.id, 292 | }); 293 | return; 294 | } 295 | postMessage({ 296 | success: true, 297 | result: result, 298 | id: msg.data.id, 299 | }); 300 | } catch (error) { 301 | postMessage({ 302 | success: false, 303 | message: error.message, 304 | id: msg.data.id, 305 | }); 306 | } 307 | }; 308 | -------------------------------------------------------------------------------- /src/workerCommunication.js: -------------------------------------------------------------------------------- 1 | import randomKey from './randomKey.js'; 2 | 3 | let messages = {}; 4 | 5 | let workerPromise; 6 | 7 | class LoamWorker { 8 | // Warning: Here be dragons. 9 | // 10 | // In order to run Loam from a CDN, we need to be able to load the worker code from an arbitrary 11 | // prefix, and the GDAL assets from another, potentially different, prefix, determined at 12 | // runtime. The only way to instantiate a Web Worker is by providing it the URL of a Javascript 13 | // source file, and the only way to specify which prefix the Emscripten wrapper code uses to 14 | // download GDAL's assets is to set Module.locateFile from inside the Web Worker. Once the Web 15 | // Worker has been created, we can only communicate with it via message-passing. Therefore, if 16 | // we used a static file to instantiate the Web Worker, we'd have to have a multi-stage 17 | // initialization process, where the worker asks the main thread which URLs to use to download 18 | // the GDAL assets, sets Module.locateFile appropriately, loads GDAL, and then reports back to 19 | // the main thread that it's ready. This seems potentially error-prone because if something goes 20 | // wrong partway through the process, Loam could get stuck in a half-initialized state and 21 | // potentially would appear to simply "hang" from the user's perspective. 22 | // 23 | // Instead, we are using something of a hack: we generate a string representing Javascript 24 | // source, with the appropriate prefixes baked in. Then we generate a Blob from that string and 25 | // call URL.createObjectURL from the blob. This gives us a URL that we can use to instantiate 26 | // the web worker, but we have immediate control over what it does because we inject the 27 | // prefixes directly into the worker source code via template literals. However, generating 28 | // Javascript from a string is far from optimal -- it can't be minified, linted, or type-checked 29 | // if we ever convert to Typescript. 30 | // 31 | // The logic in this section should be limited to functionality that absolutely must be 32 | // parameterized upon Worker initialization. Everything else should go in worker.js. 33 | constructor(loamPrefix, gdalPrefix) { 34 | const codeStr = `{ 35 | // Set up most of Module, and the rest of the worker communication logic. 36 | importScripts('${loamPrefix}' + 'loam-worker.js'); 37 | // Add locateFile that directs to gdalPrefix 38 | self.Module.locateFile = function(path) { 39 | return '${gdalPrefix}' + path; 40 | }; 41 | // Load gdal.js. This will populate the Module object, and then call 42 | // Module.onRuntimeInitialized() when it is ready for user code to interact with it. 43 | importScripts('${gdalPrefix}' + 'gdal.js'); 44 | }`; 45 | 46 | const blob = new Blob([codeStr], { type: 'application/javascript' }); 47 | 48 | return new Worker(URL.createObjectURL(blob)); 49 | } 50 | } 51 | 52 | // Cache the currently executing script at initialization so that we can use it later to figure 53 | // out where all the other scripts should be pulled from 54 | let _scripts = document.getElementsByTagName('script'); 55 | const THIS_SCRIPT = _scripts[_scripts.length - 1]; 56 | 57 | // Inspired by Emscripten's method for doing the same thing 58 | function getPathPrefix() { 59 | const prefix = THIS_SCRIPT.src.substring(0, THIS_SCRIPT.src.lastIndexOf('/')) + '/'; 60 | 61 | try { 62 | // prefix must be a valid URL so validate that it is before returning 63 | // eslint-disable-next-line no-new 64 | new URL(prefix); 65 | return prefix; 66 | } catch (error) { 67 | // Returning undefined will require the user to specify a valid absolute URL for loamPrefix 68 | // at least 69 | return undefined; 70 | } 71 | } 72 | 73 | // Destroy the worker and clear the promise so that calling initWorker will make a new one. 74 | function clearWorker() { 75 | return new Promise(function (resolve) { 76 | if (workerPromise !== undefined) { 77 | // If a worker has been initialized, wait for it to succeed or fail. If it succeeds, 78 | // kill it. Then, no matter what, set the promise to undefined so that subsequent calls 79 | // to initialize() will spawn a new worker. 80 | workerPromise 81 | .then(function (worker) { 82 | worker.terminate(); 83 | }) 84 | // No need to take any action if initialization fails -- initWorker() will have 85 | // already torn things down in that case. 86 | .finally(function () { 87 | workerPromise = undefined; 88 | resolve(); 89 | }); 90 | } else { 91 | resolve(); 92 | } 93 | }); 94 | } 95 | 96 | // Set up a WebWorker and an associated promise that resolves once it's ready 97 | function initWorker(loamPrefix, gdalPrefix) { 98 | const defaultPrefix = getPathPrefix(); 99 | 100 | // URL's relative path handling does the "right" thing for all relative paths. That is, if a 101 | // user passes in an absolute URL with a domain name, it will use the specified domain, but if a 102 | // path is passed in, it will just override the path and use the prefix domain. 103 | // https://developer.mozilla.org/en-US/docs/Web/API/URL/URL 104 | loamPrefix = loamPrefix !== undefined ? new URL(loamPrefix, defaultPrefix).href : defaultPrefix; 105 | gdalPrefix = gdalPrefix !== undefined ? new URL(gdalPrefix, defaultPrefix).href : loamPrefix; 106 | 107 | if (workerPromise === undefined) { 108 | workerPromise = new Promise(function (resolve, reject) { 109 | let _worker = new LoamWorker(loamPrefix, gdalPrefix); 110 | 111 | // The worker needs to do some initialization, and will send a message when it's ready. 112 | // The message will specify msg.data.ready == true, but that's not really important; 113 | // during initialization the worker doesn't do any error handling so any errors will 114 | // bubble up to onerror (and an error before the "ready" message arrives implies that 115 | // the ready message will never arrive, so we need to destroy the whole worker). 116 | _worker.onmessage = function (msg) { 117 | // Once the worker's ready, change the onMessage and onError functions to handle 118 | // normal operations. 119 | _worker.onmessage = function (msg) { 120 | // Execute stored promise resolver by message ID 121 | // Promise resolvers are stored by callWorker(). 122 | if (msg.data.success) { 123 | messages[msg.data.id][0](msg.data.result); 124 | } else { 125 | messages[msg.data.id][1](new Error(msg.data.message)); 126 | } 127 | delete messages[msg.data.id]; 128 | }; 129 | // Once the worker is successfully initialized, it handles GDAL errors internally, 130 | // so we no longer want to do anything special for error handling. 131 | _worker.onerror = null; 132 | resolve(_worker); 133 | }; 134 | // Error handler during initialization. This will get overridden upon successful worker 135 | // initialization. We assume that an error during initialization is fatal, so tear down 136 | // the worker if that happens. 137 | _worker.onerror = function (err) { 138 | err.preventDefault(); 139 | console.error(err); 140 | _worker.terminate(); 141 | reject(err); 142 | }; 143 | }); 144 | } 145 | return workerPromise; 146 | } 147 | 148 | // Store a listener function with a key so that we can associate it with a message later. 149 | function addMessageResolver(callback, errback) { 150 | let key = randomKey(); 151 | 152 | while (messages.hasOwnProperty(key)) { 153 | key = randomKey(); 154 | } 155 | messages[key] = [callback, errback]; 156 | return key; 157 | } 158 | 159 | // Send a message to the worker and return a promise that resolves / rejects when a message with 160 | // a matching id is returned. 161 | function workerTaskPromise(options) { 162 | return initWorker().then((worker) => { 163 | return new Promise((resolve, reject) => { 164 | let resolverId = addMessageResolver( 165 | (workerResult) => resolve(workerResult), 166 | (reason) => reject(reason) 167 | ); 168 | 169 | worker.postMessage({ id: resolverId, ...options }); 170 | }); 171 | }); 172 | } 173 | 174 | // Accessors is a list of accessors operations to run on the dataset defined by dataset. 175 | function accessFromDataset(accessor, dataset, ...otherArgs) { 176 | return workerTaskPromise({ accessor: accessor, dataset: dataset, args: otherArgs }); 177 | } 178 | 179 | // Run a single function on the worker. 180 | function runOnWorker(func, args) { 181 | return workerTaskPromise({ func, args }); 182 | } 183 | 184 | export { initWorker, clearWorker, accessFromDataset, runOnWorker }; 185 | -------------------------------------------------------------------------------- /src/wrappers/gdalClose.js: -------------------------------------------------------------------------------- 1 | /* global FS */ 2 | export default function (GDALClose, errorHandling) { 3 | return function (datasetPtr, directory, datasetPath, returnFileBytes = false) { 4 | GDALClose(datasetPtr); 5 | let result = []; 6 | 7 | if (returnFileBytes) { 8 | result = FS.readFile(datasetPath, { encoding: 'binary' }); 9 | } 10 | 11 | FS.unmount(directory); 12 | FS.rmdir(directory); 13 | 14 | let errorType = errorHandling.CPLGetLastErrorType(); 15 | 16 | // Check for errors; throw if error is detected 17 | // Note that due to https://github.com/ddohler/gdal-js/issues/38 this can only check for 18 | // CEFatal errors in order to avoid raising an exception on GDALClose 19 | if (errorType === errorHandling.CPLErr.CEFatal) { 20 | let message = errorHandling.CPLGetLastErrorMsg(); 21 | 22 | throw new Error('Error in GDALClose: ' + message); 23 | } else { 24 | return result; 25 | } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/wrappers/gdalDatasetGetLayerCount.js: -------------------------------------------------------------------------------- 1 | export default function (GDALDatasetGetLayerCount, errorHandling) { 2 | return function (datasetPtr) { 3 | const result = GDALDatasetGetLayerCount(datasetPtr); 4 | 5 | const errorType = errorHandling.CPLGetLastErrorType(); 6 | 7 | // Check for errors; clean up and throw if error is detected 8 | if ( 9 | errorType === errorHandling.CPLErr.CEFailure || 10 | errorType === errorHandling.CPLErr.CEFatal 11 | ) { 12 | const message = errorHandling.CPLGetLastErrorMsg(); 13 | 14 | throw new Error('Error in GDALDatasetGetLayerCount: ' + message); 15 | } else { 16 | return result; 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/wrappers/gdalDemProcessing.js: -------------------------------------------------------------------------------- 1 | /* global Module, FS, MEMFS */ 2 | import randomKey from '../randomKey.js'; 3 | import guessFileExtension from '../guessFileExtension.js'; 4 | import ParamParser from '../stringParamAllocator.js'; 5 | 6 | // TODO: This is another good reason to switch to Typescript #55 7 | const DEMProcessingModes = Object.freeze({ 8 | hillshade: 'hillshade', 9 | slope: 'slope', 10 | aspect: 'aspect', 11 | 'color-relief': 'color-relief', 12 | TRI: 'TRI', 13 | TPI: 'TPI', 14 | roughness: 'roughness', 15 | }); 16 | 17 | export default function (GDALDEMProcessing, errorHandling, rootPath) { 18 | /* mode: one of the options in DEMProcessingModes 19 | * colors: Array of strings matching the format of the color file defined at 20 | * https://gdal.org/programs/gdaldem.html#color-relief 21 | * args: Array of strings matching the remaining arguments of gdaldem, excluding output filename 22 | */ 23 | return function (dataset, packedArgs) { 24 | // TODO: Make this unnecessary by switching to comlink or similar (#49) 25 | const mode = packedArgs[0]; 26 | const colors = packedArgs[1]; 27 | const args = packedArgs.slice(2); 28 | 29 | if (!mode || !DEMProcessingModes.hasOwnProperty(mode)) { 30 | throw new Error(`mode must be one of {Object.keys(DEMProcessingModes)}`); 31 | } else if (mode === DEMProcessingModes['color-relief'] && !colors) { 32 | throw new Error( 33 | 'A color definition array must be provided if `mode` is "color-relief"' 34 | ); 35 | } else if (mode !== DEMProcessingModes['color-relief'] && colors && colors.length > 0) { 36 | throw new Error( 37 | 'A color definition array should not be provided if `mode` is not "color-relief"' 38 | ); 39 | } 40 | 41 | // If mode is hillshade, we need to create a color file path 42 | let colorFilePath = null; 43 | 44 | if (mode === DEMProcessingModes['color-relief']) { 45 | colorFilePath = rootPath + randomKey() + '.txt'; 46 | 47 | FS.writeFile(colorFilePath, colors.join('\n')); 48 | } 49 | let params = new ParamParser(args); 50 | 51 | params.allocate(); 52 | 53 | // argPtrsArrayPtr is now the address of the start of the list of 54 | // pointers in Emscripten heap space. Each pointer identifies the address of the start of a 55 | // parameter string, also stored in heap space. This is the direct equivalent of a char **, 56 | // which is what GDALDEMProcessingOptionsNew requires. 57 | const demOptionsPtr = Module.ccall( 58 | 'GDALDEMProcessingOptionsNew', 59 | 'number', 60 | ['number', 'number'], 61 | [params.argPtrsArrayPtr, null] 62 | ); 63 | 64 | // Validate that the options were correct 65 | const optionsErrType = errorHandling.CPLGetLastErrorType(); 66 | 67 | if ( 68 | optionsErrType === errorHandling.CPLErr.CEFailure || 69 | optionsErrType === errorHandling.CPLErr.CEFatal 70 | ) { 71 | if (colorFilePath) { 72 | FS.unlink(colorFilePath); 73 | } 74 | params.deallocate(); 75 | const message = errorHandling.CPLGetLastErrorMsg(); 76 | 77 | throw new Error('Error in GDALDEMProcessing: ' + message); 78 | } 79 | 80 | // Now that we have our options, we need to make a file location to hold the output. 81 | let directory = rootPath + randomKey(); 82 | 83 | FS.mkdir(directory); 84 | // This makes it easier to remove later because we can just unmount rather than recursing 85 | // through the whole directory structure. 86 | FS.mount(MEMFS, {}, directory); 87 | let filename = randomKey(8) + '.' + guessFileExtension(args); 88 | 89 | let filePath = directory + '/' + filename; 90 | 91 | // And then we can kick off the actual processing. 92 | // The last parameter is an int* that can be used to detect certain kinds of errors, 93 | // but I'm not sure how it works yet and whether it gives the same or different information 94 | // than CPLGetLastErrorType. 95 | // Malloc ourselves an int and set it to 0 (False) 96 | let usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT); 97 | 98 | Module.setValue(usageErrPtr, 0, 'i32'); 99 | 100 | let newDatasetPtr = GDALDEMProcessing( 101 | filePath, // Output 102 | dataset, 103 | mode, 104 | colorFilePath, 105 | demOptionsPtr, 106 | usageErrPtr 107 | ); 108 | 109 | let errorType = errorHandling.CPLGetLastErrorType(); 110 | // If we ever want to use the usage error pointer: 111 | // let usageErr = Module.getValue(usageErrPtr, 'i32'); 112 | 113 | // The final set of cleanup we need to do, in a function to avoid writing it twice. 114 | function cleanUp() { 115 | if (colorFilePath) { 116 | FS.unlink(colorFilePath); 117 | } 118 | Module.ccall('GDALDEMProcessingOptionsFree', null, ['number'], [demOptionsPtr]); 119 | Module._free(usageErrPtr); 120 | params.deallocate(); 121 | } 122 | 123 | // Check for errors; clean up and throw if error is detected 124 | if ( 125 | errorType === errorHandling.CPLErr.CEFailure || 126 | errorType === errorHandling.CPLErr.CEFatal 127 | ) { 128 | cleanUp(); 129 | const message = errorHandling.CPLGetLastErrorMsg(); 130 | 131 | throw new Error('Error in GDALDEMProcessing: ' + message); 132 | } else { 133 | const result = { 134 | datasetPtr: newDatasetPtr, 135 | filePath: filePath, 136 | directory: directory, 137 | filename: filename, 138 | }; 139 | 140 | cleanUp(); 141 | return result; 142 | } 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetGeoTransform.js: -------------------------------------------------------------------------------- 1 | /* global Module */ 2 | export default function (GDALGetGeoTransform, errorHandling) { 3 | return function (datasetPtr) { 4 | // This is the first wrapper where things get a bit hairy; the C function follows a common C 5 | // pattern where an array to store the results is allocated and passed into the function, 6 | // which populates the array with the results. Emscripten supports passing arrays to 7 | // functions, but it always creates a *copy* of the array, which means that the original JS 8 | // array remains unchanged, which isn't what we want in this case. So first, we have to 9 | // malloc an array inside the Emscripten heap with the correct size. In this case that is 6 10 | // because the GDAL affine transform array has six elements. 11 | let byteOffset = Module._malloc(6 * Float64Array.BYTES_PER_ELEMENT); 12 | 13 | // byteOffset is now a pointer to the start of the double array in Emscripten heap space 14 | // GDALGetGeoTransform dumps 6 values into the passed double array. 15 | GDALGetGeoTransform(datasetPtr, byteOffset); 16 | 17 | // Module.HEAPF64 provides a view into the Emscripten heap, as an array of doubles. 18 | // Therefore, our byte offset from _malloc needs to be converted into a double offset, so we 19 | // divide it by the number of bytes per double, and then get a subarray of those six 20 | // elements off the Emscripten heap. 21 | let geoTransform = Module.HEAPF64.subarray( 22 | byteOffset / Float64Array.BYTES_PER_ELEMENT, 23 | byteOffset / Float64Array.BYTES_PER_ELEMENT + 6 24 | ); 25 | 26 | let errorType = errorHandling.CPLGetLastErrorType(); 27 | 28 | // Check for errors; clean up and throw if error is detected 29 | if ( 30 | errorType === errorHandling.CPLErr.CEFailure || 31 | errorType === errorHandling.CPLErr.CEFatal 32 | ) { 33 | Module._free(byteOffset); 34 | let message = errorHandling.CPLGetLastErrorMsg(); 35 | 36 | throw new Error('Error in GDALGetGeoTransform: ' + message); 37 | } else { 38 | // To avoid memory leaks in the Emscripten heap, we need to free up the memory we 39 | // allocated after we've converted it into a Javascript object. 40 | let result = Array.from(geoTransform); 41 | 42 | Module._free(byteOffset); 43 | 44 | return result; 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetProjectionRef.js: -------------------------------------------------------------------------------- 1 | export default function (GDALGetProjectionRef, errorHandling) { 2 | return function (datasetPtr) { 3 | let result = GDALGetProjectionRef(datasetPtr); 4 | 5 | let errorType = errorHandling.CPLGetLastErrorType(); 6 | 7 | // Check for errors; clean up and throw if error is detected 8 | if ( 9 | errorType === errorHandling.CPLErr.CEFailure || 10 | errorType === errorHandling.CPLErr.CEFatal 11 | ) { 12 | let message = errorHandling.CPLGetLastErrorMsg(); 13 | 14 | throw new Error('Error in GDALGetProjectionRef: ' + message); 15 | } else { 16 | return result; 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetRasterCount.js: -------------------------------------------------------------------------------- 1 | export default function (GDALGetRasterCount, errorHandling) { 2 | return function (datasetPtr) { 3 | let result = GDALGetRasterCount(datasetPtr); 4 | 5 | let errorType = errorHandling.CPLGetLastErrorType(); 6 | 7 | // Check for errors; clean up and throw if error is detected 8 | if ( 9 | errorType === errorHandling.CPLErr.CEFailure || 10 | errorType === errorHandling.CPLErr.CEFatal 11 | ) { 12 | let message = errorHandling.CPLGetLastErrorMsg(); 13 | 14 | throw new Error('Error in GDALGetRasterCount: ' + message); 15 | } else { 16 | return result; 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetRasterDataType.js: -------------------------------------------------------------------------------- 1 | import { GDALDataTypes } from '../gdalDataType.js'; 2 | 3 | /* global Module */ 4 | export default function (GDALGetRasterDataType, errorHandling) { 5 | return function (datasetPtr, bandNum) { 6 | const bandPtr = Module.ccall( 7 | 'GDALGetRasterBand', 8 | 'number', 9 | ['number', 'number'], 10 | [datasetPtr, bandNum] 11 | ); 12 | // GDALGetRasterDataType will provide an integer because it's pulling from an enum 13 | // So we use that to index into an array of the corresponding type strings so that it's 14 | // easier to work with from Javascript land. 15 | const result = GDALDataTypes[GDALGetRasterDataType(bandPtr)]; 16 | 17 | const errorType = errorHandling.CPLGetLastErrorType(); 18 | 19 | // Check for errors; clean up and throw if error is detected 20 | if ( 21 | errorType === errorHandling.CPLErr.CEFailure || 22 | errorType === errorHandling.CPLErr.CEFatal 23 | ) { 24 | throw new Error( 25 | 'Error in GDALGetRasterDataType: ' + errorHandling.CPLGetLastErrorMsg() 26 | ); 27 | } else { 28 | return result; 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetRasterMaximum.js: -------------------------------------------------------------------------------- 1 | /* global Module */ 2 | export default function (GDALGetRasterMaximum, errorHandling) { 3 | return function (datasetPtr, bandNum) { 4 | const bandPtr = Module.ccall( 5 | 'GDALGetRasterBand', 6 | 'number', 7 | ['number', 'number'], 8 | [datasetPtr, bandNum] 9 | ); 10 | const result = GDALGetRasterMaximum(bandPtr); 11 | 12 | const errorType = errorHandling.CPLGetLastErrorType(); 13 | 14 | // Check for errors; clean up and throw if error is detected 15 | if ( 16 | errorType === errorHandling.CPLErr.CEFailure || 17 | errorType === errorHandling.CPLErr.CEFatal 18 | ) { 19 | throw new Error('Error in GDALGetRasterMaximum: ' + errorHandling.CPLGetLastErrorMsg()); 20 | } else { 21 | return result; 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetRasterMinimum.js: -------------------------------------------------------------------------------- 1 | /* global Module */ 2 | export default function (GDALGetRasterMinimum, errorHandling) { 3 | return function (datasetPtr, bandNum) { 4 | const bandPtr = Module.ccall( 5 | 'GDALGetRasterBand', 6 | 'number', 7 | ['number', 'number'], 8 | [datasetPtr, bandNum] 9 | ); 10 | const result = GDALGetRasterMinimum(bandPtr); 11 | 12 | const errorType = errorHandling.CPLGetLastErrorType(); 13 | 14 | // Check for errors; clean up and throw if error is detected 15 | if ( 16 | errorType === errorHandling.CPLErr.CEFailure || 17 | errorType === errorHandling.CPLErr.CEFatal 18 | ) { 19 | throw new Error('Error in GDALGetRasterMinimum: ' + errorHandling.CPLGetLastErrorMsg()); 20 | } else { 21 | return result; 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetRasterNoDataValue.js: -------------------------------------------------------------------------------- 1 | /* global Module */ 2 | export default function (GDALGetRasterNoDataValue, errorHandling) { 3 | return function (datasetPtr, bandNum) { 4 | const bandPtr = Module.ccall( 5 | 'GDALGetRasterBand', 6 | 'number', 7 | ['number', 'number'], 8 | [datasetPtr, bandNum] 9 | ); 10 | const result = GDALGetRasterNoDataValue(bandPtr); 11 | 12 | const errorType = errorHandling.CPLGetLastErrorType(); 13 | 14 | // Check for errors; clean up and throw if error is detected 15 | if ( 16 | errorType === errorHandling.CPLErr.CEFailure || 17 | errorType === errorHandling.CPLErr.CEFatal 18 | ) { 19 | throw new Error( 20 | 'Error in GDALGetRasterNoDataValue: ' + errorHandling.CPLGetLastErrorMsg() 21 | ); 22 | } else { 23 | return result; 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetRasterStatistics.js: -------------------------------------------------------------------------------- 1 | /* global Module */ 2 | export default function (GDALGetRasterStatistics, errorHandling) { 3 | return function (datasetPtr, bandNum) { 4 | const bandPtr = Module.ccall( 5 | 'GDALGetRasterBand', 6 | 'number', 7 | ['number', 'number'], 8 | [datasetPtr, bandNum] 9 | ); 10 | // We need to allocate pointers to store statistics into which will get passed into 11 | // GDALGetRasterStatistics(). They're all doubles, so allocate 8 bytes each. 12 | const minPtr = Module._malloc(8); 13 | const maxPtr = Module._malloc(8); 14 | const meanPtr = Module._malloc(8); 15 | const sdPtr = Module._malloc(8); 16 | const returnErr = GDALGetRasterStatistics( 17 | bandPtr, 18 | 0, // Approximate statistics flag -- set to false 19 | 1, // Force flag -- will always return statistics even if image must be rescanned 20 | minPtr, 21 | maxPtr, 22 | meanPtr, 23 | sdPtr 24 | ); 25 | 26 | const errorType = errorHandling.CPLGetLastErrorType(); 27 | 28 | // Check for errors; throw if error is detected 29 | // GDALGetRasterStatistics returns CE_Failure if an error occurs. 30 | try { 31 | if ( 32 | errorType === errorHandling.CPLErr.CEFailure || 33 | errorType === errorHandling.CPLErr.CEFatal || 34 | returnErr === errorHandling.CPLErr.CEFailure 35 | ) { 36 | throw new Error( 37 | 'Error in GDALGetRasterStatistics: ' + errorHandling.CPLGetLastErrorMsg() 38 | ); 39 | } else { 40 | // At this point the values at each pointer should have been written with statistics 41 | // so we can read them out and send them back. 42 | return { 43 | minimum: Module.getValue(minPtr, 'double'), 44 | maximum: Module.getValue(maxPtr, 'double'), 45 | median: Module.getValue(meanPtr, 'double'), 46 | stdDev: Module.getValue(sdPtr, 'double'), 47 | }; 48 | } 49 | } finally { 50 | Module._free(minPtr); 51 | Module._free(maxPtr); 52 | Module._free(meanPtr); 53 | Module._free(sdPtr); 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetRasterXSize.js: -------------------------------------------------------------------------------- 1 | export default function (GDALGetRasterXSize, errorHandling) { 2 | return function (datasetPtr) { 3 | let result = GDALGetRasterXSize(datasetPtr); 4 | 5 | let errorType = errorHandling.CPLGetLastErrorType(); 6 | 7 | // Check for errors; clean up and throw if error is detected 8 | if ( 9 | errorType === errorHandling.CPLErr.CEFailure || 10 | errorType === errorHandling.CPLErr.CEFatal 11 | ) { 12 | let message = errorHandling.CPLGetLastErrorMsg(); 13 | 14 | throw new Error('Error in GDALGetRasterXSize: ' + message); 15 | } else { 16 | return result; 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/wrappers/gdalGetRasterYSize.js: -------------------------------------------------------------------------------- 1 | export default function (GDALGetRasterYSize, errorHandling) { 2 | return function (datasetPtr) { 3 | let result = GDALGetRasterYSize(datasetPtr); 4 | 5 | let errorType = errorHandling.CPLGetLastErrorType(); 6 | 7 | // Check for errors; clean up and throw if error is detected 8 | if ( 9 | errorType === errorHandling.CPLErr.CEFailure || 10 | errorType === errorHandling.CPLErr.CEFatal 11 | ) { 12 | let message = errorHandling.CPLGetLastErrorMsg(); 13 | 14 | throw Error('Error in GDALGetRasterYSize: ' + message); 15 | } else { 16 | return result; 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/wrappers/gdalOpen.js: -------------------------------------------------------------------------------- 1 | import randomKey from '../randomKey.js'; 2 | 3 | // Redefine constants from https://github.com/OSGeo/gdal/blob/v2.4.4/gdal/gcore/gdal.h 4 | // Constants are hard to get Emscripten to output in a way that we can usefully reference from 5 | // Javascript. 6 | const GDAL_OF_UPDATE = 0x01; 7 | const GDAL_OF_VERBOSE_ERROR = 0x40; 8 | 9 | /* global FS WORKERFS */ 10 | export default function (GDALOpenEx, errorHandling, rootPath) { 11 | return function (file, args = [], sidecars = []) { 12 | let filename; 13 | 14 | const directory = rootPath + randomKey(); 15 | 16 | FS.mkdir(directory); 17 | 18 | if (file instanceof File) { 19 | filename = file.name; 20 | FS.mount(WORKERFS, { files: [file, ...sidecars] }, directory); 21 | } else if (file instanceof Blob) { 22 | filename = 'dataset'; 23 | FS.mount(WORKERFS, { blobs: [{ name: filename, data: file }, ...sidecars] }, directory); 24 | } else if (file instanceof Object && 'name' in file && 'data' in file) { 25 | filename = file.name; 26 | FS.mount( 27 | WORKERFS, 28 | { blobs: [{ name: filename, data: file.data }, ...sidecars] }, 29 | directory 30 | ); 31 | } 32 | const filePath = directory + '/' + filename; 33 | 34 | const datasetPtr = GDALOpenEx( 35 | filePath, 36 | // Open for update by default. We don't currently provide users a way to control this 37 | // externally and the default is read-only. 38 | GDAL_OF_UPDATE | GDAL_OF_VERBOSE_ERROR, 39 | null, 40 | null, 41 | null 42 | ); 43 | 44 | const errorType = errorHandling.CPLGetLastErrorType(); 45 | 46 | // Check for errors; clean up and throw if error is detected 47 | if ( 48 | errorType === errorHandling.CPLErr.CEFailure || 49 | errorType === errorHandling.CPLErr.CEFatal 50 | ) { 51 | FS.unmount(directory); 52 | FS.rmdir(directory); 53 | const message = errorHandling.CPLGetLastErrorMsg(); 54 | 55 | throw new Error('Error in GDALOpen: ' + message); 56 | } else { 57 | return { 58 | datasetPtr: datasetPtr, 59 | filePath: filePath, 60 | directory: directory, 61 | filename: filename, 62 | }; 63 | } 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/wrappers/gdalRasterize.js: -------------------------------------------------------------------------------- 1 | /* global Module, FS, MEMFS */ 2 | import randomKey from '../randomKey.js'; 3 | import guessFileExtension from '../guessFileExtension.js'; 4 | import ParamParser from '../stringParamAllocator.js'; 5 | 6 | export default function (GDALRasterize, errorHandling, rootPath) { 7 | return function (geojson, args = [], sidecars = []) { 8 | let params = new ParamParser(args); 9 | 10 | // Make a temporary file location to hold the geojson 11 | const geojsonPath = rootPath + randomKey() + '.geojson'; 12 | 13 | FS.writeFile(geojsonPath, JSON.stringify(geojson)); 14 | // Append the geojson path to the args so that it's read as the source. 15 | // Open the geojson using GDALOpenEx, which can handle non-raster sources. 16 | const datasetPtr = Module.ccall('GDALOpenEx', 'number', ['string'], [geojsonPath]); 17 | 18 | params.allocate(); 19 | 20 | // Whew, all finished. argPtrsArrayPtr is now the address of the start of the list of 21 | // pointers in Emscripten heap space. Each pointer identifies the address of the start of a 22 | // parameter string, also stored in heap space. This is the direct equivalent of a char **, 23 | // which is what GDALRasterizeOptionsNew requires. 24 | let rasterizeOptionsPtr = Module.ccall( 25 | 'GDALRasterizeOptionsNew', 26 | 'number', 27 | ['number', 'number'], 28 | [params.argPtrsArrayPtr, null] 29 | ); 30 | 31 | // Validate that the options were correct 32 | let optionsErrType = errorHandling.CPLGetLastErrorType(); 33 | 34 | if ( 35 | optionsErrType === errorHandling.CPLErr.CEFailure || 36 | optionsErrType === errorHandling.CPLErr.CEFatal 37 | ) { 38 | Module.ccall('GDALClose', 'number', ['number'], datasetPtr); 39 | FS.unlink(geojsonPath); 40 | params.deallocate(); 41 | const message = errorHandling.CPLGetLastErrorMsg(); 42 | 43 | throw new Error('Error in GDALRasterize: ' + message); 44 | } 45 | 46 | // Now that we have our translate options, we need to make a file location to hold the 47 | // output. 48 | let directory = rootPath + randomKey(); 49 | 50 | FS.mkdir(directory); 51 | // This makes it easier to remove later because we can just unmount rather than recursing 52 | // through the whole directory structure. 53 | FS.mount(MEMFS, {}, directory); 54 | let filename = randomKey(8) + '.' + guessFileExtension(args); 55 | 56 | let filePath = directory + '/' + filename; 57 | 58 | // And then we can kick off the actual warping process. 59 | // TODO: The last parameter is an int* that can be used to detect certain kinds of errors, 60 | // but I'm not sure how it works yet and whether it gives the same or different information 61 | // than CPLGetLastErrorType 62 | // We can get some error information out of the final pbUsageError parameter, which is an 63 | // int*, so malloc ourselves an int and set it to 0 (False) 64 | let usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT); 65 | 66 | Module.setValue(usageErrPtr, 0, 'i32'); 67 | 68 | let newDatasetPtr = GDALRasterize( 69 | filePath, // Output 70 | 0, // NULL because filePath is not NULL 71 | datasetPtr, 72 | rasterizeOptionsPtr, 73 | usageErrPtr 74 | ); 75 | 76 | let errorType = errorHandling.CPLGetLastErrorType(); 77 | // If we ever want to use the usage error pointer: 78 | // let usageErr = Module.getValue(usageErrPtr, 'i32'); 79 | 80 | // The final set of cleanup we need to do, in a function to avoid writing it twice. 81 | function cleanUp() { 82 | Module.ccall('GDALClose', 'number', ['number'], datasetPtr); 83 | FS.unlink(geojsonPath); 84 | Module.ccall('GDALRasterizeOptionsFree', null, ['number'], [rasterizeOptionsPtr]); 85 | Module._free(usageErrPtr); 86 | params.deallocate(); 87 | } 88 | 89 | // Check for errors; clean up and throw if error is detected 90 | if ( 91 | errorType === errorHandling.CPLErr.CEFailure || 92 | errorType === errorHandling.CPLErr.CEFatal 93 | ) { 94 | cleanUp(); 95 | const message = errorHandling.CPLGetLastErrorMsg(); 96 | 97 | throw new Error('Error in GDALRasterize: ' + message); 98 | } else { 99 | const result = { 100 | datasetPtr: newDatasetPtr, 101 | filePath: filePath, 102 | directory: directory, 103 | filename: filename, 104 | }; 105 | 106 | cleanUp(); 107 | return result; 108 | } 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /src/wrappers/gdalTranslate.js: -------------------------------------------------------------------------------- 1 | import randomKey from '../randomKey.js'; 2 | import guessFileExtension from '../guessFileExtension.js'; 3 | import ParamParser from '../stringParamAllocator.js'; 4 | 5 | /* global Module, FS, MEMFS */ 6 | export default function (GDALTranslate, errorHandling, rootPath) { 7 | // Args is expected to be an array of strings that could function as arguments to gdal_translate 8 | return function (dataset, args) { 9 | let params = new ParamParser(args); 10 | 11 | params.allocate(); 12 | 13 | // Whew, all finished. argPtrsArrayPtr is now the address of the start of the list of 14 | // pointers in Emscripten heap space. Each pointer identifies the address of the start of a 15 | // parameter string, also stored in heap space. This is the direct equivalent of a char **, 16 | // which is what GDALTranslateOptionsNew requires. 17 | let translateOptionsPtr = Module.ccall( 18 | 'GDALTranslateOptionsNew', 19 | 'number', 20 | ['number', 'number'], 21 | [params.argPtrsArrayPtr, null] 22 | ); 23 | 24 | // Validate that the options were correct 25 | let optionsErrType = errorHandling.CPLGetLastErrorType(); 26 | 27 | if ( 28 | optionsErrType === errorHandling.CPLErr.CEFailure || 29 | optionsErrType === errorHandling.CPLErr.CEFatal 30 | ) { 31 | Module.ccall('GDALTranslateOptionsFree', null, ['number'], [translateOptionsPtr]); 32 | params.deallocate(); 33 | const message = errorHandling.CPLGetLastErrorMsg(); 34 | 35 | throw new Error('Error in GDALTranslate: ' + message); 36 | } 37 | 38 | // Now that we have our translate options, we need to make a file location to hold the 39 | // output. 40 | let directory = rootPath + randomKey(); 41 | 42 | FS.mkdir(directory); 43 | // This makes it easier to remove later because we can just unmount rather than recursing 44 | // through the whole directory structure. 45 | FS.mount(MEMFS, {}, directory); 46 | let filename = randomKey(8) + '.' + guessFileExtension(args); 47 | 48 | let filePath = directory + '/' + filename; 49 | 50 | // And then we can kick off the actual translation process. 51 | // TODO: The last parameter is an int* that can be used to detect certain kinds of errors, 52 | // but I'm not sure how it works yet and whether it gives the same or different information 53 | // than CPLGetLastErrorType 54 | // We can get some error information out of the final pbUsageError parameter, which is an 55 | // int*, so malloc ourselves an int and set it to 0 (False) 56 | let usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT); 57 | 58 | Module.setValue(usageErrPtr, 0, 'i32'); 59 | let newDatasetPtr = GDALTranslate(filePath, dataset, translateOptionsPtr, usageErrPtr); 60 | 61 | let errorType = errorHandling.CPLGetLastErrorType(); 62 | // If we ever want to use the usage error pointer: 63 | // let usageErr = Module.getValue(usageErrPtr, 'i32'); 64 | 65 | // The final set of cleanup we need to do, in a function to avoid writing it twice. 66 | function cleanUp() { 67 | Module.ccall('GDALTranslateOptionsFree', null, ['number'], [translateOptionsPtr]); 68 | Module._free(usageErrPtr); 69 | params.deallocate(); 70 | } 71 | 72 | // Check for errors; clean up and throw if error is detected 73 | if ( 74 | errorType === errorHandling.CPLErr.CEFailure || 75 | errorType === errorHandling.CPLErr.CEFatal 76 | ) { 77 | cleanUp(); 78 | const message = errorHandling.CPLGetLastErrorMsg(); 79 | 80 | throw new Error('Error in GDALTranslate: ' + message); 81 | } else { 82 | const result = { 83 | datasetPtr: newDatasetPtr, 84 | filePath: filePath, 85 | directory: directory, 86 | filename: filename, 87 | }; 88 | 89 | cleanUp(); 90 | return result; 91 | } 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/wrappers/gdalVectorTranslate.js: -------------------------------------------------------------------------------- 1 | import randomKey from '../randomKey.js'; 2 | import guessFileExtension from '../guessFileExtension.js'; 3 | import ParamParser from '../stringParamAllocator.js'; 4 | 5 | /* global Module, FS, MEMFS */ 6 | export default function (GDALVectorTranslate, errorHandling, rootPath) { 7 | // Args is expected to be an array of strings that could function as arguments to ogr2ogr 8 | return function (dataset, args) { 9 | const params = new ParamParser(args); 10 | 11 | params.allocate(); 12 | 13 | // Whew, all finished. argPtrsArrayPtr is now the address of the start of the list of 14 | // pointers in Emscripten heap space. Each pointer identifies the address of the start of a 15 | // parameter string, also stored in heap space. This is the direct equivalent of a char **, 16 | // which is what GDALVectorTranslateOptionsNew requires. 17 | const translateOptionsPtr = Module.ccall( 18 | 'GDALVectorTranslateOptionsNew', 19 | 'number', 20 | ['number', 'number'], 21 | [params.argPtrsArrayPtr, null] 22 | ); 23 | 24 | // Validate that the options were correct 25 | const optionsErrType = errorHandling.CPLGetLastErrorType(); 26 | 27 | if ( 28 | optionsErrType === errorHandling.CPLErr.CEFailure || 29 | optionsErrType === errorHandling.CPLErr.CEFatal 30 | ) { 31 | Module.ccall('GDALVectorTranslateOptionsFree', null, ['number'], [translateOptionsPtr]); 32 | params.deallocate(); 33 | const message = errorHandling.CPLGetLastErrorMsg(); 34 | 35 | throw new Error('Error in GDALVectorTranslate: ' + message); 36 | } 37 | 38 | // Now that we have our translate options, we need to make a file location to hold the 39 | // output. 40 | const directory = rootPath + randomKey(); 41 | 42 | FS.mkdir(directory); 43 | // This makes it easier to remove later because we can just unmount rather than recursing 44 | // through the whole directory structure. 45 | FS.mount(MEMFS, {}, directory); 46 | const filename = randomKey(8) + '.' + guessFileExtension(args); 47 | 48 | const filePath = directory + '/' + filename; 49 | 50 | // GDALVectorTranslate takes a list of input datasets, even though it can only ever have one 51 | // dataset in that list, so we need to allocate space for that list and then store the 52 | // dataset pointer in that list. 53 | const dsPtrsArray = Uint32Array.from([dataset]); 54 | const dsPtrsArrayPtr = Module._malloc(dsPtrsArray.length * dsPtrsArray.BYTES_PER_ELEMENT); 55 | 56 | Module.HEAPU32.set(dsPtrsArray, dsPtrsArrayPtr / dsPtrsArray.BYTES_PER_ELEMENT); 57 | 58 | // TODO: The last parameter is an int* that can be used to detect certain kinds of errors, 59 | // but I'm not sure how it works yet and whether it gives the same or different information 60 | // than CPLGetLastErrorType 61 | // We can get some error information out of the final pbUsageError parameter, which is an 62 | // int*, so malloc ourselves an int and set it to 0 (False) 63 | const usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT); 64 | 65 | Module.setValue(usageErrPtr, 0, 'i32'); 66 | 67 | // And then we can kick off the actual translation process. 68 | const newDatasetPtr = GDALVectorTranslate( 69 | filePath, 70 | 0, // Destination dataset, which we don't use, so pass NULL 71 | 1, // nSrcCount, which must always be 1 https://gdal.org/api/gdal_utils.html 72 | dsPtrsArrayPtr, // This needs to be a list of input datasets 73 | translateOptionsPtr, 74 | usageErrPtr 75 | ); 76 | 77 | const errorType = errorHandling.CPLGetLastErrorType(); 78 | // If we ever want to use the usage error pointer: 79 | // let usageErr = Module.getValue(usageErrPtr, 'i32'); 80 | 81 | // The final set of cleanup we need to do, in a function to avoid writing it twice. 82 | function cleanUp() { 83 | Module.ccall('GDALVectorTranslateOptionsFree', null, ['number'], [translateOptionsPtr]); 84 | Module._free(usageErrPtr); 85 | Module._free(dsPtrsArrayPtr); 86 | params.deallocate(); 87 | } 88 | 89 | // Check for errors; clean up and throw if error is detected 90 | if ( 91 | errorType === errorHandling.CPLErr.CEFailure || 92 | errorType === errorHandling.CPLErr.CEFatal 93 | ) { 94 | cleanUp(); 95 | const message = errorHandling.CPLGetLastErrorMsg(); 96 | 97 | throw new Error('Error in GDALVectorTranslate: ' + message); 98 | } else { 99 | const result = { 100 | datasetPtr: newDatasetPtr, 101 | filePath: filePath, 102 | directory: directory, 103 | filename: filename, 104 | }; 105 | 106 | cleanUp(); 107 | return result; 108 | } 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /src/wrappers/gdalWarp.js: -------------------------------------------------------------------------------- 1 | import randomKey from '../randomKey.js'; 2 | import guessFileExtension from '../guessFileExtension.js'; 3 | import ParamParser from '../stringParamAllocator.js'; 4 | 5 | /* global Module, FS, MEMFS */ 6 | export default function (GDALWarp, errorHandling, rootPath) { 7 | // Args is expected to be an array of strings that could function as arguments to gdal_translate 8 | return function (dataset, args) { 9 | let params = new ParamParser(args); 10 | 11 | params.allocate(); 12 | 13 | // Whew, all finished. argPtrsArrayPtr is now the address of the start of the list of 14 | // pointers in Emscripten heap space. Each pointer identifies the address of the start of a 15 | // parameter string, also stored in heap space. This is the direct equivalent of a char **, 16 | // which is what GDALWarpAppOptionsNew requires. 17 | let warpAppOptionsPtr = Module.ccall( 18 | 'GDALWarpAppOptionsNew', 19 | 'number', 20 | ['number', 'number'], 21 | [params.argPtrsArrayPtr, null] 22 | ); 23 | 24 | // Validate that the options were correct 25 | let optionsErrType = errorHandling.CPLGetLastErrorType(); 26 | 27 | if ( 28 | optionsErrType === errorHandling.CPLErr.CEFailure || 29 | optionsErrType === errorHandling.CPLErr.CEFatal 30 | ) { 31 | params.deallocate(); 32 | const message = errorHandling.CPLGetLastErrorMsg(); 33 | 34 | throw new Error('Error in GDALWarp: ' + message); 35 | } 36 | 37 | let directory = rootPath + randomKey(); 38 | 39 | FS.mkdir(directory); 40 | // This makes it easier to remove later because we can just unmount rather than recursing 41 | // through the whole directory structure. 42 | FS.mount(MEMFS, {}, directory); 43 | let filename = randomKey(8) + '.' + guessFileExtension(args); 44 | 45 | let filePath = directory + '/' + filename; 46 | 47 | // And then we can kick off the actual warping process. 48 | // TODO: The last parameter is an int* that can be used to detect certain kinds of errors, 49 | // but I'm not sure how it works yet and whether it gives the same or different information 50 | // than CPLGetLastErrorType 51 | // We can get some error information out of the final pbUsageError parameter, which is an 52 | // int*, so malloc ourselves an int and set it to 0 (False) 53 | let usageErrPtr = Module._malloc(Int32Array.BYTES_PER_ELEMENT); 54 | 55 | Module.setValue(usageErrPtr, 0, 'i32'); 56 | 57 | // We also need a GDALDatasetH * list of datasets. Since we're just warping a single dataset 58 | // at a time, we don't need to do anything fancy here. 59 | let datasetListPtr = Module._malloc(4); // 32-bit pointer 60 | 61 | // Set datasetListPtr to the address of dataset 62 | Module.setValue(datasetListPtr, dataset, '*'); 63 | let newDatasetPtr = GDALWarp( 64 | filePath, // Output 65 | 0, // NULL because filePath is not NULL 66 | 1, // Number of input datasets; this is always called on a single dataset 67 | datasetListPtr, 68 | warpAppOptionsPtr, 69 | usageErrPtr 70 | ); 71 | 72 | // The final set of cleanup we need to do, in a function to avoid writing it twice. 73 | function cleanUp() { 74 | Module.ccall('GDALWarpAppOptionsFree', null, ['number'], [warpAppOptionsPtr]); 75 | Module._free(usageErrPtr); 76 | params.deallocate(); 77 | } 78 | 79 | let errorType = errorHandling.CPLGetLastErrorType(); 80 | // If we ever want to use the usage error pointer: 81 | // let usageErr = Module.getValue(usageErrPtr, 'i32'); 82 | 83 | // Check for errors; clean up and throw if error is detected 84 | if ( 85 | errorType === errorHandling.CPLErr.CEFailure || 86 | errorType === errorHandling.CPLErr.CEFatal 87 | ) { 88 | cleanUp(); 89 | const message = errorHandling.CPLGetLastErrorMsg(); 90 | 91 | throw new Error('Error in GDALWarp: ' + message); 92 | } else { 93 | const result = { 94 | datasetPtr: newDatasetPtr, 95 | filePath: filePath, 96 | directory: directory, 97 | filename: filename, 98 | }; 99 | 100 | cleanUp(); 101 | return result; 102 | } 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /src/wrappers/reproject.js: -------------------------------------------------------------------------------- 1 | /* global Module */ 2 | export default function (srcCRSStr, destCRSStr, xCoords, yCoords) { 3 | // This should never happen 4 | if (xCoords.length !== yCoords.length) { 5 | throw new Error('Got mismatched numbers of x and y coordinates.'); 6 | } 7 | 8 | let OSRNewSpatialReference = Module.cwrap('OSRNewSpatialReference', 'number', ['string']); 9 | 10 | let OCTNewCoordinateTransformation = Module.cwrap('OCTNewCoordinateTransformation', 'number', [ 11 | 'number', 12 | 'number', 13 | ]); 14 | 15 | // Transform arrays of coordinates in-place 16 | // Params are: 17 | // 1. Coordinate transformation to use 18 | // 2. Number of coordinates to transform 19 | // 3. Array of X coordinates to transform 20 | // 4. Array of Y coordinates to transform 21 | // 5. Array of Z coordinates to transform 22 | let OCTTransform = Module.cwrap('OCTTransform', 'number', [ 23 | 'number', 24 | 'number', 25 | 'number', 26 | 'number', 27 | 'number', 28 | ]); 29 | 30 | // We need SRSes for the source and destinations of our transformation 31 | let sourceSrs = OSRNewSpatialReference(srcCRSStr); 32 | 33 | let targetSrs = OSRNewSpatialReference(destCRSStr); 34 | 35 | // Now we can create a CoordinateTransformation object to transform between the two 36 | let coordTransform = OCTNewCoordinateTransformation(sourceSrs, targetSrs); 37 | 38 | // And lastly, we can transform the Xs and Ys. This requires a similar malloc process to the 39 | // affine transform function, since the coordinates are transformed in-place 40 | let xCoordPtr = Module._malloc(xCoords.length * xCoords.BYTES_PER_ELEMENT); 41 | 42 | let yCoordPtr = Module._malloc(yCoords.length * yCoords.BYTES_PER_ELEMENT); 43 | 44 | // But this time we copy into the memory space from our external array 45 | Module.HEAPF64.set(xCoords, xCoordPtr / xCoords.BYTES_PER_ELEMENT); 46 | Module.HEAPF64.set(yCoords, yCoordPtr / yCoords.BYTES_PER_ELEMENT); 47 | // Z is null in this case. This transforms in place. 48 | OCTTransform(coordTransform, xCoords.length, xCoordPtr, yCoordPtr, null); 49 | // Pull out the coordinates 50 | let transXCoords = Array.from( 51 | Module.HEAPF64.subarray( 52 | xCoordPtr / xCoords.BYTES_PER_ELEMENT, 53 | xCoordPtr / xCoords.BYTES_PER_ELEMENT + xCoords.length 54 | ) 55 | ); 56 | 57 | let transYCoords = Array.from( 58 | Module.HEAPF64.subarray( 59 | yCoordPtr / yCoords.BYTES_PER_ELEMENT, 60 | yCoordPtr / yCoords.BYTES_PER_ELEMENT + yCoords.length 61 | ) 62 | ); 63 | 64 | // Zip it all back up 65 | let returnVal = transXCoords.map(function (x, i) { 66 | return [x, transYCoords[i]]; 67 | }); 68 | 69 | // Clear memory 70 | Module._free(xCoordPtr); 71 | Module._free(yCoordPtr); 72 | Module.ccall('OSRDestroySpatialReference', 'number', ['number'], [sourceSrs]); 73 | Module.ccall('OSRDestroySpatialReference', 'number', ['number'], [targetSrs]); 74 | Module.ccall('OCTDestroyCoordinateTransformation', 'number', ['number'], [coordTransform]); 75 | return returnVal; 76 | } 77 | -------------------------------------------------------------------------------- /test/assets/geom.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { "type": "Point", "coordinates": [-75.16384363174437, 39.952533336963306] } 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/assets/not-a-tiff.bytes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azavea/loam/dcd3e67cdad38665607e9a12f0a1ecae4775e7f5/test/assets/not-a-tiff.bytes -------------------------------------------------------------------------------- /test/assets/point.dbf: -------------------------------------------------------------------------------- 1 | z 2 | ! -------------------------------------------------------------------------------- /test/assets/point.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /test/assets/point.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azavea/loam/dcd3e67cdad38665607e9a12f0a1ecae4775e7f5/test/assets/point.shp -------------------------------------------------------------------------------- /test/assets/point.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azavea/loam/dcd3e67cdad38665607e9a12f0a1ecae4775e7f5/test/assets/point.shx -------------------------------------------------------------------------------- /test/assets/tiny.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azavea/loam/dcd3e67cdad38665607e9a12f0a1ecae4775e7f5/test/assets/tiny.tif -------------------------------------------------------------------------------- /test/assets/tiny_dem.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azavea/loam/dcd3e67cdad38665607e9a12f0a1ecae4775e7f5/test/assets/tiny_dem.tif -------------------------------------------------------------------------------- /test/loam.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, afterEach, expect, loam */ 2 | const tinyTifPath = '/base/test/assets/tiny.tif'; 3 | const tinyDEMPath = '/base/test/assets/tiny_dem.tif'; 4 | const invalidTifPath = 'base/test/assets/not-a-tiff.bytes'; 5 | const geojsonPath = '/base/test/assets/geom.geojson'; 6 | const shpPath = '/base/test/assets/point.shp'; 7 | const shxPath = '/base/test/assets/point.shx'; 8 | const dbfPath = '/base/test/assets/point.dbf'; 9 | const prjPath = '/base/test/assets/point.prj'; 10 | const epsg4326 = 11 | '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"]]'; 12 | const epsg3857 = 13 | '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"]]'; 14 | const geojson = { 15 | type: 'FeatureCollection', 16 | features: [ 17 | { 18 | type: 'Feature', 19 | properties: {}, 20 | geometry: { 21 | type: 'Polygon', 22 | coordinates: [ 23 | [ 24 | [-75.15416622161865, 39.96212240336062], 25 | [-75.15519618988037, 39.96115204441345], 26 | [-75.15409111976624, 39.96055173071228], 27 | [-75.15339374542236, 39.96149742799007], 28 | [-75.15416622161865, 39.96212240336062], 29 | ], 30 | ], 31 | }, 32 | }, 33 | ], 34 | }; 35 | 36 | function xhrAsPromiseBlob(url) { 37 | let xhr = new XMLHttpRequest(); 38 | 39 | xhr.open('GET', url); 40 | xhr.responseType = 'blob'; 41 | return new Promise(function (resolve, reject) { 42 | xhr.onload = function (oEvent) { 43 | resolve(xhr.response); 44 | }; 45 | xhr.onerror = function (oEvent) { 46 | reject(oEvent); 47 | }; 48 | xhr.send(); 49 | }); 50 | } 51 | 52 | // Tests for the Loam initialization / teardown process 53 | describe('Given that loam exists', () => { 54 | afterEach(function () { 55 | return loam.reset(); 56 | }); 57 | 58 | describe('calling initialize with a prefix', function () { 59 | it('should attempt to load loam-worker from the prefix', () => { 60 | // This is the same prefix as it would determine by default, so this should work (unless 61 | // something changes regarding the test server setup). 62 | return loam.initialize('/base/lib/').then((worker) => { 63 | expect(worker).to.be.an.instanceof(Worker); 64 | }); 65 | }); 66 | }); 67 | describe('calling initialize with a bad Loam prefix', function () { 68 | it('should attempt to load loam-worker from the prefix and fail', () => { 69 | return loam.initialize('/foo/').then( 70 | () => { 71 | throw new Error( 72 | 'initialize() should have been rejected, but it was resolved instead' 73 | ); 74 | }, 75 | (error) => { 76 | expect(error.message).to.include('NetworkError'); 77 | } 78 | ); 79 | }); 80 | }); 81 | describe('calling initialize with a bad GDAL prefix', function () { 82 | it('should attempt to load GDAL from the prefix and fail', () => { 83 | return loam.initialize('/base/lib/', '/bad/path/').then( 84 | () => { 85 | throw new Error( 86 | 'initialize() should have been rejected, but it was resolved instead' 87 | ); 88 | }, 89 | (error) => { 90 | expect(error.message).to.include('NetworkError'); 91 | } 92 | ); 93 | }); 94 | }); 95 | }); 96 | 97 | // Tests for Loam GDAL functionality 98 | describe('Given that loam exists', () => { 99 | before(function () { 100 | this.timeout(15000); 101 | return loam.reset().then(() => loam.initialize()); 102 | }); 103 | 104 | describe('calling open with a Blob', function () { 105 | it('should return a GDALDataset', () => { 106 | return xhrAsPromiseBlob(tinyTifPath) 107 | .then((tifBlob) => loam.open(tifBlob)) 108 | .then((ds) => { 109 | expect(ds).to.be.an.instanceof(loam.GDALDataset); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('calling open with a File', function () { 115 | it('should return a GDALDataset', () => { 116 | return xhrAsPromiseBlob(tinyTifPath) 117 | .then((tifBlob) => new File([tifBlob], 'tinyTif.tif')) 118 | .then((tinyTifFile) => loam.open(tinyTifFile)) 119 | .then((ds) => { 120 | expect(ds).to.be.an.instanceof(loam.GDALDataset); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('calling open with a vector', function () { 126 | it('should return a GDALDataset', () => { 127 | return xhrAsPromiseBlob(geojsonPath) 128 | .then((geojsonBlob) => loam.open(geojsonBlob)) 129 | .then((ds) => { 130 | expect(ds).to.be.an.instanceof(loam.GDALDataset); 131 | }); 132 | }); 133 | }); 134 | 135 | describe('calling open with a multi-file format', function () { 136 | it('should return a GDALDataset', () => { 137 | return Promise.all([ 138 | xhrAsPromiseBlob(shpPath), 139 | xhrAsPromiseBlob(shxPath), 140 | xhrAsPromiseBlob(dbfPath), 141 | xhrAsPromiseBlob(prjPath), 142 | ]) 143 | .then(([shpBlob, shxBlob, dbfBlob, prjBlob]) => 144 | loam.open({ name: 'shp.shp', data: shpBlob }, [ 145 | { name: 'shp.shx', data: shxBlob }, 146 | { name: 'shp.dbf', data: dbfBlob }, 147 | { name: 'shp.prj', data: prjBlob }, 148 | ]) 149 | ) 150 | .then((ds) => { 151 | expect(ds).to.be.an.instanceof(loam.GDALDataset); 152 | }); 153 | }); 154 | }); 155 | 156 | describe('calling reproject()', function () { 157 | it('should reproject points from one CRS to another', () => { 158 | return loam 159 | .reproject(epsg4326, epsg3857, [ 160 | [-75.1652, 39.9526], 161 | [44.8271, 41.7151], 162 | [-47.9218, -15.8267], 163 | ]) 164 | .then((coords) => { 165 | expect(coords).to.deep.equal([ 166 | [-8367351.7893745685, 4859056.629543971], 167 | [4990129.945739155, 5118397.8827427635], 168 | [-5334630.373897098, -1784662.322609764], 169 | ]); 170 | }); 171 | }); 172 | }); 173 | 174 | describe('calling count() on a raster', function () { 175 | it('should return the number of bands in the GeoTiff', () => { 176 | return xhrAsPromiseBlob(tinyTifPath).then((tifBlob) => 177 | loam.open(tifBlob).then((ds) => { 178 | return ds.count().then((count) => expect(count).to.equal(1)); 179 | }) 180 | ); 181 | }); 182 | }); 183 | 184 | describe('calling count() on a vector dataset', function () { 185 | it('should return 0', () => { 186 | return xhrAsPromiseBlob(geojsonPath).then((geojsonBlob) => 187 | loam.open(geojsonBlob).then((ds) => { 188 | return ds.count().then((count) => expect(count).to.equal(0)); 189 | }) 190 | ); 191 | }); 192 | }); 193 | 194 | describe('calling layerCount() on a raster', function () { 195 | it('should return 0', () => { 196 | return xhrAsPromiseBlob(tinyTifPath).then((tifBlob) => 197 | loam.open(tifBlob).then((ds) => { 198 | return ds.layerCount().then((count) => expect(count).to.equal(0)); 199 | }) 200 | ); 201 | }); 202 | }); 203 | 204 | describe('calling layerCount() on a vector dataset', function () { 205 | it('should return 1', () => { 206 | return xhrAsPromiseBlob(geojsonPath).then((geojsonBlob) => 207 | loam.open(geojsonBlob).then((ds) => { 208 | return ds.layerCount().then((count) => expect(count).to.equal(1)); 209 | }) 210 | ); 211 | }); 212 | }); 213 | 214 | describe('calling width()', function () { 215 | it('should return the x-size of the GeoTiff', () => { 216 | return xhrAsPromiseBlob(tinyTifPath) 217 | .then((tifBlob) => loam.open(tifBlob)) 218 | .then((ds) => ds.width()) 219 | .then((width) => expect(width).to.equal(15)); 220 | }); 221 | }); 222 | 223 | describe('calling height()', function () { 224 | it('should return the y-size of the GeoTiff', () => { 225 | return xhrAsPromiseBlob(tinyTifPath) 226 | .then((tifBlob) => loam.open(tifBlob)) 227 | .then((ds) => ds.height()) 228 | .then((height) => expect(height).to.equal(16)); 229 | }); 230 | }); 231 | 232 | describe('calling bandMinimum()', function () { 233 | it('should return the minimum possible value of the raster band', () => { 234 | return xhrAsPromiseBlob(tinyTifPath) 235 | .then((tifBlob) => loam.open(tifBlob)) 236 | .then((ds) => ds.bandMinimum(1)) 237 | .then((min) => expect(min).to.equal(0)); 238 | }); 239 | }); 240 | 241 | describe('calling bandMaximum()', function () { 242 | it('should return the maximum possible value of the raster band', () => { 243 | return xhrAsPromiseBlob(tinyTifPath) 244 | .then((tifBlob) => loam.open(tifBlob)) 245 | .then((ds) => ds.bandMaximum(1)) 246 | .then((max) => expect(max).to.equal(255)); // Determined with gdalinfo 247 | }); 248 | }); 249 | 250 | describe('calling bandStatistics()', function () { 251 | it('should return the statistics for the raster band', () => { 252 | return xhrAsPromiseBlob(tinyTifPath) 253 | .then((tifBlob) => loam.open(tifBlob)) 254 | .then((ds) => ds.bandStatistics(1)) 255 | .then((stats) => { 256 | expect(stats.minimum).to.equal(15); 257 | expect(stats.maximum).to.equal(255); 258 | expect(stats.median).to.be.approximately(246.52, 0.01); 259 | expect(stats.stdDev).to.be.approximately(39.941, 0.01); 260 | }); 261 | }); 262 | }); 263 | 264 | describe('calling bandNoDataValue()', function () { 265 | it('should return the no-data value of the raster band', () => { 266 | return xhrAsPromiseBlob(tinyTifPath) 267 | .then((tifBlob) => loam.open(tifBlob)) 268 | .then((ds) => ds.bandNoDataValue(1)) 269 | .then((ndValue) => expect(ndValue).to.equal(0)); // Determined with gdalinfo 270 | }); 271 | }); 272 | 273 | describe('calling bandDataType()', function () { 274 | it('should return the data type of the raster band for all band types', () => { 275 | const validDataTypes = [ 276 | 'Byte', 277 | 'UInt16', 278 | 'Int16', 279 | 'UInt32', 280 | 'Int32', 281 | 'Float32', 282 | 'Float64', 283 | 'CInt16', 284 | 'CInt32', 285 | 'CFloat32', 286 | 'CFloat64', 287 | ]; 288 | return ( 289 | xhrAsPromiseBlob(tinyTifPath) 290 | .then((tifBlob) => loam.open(tifBlob)) 291 | // Create an array of datasources that each has been converted to one of the different 292 | // valid data types using gdal_translate 293 | .then((ds) => Promise.all(validDataTypes.map((dt) => ds.convert(['-ot', dt])))) 294 | // Then pull the data types back out of the converted datasources... 295 | .then((everyDataTypeDataset) => 296 | Promise.all(everyDataTypeDataset.map((dtDs) => dtDs.bandDataType(1))) 297 | ) 298 | // ...and expect that we get the same set of data types as we put in. 299 | .then((everyDataTypeResult) => 300 | expect(everyDataTypeResult).to.deep.equal(validDataTypes) 301 | ) 302 | ); 303 | }); 304 | }); 305 | 306 | describe('calling wkt()', function () { 307 | it("should return the GeoTiff's WKT CRS string", () => { 308 | return xhrAsPromiseBlob(tinyTifPath) 309 | .then((tifBlob) => loam.open(tifBlob)) 310 | .then((ds) => ds.wkt()) 311 | .then((wkt) => { 312 | expect(wkt).to.equal( 313 | 'PROJCS["unnamed",' + 314 | 'GEOGCS["unnamed ellipse",' + 315 | 'DATUM["unknown",' + 316 | 'SPHEROID["unnamed",6378137,0]],' + 317 | 'PRIMEM["Greenwich",0],' + 318 | 'UNIT["degree",0.0174532925199433]],' + 319 | 'PROJECTION["Mercator_1SP"],' + 320 | 'PARAMETER["central_meridian",0],' + 321 | 'PARAMETER["scale_factor",1],' + 322 | 'PARAMETER["false_easting",0],' + 323 | 'PARAMETER["false_northing",0],' + 324 | 'UNIT["metre",1,' + 325 | 'AUTHORITY["EPSG","9001"]]]' 326 | ); 327 | }); 328 | }); 329 | }); 330 | 331 | describe('calling transform()', function () { 332 | it("should return the GeoTiff's 6-element GDAL transform array", () => { 333 | return xhrAsPromiseBlob(tinyTifPath) 334 | .then((tifBlob) => loam.open(tifBlob)) 335 | .then((ds) => ds.transform()) 336 | .then((transform) => { 337 | expect(transform).to.deep.equal([ 338 | -8380165.213197844, 2416.6666666666665, 0, 4886134.645645497, 0, -2468.75, 339 | ]); 340 | }); 341 | }); 342 | }); 343 | 344 | describe('calling close', function () { 345 | it('should succeed and clear the GDALDataset', function () { 346 | return xhrAsPromiseBlob(tinyTifPath).then((tifBlob) => { 347 | return loam.open(tifBlob).then((ds) => { 348 | return ds.close().then((result) => { 349 | expect(result).to.deep.equal([]); 350 | }); 351 | }); 352 | }); 353 | }); 354 | }); 355 | 356 | describe('calling bytes', function () { 357 | it('should succeed and return the file contents', function () { 358 | return xhrAsPromiseBlob(tinyTifPath) 359 | .then((tifBlob) => loam.open(tifBlob)) 360 | .then((ds) => ds.bytes()) 361 | .then((bytes) => expect(bytes.length).to.equal(862)); 362 | }); 363 | }); 364 | 365 | describe('calling convert', function () { 366 | it('should succeed and return a new Dataset with the transformed values', function () { 367 | return xhrAsPromiseBlob(tinyTifPath) 368 | .then((tifBlob) => loam.open(tifBlob)) 369 | .then((ds) => ds.convert(['-outsize', '200%', '200%'])) 370 | .then((newDs) => newDs.width()) 371 | .then((width) => expect(width).to.equal(30)); 372 | }); 373 | }); 374 | 375 | describe('calling vectorConvert', function () { 376 | it('should succeed and return a new Dataset in the new format', function () { 377 | return Promise.all([ 378 | xhrAsPromiseBlob(shpPath), 379 | xhrAsPromiseBlob(shxPath), 380 | xhrAsPromiseBlob(dbfPath), 381 | xhrAsPromiseBlob(prjPath), 382 | ]) 383 | .then(([shpBlob, shxBlob, dbfBlob, prjBlob]) => 384 | loam.open({ name: 'shp.shp', data: shpBlob }, [ 385 | { name: 'shp.shx', data: shxBlob }, 386 | { name: 'shp.dbf', data: dbfBlob }, 387 | { name: 'shp.prj', data: prjBlob }, 388 | ]) 389 | ) 390 | .then((ds) => ds.vectorConvert(['-f', 'GeoJSON'])) 391 | .then((newDs) => newDs.bytes()) 392 | .then((jsonBytes) => { 393 | const utf8Decoder = new TextDecoder(); 394 | expect(utf8Decoder.decode(jsonBytes)).to.include('FeatureCollection'); 395 | }); 396 | }); 397 | }); 398 | 399 | describe('calling warp', function () { 400 | it('should succeed and return a new Dataset that has been warped', function () { 401 | return ( 402 | xhrAsPromiseBlob(tinyTifPath) 403 | .then((tifBlob) => loam.open(tifBlob)) 404 | .then((ds) => ds.warp(['-s_srs', 'EPSG:3857', '-t_srs', 'EPSG:4326'])) 405 | .then((newDS) => newDS.transform()) 406 | // Determined out-of-band by executing gdalwarp on the command line. 407 | .then((transform) => { 408 | expect(transform).to.deep.equal([ 409 | -75.2803049446235, 0.019340471787624117, 0.0, 40.13881222863268, 0.0, 410 | -0.019340471787624117, 411 | ]); 412 | }) 413 | ); 414 | }); 415 | }); 416 | 417 | describe('calling rasterize', function () { 418 | it('should succeed and return a rasterized version of the GeoJSON', function () { 419 | return ( 420 | loam 421 | .rasterize(geojson, ['-burn', '1', '-of', 'GTiff', '-ts', '10', '10']) 422 | .then((ds) => ds.bytes()) 423 | // Byte length was experimentally determined by running gdal_rasterize from the 424 | // command-line 425 | .then((bytes) => expect(bytes.length).to.equal(1166)) 426 | ); 427 | }); 428 | }); 429 | 430 | describe('calling render with color-relief', function () { 431 | it('should succeed and return a rendered version of the GeoTIFF', function () { 432 | return ( 433 | xhrAsPromiseBlob(tinyDEMPath) 434 | .then((tifBlob) => loam.open(tifBlob)) 435 | .then((ds) => ds.render('color-relief', ['-of', 'PNG'], ['993.0 255 0 0'])) 436 | .then((ds) => ds.bytes()) 437 | // Determined out-of-band by executing gdaldem on the command line. 438 | .then((bytes) => expect(bytes.length).to.equal(80)) 439 | ); 440 | }); 441 | }); 442 | 443 | describe('calling render with hillshade', function () { 444 | it('should succeed and return a rendered version of the GeoTIFF', function () { 445 | return ( 446 | xhrAsPromiseBlob(tinyDEMPath) 447 | .then((tifBlob) => loam.open(tifBlob)) 448 | .then((ds) => ds.render('hillshade', ['-of', 'PNG'])) 449 | .then((ds) => ds.bytes()) 450 | // Determined out-of-band by executing gdaldem on the command line. 451 | .then((bytes) => expect(bytes.length).to.equal(246)) 452 | ); 453 | }); 454 | }); 455 | 456 | /** 457 | * Failure cases 458 | **/ 459 | describe('calling open() on an invalid file', function () { 460 | it('should fail and return an error message', function () { 461 | return xhrAsPromiseBlob(invalidTifPath) 462 | .then((garbage) => loam.open(garbage)) 463 | .then( 464 | () => { 465 | throw new Error('GDALOpen promise should have been rejected'); 466 | }, 467 | (error) => 468 | expect(error.message).to.include( 469 | 'not recognized as a supported file format' 470 | ) 471 | ); 472 | }); 473 | }); 474 | 475 | describe('calling bandDataType() with incorrect band number', function () { 476 | it('should fail and return an error message', function () { 477 | return xhrAsPromiseBlob(tinyTifPath) 478 | .then((tinyTif) => loam.open(tinyTif)) 479 | .then((ds) => ds.bandDataType(2)) 480 | .then( 481 | () => { 482 | throw new Error('bandDataType promise should have been rejected'); 483 | }, 484 | (error) => expect(error.message).to.include("Pointer 'hBand' is NULL") 485 | ); 486 | }); 487 | }); 488 | 489 | describe('calling convert with invalid arguments', function () { 490 | it('should fail and return an error message', function () { 491 | return xhrAsPromiseBlob(tinyTifPath) 492 | .then((tifBlob) => loam.open(tifBlob)) 493 | .then((ds) => ds.convert(['-notreal', 'xyz%', 'oink%'])) 494 | .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution 495 | .then( 496 | (result) => { 497 | throw new Error( 498 | 'convert() promise should have been rejected but got ' + 499 | result + 500 | ' instead.' 501 | ); 502 | }, 503 | (error) => expect(error.message).to.include('Unknown option name') 504 | ); 505 | }); 506 | }); 507 | 508 | describe('calling warp with invalid arguments', function () { 509 | it('should fail and return an error message', function () { 510 | return xhrAsPromiseBlob(tinyTifPath) 511 | .then((tifBlob) => loam.open(tifBlob)) 512 | .then((ds) => ds.warp(['-s_srs', 'EPSG:Fake', '-t_srs', 'EPSG:AlsoFake'])) 513 | .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution 514 | .then( 515 | (result) => { 516 | throw new Error( 517 | 'warp() promise should have been rejected but got ' + 518 | result + 519 | ' instead.' 520 | ); 521 | }, 522 | (error) => expect(error.message).to.include('source or target SRS failed') 523 | ); 524 | }); 525 | }); 526 | 527 | describe('calling rasterize with invalid arguments', function () { 528 | it('should fail and return an error message', function () { 529 | // The -ts parameter is for output image size, so negative values are nonsensical 530 | return loam 531 | .rasterize(geojson, ['-burn', '1', '-of', 'GTiff', '-ts', '-10', '10']) 532 | .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution 533 | .then( 534 | (result) => { 535 | throw new Error( 536 | 'rasterize() promise should have been rejected but got ' + 537 | result + 538 | ' instead.' 539 | ); 540 | }, 541 | (error) => 542 | expect(error.message).to.include('Wrong value for -outsize parameter.') 543 | ); 544 | }); 545 | }); 546 | 547 | describe('calling convert with non-string arguments', function () { 548 | it('should fail and return an error message', function () { 549 | return xhrAsPromiseBlob(tinyTifPath) 550 | .then((tifBlob) => loam.open(tifBlob)) 551 | .then((ds) => ds.convert(['-outsize', 25])) 552 | .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution 553 | .then( 554 | (result) => { 555 | throw new Error( 556 | 'convert() promise should have been rejected but got ' + 557 | result + 558 | ' instead.' 559 | ); 560 | }, 561 | (error) => 562 | expect(error.message).to.include( 563 | 'All items in the argument list must be strings' 564 | ) 565 | ); 566 | }); 567 | }); 568 | 569 | describe('calling warp with non-string arguments', function () { 570 | it('should fail and return an error message', function () { 571 | return xhrAsPromiseBlob(tinyTifPath) 572 | .then((tifBlob) => loam.open(tifBlob)) 573 | .then((ds) => ds.warp(['-order', 2])) 574 | .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution 575 | .then( 576 | (result) => { 577 | throw new Error( 578 | 'warp() promise should have been rejected but got ' + 579 | result + 580 | ' instead.' 581 | ); 582 | }, 583 | (error) => 584 | expect(error.message).to.include( 585 | 'All items in the argument list must be strings' 586 | ) 587 | ); 588 | }); 589 | }); 590 | 591 | describe('calling rasterize with non-string arguments', function () { 592 | it('should fail and return an error message', function () { 593 | return loam 594 | .rasterize(geojson, ['-burn', 1, '-of', 'GTiff', '-ts', 10, 10]) 595 | .then((ds) => ds.bytes()) // Need to call an accessor to trigger operation execution 596 | .then( 597 | (result) => { 598 | throw new Error( 599 | 'rasterize() promise should have been rejected but got ' + 600 | result + 601 | ' instead.' 602 | ); 603 | }, 604 | (error) => 605 | expect(error.message).to.include( 606 | 'All items in the argument list must be strings' 607 | ) 608 | ); 609 | }); 610 | }); 611 | 612 | describe('calling render with an invalid mode', function () { 613 | it('should fail and return an error message', function () { 614 | return xhrAsPromiseBlob(tinyTifPath) 615 | .then((tifBlob) => loam.open(tifBlob)) 616 | .then((ds) => ds.render('gobbledegook', [])) 617 | .then((ds) => ds.bytes()) // Call an accessor to trigger operation execution 618 | .then( 619 | (result) => { 620 | throw new Error( 621 | 'render() promise should have been rejected but got ' + 622 | result + 623 | ' instead.' 624 | ); 625 | }, 626 | (error) => expect(error.message).to.include('mode must be one of') 627 | ); 628 | }); 629 | }); 630 | describe('calling render with color-relief but no colors', function () { 631 | it('should fail and return an error message', function () { 632 | return xhrAsPromiseBlob(tinyTifPath) 633 | .then((tifBlob) => loam.open(tifBlob)) 634 | .then((ds) => ds.render('color-relief', [])) 635 | .then((ds) => ds.bytes()) // Call an accessor to trigger operation execution 636 | .then( 637 | (result) => { 638 | throw new Error( 639 | 'render() promise should have been rejected but got ' + 640 | result + 641 | ' instead.' 642 | ); 643 | }, 644 | (error) => 645 | expect(error.message).to.include('color definition array must be provided') 646 | ); 647 | }); 648 | }); 649 | describe('calling render with non-color-relief but providing colors', function () { 650 | it('should fail and return an error message', function () { 651 | return xhrAsPromiseBlob(tinyTifPath) 652 | .then((tifBlob) => loam.open(tifBlob)) 653 | .then((ds) => ds.render('hillshade', [], ['0.5 100 100 100'])) 654 | .then((ds) => ds.bytes()) // Call an accessor to trigger operation execution 655 | .then( 656 | (result) => { 657 | throw new Error( 658 | 'render() promise should have been rejected but got ' + 659 | result + 660 | ' instead.' 661 | ); 662 | }, 663 | (error) => 664 | expect(error.message).to.include( 665 | 'color definition array should not be provided' 666 | ) 667 | ); 668 | }); 669 | }); 670 | 671 | describe('calling render() with a vector dataset', function () { 672 | it('should fail and return a useful error message', function () { 673 | return xhrAsPromiseBlob(geojsonPath) 674 | .then((geojsonBlob) => loam.open(geojsonBlob)) 675 | .then((ds) => ds.render('hillshade', ['-of', 'PNG'])) 676 | .then((ds) => ds.bytes()) 677 | .then( 678 | (result) => { 679 | throw new Error( 680 | `render() should have failed for a vector dataset but got ${result}` 681 | ); 682 | }, 683 | (error) => expect(error.message).to.include('Error in GDALDEMProcessing') 684 | ); 685 | }); 686 | }); 687 | 688 | describe('calling convert with a vector dataset', function () { 689 | it('should fail because the vector dataset has no raster bands', function () { 690 | return Promise.all([ 691 | xhrAsPromiseBlob(shpPath), 692 | xhrAsPromiseBlob(shxPath), 693 | xhrAsPromiseBlob(dbfPath), 694 | xhrAsPromiseBlob(prjPath), 695 | ]) 696 | .then(([shpBlob, shxBlob, dbfBlob, prjBlob]) => 697 | loam.open({ name: 'shp.shp', data: shpBlob }, [ 698 | { name: 'shp.shx', data: shxBlob }, 699 | { name: 'shp.dbf', data: dbfBlob }, 700 | { name: 'shp.prj', data: prjBlob }, 701 | ]) 702 | ) 703 | .then((ds) => ds.convert(['-outsize', '200%', '200%'])) 704 | .then((newDs) => newDs.width()) 705 | .then( 706 | (result) => { 707 | throw new Error( 708 | `convert() should have failed for vector dataset but got ${result}` 709 | ); 710 | }, 711 | (error) => expect(error.message).to.include('Input file has no bands') 712 | ); 713 | }); 714 | }); 715 | }); 716 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ESLintPlugin = require('eslint-webpack-plugin'); 3 | 4 | const config = { 5 | mode: 'development', 6 | entry: { 7 | loam: path.join(__dirname, 'src', 'index.js'), 8 | 'loam-worker': path.join(__dirname, 'src', 'worker.js'), 9 | }, 10 | output: { 11 | path: path.join(__dirname, 'lib'), 12 | filename: '[name].js', 13 | library: { 14 | name: 'loam', 15 | type: 'umd', 16 | umdNamedDefine: true, 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /(\.js)$/, 23 | loader: 'babel-loader', 24 | exclude: /node_modules/, 25 | }, 26 | ], 27 | }, 28 | resolve: { 29 | modules: [path.resolve('./node_modules'), path.resolve('./src')], 30 | extensions: ['.json', '.js'], 31 | }, 32 | plugins: [new ESLintPlugin()], 33 | devtool: 'source-map', 34 | devServer: { 35 | static: { 36 | directory: path.join(__dirname, 'demo'), 37 | watch: true, 38 | }, 39 | }, 40 | }; 41 | 42 | module.exports = config; 43 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ESLintPlugin = require('eslint-webpack-plugin'); 3 | 4 | const config = { 5 | mode: 'production', 6 | entry: { 7 | loam: path.join(__dirname, 'src', 'index.js'), 8 | 'loam-worker': path.join(__dirname, 'src', 'worker.js'), 9 | }, 10 | output: { 11 | path: path.join(__dirname, 'lib'), 12 | filename: '[name].min.js', 13 | library: { 14 | name: 'loam', 15 | type: 'umd', 16 | umdNamedDefine: true, 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /(\.js)$/, 23 | loader: 'babel-loader', 24 | exclude: /node_modules/, 25 | }, 26 | ], 27 | }, 28 | resolve: { 29 | modules: [path.resolve('./node_modules'), path.resolve('./src')], 30 | extensions: ['.json', '.js'], 31 | }, 32 | plugins: [new ESLintPlugin()], 33 | }; 34 | 35 | module.exports = config; 36 | --------------------------------------------------------------------------------