├── .eslintrc.json ├── .github └── workflows │ └── test-main.yml ├── .gitignore ├── LICENSE ├── README.md ├── geowarp.d.ts ├── geowarp.js ├── package.json ├── test-data ├── setup.sh ├── sri-lanka-hi-res.geojson └── sri-lanka.geojson ├── test-output └── .gitkeep ├── test.js └── test.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 2021, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "arrow-parens": ["error", "as-needed"], 14 | "comma-dangle": ["error", "never"], 15 | "linebreak-style": ["error", "unix"], 16 | "no-console": "off", 17 | "no-multiple-empty-lines": "error", 18 | "no-unused-vars": ["error", { "args": "none" }], 19 | "no-var": "error", 20 | "prefer-arrow-callback": "error", 21 | "prefer-const": "error", 22 | "prefer-spread": "error", 23 | "quotes": ["error", "double", { "avoidEscape": true }], 24 | "semi": ["error", "always"], 25 | "space-before-function-paren": ["error", { 26 | "anonymous": "always", 27 | "named": "never", 28 | "asyncArrow": "always" 29 | }] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/test-main.yml: -------------------------------------------------------------------------------- 1 | name: test-main 2 | run-name: test-main 3 | on: [push] 4 | jobs: 5 | perf: 6 | runs-on: ubuntu-latest 7 | name: test performance 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | - run: npm install 12 | - run: ls 13 | - run: pwd 14 | - run: npm run setup 15 | - run: npm run perf 16 | test: 17 | runs-on: ubuntu-latest 18 | name: run all tests 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: latest 24 | - run: npm install 25 | - run: npm run clean 26 | - run: npm run lint 27 | - run: npm run setup 28 | - run: npm run test:tsc 29 | - run: npm run test:js 30 | - run: npm run test:ts 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # other files 107 | .DS_Store 108 | pnpm-lock.yaml 109 | tmp/ 110 | test-data/ 111 | test-output/ 112 | 113 | # backup files 114 | *bak* 115 | 116 | package-lock.json 117 | *.geojson 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geowarp 2 | > Super Low-Level Raster Reprojection and Resampling Library 3 | 4 | # install 5 | ```bash 6 | npm install -S geowarp 7 | ``` 8 | 9 | # usage 10 | ```javascript 11 | const geowarp = require("geowarp"); 12 | const proj4 = require("proj4-fully-loaded"); 13 | 14 | const result = geowarp({ 15 | // control the level of console log output 16 | // set debug_level to zero to turn off console logging 17 | debug_level: 2, 18 | 19 | // reproject from an [x, y] point in the input spatial reference system 20 | // to an [x, y] point in the output spatial reference system 21 | forward: proj4("EPSG:" + in_srs, "EPSG:3857").forward, 22 | 23 | // reproject from an [x, y] point in the output spatial reference system 24 | // to an [x, y] point in the input spatial reference system 25 | inverse: proj4("EPSG:" + in_srs, "EPSG:3857").inverse, 26 | 27 | // two-dimensional array of pixel data organized by band 28 | // usually [ r, g, b ] or [ r, g, b, a ] 29 | // pixel data for each band is usually flattened, 30 | // so the end of one row is immediately followed by the next row 31 | in_data: [ 32 | [0, 123, 123, 162, ...], 33 | [213, 41, 62, 124, ...], 34 | [84, 52, 124, 235, ...] 35 | ], 36 | 37 | // bounding box of input data (in_data) 38 | // in [xmin, ymin, xmax, ymax] format 39 | in_bbox: [ -122.51, 40.97, -122.34, 41.11 ], 40 | 41 | // layout of the in_data using xdim layout syntax 42 | // see: https://github.com/danieljdufour/xdim 43 | in_layout: "[band][row,column]", 44 | 45 | // a number or array of numbers 46 | in_no_data: -99, 47 | 48 | // a number or string representing the spatial reference system of the input data 49 | // could be 4326 or "EPSG:4326" 50 | in_srs: 4326, 51 | 52 | // only necessary when in_data is skewed or rotated 53 | // 6-parameter geotransform using the order from https://gdal.org/tutorials/geotransforms_tut.html 54 | in_geotransform: [337934.48363, -0.142999, -0.576775, 7840518.4648, -0.57677, 0.14299], 55 | 56 | // how many pixels wide the input data is 57 | in_width: 1032, 58 | 59 | // how many pixels tall the input data is 60 | in_height: 1015, 61 | 62 | // optional array of constructor names for each array level 63 | // default is to use Array (untyped) for everything 64 | out_array_types: ["Array", "Array", "Uint8Array"], 65 | 66 | // optional array for sampling and/or changing band order 67 | // array is the order of the bands in the output with numbers 68 | // indicating the band index in the input (starting at zero) 69 | // for example, [13, 12, 11] skips the first 11 bands, 70 | // and takes the 12th, 13th, and 14th in reverse order 71 | out_bands: [13, 12, 11], 72 | 73 | // bounding box of output 74 | // this is the space that you want to paint 75 | // in same format as in_bbox 76 | out_bbox: [-13638811.83098057, 5028944.964938315, -13619243.951739563, 5028944.964938315], 77 | 78 | // optional 79 | // single or multi-dimensional array that will hold the output 80 | out_data: [[[[],[],[],...],[[],[],[],...],[[],[],[],...],[[],[],[],...],...]], 81 | 82 | // optional, default is null 83 | // the no data value for your output data 84 | out_no_data: -99, 85 | 86 | // layout of the result using xdim layout syntax 87 | // see: https://github.com/danieljdufour/xdim 88 | out_layout: "[row][column][band]", 89 | 90 | // a number or string representing the spatial reference system of the input data 91 | // could be 4326 or "EPSG:4326" 92 | out_srs: 3857, 93 | 94 | // optional 95 | // number of bands in the output 96 | out_pixel_depth: 3, 97 | 98 | // height of the output image in pixels 99 | out_height: 256, 100 | 101 | // width of the output image in pixels 102 | out_width: 256, 103 | 104 | // horizontal and vertical resolution 105 | // resolution of [0.25, 0.25] (i.e. 25%) means that you sample once for every 4 x 4 pixels 106 | // this is useful if you need your output to be a certain height or width (like 256 x 256) 107 | // but don't necessarily want to render data at that high resolution 108 | out_resolution: [0.5, 0.5], 109 | 110 | // method for sampling pixels 111 | // current supported methods are: 112 | // "near", "vectorize", "near-vectorize", "bilinear", "max", "mean", "median", "min", "mode", "mode-max", "mode-mean", "mode-median", and "mode-min" 113 | // you can also pass in a custom function that takes in ({ values }) and returns a number 114 | method: 'median', 115 | 116 | // round output pixel values to closest integer 117 | // do this if you will convert your output to a PNG or JPG 118 | round: true, 119 | 120 | // optional 121 | // the lowest possible pixel value considering the bit-depth of the data 122 | // this is used to speed up the min and mode-min resampling 123 | // if in_data is an array of typed arrays, this will be automatically calculated 124 | theoretical_min: 0, 125 | 126 | // optional 127 | // the highest possible pixel value considering the bit-depth of the data 128 | // this is used to speed up the max and mode-max resampling 129 | // if in_data is an array of typed arrays, this will be automatically calculated 130 | theoretical_max: 255, 131 | 132 | // optional 133 | // band math expression that maps a pixel from the read bands to the output 134 | // if expr is async (i.e. returns a promise), geowarp will return a promise 135 | expr: ({ pixel }) => { 136 | // clamp values above 100 137 | return pixel.map(value => Math.min(value, 100)); 138 | }, 139 | 140 | // optional 141 | // whether to insert or skip null values 142 | // "skip" - default, don't insert null values 143 | // "insert" - try to insert null values into output data 144 | insert_null_strategy: "skip", 145 | 146 | // optional 147 | // array of band indexes to read from 148 | // use this if your expr function only uses select bands 149 | read_bands: [0, 1, 2], 150 | 151 | // optional 152 | // polygon or multi-polygons defining areas to cut out (everything outside becomes no data) 153 | // terminology taken from https://gdal.org/programs/gdalwarp.html 154 | cutline: geojson, 155 | 156 | // if you specify a cutline, this is required 157 | cutline_srs: 4326, 158 | 159 | // function to reproject [x, y] point from cutline_srs to out_srs 160 | // required if you specify a cutline and the cutline is a different srs than out_srs, 161 | cutline_forward: proj4("EPSG:4326", "EPSG:3857").forward, 162 | 163 | // optional, default is "outside" 164 | // cut out the pixels "inside" or "outside" the cutline 165 | // in other words, if your cutline_strategy is "inside", 166 | // geowarp will set every pixel inside your cutline geometry to out_no_data 167 | cutline_strategy: "inside", 168 | 169 | // optional, default is false 170 | // enable experimental turbocharging via proj-turbo 171 | turbo: true, 172 | 173 | // completely optional and not recommended 174 | // you don't want this in most cases 175 | // over-ride the default function for inserting data into the output multidimensional array 176 | // useful if writing to an alternative object like a canvas 177 | insert_pixel: ({ row, column, pixel }) => { 178 | context.fillStyle = toColor(pixel); 179 | context.fillRect(column, row, 1, 1); 180 | }, 181 | 182 | // completely optional and not recommended in most cases 183 | // take pixel values for a given sample located by sample row and column 184 | // and insert into the output multidimensional array 185 | // by default, this will call insert_pixel 186 | insert_sample: ({ row, column, pixel }) => { 187 | const [xmin, ymin, xmax, ymax] = scalePixel([column, row], [x_scale, y_scale]); 188 | for (let y = ymin; y < ymax; y++) { 189 | for (let x = xmin; x < xmax; x++) { 190 | insert_pixel({ row: y, column: x, pixel }); 191 | } 192 | } 193 | }, 194 | 195 | // skip writing a pixel if "any" or "all" its values are no data 196 | // default is undefined, meaning don't skip no data values 197 | skip_no_data_strategy: "any" 198 | }); 199 | ``` 200 | result is 201 | ```js 202 | { 203 | // a multi-dimensional array of pixel values with the structure defined by out_layout 204 | data: [ 205 | [ [...], [...], [...] ], // band 1 206 | // ... 207 | ] 208 | } 209 | ``` 210 | -------------------------------------------------------------------------------- /geowarp.d.ts: -------------------------------------------------------------------------------- 1 | type bbox = Readonly<[number, number, number, number]> | Readonly<[string, string, string, string]> | number[] | string[]; 2 | type srs = number | string; 3 | type reproject = (pt: [number, number]) => [number, number]; 4 | type data = number[] | number[][] | number[][][] | any; 5 | 6 | export default function geowarp(options: { 7 | debug_level?: number; 8 | forward?: reproject; 9 | inverse?: reproject; 10 | in_data: data; 11 | in_bbox: bbox; 12 | in_geotransform?: number[] | Readonly<[number, number, number, number, number, number]> | undefined; 13 | in_layout?: string; 14 | in_no_data?: number | number[] | readonly number[] | null | undefined; 15 | in_srs?: srs | undefined; 16 | in_width: number; 17 | in_height: number; 18 | out_array_types?: 19 | | ReadonlyArray< 20 | | "Array" 21 | | "Int8Array" 22 | | "Uint8Array" 23 | | "Uint8ClampedArray" 24 | | "Int16Array" 25 | | "Uint16Array" 26 | | "Float32Array" 27 | | "Float64Array" 28 | | "BigInt64Array" 29 | | "BigUint64Array" 30 | > 31 | | undefined; 32 | out_bands?: number[] | Readonly | undefined; 33 | out_bbox?: bbox | undefined; 34 | out_data?: data; 35 | out_layout?: string | undefined; 36 | out_no_data?: number | null | undefined; 37 | out_resolution?: number[] | [number, number] | Readonly | Readonly<[number, number]>; 38 | out_srs?: srs | undefined; 39 | out_pixel_depth?: number | undefined; 40 | out_height: number; 41 | out_width: number; 42 | method?: string | ((arg: { values: number[] }) => number) | undefined; 43 | round?: boolean | undefined; 44 | row_start?: number | undefined; 45 | row_end?: number | undefined; 46 | theoretical_min?: number | undefined; 47 | theoretical_max?: number | undefined; 48 | expr?: ((arg: { pixel: number[] }) => number[]) | undefined; 49 | read_bands?: number[] | undefined; 50 | cutline?: any; 51 | cutline_bbox?: bbox | undefined; 52 | cutline_srs?: number | string | undefined; 53 | cutline_forward?: reproject | undefined; 54 | cutline_strategy?: "inside" | "outside" | string | undefined; 55 | insert_null_strategy?: "skip" | "insert" | undefined; 56 | skip_no_data_strategy?: "any" | "all" | undefined; 57 | cache_process?: true | false | boolean | undefined; 58 | turbo?: boolean | undefined; 59 | }): { 60 | data: number[] | number[][] | number[][][]; 61 | out_bands: number[]; 62 | out_height: number; 63 | out_layout: string; 64 | out_pixel_depth: number | undefined; 65 | out_pixel_height: number; 66 | out_pixel_width: number; 67 | read_bands: number[]; 68 | out_width: number; 69 | }; 70 | -------------------------------------------------------------------------------- /geowarp.js: -------------------------------------------------------------------------------- 1 | const { booleanIntersects, calc: getBoundingBox, intersect, polygon } = require("bbox-fns"); 2 | const dufour_peyton_intersection = require("dufour-peyton-intersection"); 3 | const fastMax = require("fast-max"); 4 | const fastMin = require("fast-min"); 5 | const Geotransform = require("geoaffine/Geotransform.js"); 6 | const getDepth = require("get-depth"); 7 | const getTheoreticalMax = require("typed-array-ranges/get-max"); 8 | const getTheoreticalMin = require("typed-array-ranges/get-min"); 9 | const calcMedian = require("mediana").calculate; 10 | const reprojectBoundingBox = require("bbox-fns/reproject.js"); 11 | const reprojectGeoJSON = require("reproject-geojson/pluggable"); 12 | const { turbocharge } = require("proj-turbo"); 13 | const quickResolve = require("quick-resolve"); 14 | const segflip = require("segflip"); 15 | const xdim = require("xdim"); 16 | 17 | // l = console.log; 18 | 19 | const clamp = (n, min, max) => (n < min ? min : n > max ? max : n); 20 | 21 | const isInvalid = n => n === undefined || n === null || n !== n; 22 | 23 | const scaleInteger = (n, r) => { 24 | const n2 = Math.round(n * r); 25 | return [n2, n2 / n, n / n2]; 26 | }; 27 | 28 | // result as [xmin, ymin, xmax, ymax] 29 | // for (let column = xmin; column < xmax; column++) 30 | const scalePixel = ([column, row], [x_scale, y_scale]) => [ 31 | Math.round(column * x_scale), 32 | Math.round(row * y_scale), 33 | Math.round((column + 1) * x_scale), 34 | Math.round((row + 1) * y_scale) 35 | ]; 36 | 37 | const uniq = arr => Array.from(new Set(arr)).sort((a, b) => b - a); 38 | 39 | const range = ct => new Array(ct).fill(0).map((_, i) => i); 40 | 41 | const forEach = (nums, no_data, cb) => { 42 | const len = nums.length; 43 | if (no_data) { 44 | for (let i = 0; i < len; i++) { 45 | const n = nums[i]; 46 | if (no_data.indexOf(n) === -1) cb(n); 47 | } 48 | } else { 49 | for (let i = 0; i < len; i++) { 50 | cb(nums[i]); 51 | } 52 | } 53 | }; 54 | 55 | const mean = (nums, in_no_data) => { 56 | let running_sum = 0; 57 | let count = 0; 58 | forEach(nums, in_no_data, n => { 59 | count++; 60 | running_sum += n; 61 | }); 62 | return count === 0 ? undefined : running_sum / count; 63 | }; 64 | 65 | const mode = (nums, no_data) => { 66 | if (nums.length === 0) return undefined; 67 | const counts = {}; 68 | if (no_data) { 69 | for (let i = 0; i < nums.length; i++) { 70 | const n = nums[i]; 71 | if (typeof n === "number" && n === n && no_data.indexOf(n) === -1) { 72 | if (n in counts) counts[n].count++; 73 | else counts[n] = { n, count: 1 }; 74 | } 75 | } 76 | } else { 77 | for (let i = 0; i < nums.length; i++) { 78 | const n = nums[i]; 79 | if (n in counts) counts[n].count++; 80 | else counts[n] = { n, count: 1 }; 81 | } 82 | } 83 | const items = Object.values(counts); 84 | const count = items.sort((a, b) => Math.sign(b.count - a.count))[0].count; 85 | return items.filter(it => it.count === count).map(it => it.n); 86 | }; 87 | 88 | // returns [functionCached, clearCache] 89 | const cacheFunction = (f, str = it => it.toString()) => { 90 | let cache = {}; 91 | return [xy => (cache[str(xy)] ??= f(xy)), () => (cache = {})]; 92 | }; 93 | 94 | // generate a histogram from evenly spaced sample points 95 | // purpose is to give us a sense of the distribution of pixel values 96 | // without spending a lot of time reading every point 97 | const quickHistogram = ({ select, width, height }, [across, down]) => { 98 | const hist = {}; 99 | const x_scale = width / across; 100 | const y_scale = height / down; 101 | const rows = new Array(down).fill(null).map((_, i) => Math.floor(i * y_scale)); 102 | const cols = new Array(across).fill(null).map((_, i) => Math.floor(i * x_scale)); 103 | rows.forEach(row => { 104 | cols.forEach(column => { 105 | const value = select({ row, column }); 106 | if (value in hist) hist[value]++; 107 | else hist[value] = 1; 108 | }); 109 | }); 110 | return Object.entries(hist).sort(([apx, act], [bpx, bct]) => Math.sign(bct - act)); 111 | }; 112 | 113 | const geowarp = function geowarp({ 114 | debug_level = 0, 115 | in_data, 116 | in_bbox = undefined, 117 | in_geotransform = undefined, // 6-parameter geotransform, only necessary when in_data is skewed or rotated 118 | in_layout = "[band][row,column]", 119 | in_srs, 120 | in_height, 121 | in_pixel_depth, // number of input bands 122 | in_pixel_height, // optional, automatically calculated from in_bbox 123 | in_pixel_width, // optional, automatically calculated from in_bbox 124 | in_width, 125 | in_no_data, // optional, supports one number or an array of unique no data values 126 | out_array_types, // array of constructor names passed to internal call to xdim's prepareData function 127 | out_bands, // array of bands to keep and order, default is keeping all the bands in same order 128 | out_data, // single or multi-dimensional array that geowarp will fill in with the output 129 | out_pixel_depth, // optional, number of output bands 130 | out_pixel_height, // optional, automatically calculated from out_bbox 131 | out_pixel_width, // optional, automatically calculated from out_bbox 132 | out_bbox = null, 133 | out_bbox_in_srs, // very optional, output bbox reprojected into the srs of the input 134 | out_layout, 135 | out_resolution = [1, 1], 136 | out_srs, 137 | out_width = 256, 138 | out_height = 256, 139 | out_no_data = null, 140 | // out_no_data_strategy = "keep", 141 | method = "median", 142 | read_bands = undefined, // which bands to read, used in conjunction with expr 143 | row_start = 0, // which sample row to start writing with 144 | row_end, // last sample row to write 145 | expr = undefined, // band expression function 146 | round = false, // whether to round output 147 | theoretical_min, // minimum theoretical value (e.g., 0 for unsigned integer arrays) 148 | theoretical_max, // maximum values (e.g., 255 for 8-bit unsigned integer arrays), 149 | inverse, // function to reproject [x, y] point from out_srs back to in_srs 150 | forward, // function to reproject [x, y] point from in_srs to out_srs 151 | cutline, // polygon or polygons defining areas to cut out (everything outside becomes no data) 152 | cutline_bbox, // bounding box of the cutline geometry, can lead to a performance increase when combined with turbo 153 | cutline_srs, // spatial reference system of the cutline 154 | cutline_forward, // function to reproject [x, y] point from cutline_srs to out_srs 155 | cutline_strategy = "outside", // cut out the pixels inside or outside the cutline 156 | turbo = false, // enable experimental turbocharging via proj-turbo 157 | insert_pixel, // over-ride function that inserts data into output multi-dimensional array 158 | insert_sample, // over-ride function that inserts each sample into the output multi-dimensional array (calls insert) 159 | insert_null_strategy = "skip", // whether to insert or skip null values 160 | skip_no_data_strategy, // skip processing pixels if "any" or "all" values are "no data" 161 | cache_process = false // whether to try to cache the processing step 162 | // cache_functions // this really helps if functions are asynchronous and require posting to a web worker 163 | }) { 164 | if (debug_level >= 1) console.log("[geowarp] starting"); 165 | const start_time = debug_level >= 1 ? performance.now() : 0; 166 | 167 | if (isNaN(out_height)) throw new Error("[geowarp] out_height is NaN"); 168 | if (isNaN(out_width)) throw new Error("[geowarp] out_width is NaN"); 169 | 170 | // track pending promises without the memory overhead 171 | // of holding all the promises in memory 172 | let pending = 0; 173 | 174 | const [out_height_in_samples, y_resolution, y_scale] = scaleInteger(out_height, out_resolution[1]); 175 | const [out_width_in_samples, x_resolution, x_scale] = scaleInteger(out_width, out_resolution[0]); 176 | 177 | if (debug_level >= 1) console.log("[geowarp] scaled size:", [out_width_in_samples, out_height_in_samples]); 178 | if (debug_level >= 1) console.log("[geowarp] resolution:", [x_resolution, y_resolution]); 179 | if (debug_level >= 1) console.log("[geowarp] scale:", [x_scale, y_scale]); 180 | 181 | const same_srs = in_srs === out_srs; 182 | if (debug_level >= 1) console.log("[geowarp] input and output srs are the same:", same_srs); 183 | 184 | if (debug_level >= 1) console.log("[geowarp] skip_no_data_strategy:", skip_no_data_strategy); 185 | 186 | // support for deprecated alias of inverse 187 | inverse ??= arguments[0].reproject; 188 | 189 | // support for deprecated insert 190 | insert_pixel ??= arguments[0].insert; 191 | 192 | let in_bbox_out_srs, intersect_bbox_in_srs, intersect_bbox_out_srs; 193 | 194 | if (!same_srs) { 195 | if (!in_bbox) throw new Error("[geowarp] can't reproject without in_bbox"); 196 | if (!out_bbox) { 197 | if (forward) out_bbox = in_bbox_out_srs = intersect_bbox_out_srs = reprojectBoundingBox(in_bbox, forward, { density: 100 }); 198 | else throw new Error("[geowarp] must specify out_bbox or forward"); 199 | } 200 | } 201 | 202 | if (!same_srs && typeof inverse !== "function") { 203 | throw new Error("[geowarp] you must specify a reproject function"); 204 | } 205 | 206 | if (!in_height) throw new Error("[geowarp] you must provide in_height"); 207 | if (!in_width) throw new Error("[geowarp] you must provide in_width"); 208 | 209 | // if no output layout is specified 210 | // just return the data in the same layout as it is provided 211 | if (!out_layout) out_layout = in_layout; 212 | 213 | if (in_pixel_depth === undefined || in_pixel_depth === null) { 214 | if (in_layout.startsWith("[band]")) { 215 | in_pixel_depth = in_data.length; 216 | } else { 217 | const depth = getDepth(in_data); 218 | if (depth === 1) { 219 | // could be [row,column,band] or [band,row,column] 220 | in_pixel_depth = in_data.length / in_height / in_width; 221 | } else if (depth === 2) { 222 | // probably [row,column][band] 223 | in_pixel_depth = in_data[0].length; 224 | } else if (depth === 3) { 225 | // probably [row][column][band] 226 | in_pixel_depth = in_data[0][0].length; 227 | } 228 | } 229 | } 230 | 231 | if (debug_level >= 1) console.log("[geowarp] number of bands in source data:", in_pixel_depth); 232 | 233 | if (!read_bands) { 234 | if (expr) read_bands = range(in_pixel_depth); 235 | else if (out_bands) read_bands = uniq(out_bands); 236 | else read_bands = range(in_pixel_depth); 237 | } 238 | 239 | out_bands ??= read_bands; 240 | 241 | if (round && typeof out_no_data === "number") out_no_data = Math.round(out_no_data); 242 | // if (out_no_data === null && out_no_data_strategy === "keep") out_no_data = in_no_data; 243 | 244 | if (Array.isArray(in_no_data) === false) { 245 | if ("in_no_data" in arguments[0]) { 246 | in_no_data = [in_no_data]; 247 | } else { 248 | in_no_data = []; 249 | } 250 | } 251 | const primary_in_no_data = in_no_data[0]; 252 | 253 | // processing step after we have read the raw pixel values 254 | let process; 255 | if (expr) { 256 | if (round) { 257 | process = ({ pixel }) => quickResolve(expr({ pixel })).then(pixel => pixel.map(n => Math.round(n))); 258 | } else { 259 | process = expr; // maps ({ pixel }) to new pixel 260 | } 261 | } else { 262 | // mapping index of band in output pixel to index in read band 263 | const out_bands_to_read_bands = out_bands.map(iband => read_bands.indexOf(iband)); 264 | 265 | // we create a different processing pipeline depending on rounding 266 | // because we don't want to check if we should round for every single pixel 267 | if (round) { 268 | process = ({ pixel }) => 269 | out_bands_to_read_bands.map(iband => { 270 | const value = pixel[iband]; 271 | return isInvalid(value) || in_no_data.includes(value) ? out_no_data : Math.round(value); 272 | }); 273 | } else { 274 | // without rounding 275 | process = ({ pixel }) => 276 | out_bands_to_read_bands.map(iband => { 277 | const value = pixel[iband]; 278 | return isInvalid(value) || in_no_data.includes(value) ? out_no_data : value; 279 | }); 280 | } 281 | } 282 | 283 | let clear_process_cache; 284 | if (cache_process) { 285 | // eslint-disable-next-line no-unused-vars 286 | [process, clear_process_cache] = cacheFunction(process, ({ pixel }) => pixel.toString()); 287 | } 288 | 289 | if (debug_level >= 1) console.log("[geowarp] read_bands:", read_bands); 290 | if (debug_level >= 1) console.log("[geowarp] out_height:", out_height); 291 | if (debug_level >= 1) console.log("[geowarp] out_width:", out_width); 292 | 293 | if (same_srs && in_bbox && !out_bbox) { 294 | out_bbox = in_bbox; 295 | } 296 | 297 | const [in_xmin, in_ymin, in_xmax, in_ymax] = in_bbox; 298 | 299 | in_pixel_height ??= (in_ymax - in_ymin) / in_height; 300 | in_pixel_width ??= (in_xmax - in_xmin) / in_width; 301 | if (debug_level >= 1) console.log("[geowarp] pixel height of source data:", in_pixel_height); 302 | if (debug_level >= 1) console.log("[geowarp] pixel width of source data:", in_pixel_width); 303 | 304 | in_geotransform ??= [in_xmin, in_pixel_width, 0, in_ymax, 0, -1 * in_pixel_height]; 305 | 306 | const { forward: in_img_pt_to_srs_pt, inverse: in_srs_pt_to_in_img_pt } = Geotransform(in_geotransform); 307 | 308 | // convert point in output srs to image pixel coordinate in input image 309 | const out_srs_pt_to_in_img_pt = same_srs ? in_srs_pt_to_in_img_pt : pt => in_srs_pt_to_in_img_pt(inv(pt)); 310 | 311 | const [out_xmin, out_ymin, out_xmax, out_ymax] = out_bbox; 312 | if (debug_level >= 1) console.log("[geowarp] out_xmin:", out_xmin); 313 | if (debug_level >= 1) console.log("[geowarp] out_ymin:", out_ymin); 314 | if (debug_level >= 1) console.log("[geowarp] out_xmax:", out_xmax); 315 | if (debug_level >= 1) console.log("[geowarp] out_ymax:", out_ymax); 316 | 317 | out_pixel_height ??= (out_ymax - out_ymin) / out_height; 318 | out_pixel_width ??= (out_xmax - out_xmin) / out_width; 319 | if (debug_level >= 1) console.log("[geowarp] out_pixel_height:", out_pixel_height); 320 | if (debug_level >= 1) console.log("[geowarp] out_pixel_width:", out_pixel_width); 321 | 322 | const out_sample_height = out_pixel_height * y_scale; 323 | const out_sample_width = out_pixel_width * x_scale; 324 | if (debug_level >= 1) console.log("[geowarp] out_sample_height:", out_sample_height); 325 | if (debug_level >= 1) console.log("[geowarp] out_sample_width:", out_sample_width); 326 | 327 | const half_out_sample_height = out_sample_height / 2; 328 | const half_out_sample_width = out_sample_width / 2; 329 | 330 | // const out_geotransform = [out_xmin, out_pixel_width, 0, out_ymax, 0, -1 * out_pixel_height]; 331 | // const { forward: out_img_pt_to_srs_pt, inverse: out_srs_pt_to_img_pt } = Geotransform(out_geotransform); 332 | 333 | const in_img_pt_to_out_srs_pt = same_srs ? in_img_pt_to_srs_pt : pt => fwd(in_img_pt_to_srs_pt(pt)); 334 | // const in_img_pt_to_out_img_pt = same_srs ? pt => out_srs_pt_to_img_pt(in_img_pt_to_srs_pts(pt)) : pt => out_srs_pt_to_img_pt(fwd(in_img_pt_to_srs_pt(pt))); 335 | 336 | if (theoretical_min === undefined || theoretical_max === undefined) { 337 | try { 338 | const data_constructor = in_data[0].constructor.name; 339 | if (debug_level >= 1) console.log("[geowarp] data_constructor:", data_constructor); 340 | theoretical_min ??= getTheoreticalMin(data_constructor); 341 | theoretical_max ??= getTheoreticalMax(data_constructor); 342 | if (debug_level >= 1) console.log("[geowarp] theoretical_min:", theoretical_min); 343 | if (debug_level >= 1) console.log("[geowarp] theoretical_max:", theoretical_max); 344 | } catch (error) { 345 | // we want to log an error if it happens 346 | // even if we don't strictly need it to succeed 347 | console.error(error); 348 | } 349 | } 350 | 351 | if (![undefined, null, ""].includes(cutline_forward) && typeof cutline_forward !== "function") { 352 | throw new Error("[geowarp] cutline_forward must be of type function not " + typeof cutline); 353 | } 354 | 355 | // if cutline isn't in the projection of the output, reproject it 356 | let segments_by_row = new Array(out_height_in_samples).fill(0).map(() => []); 357 | if (cutline && cutline_srs !== out_srs) { 358 | if (!cutline_forward) { 359 | // fallback to checking if we can use forward 360 | if (in_srs === cutline_srs) cutline_forward = forward; 361 | throw new Error("[geowarp] must specify cutline_forward when cutline_srs and out_srs differ"); 362 | } 363 | 364 | let cutline_forward_turbocharged; 365 | if (cutline_forward && cutline_bbox) { 366 | cutline_forward_turbocharged = turbocharge({ 367 | bbox: cutline_bbox, 368 | debug_level, 369 | quiet: true, 370 | reproject: cutline_forward, 371 | threshold: [half_out_sample_width, half_out_sample_height] 372 | })?.reproject; 373 | } 374 | 375 | cutline = reprojectGeoJSON(cutline, { reproject: cutline_forward_turbocharged || cutline_forward }); 376 | } 377 | 378 | const out_column_max = out_width_in_samples - 1; 379 | const full_width_row_segment = [0, out_column_max]; 380 | const full_width_row = [full_width_row_segment]; 381 | 382 | if (cutline) { 383 | const intersections = dufour_peyton_intersection.calculate({ 384 | raster_bbox: out_bbox, 385 | raster_height: out_height_in_samples, 386 | raster_width: out_width_in_samples, 387 | geometry: cutline 388 | }); 389 | 390 | // we don't use per_row_segment because that can lead to overlap 391 | intersections.rows.forEach((segs, irow) => { 392 | segments_by_row[irow] = segs; 393 | }); 394 | 395 | if (cutline_strategy === "inside") { 396 | // flip the inside/outside segments 397 | 398 | segments_by_row = segments_by_row.map(segs => { 399 | if (segs.length === 0) { 400 | return full_width_row; 401 | } else { 402 | return segflip({ 403 | segments: segs, 404 | min: 0, 405 | max: out_column_max, 406 | debug: false 407 | }); 408 | } 409 | }); 410 | } 411 | } else { 412 | for (let row_index = 0; row_index < out_height_in_samples; row_index++) { 413 | segments_by_row[row_index].push(full_width_row_segment); 414 | } 415 | } 416 | 417 | const in_sizes = { 418 | band: in_pixel_depth, 419 | row: in_height, 420 | column: in_width 421 | }; 422 | 423 | const select = xdim.prepareSelect({ data: in_data, layout: in_layout, sizes: in_sizes }); 424 | 425 | const selectPixel = ({ row, column }) => 426 | read_bands.map( 427 | band => 428 | select({ 429 | point: { 430 | band, 431 | row, 432 | column 433 | } 434 | }).value 435 | ); 436 | 437 | const hist = quickHistogram({ select: selectPixel, width: in_width, height: in_height }, [10, 10]); 438 | const { hits, total } = hist.reduce( 439 | (acc, [px, ct]) => { 440 | acc.total += ct; 441 | acc.hits += ct - 1; // subtracting 1 because the first instance of something won't use the cache 442 | return acc; 443 | }, 444 | { hits: 0, total: 0 } 445 | ); 446 | const predicted_cache_hit_rate = hits / total; 447 | 448 | if (cache_process === undefined || cache_process === null) { 449 | cache_process = predicted_cache_hit_rate >= 0.85; 450 | } 451 | 452 | if (typeof insert_pixel !== "function") { 453 | let update; 454 | 455 | // only works once update is defined later on 456 | const update_pixel = ({ row, column, pixel }) => { 457 | pixel.forEach((value, band) => { 458 | update({ 459 | point: { band, row, column }, 460 | value 461 | }); 462 | }); 463 | }; 464 | 465 | let insert_pixel_sync = ({ pixel, ...rest }) => { 466 | try { 467 | out_pixel_depth ??= pixel.length; 468 | if (debug_level >= 1) console.log("[geowarp] out_pixel_depth:", out_pixel_depth); 469 | 470 | const out_sizes = { 471 | band: out_pixel_depth, 472 | row: out_height, 473 | column: out_width 474 | }; 475 | if (debug_level >= 1) console.log("[geowarp] out_sizes:", out_sizes); 476 | 477 | out_data ??= xdim.prepareData({ 478 | fill: out_no_data, 479 | layout: out_layout, 480 | sizes: out_sizes, 481 | arrayTypes: out_array_types 482 | }).data; 483 | if (debug_level >= 1) console.log("[geowarp] out_data:", typeof out_data); 484 | 485 | update = xdim.prepareUpdate({ data: out_data, layout: out_layout, sizes: out_sizes }); 486 | if (debug_level >= 1) console.log("[geowarp] prepared update function"); 487 | 488 | // replace self, so subsequent calls go directly to update_pixel 489 | insert_pixel_sync = update_pixel; 490 | 491 | update_pixel({ pixel, ...rest }); 492 | } catch (error) { 493 | console.error("first call to insert_pixel_sync failed:", error); 494 | } 495 | }; 496 | 497 | insert_pixel = ({ pixel, ...rest }) => { 498 | pending++; 499 | quickResolve(pixel).then(px => { 500 | insert_pixel_sync({ pixel: px, ...rest }); 501 | pending--; 502 | }); 503 | }; 504 | } 505 | 506 | if (typeof insert_sample !== "function") { 507 | if (x_resolution === 1 && y_resolution === 1) { 508 | // we call insert_pixel instead of setting insert_sample = insert_pixel 509 | // because insert_pixel might have been hot swapped 510 | insert_sample = params => insert_pixel(params); 511 | } else { 512 | insert_sample = ({ row, column, pixel, ...rest }) => { 513 | const [xmin, ymin, xmax, ymax] = scalePixel([column, row], [x_scale, y_scale]); 514 | for (let y = ymin; y < ymax; y++) { 515 | for (let x = xmin; x < xmax; x++) { 516 | insert_pixel({ row: y, column: x, pixel, ...rest }); 517 | } 518 | } 519 | }; 520 | } 521 | } 522 | 523 | row_end ??= out_height_in_samples; 524 | 525 | if (debug_level >= 1) console.log("[geowarp] method:", method); 526 | 527 | // see if can create direct pixel affine transformation 528 | // skipping over spatial reference system 529 | let inverse_pixel = ([c, r]) => { 530 | const x = out_xmin + c * out_sample_width + half_out_sample_width; 531 | const y = out_ymax - r * out_sample_height - half_out_sample_height; 532 | const pt_out_srs = [x, y]; 533 | const pt_in_srs = same_srs ? pt_out_srs : inverse(pt_out_srs); 534 | const pt_in_img = in_srs_pt_to_in_img_pt(pt_in_srs).map(n => Math.floor(n)); 535 | return pt_in_img; 536 | }; 537 | 538 | if (turbo) { 539 | const reproject = turbocharge({ 540 | bbox: [0, 0, out_width, out_height], 541 | debug_level, 542 | quiet: true, 543 | reproject: inverse_pixel, 544 | threshold: [0.5, 0.5] 545 | })?.reproject; 546 | if (reproject) inverse_pixel = pt => reproject(pt).map(n => Math.round(n)); 547 | } 548 | 549 | let forward_turbocharged, inverse_turbocharged; 550 | if (turbo) { 551 | if (forward) { 552 | out_bbox_in_srs ??= reprojectBoundingBox(out_bbox, inverse, { density: 100, nan_strategy: "skip" }); 553 | intersect_bbox_in_srs ??= intersect(in_bbox, out_bbox_in_srs); 554 | forward_turbocharged = turbocharge({ 555 | bbox: intersect_bbox_in_srs, 556 | debug_level, 557 | quiet: true, 558 | reproject: forward, 559 | threshold: [half_out_sample_width, half_out_sample_height] 560 | }); 561 | } 562 | if (inverse) { 563 | in_bbox_out_srs ??= reprojectBoundingBox(in_bbox, forward, { density: 100 }); 564 | intersect_bbox_out_srs ??= intersect(out_bbox, in_bbox_out_srs); 565 | inverse_turbocharged = turbocharge({ 566 | bbox: intersect_bbox_out_srs, 567 | debug_level, 568 | quiet: true, 569 | reproject: inverse, 570 | threshold: [half_out_sample_width, half_out_sample_height] 571 | }); 572 | } 573 | } 574 | if (debug_level >= 2) { 575 | if (forward_turbocharged) console.log("[geowarp] turbocharged forward"); 576 | if (inverse_turbocharged) console.log("[geowarp] turbocharged inverse"); 577 | } 578 | const fwd = forward_turbocharged?.reproject || forward; 579 | const inv = inverse_turbocharged?.reproject || inverse; 580 | // const [invCached, clearInvCache] = cacheFunction(inv); 581 | 582 | let out_sample_height_in_srs, out_sample_width_in_srs, pixel_height_ratio, pixel_width_ratio; 583 | if (method === "near-vectorize" || method === "nearest-vectorize") { 584 | if (debug_level >= 2) console.log('[geowarp] choosing between "near" and "vectorize" for best speed'); 585 | 586 | out_bbox_in_srs ??= same_srs ? out_bbox : reprojectBoundingBox(out_bbox, inverse, { density: 100, nan_strategy: "skip" }); 587 | 588 | // average of how large each output pixel is in the input spatial reference system 589 | out_sample_height_in_srs = (out_bbox_in_srs[3] - out_bbox_in_srs[1]) / out_height_in_samples; 590 | out_sample_width_in_srs = (out_bbox_in_srs[2] - out_bbox_in_srs[0]) / out_width_in_samples; 591 | 592 | pixel_height_ratio = out_sample_height_in_srs / in_pixel_height; 593 | pixel_width_ratio = out_sample_width_in_srs / in_pixel_width; 594 | 595 | if (debug_level >= 2) console.log("[geowarp] pixel_height_ratio:", pixel_height_ratio); 596 | if (debug_level >= 2) console.log("[geowarp] pixel_width_ratio:", pixel_width_ratio); 597 | if (pixel_height_ratio < 0.1 && pixel_width_ratio < 0.1) { 598 | method = "vectorize"; 599 | if (debug_level >= 1) console.log('[geowarp] selected "vectorize" method as it is likely to be faster'); 600 | } else { 601 | method = "near"; 602 | if (debug_level >= 1) console.log('[geowarp] selected "near" method as it is likely to be faster'); 603 | } 604 | } 605 | 606 | const should_skip = 607 | skip_no_data_strategy === "any" 608 | ? px => px.some(n => isInvalid(n) || in_no_data.includes(n)) 609 | : skip_no_data_strategy === "all" 610 | ? px => px.every(n => isInvalid(n) || in_no_data.includes(n)) 611 | : () => false; 612 | 613 | if (method === "vectorize") { 614 | // const [cfwd, clear_forward_cache] = cacheFunction(fwd); 615 | 616 | // reproject bounding box of output (e.g. a tile) into the spatial reference system of the input data 617 | // setting nan_strategy to skip trims the box in case the output bbox extends over the bounds of the input projection 618 | out_bbox_in_srs ??= same_srs ? out_bbox : reprojectBoundingBox(out_bbox, inverse, { density: 100, nan_strategy: "skip" }); 619 | let [left, bottom, right, top] = out_bbox_in_srs; 620 | 621 | out_sample_height_in_srs ??= (top - bottom) / out_height_in_samples; 622 | if (in_pixel_height < out_sample_height_in_srs) { 623 | if (debug_level >= 1) { 624 | console.warn(`[geowarp] normalized height of sample area of ${out_sample_height_in_srs} is larger than input pixel height of ${in_pixel_height}`); 625 | } 626 | } 627 | 628 | out_sample_width_in_srs ??= (right - left) / out_width; 629 | if (in_pixel_width < out_sample_width_in_srs) { 630 | if (debug_level >= 1) { 631 | console.warn(`[geowarp] normalized width of sample area of ${out_sample_width_in_srs} is larger than input pixel width of ${in_pixel_width}`); 632 | } 633 | } 634 | 635 | // if have a cutline do additional clamping 636 | const cutline_in_srs = cutline && reprojectGeoJSON(cutline, { reproject: inverse }); 637 | 638 | // in the future we might want to pull the function getBoundingBox into its own repo 639 | const cutline_bbox_in_srs = cutline && getBoundingBox(cutline_in_srs); 640 | 641 | if (!cutline || booleanIntersects(in_bbox, cutline_bbox_in_srs)) { 642 | // update bounding box we sample from based on extent of cutline 643 | [left, bottom, right, top] = cutline && cutline_strategy !== "inside" ? intersect(out_bbox_in_srs, cutline_bbox_in_srs) : out_bbox_in_srs; 644 | if (debug_level >= 1) console.log("[geowarp] [left, bottom, right, top]:", [left, bottom, right, top]); 645 | 646 | if ((left < in_xmax && bottom < in_ymax && right > in_xmin) || top < in_ymin) { 647 | const out_bbox_in_input_image_coords = reprojectBoundingBox(out_bbox_in_srs, in_srs_pt_to_in_img_pt); 648 | if (debug_level >= 1) console.log("[geowarp] out_bbox_in_input_image_coords:", out_bbox_in_input_image_coords); 649 | 650 | // need to double check intersection in image space in case of rotation/skew 651 | if (booleanIntersects(out_bbox_in_input_image_coords, [0, 0, in_width, in_height])) { 652 | // snap to pixel array inidices 653 | const [in_column_start, in_row_start, in_column_end, in_row_end] = out_bbox_in_input_image_coords.map(n => Math.floor(n)); 654 | const in_row_start_clamped = clamp(in_row_start, 0, in_height - 1); 655 | const in_row_end_clamped = clamp(in_row_end, 0, in_height - 1); 656 | const in_column_start_clamped = clamp(in_column_start, 0, in_width - 1); 657 | const in_column_end_clamped = clamp(in_column_end, 0, in_width - 1); 658 | 659 | for (let r = in_row_start_clamped; r <= in_row_end_clamped; r++) { 660 | // if (clear_process_cache) clear_process_cache(); 661 | // clear_forward_cache(); // don't want cache to get too large, so just cache each row 662 | for (let c = in_column_start_clamped; c <= in_column_end_clamped; c++) { 663 | const raw_values = read_bands.map(band => select({ point: { band, row: r, column: c } }).value); 664 | 665 | if (should_skip(raw_values)) continue; 666 | 667 | const rect = polygon([c, r, c + 1, r + 1]); 668 | 669 | // to-do: reproject to [I, J] (output image point) because 670 | // intersection algorithm assumes an unskewed space 671 | // we'll only have to do this if we want to support rotated/skewed output 672 | const pixel_geometry_in_out_srs = reprojectGeoJSON(rect, { reproject: in_img_pt_to_out_srs_pt }); 673 | 674 | const intersect_options = { 675 | debug: false, 676 | raster_bbox: out_bbox, 677 | raster_height: out_height_in_samples, 678 | raster_width: out_width_in_samples, 679 | geometry: pixel_geometry_in_out_srs 680 | }; 681 | 682 | // apply band math expression, no-data mapping, and rounding when applicable 683 | const pixel = process({ pixel: raw_values }); 684 | 685 | if (pixel !== null || insert_null_strategy === "insert") { 686 | if (cutline) { 687 | intersect_options.per_pixel = ({ row, column }) => { 688 | if (segments_by_row[row].some(([start, end]) => column >= start && column <= end)) { 689 | insert_sample({ raw: raw_values, pixel, row, column }); 690 | } 691 | }; 692 | } else { 693 | intersect_options.per_pixel = ({ row, column }) => { 694 | insert_sample({ raw: raw_values, pixel, row, column }); 695 | }; 696 | } 697 | dufour_peyton_intersection.calculate(intersect_options); 698 | } 699 | } 700 | } 701 | } 702 | } 703 | } 704 | } else if (method === "near" || method === "nearest") { 705 | const rmax = Math.min(row_end, out_height_in_samples); 706 | for (let r = row_start; r < rmax; r++) { 707 | // if (clear_process_cache) clear_process_cache(); 708 | const segments = segments_by_row[r]; 709 | for (let iseg = 0; iseg < segments.length; iseg++) { 710 | const [cstart, cend] = segments[iseg]; 711 | for (let c = cstart; c <= cend; c++) { 712 | const [x_in_raster_pixels, y_in_raster_pixels] = inverse_pixel([c, r]); 713 | 714 | let raw_values = []; 715 | 716 | if (x_in_raster_pixels < 0 || y_in_raster_pixels < 0 || x_in_raster_pixels >= in_width || y_in_raster_pixels >= in_height) { 717 | // through reprojection, we can sometimes find ourselves just across the edge 718 | raw_values = new Array(read_bands.length).fill(primary_in_no_data); 719 | } else { 720 | raw_values = selectPixel({ 721 | row: y_in_raster_pixels, 722 | column: x_in_raster_pixels 723 | }); 724 | } 725 | 726 | if (should_skip(raw_values)) continue; 727 | const pixel = process({ pixel: raw_values }); 728 | if (pixel !== null || insert_null_strategy === "insert") { 729 | insert_sample({ 730 | row: r, 731 | column: c, 732 | pixel, 733 | raw: raw_values, 734 | x_in_raster_pixels, 735 | y_in_raster_pixels 736 | }); 737 | } 738 | } 739 | } 740 | } 741 | } else if (method === "bilinear") { 742 | // see https://en.wikipedia.org/wiki/Bilinear_interpolation 743 | 744 | const rmax = Math.min(row_end, out_height_in_samples); 745 | 746 | let y = out_ymax + half_out_sample_height - row_start * out_sample_height; 747 | for (let r = row_start; r < rmax; r++) { 748 | // if (clear_process_cache) clear_process_cache(); 749 | y -= out_sample_height; 750 | const segments = segments_by_row[r]; 751 | for (let iseg = 0; iseg < segments.length; iseg++) { 752 | const [cstart, cend] = segments[iseg]; 753 | for (let c = cstart; c <= cend; c++) { 754 | const x = out_xmin + c * out_sample_width + half_out_sample_width; 755 | const pt_out_srs = [x, y]; 756 | const pt_in_srs = same_srs ? pt_out_srs : inv(pt_out_srs); 757 | const [xInRasterPixels, yInRasterPixels] = in_srs_pt_to_in_img_pt(pt_in_srs); 758 | 759 | const left = Math.floor(xInRasterPixels); 760 | const right = Math.ceil(xInRasterPixels); 761 | const top = Math.floor(yInRasterPixels); 762 | const bottom = Math.ceil(yInRasterPixels); 763 | 764 | // if xInRaster pixels is an integer, 765 | // then leftWeight and rightWeight will equal zero 766 | // that's not a problem though, because we ignore 767 | // the weighting when values on each side are the same 768 | const leftWeight = right - xInRasterPixels; 769 | const rightWeight = xInRasterPixels - left; 770 | const topWeight = top === bottom ? 0.5 : bottom - yInRasterPixels; 771 | const bottomWeight = top === bottom ? 0.5 : yInRasterPixels - top; 772 | 773 | const leftOutside = left < 0 || left >= in_width; 774 | const rightOutside = right < 0 || right >= in_width; 775 | const topOutside = top < 0 || top >= in_height; 776 | const bottomOutside = bottom < 0 || bottom >= in_height; 777 | 778 | const upperleftOutside = topOutside || leftOutside; 779 | const upperRightOutside = topOutside || rightOutside; 780 | const lowerleftOutside = bottomOutside || leftOutside; 781 | const lowerRightOutside = bottomOutside || rightOutside; 782 | 783 | const raw_values = new Array(); 784 | for (let i = 0; i < read_bands.length; i++) { 785 | const read_band = read_bands[i]; 786 | 787 | const upperLeftValue = upperleftOutside ? primary_in_no_data : select({ point: { band: read_band, row: top, column: left } }).value; 788 | const upperRightValue = upperRightOutside ? primary_in_no_data : select({ point: { band: read_band, row: top, column: right } }).value; 789 | const lowerLeftValue = lowerleftOutside ? primary_in_no_data : select({ point: { band: read_band, row: bottom, column: left } }).value; 790 | const lowerRightValue = lowerRightOutside ? primary_in_no_data : select({ point: { band: read_band, row: bottom, column: right } }).value; 791 | 792 | let topValue; 793 | const upperLeftInvalid = isInvalid(upperLeftValue) || in_no_data.includes(upperLeftValue); 794 | const upperRightInvalid = isInvalid(upperRightValue) || in_no_data.includes(upperRightValue); 795 | if (upperLeftInvalid && upperRightInvalid) { 796 | // keep topValue undefined 797 | } else if (upperLeftInvalid) { 798 | topValue = upperRightValue; 799 | } else if (upperRightInvalid) { 800 | topValue = upperLeftValue; 801 | } else if (upperLeftValue === upperRightValue) { 802 | // because the upper-left and upper-right values are the same, no weighting is necessary 803 | topValue = upperLeftValue; 804 | } else { 805 | topValue = leftWeight * upperLeftValue + rightWeight * upperRightValue; 806 | } 807 | 808 | let bottomValue; 809 | const lowerLeftInvalid = isInvalid(lowerLeftValue) || in_no_data.includes(lowerLeftValue); 810 | const lowerRightInvalid = isInvalid(lowerRightValue) || in_no_data.includes(lowerRightValue); 811 | if (lowerLeftInvalid && lowerRightInvalid) { 812 | // keep bottom value undefined 813 | } else if (lowerLeftInvalid) { 814 | bottomValue = lowerRightValue; 815 | } else if (lowerRightInvalid) { 816 | bottomValue = lowerLeftValue; 817 | } else if (lowerLeftValue === lowerRightValue) { 818 | // because the lower-left and lower-right values are the same, no weighting is necessary 819 | bottomValue = lowerLeftValue; 820 | } else { 821 | bottomValue = leftWeight * lowerLeftValue + rightWeight * lowerRightValue; 822 | } 823 | 824 | let value; 825 | if (topValue === undefined && bottomValue === undefined) { 826 | value = primary_in_no_data; 827 | } else if (topValue === undefined) { 828 | value = bottomValue; 829 | } else if (bottomValue === undefined) { 830 | value = topValue; 831 | } else { 832 | value = bottomWeight * bottomValue + topWeight * topValue; 833 | } 834 | 835 | raw_values.push(value); 836 | } 837 | if (should_skip(raw_values)) continue; 838 | const pixel = process({ pixel: raw_values }); 839 | if (pixel !== null || insert_null_strategy === "insert") { 840 | insert_sample({ row: r, column: c, pixel, raw: raw_values }); 841 | } 842 | } 843 | } 844 | } 845 | } else { 846 | // Q: why don't we pass no_data to the following statistical methods (e.g. fastMax)? 847 | // A: we are already filtering out invalid and no-data values beforehand 848 | let calc; 849 | if (typeof method === "function") { 850 | calc = values => method({ values }); 851 | } else if (method === "max") { 852 | calc = values => fastMax(values, { theoretical_max }); 853 | } else if (method === "mean") { 854 | calc = values => mean(values); 855 | } else if (method === "median") { 856 | calc = values => calcMedian(values); 857 | } else if (method === "min") { 858 | calc = values => fastMin(values, { theoretical_min }); 859 | } else if (method === "mode") { 860 | calc = values => mode(values)[0]; 861 | } else if (method === "mode-max") { 862 | calc = values => fastMax(mode(values)); 863 | } else if (method === "mode-mean") { 864 | calc = values => mean(mode(values)); 865 | } else if (method === "mode-median") { 866 | calc = values => calcMedian(mode(values)); 867 | } else if (method === "mode-min") { 868 | calc = values => fastMin(mode(values)); 869 | } else { 870 | throw new Error(`[geowarp] unknown method "${method}"`); 871 | } 872 | 873 | let top, left, bottom, right; 874 | bottom = out_ymax - row_start * row_start; 875 | const rmax = Math.min(row_end, out_height_in_samples); 876 | for (let r = row_start; r < rmax; r++) { 877 | // if (clear_process_cache) clear_process_cache(); 878 | top = bottom; 879 | bottom = top - out_sample_height; 880 | const segments = segments_by_row[r]; 881 | for (let iseg = 0; iseg < segments.length; iseg++) { 882 | const [cstart, cend] = segments[iseg]; 883 | right = out_xmin + out_sample_width * cstart; 884 | for (let c = cstart; c <= cend; c++) { 885 | left = right; 886 | right = left + out_sample_width; 887 | // top, left, bottom, right is the sample area in the coordinate system of the output 888 | 889 | // convert bbox in output srs to image px of input 890 | // combing srs reprojection and srs-to-image mapping, ensures that bounding box corners 891 | // are reprojected fully before calculating containing bbox 892 | // (prevents drift in increasing bbox twice if image is warped) 893 | let leftInRasterPixels, topInRasterPixels, rightInRasterPixels, bottomInRasterPixels; 894 | try { 895 | [leftInRasterPixels, topInRasterPixels, rightInRasterPixels, bottomInRasterPixels] = reprojectBoundingBox( 896 | [left, bottom, right, top], 897 | out_srs_pt_to_in_img_pt, 898 | { nan_strategy: "throw" } 899 | ); 900 | } catch (error) { 901 | // if only one pixel (or row of pixels) extends over the edge of the projection's bounds, we probably don't want to fail the whole thing 902 | // an example would be warping the globe from 3857 to 4326 903 | continue; 904 | } 905 | if (debug_level >= 4) console.log("[geowarp] leftInRasterPixels:", leftInRasterPixels); 906 | if (debug_level >= 4) console.log("[geowarp] rightInRasterPixels:", rightInRasterPixels); 907 | if (debug_level >= 4) console.log("[geowarp] topInRasterPixels:", topInRasterPixels); 908 | if (debug_level >= 4) console.log("[geowarp] bottomInRasterPixels:", bottomInRasterPixels); 909 | 910 | let leftSample = Math.round(leftInRasterPixels); 911 | let rightSample = Math.round(rightInRasterPixels); 912 | let topSample = Math.round(topInRasterPixels); 913 | let bottomSample = Math.round(bottomInRasterPixels); 914 | 915 | // if output pixel isn't large enough to sample an input pixel 916 | // just pick input pixel at the center of the output pixel 917 | if (leftSample === rightSample) { 918 | const xCenterSample = (rightInRasterPixels + leftInRasterPixels) / 2; 919 | leftSample = Math.floor(xCenterSample); 920 | rightSample = leftSample + 1; 921 | } 922 | if (topSample === bottomSample) { 923 | const yCenterSample = (topInRasterPixels + bottomInRasterPixels) / 2; 924 | topSample = Math.floor(yCenterSample); 925 | bottomSample = topSample + 1; 926 | } 927 | 928 | let raw_values = []; 929 | if (leftSample >= in_width || rightSample < 0 || bottomSample < 0 || topSample >= in_height) { 930 | raw_values = new Array(read_bands.length).fill(primary_in_no_data); 931 | } else { 932 | // clamp edges to prevent clipping outside bounds 933 | leftSample = Math.max(0, leftSample); 934 | rightSample = Math.min(rightSample, in_width); 935 | topSample = Math.max(0, topSample); 936 | bottomSample = Math.min(bottomSample, in_height); 937 | 938 | for (let i = 0; i < read_bands.length; i++) { 939 | const read_band = read_bands[i]; 940 | const { data: values } = xdim.clip({ 941 | data: in_data, 942 | flat: true, 943 | layout: in_layout, 944 | sizes: in_sizes, 945 | rect: { 946 | band: [read_band, read_band], 947 | row: [topSample, Math.max(topSample, bottomSample - 1)], 948 | column: [leftSample, Math.max(leftSample, rightSample - 1)] 949 | } 950 | }); 951 | const valid_values = values.filter(v => typeof v === "number" && v === v && in_no_data.indexOf(v) === -1); 952 | if (valid_values.length === 0) { 953 | raw_values.push(primary_in_no_data); 954 | } else { 955 | raw_values.push(calc(valid_values)); 956 | } 957 | } 958 | } 959 | 960 | if (should_skip(raw_values)) continue; 961 | const pixel = process({ pixel: raw_values }); 962 | if (pixel !== null || insert_null_strategy === "insert") { 963 | insert_sample({ row: r, column: c, pixel, raw: raw_values }); 964 | } 965 | } 966 | } 967 | } 968 | } 969 | 970 | const generate_result = () => { 971 | if (debug_level >= 1) console.log("[geowarp] took " + (performance.now() - start_time).toFixed(0) + "ms"); 972 | return { 973 | data: out_data, 974 | out_bands, 975 | out_height, 976 | out_layout, 977 | out_pixel_depth, 978 | out_pixel_height, 979 | out_pixel_width, 980 | out_sample_height, 981 | out_sample_width, 982 | out_width, 983 | read_bands 984 | }; 985 | }; 986 | 987 | if (pending > 0) { 988 | // async return 989 | return new Promise(resolve => { 990 | const ms = 5; // re-check every 5 milliseconds 991 | const intervalId = setInterval(() => { 992 | if (pending === 0) { 993 | clearInterval(intervalId); 994 | resolve(generate_result()); 995 | } 996 | }, ms); 997 | }); 998 | } else { 999 | // sync return 1000 | return generate_result(); 1001 | } 1002 | }; 1003 | 1004 | if (typeof module === "object") { 1005 | module.exports = geowarp; 1006 | module.exports.default = geowarp; 1007 | } 1008 | if (typeof window === "object") window.geowarp = geowarp; 1009 | if (typeof self === "object") self.geowarp = geowarp; 1010 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geowarp", 3 | "version": "1.26.2", 4 | "description": "Super Low-Level Raster Reprojection and Resampling Library", 5 | "main": "./geowarp.js", 6 | "types": "geowarp.d.ts", 7 | "files": [ 8 | "geowarp.js", 9 | "geowarp.d.ts" 10 | ], 11 | "scripts": { 12 | "clean": "rm -fr ./test-data/*.png && rm -fr ./test-output/*.png", 13 | "format": "npm run lint -- --fix && prettier --arrow-parens=avoid --print-width=160 --trailing-comma=none --write *.js *.ts", 14 | "lint": "eslint *.js", 15 | "perf": "TEST_NAME=*perf* LOG_SKIP=false TEST_TIMED=true node test.js", 16 | "prepublish": "npm run lint", 17 | "setup": "cd ./test-data && ./setup.sh", 18 | "test": "npm run clean && npm run test:js && npm run test:ts", 19 | "test:js": "LOG_SKIP=false TEST_TIMED=true node test.js", 20 | "test:ts": "LOG_SKIP=false TEST_TIMED=true npx ts-node ./test.ts", 21 | "test:tsc": "npx tsc --moduleResolution node --noEmit --noImplicitAny --skipLibCheck --target es2020 ./test.ts" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/DanielJDufour/geowarp.git" 26 | }, 27 | "keywords": [ 28 | "geo", 29 | "gis", 30 | "image", 31 | "map", 32 | "maps", 33 | "proj", 34 | "proj4", 35 | "projection", 36 | "reprojection", 37 | "resample", 38 | "sample", 39 | "warp" 40 | ], 41 | "author": "Daniel J. Dufour", 42 | "license": "CC0-1.0", 43 | "bugs": { 44 | "url": "https://github.com/DanielJDufour/geowarp/issues" 45 | }, 46 | "homepage": "https://github.com/DanielJDufour/geowarp#readme", 47 | "devDependencies": { 48 | "@mapbox/tilebelt": "^1.0.2", 49 | "@types/node": "^20.11.0", 50 | "eslint": "^8.56.0", 51 | "fast-counter": "^0.1.0", 52 | "find-and-read": "^1.2.0", 53 | "flug": "^2.7.2", 54 | "geotiff": "1.0.9", 55 | "geotiff-palette": "^0.1.0", 56 | "geotiff-precise-bbox": "^0.2.0", 57 | "geotiff-read-bbox": "^2.2.0", 58 | "pngjs": "^7.0.0", 59 | "prettier": "^3.2.2", 60 | "proj4-fully-loaded": "^0.2.0", 61 | "typescript": "^5.3.3", 62 | "write-image": "^0.2.0" 63 | }, 64 | "dependencies": { 65 | "bbox-fns": "^0.20.2", 66 | "calc-image-stats": "^0.9.0", 67 | "dufour-peyton-intersection": "^0.2.0", 68 | "fast-max": "^0.5.1", 69 | "fast-min": "^0.4.0", 70 | "geoaffine": "^0.2.0", 71 | "get-depth": "^0.0.3", 72 | "mediana": "^2.0.0", 73 | "proj-turbo": "^0.0.1", 74 | "quick-resolve": "^0.0.1", 75 | "reproject-bbox": "^0.13.1", 76 | "reproject-geojson": "^0.5.0", 77 | "segflip": "^0.0.2", 78 | "typed-array-ranges": "^0.0.0", 79 | "xdim": "^1.10.1" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test-data/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # download from https://github.com/GeoTIFF/test-data/ 4 | wget https://github.com/GeoTIFF/test-data/archive/refs/heads/main.zip -O geotiff-test-data.zip 5 | unzip -j -o geotiff-test-data.zip "test-data-*/files/*" -d . 6 | rm geotiff-test-data.zip 7 | 8 | wget https://geowarp.s3.amazonaws.com/SkySat_Freeport_s03_20170831T162740Z3.tif 9 | 10 | wget https://raw.githubusercontent.com/GeoTIFF/georaster-layer-for-leaflet-example/master/example_4326.tif 11 | 12 | wget https://georaster-layer-for-leaflet.s3.amazonaws.com/check.tif 13 | -------------------------------------------------------------------------------- /test-data/sri-lanka-hi-res.geojson: -------------------------------------------------------------------------------- 1 | {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"scalerank":0,"featurecla":"Admin-0 country","labelrank":3,"sovereignt":"Sri Lanka","sov_a3":"LKA","adm0_dif":0,"level":2,"type":"Sovereign country","admin":"Sri Lanka","adm0_a3":"LKA","geou_dif":0,"geounit":"Sri Lanka","gu_a3":"LKA","su_dif":0,"subunit":"Sri Lanka","su_a3":"LKA","brk_diff":0,"name":"Sri Lanka","name_long":"Sri Lanka","brk_a3":"LKA","brk_name":"Sri Lanka","brk_group":null,"abbrev":"Sri L.","postal":"LK","formal_en":"Democratic Socialist Republic of Sri Lanka","formal_fr":null,"note_adm0":null,"note_brk":null,"name_sort":"Sri Lanka","name_alt":null,"mapcolor7":3,"mapcolor8":5,"mapcolor9":4,"mapcolor13":9,"pop_est":21324791,"gdp_md_est":91870,"pop_year":-99,"lastcensus":2001,"gdp_year":-99,"economy":"6. Developing region","income_grp":"4. Lower middle income","wikipedia":-99,"fips_10_":"CE","iso_a2":"LK","iso_a3":"LKA","iso_n3":"144","un_a3":"144","wb_a2":"LK","wb_a3":"LKA","woe_id":23424778,"woe_id_eh":23424778,"woe_note":"Exact WOE match as country","adm0_a3_is":"LKA","adm0_a3_us":"LKA","adm0_a3_un":-99,"adm0_a3_wb":-99,"continent":"Asia","region_un":"Asia","subregion":"Southern Asia","region_wb":"South Asia","name_len":9,"long_len":9,"abbrev_len":6,"tiny":-99,"homepart":1,"filename":"LKA.geojson"},"geometry":{"type":"MultiPolygon","coordinates":[[[[81.82260175900008,7.491359768000152],[81.80779056100008,7.479193427000084],[81.79371178500011,7.490220445000118],[81.7893986340001,7.494737046000168],[81.78736412900015,7.496812242000189],[81.78671308700021,7.501776434000148],[81.7859806650001,7.506699937000107],[81.7859806650001,7.506740627000099],[81.78614342500006,7.508775132000125],[81.79029381600016,7.571722723000065],[81.78736412900015,7.593451239000131],[81.77637780000012,7.602728583000086],[81.77222741000016,7.611639716000127],[81.73878014400012,7.660467841000127],[81.73707116000011,7.670965887000094],[81.7331649100001,7.679103908000116],[81.7283634770001,7.689113674000096],[81.72510826900006,7.698919989000132],[81.72559655000006,7.709295966000126],[81.72868899800008,7.718491929000095],[81.72999108200023,7.728176174000154],[81.72999108200023,7.728216864000145],[81.72510826900006,7.739813544000157],[81.72510826900006,7.739935614000131],[81.73796634200019,7.727484442000119],[81.75025475400017,7.698635158000101],[81.75928795700005,7.685288804000081],[81.76148522200006,7.679510809000121],[81.770762566,7.654689846000153],[81.79509524800008,7.61627838700015],[81.80632571700019,7.598578192000147],[81.82325280000006,7.511379299000126],[81.82325280000006,7.511297919000142],[81.82260175900008,7.491359768000152]]],[[[79.88233483200023,9.053778387000179],[79.91651451900012,9.020982164000158],[79.91643313900012,9.021022854000151],[79.900075717,9.024400132000082],[79.8712671230002,9.038560289000188],[79.8543400400001,9.041489976000122],[79.88030032600008,9.012193101000108],[79.9007267590001,8.989081122000144],[79.9096785820001,8.973211981000118],[79.88575280000006,8.976223049000126],[79.87012780000006,8.98818594000015],[79.85938561300006,9.001206773000149],[79.851328972,9.007391669000114],[79.84099368600019,9.012600002000113],[79.79981530000005,9.0489769550001],[79.77938886800004,9.058010158000101],[79.75896243600019,9.067043361000117],[79.74398847700016,9.071071682000166],[79.73601321700019,9.073228257000082],[79.71412194100017,9.075628973000121],[79.70297285200016,9.079982815000122],[79.69800866000011,9.089585679000095],[79.70118248800006,9.099188544000171],[79.71412194100017,9.103583075000145],[79.75879967500012,9.103583075000145],[79.77857506600006,9.098537502000127],[79.79900149800012,9.093329169000128],[79.84164472700016,9.07721588700015],[79.88233483200023,9.053778387000179]]],[[[79.71469160200016,9.485052802000084],[79.68946373800006,9.483587958000115],[79.66602623800011,9.491278387000136],[79.65577233200005,9.50389232000012],[79.65577233200005,9.544867255000128],[79.65870201900012,9.555365302000096],[79.66529381600017,9.554022528000116],[79.67212975400017,9.545599677000068],[79.67628014400012,9.53497955900012],[79.69353274800002,9.529608466000155],[79.71412194100017,9.528062242000118],[79.71924889400006,9.524359442000074],[79.7308048840001,9.500189520000077],[79.7308048840001,9.500148830000086],[79.72282962300008,9.492621161000116],[79.71469160200016,9.485052802000084]]],[[[79.90284264400012,9.678941148000192],[79.9070744150001,9.677883205000143],[79.92644290500002,9.678941148000192],[79.92969811300006,9.67597077000009],[79.93116295700023,9.660956122000158],[79.93360436300004,9.654730536000102],[79.94499759200002,9.647609768000152],[79.96045983200017,9.641058661000073],[79.97339928500017,9.633775132000068],[79.9744572270001,9.631496486000088],[79.97787519600008,9.624335028000132],[79.97095787900011,9.617254950000174],[79.95777428500016,9.61717357000019],[79.94361412900017,9.621527411000088],[79.93360436300004,9.627752997000144],[79.92652428500011,9.629461981000148],[79.9085392590001,9.618963934000178],[79.89592532600008,9.617499091000127],[79.8958439460001,9.617499091000127],[79.88038170700011,9.628607489000144],[79.86491946700008,9.651516018000152],[79.8406681650001,9.699408270000076],[79.85173587300014,9.716538804000109],[79.85425866000011,9.73847077000012],[79.86052493600017,9.756252346000108],[79.88233483200023,9.760891018000137],[79.89389082100016,9.757432359000134],[79.89405358200021,9.757391669000143],[79.89503014400012,9.748968817000103],[79.89112389400012,9.737209377000113],[79.88843834700006,9.723618882000068],[79.88884524800002,9.709906317000133],[79.89031009200019,9.69896067900008],[79.89039147200018,9.698309637000122],[79.89128665500007,9.69611237200013],[79.89470462300008,9.68817780200007],[79.89926191500008,9.683010158000158],[79.90284264400012,9.678941148000192]]],[[[80.46827233200023,9.581366278000175],[80.74439537900011,9.359686591000084],[80.7454533210001,9.357896226000108],[80.75245201900006,9.34625885600009],[80.76368248800006,9.332424221000096],[80.78516686300011,9.312404690000122],[80.79883873800006,9.294867255000085],[80.78671308700004,9.288560289000145],[80.78663170700004,9.28851959800015],[80.77979576900006,9.291571356000148],[80.77898196700002,9.291937567000076],[80.77881920700005,9.291978257000068],[80.76832116000011,9.305405992000146],[80.76254316500015,9.30833567900008],[80.76246178500016,9.30841705900015],[80.74439537900011,9.311346747000172],[80.73650149800008,9.308986721000124],[80.74040774800008,9.301011460000069],[80.74170983200017,9.298488674000138],[80.79363040500007,9.250555731000134],[80.81389407600014,9.239528713000183],[80.81397545700023,9.239488023000192],[80.80591881600012,9.251898505000128],[80.80103600400011,9.263210354000108],[80.80103600400011,9.2632510440001],[80.80355879000015,9.271633205000157],[80.81739342500005,9.274847723000121],[80.823903842,9.270493882000125],[80.83448326900012,9.239488023000192],[80.84416751400013,9.220404364000117],[80.88217207100016,9.145209052000126],[80.88217207100016,9.145168361000131],[80.87452233200023,9.147975979000178],[80.8533634770001,9.155747789000172],[80.84009850400011,9.155991929000123],[80.83448326900012,9.141750393000123],[80.84229576900006,9.13812897300015],[80.85857181100013,9.13548411700016],[80.87305748800006,9.12645091400016],[80.87322024800014,9.125230210000138],[80.87533613400015,9.103583075000145],[80.87549889400012,9.103583075000145],[80.88217207100016,9.103583075000145],[80.88550866000017,9.109320380000128],[80.8885197270001,9.111802476000065],[80.8919376960001,9.113430080000086],[80.89649498800006,9.116603908000144],[80.89649498800006,9.11652252800016],[80.90162194100006,9.096665757000068],[80.9021916020001,9.09418366100013],[80.91635175900015,9.062160549000138],[80.93531334700006,9.033433335000097],[80.95460045700011,9.020982164000158],[80.95606530000006,9.019802151000135],[80.9583439460001,9.017889716000083],[80.95826256600006,9.010891018000109],[80.95362389400012,9.003851630000128],[80.94434655000006,9.000555731000105],[80.94166100400017,9.003485419000114],[80.93067467500012,9.020982164000158],[80.91871178500011,9.027248440000122],[80.89128665500007,9.034247137000108],[80.87533613400015,9.041489976000122],[80.88266035200014,9.031398830000157],[80.8867293630001,9.025783596000153],[80.92286217500006,8.992254950000117],[80.92318769600014,8.973211981000118],[80.93506920700011,8.964911200000145],[80.93498782600014,8.964829820000162],[80.92432701900012,8.955023505000128],[80.9148869150001,8.943915106000105],[80.91496829500007,8.943874416000114],[80.93067467500012,8.931626695000148],[80.9476017590001,8.932928778000145],[80.96143639400012,8.945746161000088],[80.97169030000005,8.96356842700007],[80.9783634770001,8.980047919000143],[80.97852623800006,8.97996653900016],[81.01465905000012,8.936712958000072],[81.04672285200016,8.882147528000104],[81.05982506600006,8.86863841400013],[81.07813561300011,8.863348700000145],[81.09278405000012,8.85162995000016],[81.11426842500006,8.799994208000115],[81.1197208990001,8.794338283000116],[81.12549889400006,8.788275458000129],[81.1255802740002,8.788234768000137],[81.15015709700005,8.77716705900009],[81.1624455090001,8.751166083000115],[81.1765242850001,8.698879299000112],[81.17904707100016,8.708482164000188],[81.18230228000019,8.712225653000118],[81.18775475400011,8.711004950000103],[81.1878361340001,8.71096426000011],[81.19752037900015,8.705715236000131],[81.19459069100006,8.696112372000144],[81.22242272199999,8.66600169500012],[81.23170006600017,8.651068427000084],[81.23161868600019,8.63304271000014],[81.22429446700008,8.62579987200013],[81.21387780000006,8.621039130000128],[81.20435631600017,8.610093492000159],[81.20451907600014,8.610174872000144],[81.2117619150001,8.612005927000112],[81.21924889400006,8.613918361000074],[81.22112063900013,8.614935614000132],[81.22486412900017,8.61692942900008],[81.22494550900015,8.616848049000097],[81.24659264400012,8.570135809000107],[81.24976647200018,8.545803127000141],[81.22494550900015,8.541815497000158],[81.22486412900017,8.541815497000158],[81.22999108200017,8.557928778000132],[81.22331790500019,8.570827541000142],[81.22331790500019,8.570868231000134],[81.21225019600016,8.572699286000116],[81.20435631600017,8.555487372000101],[81.20704186300004,8.542141018000095],[81.21412194100006,8.532660223000079],[81.21802819100006,8.522528387000122],[81.21062259200002,8.507106838000183],[81.21062259200002,8.507066148000192],[81.20476321700018,8.511542059000163],[81.19385826900012,8.519964911000116],[81.1765242850001,8.53058502800016],[81.1590275400001,8.53457265800013],[81.14234459700006,8.527573960000069],[81.12940514400006,8.510321356000148],[81.13396243600019,8.501288153000132],[81.14942467500012,8.503648179000095],[81.1702580090001,8.520697333000143],[81.18458092500006,8.505601304000137],[81.19752037900015,8.487494208000129],[81.21314537900017,8.472398179000123],[81.23511803500017,8.466131903000175],[81.2810978520001,8.467027085000069],[81.29664147200006,8.476792710000112],[81.286387566,8.500230210000083],[81.31267337300014,8.51215241100013],[81.3230900400001,8.515326239000089],[81.33415774800007,8.51455312700007],[81.33423912900011,8.514471747000087],[81.34636478000002,8.507554429000095],[81.35743248800006,8.495835679000109],[81.36573326900006,8.482163804000066],[81.372813347,8.44212474200013],[81.38843834700018,8.390611070000148],[81.38892662900017,8.377183335000069],[81.38941491000017,8.363714911000088],[81.3767195970001,8.37254466400013],[81.36345462300008,8.399074611000103],[81.35181725400017,8.404689846000096],[81.35141035200016,8.397447007000082],[81.35621178500017,8.364976304000095],[81.35661868600006,8.362494208000072],[81.36207116000011,8.350043036000145],[81.36874433700004,8.346909898000163],[81.37484785200016,8.344061591000127],[81.3750106130001,8.344061591000127],[81.38851972700016,8.344061591000127],[81.3992619150001,8.33926015800013],[81.4056095710001,8.301011460000097],[81.41325931100008,8.274481512000122],[81.41456139400005,8.270086981000148],[81.41667728000007,8.2538516300001],[81.41667728000007,8.253770249000127],[81.41976972700016,8.24339427300012],[81.44060306100013,8.173529364000089],[81.44849694100016,8.127997137000122],[81.43034915500019,8.103135484000092],[81.43034915500019,8.103013414000117],[81.42025800900015,8.144232489000075],[81.41879316500015,8.150051174000124],[81.40357506600017,8.191799221000153],[81.39625084700018,8.191799221000153],[81.39503014400006,8.1601016300001],[81.39625084700018,8.150132554000109],[81.39551842500006,8.150458075000131],[81.39478600400011,8.147609768000095],[81.39478600400011,8.147528387000108],[81.39478600400011,8.142564195000148],[81.39625084700018,8.13654205900015],[81.39958743600017,8.13263580900015],[81.40805097700016,8.130072333000143],[81.40813235800013,8.130031643000152],[81.40984134200002,8.126898505000085],[81.41797936300006,8.105943101000136],[81.43555748800011,8.093166408000087],[81.4356388680001,8.093166408000087],[81.45240319100017,8.094549872000158],[81.45826256600006,8.116034247000101],[81.46436608200023,8.116034247000101],[81.47071373800006,8.10455963700015],[81.47242272200006,8.10146719000008],[81.48064212300008,8.065375067000119],[81.48902428500011,8.051499742000132],[81.50017337300002,8.039374091000141],[81.50733483200005,8.027573960000069],[81.5197046230002,7.999335028000103],[81.52458743600019,8.00421784100017],[81.53101647200018,8.009019273000163],[81.53394616000011,8.0136172550001],[81.5383406910001,8.004299221000153],[81.53394616000011,7.999335028000103],[81.53614342500012,7.995754299000126],[81.53809655000006,7.993394273000177],[81.5384220710001,7.992783921000125],[81.53956139400012,7.990790106000175],[81.54021243600008,7.986314195000118],[81.54029381600006,7.986395575000102],[81.5452580090001,7.990464585000068],[81.5555119150001,7.995347398000121],[81.56072024800014,7.999335028000103],[81.56072024800014,7.999253648000121],[81.5630802740001,7.993638414000117],[81.56592858200011,7.986802476000107],[81.5638126960001,7.978705145000077],[81.560313347,7.970648505000141],[81.56047610800007,7.964422919000171],[81.56072024800014,7.958400783000087],[81.5677189460001,7.944973049000083],[81.58008873800006,7.935573635000154],[81.58814537900017,7.929429429000081],[81.59546959700016,7.91742584800015],[81.59083092500006,7.917141018000122],[81.5884708990001,7.915554104000179],[81.58545983200011,7.913519598000064],[81.58358808700021,7.91233958500014],[81.58171634200008,7.911200262000108],[81.57878665500007,7.900295315000121],[81.57585696700014,7.889390367000146],[81.58757571700019,7.855292059000135],[81.59392337300002,7.84804922100011],[81.6082462900001,7.831691799000097],[81.60840905000006,7.831691799000097],[81.62956790500017,7.835516669000114],[81.63640384200019,7.835516669000114],[81.64332116000011,7.814642645000148],[81.6512150400001,7.801743882000125],[81.6536564460001,7.797756252000156],[81.69678795700023,7.752997137000108],[81.71070397199999,7.731146552000069],[81.71314537900017,7.711818752000141],[81.6910099620001,7.705186265000164],[81.69092858200011,7.705145575000174],[81.68067467500006,7.712062893000096],[81.65609785200016,7.743109442000104],[81.64323978000002,7.75413646000014],[81.62663821700008,7.760199286000145],[81.62647545700011,7.760199286000145],[81.6247664720001,7.760036526000177],[81.61296634200002,7.759019273000121],[81.60254967500006,7.748439846000068],[81.59546959700016,7.726263739000104],[81.60743248800011,7.720851955000142],[81.61898847700016,7.719224351000122],[81.62891686300006,7.722886460000082],[81.63640384200019,7.733099677000112],[81.64820397200018,7.728705145000134],[81.65902754000015,7.723334052000084],[81.66277103000019,7.718939520000092],[81.66529381599999,7.715887762000108],[81.66529381599999,7.715847072000117],[81.66374759200008,7.705145575000174],[81.677012566,7.692816473000136],[81.69027754000015,7.680487372000101],[81.70069420700023,7.668605861000145],[81.71159915500019,7.650539455000128],[81.71151777400021,7.650498765000136],[81.70639082100008,7.644598700000103],[81.70508873800011,7.63886139500012],[81.706797722,7.631984768000124],[81.71159915500019,7.622544664000187],[81.71778405000006,7.630072333000071],[81.71404056100002,7.643662828000116],[81.71404056100002,7.643744208000101],[81.72584069099999,7.639593817000146],[81.74903405000006,7.622544664000187],[81.75456790500007,7.614569403000146],[81.77295983200011,7.568019924000126],[81.77295983200011,7.567979234000134],[81.77182050900015,7.5659854190001],[81.76612389400006,7.540676174000139],[81.7669376960001,7.537909247000087],[81.76710045700017,7.537502346000083],[81.77182050900015,7.527818101000122],[81.77295983200011,7.523911851000121],[81.76905358200011,7.517035223000121],[81.76189212300007,7.511542059000177],[81.75757897200016,7.5054385440001],[81.76270592500006,7.49656810100015],[81.76758873800006,7.490179755000128],[81.76929772200006,7.4851748720001],[81.76954186300011,7.484523830000142],[81.77247155000012,7.478827216000155],[81.77979576900012,7.47235748900006],[81.76905358200011,7.464667059000135],[81.76905358200011,7.464626369000142],[81.77003014400006,7.460842190000121],[81.77100670700015,7.456976630000113],[81.77906334700018,7.455308335000112],[81.78711998800011,7.465562242000132],[81.7933048840001,7.465562242000132],[81.79338626400006,7.465521552000141],[81.80103600400011,7.425197658000072],[81.80079186300006,7.410345770000105],[81.80095462300012,7.410345770000105],[81.80779056100008,7.410345770000105],[81.80779056100008,7.410711981000119],[81.80860436300006,7.424709377000084],[81.81104576900012,7.438544012000178],[81.81527754000008,7.450344143000152],[81.82146243600019,7.458726304000109],[81.82146243600019,7.465521552000141],[81.82146243600019,7.465562242000132],[81.82089277400021,7.46710846600017],[81.8186955090001,7.473374742000117],[81.8204858730002,7.476507880000098],[81.8260197270001,7.474188544000128],[81.83497155000005,7.465562242000132],[81.83806399800002,7.457668361000159],[81.84034264400012,7.436468817000076],[81.85572350400015,7.412176825000174],[81.88070722700016,7.327093817000075],[81.87598717500006,7.0919457050001],[81.87842858200011,7.076117255000071],[81.88868248800011,7.040757554000137],[81.89031009200008,7.019924221000152],[81.88461347700016,6.980169989000075],[81.83855228,6.821437893000095],[81.8359481130001,6.805080471000082],[81.83497155000005,6.783433335000097],[81.83155358200023,6.765366929000081],[81.81714928500017,6.737494208000128],[81.81397545700011,6.725043036000102],[81.80827884200002,6.707953192000062],[81.7889103520001,6.67780182500013],[81.77979576900012,6.663641669000113],[81.77979576900012,6.663600979000122],[81.78663170700023,6.639308986000144],[81.78663170700023,6.639268296000154],[81.77564537900017,6.610419012000136],[81.68148847700016,6.462144273000149],[81.65007571700002,6.430853583000085],[81.59205162900017,6.389878648000163],[81.57309004000015,6.384222723000079],[81.54769941500015,6.370672919000114],[81.36207116000011,6.22540924700013],[81.32496178500017,6.205389716000155],[81.20443769600016,6.162746486000131],[81.20435631600017,6.162746486000131],[81.2027287120001,6.177191473000093],[81.20264733200011,6.178045966000099],[81.19548587300002,6.183661200000102],[81.19532311300006,6.183620510000111],[81.19369550900014,6.183213609000105],[81.18767337300008,6.181586005000085],[81.1838485040001,6.173570054000123],[81.18189537900011,6.156724351000122],[81.17644290500007,6.15037669500019],[81.16765384200019,6.148016669000143],[81.15601647200018,6.142889716000127],[81.13428795700011,6.129828192000147],[81.11255944100006,6.120306708000058],[81.008474155,6.09804922100011],[80.95801842500012,6.080796617000188],[80.9026798840001,6.070786851000122],[80.87745201900006,6.061102606000148],[80.86231530000006,6.039862372000171],[80.835215691,6.04515208500014],[80.81853274800008,6.038763739000132],[80.80909264400006,6.035101630000085],[80.77059980600009,6.006740627000141],[80.73210696700019,5.978420315000093],[80.7143660820001,5.957912502000141],[80.67343183700004,5.960313218000081],[80.67335045700005,5.960353908000073],[80.65626061300006,5.957912502000141],[80.6429142590001,5.952866929000108],[80.62916100400011,5.945054429000109],[80.60482832100016,5.926906643000109],[80.59782962300008,5.925279039000188],[80.59083092500006,5.92373281500015],[80.57422936300011,5.930121161000073],[80.55665123800011,5.939154364000088],[80.54656009200014,5.942206122000172],[80.53956139400006,5.94428131700009],[80.48170006600004,5.937445380000085],[80.46517988400015,5.940659898000135],[80.45679772200018,5.948919989000117],[80.45142662900011,5.959784247000101],[80.44719485800013,5.966131903000132],[80.44402103000007,5.970892645000134],[80.44402103000007,5.970933335000126],[80.42709394600014,5.961086330000099],[80.41195722700014,5.959947007000067],[80.38502037900011,5.963812567000076],[80.37859134200019,5.96474844000015],[80.37378991000011,5.966864325000159],[80.3704533210001,5.971584377000084],[80.36622155000006,5.976263739000089],[80.35840905000006,5.978420315000093],[80.3537703790001,5.976792710000068],[80.35010826900006,5.973537502000127],[80.34620201900012,5.970770575000159],[80.34099368600019,5.970933335000126],[80.3392033210001,5.972398179000095],[80.33757571700019,5.973822333000058],[80.33122806100008,5.982814846000082],[80.32813561300006,5.984686591000141],[80.32732181100008,5.985174872000129],[80.32732181100008,5.985256252000113],[80.32024173300007,5.987738348000136],[80.2775171230002,6.002997137000108],[80.2600203790001,6.006577867000175],[80.24756920700011,6.009182033000073],[80.22584069100006,6.026556708000143],[80.22095787900017,6.028916734000106],[80.21135501400006,6.033555406000133],[80.21119225400011,6.033636786000115],[80.20386803500011,6.02619049700013],[80.18523196700002,6.041164455000071],[80.10377037900011,6.127142645000077],[80.09709720100014,6.132350979000164],[80.09701582100016,6.132391669000157],[80.09628339900021,6.133327541000142],[80.09400475400015,6.136053778000118],[80.09278405000006,6.140244859000134],[80.09278405000006,6.140285549000126],[80.09310957100016,6.14874909100017],[80.0905867850001,6.153143622000157],[80.08716881600006,6.157578843000123],[80.07813561300011,6.169338283000116],[80.03199303500011,6.266343492000161],[80.03003991000017,6.275620835000112],[80.03003991000017,6.275702216000098],[80.03028405000012,6.284002997000172],[80.03052819100006,6.292303778000147],[80.03052819100006,6.29238515800013],[80.02881920700023,6.297430731000161],[80.02051842500006,6.314642645000091],[80.0120548840001,6.361314195000176],[79.99366295700005,6.3941104190001],[79.98210696700008,6.441026109000133],[79.97787519600008,6.458197333000072],[79.97575931100012,6.459865627000084],[79.9714461600001,6.46246979400017],[79.96697024800008,6.466376044000157],[79.9642033210001,6.471869208000101],[79.9653426440001,6.477728583000142],[79.9702254570001,6.479152736000117],[79.97535241000011,6.478583075000145],[79.9754337900001,6.478583075000145],[79.97787519600008,6.47870514500012],[79.9702254570001,6.531154690000079],[79.96957441500015,6.535345770000106],[79.94874108200004,6.588568427000097],[79.8630477220001,6.807196356000091],[79.84945722700016,6.882717190000122],[79.84815514400006,6.890204169000171],[79.84815514400006,6.962144273000134],[79.84929446700019,6.964951890000094],[79.8592228520001,6.988674221000095],[79.86182701900006,7.000067450000159],[79.86158287900011,7.011664130000084],[79.86158287900011,7.011867580000128],[79.813731316,7.18073151200015],[79.82032311300011,7.205511786000115],[79.81999759200008,7.190252997000143],[79.82374108200011,7.180650132000068],[79.82911217500006,7.173244533000087],[79.83383222700016,7.164496161000102],[79.84343509200002,7.131008205000157],[79.84815514400006,7.122951565000122],[79.8543400400001,7.122951565000122],[79.8558048840001,7.132513739000102],[79.858653191,7.136542059000162],[79.86117597700016,7.137193101000122],[79.86125735800013,7.137193101000122],[79.86182701900006,7.13662344000015],[79.86182701900006,7.136664130000142],[79.86158287900011,7.167222398000162],[79.86158287900011,7.167425848000121],[79.85938561300006,7.180812893000124],[79.8543400400001,7.191839911000089],[79.84961998800011,7.195054429000137],[79.83643639400006,7.197902736000074],[79.83057701900012,7.201808986000159],[79.82992597700016,7.203192450000146],[79.8263452480002,7.210394598000093],[79.8279728520001,7.218329169000142],[79.83179772200006,7.225734768000138],[79.83383222700016,7.232814846000097],[79.78467858200023,7.589789130000084],[79.77857506600006,7.602728583000086],[79.78272545700011,7.607123114000074],[79.78581790500019,7.607570705000156],[79.79297936300011,7.602728583000086],[79.79460696700002,7.620021877000084],[79.7995711600001,7.640082098000135],[79.8007918630001,7.649684963000126],[79.80201256600006,7.659328518000094],[79.79639733200023,7.674383856000105],[79.78882897200018,7.688177802000112],[79.78736412900011,7.697333075000103],[79.78589928500017,7.706488348000079],[79.78598066500015,7.743353583000143],[79.76295006600016,7.85545482000019],[79.76026451900012,7.868394273000192],[79.74838300900014,7.893011786000115],[79.73829186300006,7.944728908000144],[79.72193444100017,7.985541083000101],[79.69678795700005,8.098293361000103],[79.69678795700005,8.130316473000093],[79.70085696700002,8.128485419000114],[79.70110110800007,8.128200588000169],[79.70142662900011,8.127915757000139],[79.70142662900011,8.126898505000085],[79.70362389400012,8.123480536000073],[79.70720462300008,8.142075914000158],[79.70191491000011,8.20115794500019],[79.69678795700005,8.219061591000155],[79.7132267590001,8.22980377800016],[79.72877037900017,8.249701239000075],[79.74000084700016,8.272365627000113],[79.74447675900015,8.291408596000108],[79.74903405000006,8.303859768000123],[79.77857506600006,8.350043036000145],[79.77824954500014,8.349269924000126],[79.76335696700008,8.3143985050001],[79.74447675900015,8.281154690000093],[79.74862714900009,8.273057359000148],[79.74968509200002,8.271063544000114],[79.75709069100006,8.263739325000117],[79.76677493600017,8.2599144550001],[79.77857506600006,8.259995835000081],[79.7688908210001,8.232814846000082],[79.75879967500012,8.212876695000176],[79.75440514400006,8.209418036000088],[79.749766472,8.2089297550001],[79.74610436300006,8.20612213700015],[79.74447675900015,8.195502020000106],[79.74537194100004,8.171861070000176],[79.74447675900015,8.164496161000088],[79.72754967500006,8.110907294000171],[79.72820071700019,8.100978908000087],[79.72950280000012,8.081122137000165],[79.74699954500008,8.060492255000142],[79.75196373800011,8.054632880000113],[79.73194420700023,8.013128973000107],[79.76384524800008,7.990139065000135],[79.81023196700002,7.986517645000077],[79.83383222700016,8.003078518000137],[79.82935631600012,8.020900783000116],[79.82178795700023,8.034857489000089],[79.81885826900012,8.040187893000152],[79.81527754000015,8.044582424000126],[79.806407097,8.055650132000082],[79.79639733200023,8.061997789000186],[79.7934676440001,8.072821356000103],[79.82715905000012,8.13654205900015],[79.82585696700019,8.148098049000083],[79.821543816,8.156642971000096],[79.81657962300008,8.163804429000137],[79.81348717500006,8.171291408000116],[79.81267337300008,8.183335679000123],[79.8138126960001,8.204087632000125],[79.81348717500006,8.212632554000137],[79.81348717500006,8.212876695000176],[79.81291751400008,8.214504299000112],[79.81023196700002,8.221625067000062],[79.80860436300011,8.223089911000116],[79.80665123800006,8.224920966000099],[79.80469811300011,8.228989976000065],[79.80665123800006,8.24017975500007],[79.81137129000015,8.249660549000083],[79.82447350400011,8.266669012000136],[79.82715905000012,8.277736721000082],[79.82927493600013,8.298651434000135],[79.83863366000016,8.34052155200014],[79.84815514400006,8.43817780200014],[79.85425866000011,8.449286200000174],[79.86085045700011,8.454779364000103],[79.86589603000007,8.46141185100008],[79.86801191500015,8.476019598000093],[79.86801191500015,8.517645575000145],[79.87378991000011,8.538072007000125],[79.88738040500007,8.551214911000088],[79.90349368600008,8.562079169000171],[79.91651451900012,8.57599518400015],[79.91814212300008,8.580918687000107],[79.92807050900014,8.611517645000134],[79.92945397200006,8.612819729000122],[79.93018639400006,8.613511460000069],[79.93238366000016,8.622463283000101],[79.94166100400011,8.642482815000065],[79.95053144600016,8.739813544000143],[79.92644290500002,8.80190664300008],[79.92839603000019,8.812689520000077],[79.93018639400006,8.883490302000098],[79.93018639400006,8.884426174000081],[79.93018639400006,8.884466864000075],[79.92774498800006,8.908107815000093],[79.92481530000012,8.919745184000107],[79.91960696700019,8.928534247000158],[79.91553795700017,8.93854401200015],[79.92481530000012,8.9435082050001],[79.93824303500011,8.945990302000126],[79.94711347700016,8.948675848000107],[80.005137566,9.000555731000105],[80.03541100400011,9.015326239000089],[80.04802493600017,9.026271877000141],[80.05469811300006,9.05060455900012],[80.06666100400017,9.075628973000121],[80.07349694100012,9.110419012000165],[80.10564212300008,9.192857164000188],[80.10678144600008,9.197414455000143],[80.10979251400008,9.209458726000065],[80.11597741000017,9.233832098000107],[80.11597741000017,9.291937567000076],[80.11817467500006,9.300441799000096],[80.11622155000012,9.306179104000165],[80.11622155000012,9.306219794000157],[80.10450280000012,9.30841705900015],[80.07691491000011,9.322658596000153],[80.06617272200018,9.332586981000146],[80.05941816500015,9.351385809000107],[80.05827884200014,9.354803778000118],[80.05347741000011,9.37824127800009],[80.05298912900017,9.391546942000119],[80.06495201900006,9.404038804000123],[80.08708743600008,9.41498444200009],[80.11166425900015,9.422756252000099],[80.13184655000012,9.4257266300001],[80.14087975400011,9.430324611000131],[80.16285241000011,9.459214585000055],[80.16537519600016,9.461086330000114],[80.16920006600006,9.464016018000137],[80.17652428500017,9.468329169000143],[80.18555748800006,9.471665757000082],[80.197032097,9.473456122000158],[80.18751061300011,9.490871486000131],[80.18181399800002,9.498521226000065],[80.17603600400017,9.506170966000084],[80.16130618600019,9.52057526200015],[80.14185631600017,9.53497955900012],[80.06788170700011,9.57802969000015],[80.05298912900017,9.596380927000112],[80.08521569100017,9.59536367400014],[80.11573326900012,9.583238023000149],[80.1759546230002,9.54857005400008],[80.23340905000012,9.534247137000179],[80.24854576900012,9.524644273000106],[80.27222741000011,9.504136460000069],[80.27808678500017,9.493312893000152],[80.27808678500017,9.49327220300016],[80.26587975400017,9.487127997000101],[80.26856530000006,9.480292059000163],[80.27214603,9.473456122000158],[80.26587975400017,9.466701565000122],[80.26596113400015,9.466660874000125],[80.286875847,9.452704169000157],[80.3362736340001,9.464667059000178],[80.4266056650001,9.500189520000077],[80.45093834700006,9.497870184000105],[80.5042423840001,9.483303127000084],[80.56714928500017,9.455226955000086],[80.6145939460001,9.446193752000156],[80.61475670700023,9.446193752000156],[80.5467228520001,9.492417710000069],[80.5232853520001,9.503159898000177],[80.50245201900012,9.50763580900015],[80.49935957100016,9.509059963000112],[80.49634850400011,9.510484117000173],[80.47828209699999,9.524074611000145],[80.47144616000011,9.528062242000118],[80.46461022200018,9.529120184000163],[80.4563908210001,9.528713283000158],[80.44402103000007,9.528062242000118],[80.43620853000007,9.525091864000103],[80.43506920700023,9.5233828800001],[80.43287194100006,9.519761460000138],[80.42896569100006,9.517767645000104],[80.41570071700019,9.527492580000143],[80.41041100400011,9.528387762000136],[80.37794030000006,9.527533270000133],[80.36866295700011,9.530218817000119],[80.29753665500007,9.591253973000093],[80.26701907600014,9.61082591400016],[80.26693769600016,9.61082591400016],[80.25603274800008,9.61558665600016],[80.20460045700017,9.6378848330001],[80.18604576900006,9.648504950000143],[80.17359459700006,9.648667710000112],[80.1759546230002,9.624335028000132],[80.18287194100016,9.61717357000019],[80.19361412900011,9.609076239000075],[80.19996178500011,9.599188544000157],[80.19361412900011,9.586411851000122],[80.18604576900006,9.584133205000143],[80.17872155000006,9.588039455000143],[80.17204837300018,9.59357330900015],[80.15284264400006,9.60346100500007],[80.11548912900017,9.63572825700011],[80.09400475400015,9.644842841000099],[80.09408613400015,9.64476146000011],[80.10499108200011,9.63287995000016],[80.12468509200008,9.616034247000158],[80.13559004000014,9.603827216000083],[80.11133873800006,9.608465887000108],[80.08741295700005,9.618353583000129],[80.03931725400011,9.644842841000099],[80.00098717500006,9.674709377000084],[79.97820071700008,9.682684637000136],[79.9759220710001,9.684068101000122],[79.96729576900006,9.689601955000128],[79.95923912900011,9.692816473000093],[79.9580184250001,9.685736395000134],[79.94703209700018,9.693182684000107],[79.94288170700023,9.702826239000075],[79.94092858200005,9.714341539000117],[79.93702233200011,9.727362372000101],[79.93043053500011,9.73847077000012],[79.92652428500011,9.743516343000152],[79.91627037900017,9.75678131700009],[79.9096785820001,9.768296617000118],[79.93132571700019,9.779038804000137],[79.97071373800011,9.815130927000082],[79.98845462300008,9.822943427000084],[79.98853600400017,9.822943427000084],[80.06666100400017,9.81610748900006],[80.10743248800006,9.820624091000113],[80.12232506600012,9.818915106000103],[80.12818444100006,9.805568752000099],[80.12720787900015,9.78461334800015],[80.13013756600017,9.777329820000132],[80.13868248800011,9.77456289300008],[80.14698326900012,9.773382880000142],[80.17090905000006,9.76727936400006],[80.1759546230002,9.76459381700009],[80.18043053500016,9.755804755000113],[80.19117272200018,9.756170966000127],[80.19963626400013,9.758734442000131],[80.20289147200006,9.759711005000113],[80.20777428500011,9.760402736000145],[80.21119225400011,9.760891018000137],[80.2112736340001,9.760850328000146],[80.21900475400011,9.75637441600017],[80.22234134200008,9.75446198100012],[80.2503361340001,9.724269924000112],[80.33855228000007,9.629217841000113],[80.36963951900006,9.609849351000094],[80.4417423840001,9.576890367000118],[80.44402103000007,9.575873114000146],[80.44402103000007,9.575913804000137],[80.44402103000007,9.583319403000132],[80.40772545700023,9.601507880000128],[80.31983483200011,9.672756252000127],[80.24781334700006,9.755072333000086],[80.21119225400011,9.774522203000089],[80.21119225400011,9.77456289300008],[80.15381920700011,9.787990627000156],[80.13868248800011,9.79873281500008],[80.13672936300006,9.813910223000079],[80.1577254570001,9.822943427000084],[80.16456139400012,9.824082749000112],[80.18539472700009,9.827541408000116],[80.22681725400011,9.829575914000145],[80.24675540500019,9.825873114000103],[80.2605900400001,9.816229559000135],[80.26978600400017,9.783514716000097],[80.27898196700008,9.769842841000155],[80.40992272200006,9.617499091000127],[80.42920983200005,9.60346100500007],[80.46827233200023,9.581366278000175]]]]}}]} -------------------------------------------------------------------------------- /test-data/sri-lanka.geojson: -------------------------------------------------------------------------------- 1 | {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"scalerank":1,"featurecla":"Admin-0 country","labelrank":3,"sovereignt":"Sri Lanka","sov_a3":"LKA","adm0_dif":0,"level":2,"type":"Sovereign country","admin":"Sri Lanka","adm0_a3":"LKA","geou_dif":0,"geounit":"Sri Lanka","gu_a3":"LKA","su_dif":0,"subunit":"Sri Lanka","su_a3":"LKA","brk_diff":0,"name":"Sri Lanka","name_long":"Sri Lanka","brk_a3":"LKA","brk_name":"Sri Lanka","brk_group":null,"abbrev":"Sri L.","postal":"LK","formal_en":"Democratic Socialist Republic of Sri Lanka","formal_fr":null,"note_adm0":null,"note_brk":null,"name_sort":"Sri Lanka","name_alt":null,"mapcolor7":3,"mapcolor8":5,"mapcolor9":4,"mapcolor13":9,"pop_est":21324791,"gdp_md_est":91870,"pop_year":-99,"lastcensus":2001,"gdp_year":-99,"economy":"6. Developing region","income_grp":"4. Lower middle income","wikipedia":-99,"fips_10":null,"iso_a2":"LK","iso_a3":"LKA","iso_n3":"144","un_a3":"144","wb_a2":"LK","wb_a3":"LKA","woe_id":-99,"adm0_a3_is":"LKA","adm0_a3_us":"LKA","adm0_a3_un":-99,"adm0_a3_wb":-99,"continent":"Asia","region_un":"Asia","subregion":"Southern Asia","region_wb":"South Asia","name_len":9,"long_len":9,"abbrev_len":6,"tiny":-99,"homepart":1,"filename":"LKA.geojson"},"geometry":{"type":"Polygon","coordinates":[[[81.7879590188914,7.523055324733164],[81.63732221876059,6.481775214051921],[81.21801964714433,6.197141424988288],[80.34835696810441,5.968369859232155],[79.87246870312853,6.76346344647493],[79.69516686393513,8.200843410673386],[80.14780073437964,9.824077663609557],[80.83881798698656,9.268426825391188],[81.30431928907177,8.56420624433369],[81.7879590188914,7.523055324733164]]]}}]} -------------------------------------------------------------------------------- /test-output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielJDufour/geowarp/ab71dfb838918170d47dc69e2cfdb55f332d93db/test-output/.gitkeep -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const findAndRead = require("find-and-read"); 3 | const path = require("path"); 4 | 5 | const count = require("fast-counter"); 6 | const test = require("flug"); 7 | const GeoTIFF = require("geotiff"); 8 | const readBoundingBox = require("geotiff-read-bbox"); 9 | const proj4 = require("proj4-fully-loaded"); 10 | const reprojectBoundingBox = require("reproject-bbox"); 11 | const tilebelt = require("@mapbox/tilebelt"); 12 | const { getPalette } = require("geotiff-palette"); 13 | const getPreciseBoundingBox = require("geotiff-precise-bbox"); 14 | const xdim = require("xdim"); 15 | const writeImage = require("write-image"); 16 | 17 | const geowarp = require("./geowarp"); 18 | 19 | const range = ct => new Array(ct).fill(0).map((_, i) => i); 20 | 21 | const exit = process.exit; 22 | 23 | const writePNGSync = ({ h, w, data, filepath }) => { 24 | const { data: buf } = writeImage({ data, height: h, format: "PNG", width: w }); 25 | fs.writeFileSync(`${filepath}.png`, buf); 26 | }; 27 | 28 | const getBoundingBox = image => getPreciseBoundingBox(image).map(n => Number(n)); 29 | 30 | ["vectorize", "near", "median", "bilinear"].forEach(method => { 31 | ["inside", "outside"].forEach(cutline_strategy => { 32 | test("cutline " + cutline_strategy + " " + method, async ({ eq }) => { 33 | // console.log("starting:", "cutline " + cutline_strategy + " " + method); 34 | const cutline = JSON.parse(findAndRead("sri-lanka-hi-res.geojson", { encoding: "utf-8" })); 35 | const filename = "gadas.tif"; 36 | const filepath = path.resolve(__dirname, "./test-data", filename); 37 | const geotiff = await GeoTIFF.fromFile(filepath); 38 | const image = await geotiff.getImage(0); 39 | const rasters = await image.readRasters(); 40 | const in_bbox = getBoundingBox(image); 41 | const height = image.getHeight(); 42 | const width = image.getWidth(); 43 | // ProjectedCSTypeGeoKey says 32767, but PCSCitationGeoKey says ESRI PE String = 3857.esriwkt 44 | const in_srs = 3857; 45 | const out_srs = "EPSG:5234"; // Kandawala / Sri Lanka Grid 46 | const { forward, inverse } = proj4("EPSG:" + in_srs, out_srs); 47 | 48 | const { data } = geowarp({ 49 | debug_level: 0, 50 | in_bbox, 51 | in_data: rasters, 52 | in_layout: "[band][row,column]", 53 | in_srs, 54 | in_height: height, 55 | in_width: width, 56 | out_array_types: ["Array", "Array", "Uint8ClampedArray"], 57 | out_height: height, 58 | out_width: width, 59 | out_layout: "[band][row][column]", 60 | out_srs, 61 | forward, 62 | inverse, 63 | cutline, 64 | cutline_srs: 4326, 65 | cutline_forward: proj4("EPSG:4326", out_srs).forward, 66 | cutline_strategy, 67 | method 68 | }); 69 | 70 | if (process.env.WRITE) { 71 | writePNGSync({ h: height, w: width, data, filepath: `./test-output/gadas-cutline-${cutline_strategy}-${method}` }); 72 | } 73 | eq(data.length, 4); // check band count 74 | eq(data[0][0].constructor.name, "Uint8ClampedArray"); 75 | }); 76 | }); 77 | }); 78 | 79 | test("reproject without clipping", async ({ eq }) => { 80 | const filename = "wildfires.tiff"; 81 | const filepath = path.resolve(__dirname, "./test-data", filename); 82 | const geotiff = await GeoTIFF.fromFile(filepath); 83 | const image = await geotiff.getImage(0); 84 | const geoKeys = image.getGeoKeys(); 85 | const { GeographicTypeGeoKey, ProjectedCSTypeGeoKey } = geoKeys; 86 | const rasters = await image.readRasters(); 87 | const height = image.getHeight(); 88 | const width = image.getWidth(); 89 | const in_srs = ProjectedCSTypeGeoKey || GeographicTypeGeoKey; 90 | const [xmin, ymax] = image.getOrigin(); 91 | const [resolutionX, resolutionY] = image.getResolution(); 92 | const ymin = ymax - height * Math.abs(resolutionY); 93 | const xmax = xmin + width * Math.abs(resolutionX); 94 | const in_bbox = [xmin, ymin, xmax, ymax]; 95 | const out_srs = "EPSG:26910"; // NAD83 / UTM zone 10N 96 | const { forward, inverse } = proj4("EPSG:" + in_srs, out_srs); 97 | const { data } = geowarp({ 98 | in_bbox, 99 | in_data: rasters, 100 | in_layout: "[band][row,column]", 101 | in_srs, 102 | in_height: height, 103 | in_width: width, 104 | out_height: height, 105 | out_width: width, 106 | out_layout: "[band][row][column]", 107 | out_srs, 108 | forward, 109 | inverse, 110 | method: "median", 111 | round: true 112 | }); 113 | 114 | if (process.env.WRITE) { 115 | writePNGSync({ h: height, w: width, data, filepath: "./test-output/reproject-without-clipping.tif" }); 116 | } 117 | eq(data.length, 3); // check band count 118 | }); 119 | 120 | test("bug: reprojecting to EPSG:26910", async ({ eq }) => { 121 | const filename = "wildfires.tiff"; 122 | const filepath = path.resolve(__dirname, "./test-data", filename); 123 | const geotiff = await GeoTIFF.fromFile(filepath); 124 | const image = await geotiff.getImage(0); 125 | const geoKeys = image.getGeoKeys(); 126 | const { GeographicTypeGeoKey, ProjectedCSTypeGeoKey } = geoKeys; 127 | const rasters = await image.readRasters(); 128 | const height = image.getHeight(); 129 | const width = image.getWidth(); 130 | const in_srs = ProjectedCSTypeGeoKey || GeographicTypeGeoKey; 131 | const [xmin, ymax] = image.getOrigin(); 132 | const [resolutionX, resolutionY] = image.getResolution(); 133 | const ymin = ymax - height * Math.abs(resolutionY); 134 | const xmax = xmin + width * Math.abs(resolutionX); 135 | const in_bbox = [xmin, ymin, xmax, ymax]; 136 | const out_srs = 26910; // NAD83 / UTM zone 10N 137 | const factor = 0.05; 138 | let out_bbox = reprojectBoundingBox({ bbox: [xmin, ymin, xmax, ymax], from: in_srs, to: out_srs }); 139 | // change out_bbox to top left quarter 140 | out_bbox = [ 141 | out_bbox[0], 142 | Math.round(out_bbox[3] - (out_bbox[3] - out_bbox[1]) * factor), 143 | Math.round(out_bbox[0] + (out_bbox[2] - out_bbox[0]) * factor), 144 | out_bbox[3] 145 | ]; 146 | const { inverse } = proj4("EPSG:" + in_srs, "EPSG:" + out_srs); 147 | const { data } = geowarp({ 148 | in_bbox, 149 | in_data: rasters, 150 | in_layout: "[band][row,column]", 151 | in_srs, 152 | in_height: height, 153 | in_width: width, 154 | out_bbox, 155 | out_height: height, 156 | out_width: width, 157 | out_layout: "[band][row][column]", 158 | out_srs, 159 | inverse 160 | }); 161 | 162 | if (process.env.WRITE) { 163 | const filepath = "./test-output/wildfires-26910"; 164 | writePNGSync({ h: height, w: width, data, filepath }); 165 | console.log("wrote:", filepath); 166 | } 167 | eq(data.length, 3); // check band count 168 | }); 169 | 170 | const tileCache = {}; 171 | 172 | const readTile = async ({ x, y, z, filename }) => { 173 | const key = JSON.stringify({ x, y, z, filename }); 174 | if (!tileCache[key]) { 175 | const filepath = path.resolve(__dirname, "./test-data", filename); 176 | const bbox4326 = tilebelt.tileToBBOX([x, y, z]); 177 | const bbox3857 = reprojectBoundingBox({ bbox: bbox4326, from: 4326, to: 3857 }); 178 | const geotiff = await GeoTIFF.fromFile(filepath); 179 | const { data, read_bbox, height, width, srs_of_geotiff } = await readBoundingBox({ 180 | bbox: bbox3857, 181 | geotiff, 182 | srs: 3857 183 | }); 184 | tileCache[key] = { 185 | data, 186 | depth: data.length, // num bands 187 | geotiff_srs: srs_of_geotiff, 188 | height, 189 | layout: "[band][row,column]", 190 | tile_bbox: bbox3857, 191 | geotiff_bbox: read_bbox, 192 | width 193 | }; 194 | } 195 | return tileCache[key]; 196 | }; 197 | 198 | const runTileTests = async ({ 199 | x, 200 | y, 201 | z, 202 | filename, 203 | methods, 204 | out_bands_array, 205 | out_layouts = ["[row][column][band]", "[band][row][column]", "[band][row,column]"], 206 | sizes = [64, 256, 512], 207 | most_common_pixels, 208 | turbos = [false, true] 209 | }) => { 210 | try { 211 | let readTilePromise; 212 | sizes.forEach(size => { 213 | methods.forEach(method => { 214 | out_layouts.forEach(out_layout => { 215 | out_bands_array.forEach(out_bands => { 216 | turbos.forEach(turbo => { 217 | const testName = `${filename.split(".")[0]}-${method}-${size}-${out_layout}-${out_bands}${turbo ? "-turbo" : ""}`; 218 | test(testName, async ({ eq }) => { 219 | if (!readTilePromise) readTilePromise = readTile({ x, y, z, filename }); 220 | 221 | const info = await readTilePromise; 222 | // console.log("info got", info); 223 | 224 | const in_srs = info.geotiff_srs; 225 | 226 | const { forward, inverse } = proj4("EPSG:" + in_srs, "EPSG:" + 3857); 227 | 228 | const result = geowarp({ 229 | debug_level: 0, 230 | forward, 231 | inverse, 232 | 233 | // regarding input data 234 | in_data: info.data, 235 | in_bbox: info.geotiff_bbox, 236 | in_layout: info.layout, 237 | in_srs: info.geotiff_srs, 238 | in_width: info.width, 239 | in_height: info.height, 240 | 241 | // regarding location to paint 242 | out_bands, 243 | out_bbox: info.tile_bbox, 244 | out_layout, 245 | out_srs: 3857, 246 | out_height: size, 247 | out_width: size, 248 | method: method === "first" ? ({ values }) => values[0] : method, 249 | round: true, 250 | turbo 251 | }); 252 | 253 | if (process.env.WRITE) { 254 | writePNGSync({ h: size, w: size, data: result.data, filepath: `./test-output/${testName}` }); 255 | } 256 | 257 | eq(result.read_bands, out_bands || range(info.depth)); 258 | 259 | let counts; 260 | if (out_layout === "[row][column][band]") { 261 | eq(result.data.length, size); 262 | eq(result.data[0].length, size); 263 | eq(result.data[0][0].length, out_bands?.length ?? 3); 264 | counts = count(result.data, { depth: 2 }); 265 | const sortedCounts = Object.entries(counts).sort((a, b) => Math.sign(b[1] - a[1])); 266 | const top = sortedCounts[0][0]; 267 | if (!["first", "min", "max"].includes(method) && !out_bands) { 268 | try { 269 | eq(most_common_pixels.includes(top), true); 270 | } catch (error) { 271 | console.dir(result.data, { depth: 5, maxArrayLength: 5 }); 272 | console.log("sortedCounts:", sortedCounts.slice(0, 5), "..."); 273 | console.error(top); 274 | throw error; 275 | } 276 | } 277 | } else if (out_layout === "[band][row][column]") { 278 | eq(result.data.length, out_bands?.length ?? 3); 279 | eq(result.data[0].length, size); 280 | eq(result.data[0][0].length, size); 281 | } else if (out_layout === "[band][row,column]") { 282 | eq(result.data.length, out_bands?.length ?? 3); 283 | eq( 284 | result.data.every(b => b.length === size * size), 285 | true 286 | ); 287 | counts = count(result.data, { depth: 1 }); 288 | } else if (out_layout === "[row,column,band]") { 289 | eq(result.data.length, 3 * size * size); 290 | eq( 291 | result.data.every(n => typeof n === "number"), 292 | true 293 | ); 294 | } 295 | }); 296 | }); 297 | }); 298 | }); 299 | }); 300 | }); 301 | } catch (error) { 302 | console.error(error); 303 | exit(); 304 | } 305 | }; 306 | 307 | [ 308 | { 309 | x: 40, 310 | y: 96, 311 | z: 8, 312 | sizes: [64, 256, 512], 313 | filename: "wildfires.tiff", 314 | methods: ["first", "bilinear", "near", "max", "mean", "median", "min", "mode", "mode-mean", "mode-max", "mode-min"], 315 | out_bands_array: [undefined, [0], [2, 1, 0]], 316 | most_common_pixels: [ 317 | "0,0,0", 318 | "11,16,7", 319 | "11,16,8", 320 | "15,23,10", 321 | "16,24,11", 322 | "17,25,12", 323 | "17,25,14", 324 | "18,26,11", 325 | "18,26,12", 326 | "19,27,12", 327 | "20,28,13", 328 | "21,29,14", 329 | "13,18,9", 330 | "19,25,13", 331 | "22,30,17", 332 | "23,31,18", 333 | "33,43,34" 334 | ] 335 | }, 336 | { 337 | x: 3853, 338 | y: 6815, 339 | z: 14, 340 | sizes: [64, 256, 512], 341 | filename: "SkySat_Freeport_s03_20170831T162740Z3.tif", 342 | methods: ["first", "bilinear", "near", "max", "mean", "median", "min", "mode", "mode-mean", "mode-max", "mode-min"], 343 | out_bands_array: [undefined, [0], [2, 1, 0]], 344 | most_common_pixels: [ 345 | "104,89,75", 346 | "105,88,75", 347 | "105,90,76", 348 | "106,89,75", 349 | "106,90,77", 350 | "107,90,76", 351 | "107,90,77", 352 | "107,91,79", 353 | "107,92,79", 354 | "108,91,77", 355 | "121,110,99", 356 | "128,124,122", 357 | "132,127,125", 358 | "136,130,128", 359 | "136,133,139", 360 | "139,132,128", 361 | "140,133,129", 362 | "141,134,131", 363 | "142,135,131", 364 | "142,135,132", 365 | "142,136,132", 366 | "143,136,133", 367 | "143,137,132", 368 | "146,133,139", 369 | "146,140,135", 370 | "146,140,137", 371 | "146,141,139", 372 | "147,133,139", 373 | "147,140,136", 374 | "147,140,137", 375 | "147,141,137", 376 | "147,141,139", 377 | "150,144,142", 378 | "152,146,142", 379 | "152,146,143", 380 | "153,133,143", 381 | "154,147,144", 382 | "157,133,139", 383 | "157,152,150", 384 | "208,205,204", 385 | "208,204,204" 386 | ] 387 | } 388 | ].forEach(runTileTests); 389 | 390 | ["bilinear", "near", "min", "max", "median", "vectorize"].forEach(method => { 391 | test(method + " performance", async ({ eq }) => { 392 | const info = await readTile({ x: 3853, y: 6815, z: 14, filename: "SkySat_Freeport_s03_20170831T162740Z3.tif" }); 393 | 394 | const { forward, inverse } = proj4("EPSG:" + info.geotiff_srs, "EPSG:" + 3857); 395 | const result = geowarp({ 396 | debug_level: 0, 397 | forward, 398 | inverse, 399 | 400 | // regarding input data 401 | in_data: info.data, 402 | in_bbox: info.geotiff_bbox, 403 | in_srs: info.geotiff_srs, 404 | in_width: info.width, 405 | in_height: info.height, 406 | 407 | // regarding location to paint 408 | out_bbox: info.tile_bbox, 409 | out_layout: "[row][column][band]", 410 | out_srs: 3857, 411 | out_height: 256, 412 | out_width: 256, 413 | method, 414 | round: true 415 | }); 416 | 417 | if (process.env.WRITE) { 418 | writePNGSync({ h: 256, w: 256, data: result.data, filepath: "./test-output/" + method + "-performance" }); 419 | } 420 | }); 421 | }); 422 | 423 | ["bilinear", "near", "min", "max", "median"].forEach(method => { 424 | ["sync", "async"].forEach(sync_or_async => { 425 | test(`${method} + performance + expr + ${sync_or_async}`, async ({ eq }) => { 426 | const info = await readTile({ x: 3853, y: 6815, z: 14, filename: "SkySat_Freeport_s03_20170831T162740Z3.tif" }); 427 | 428 | let expr; 429 | if (sync_or_async === "sync") { 430 | expr = ({ pixel }) => pixel.map(v => v / 255).concat([0, 1]); 431 | } else { 432 | expr = async ({ pixel }) => pixel.map(v => v / 255).concat([0, 1]); 433 | } 434 | 435 | const result = await geowarp({ 436 | debug_level: 0, 437 | // rescale and add alpha channel 438 | read_bands: [0, 1], // only read the first two bands 439 | expr, 440 | reproject: proj4("EPSG:" + 3857, "EPSG:" + info.geotiff_srs).forward, 441 | 442 | // regarding input data 443 | in_data: info.data, 444 | in_bbox: info.geotiff_bbox, 445 | in_srs: info.geotiff_srs, 446 | in_width: info.width, 447 | in_height: info.height, 448 | 449 | // regarding location to paint 450 | out_bbox: info.tile_bbox, 451 | out_layout: "[row,column,band]", 452 | out_pixel_depth: 4, 453 | out_srs: 3857, 454 | out_height: 256, 455 | out_width: 256, 456 | method, 457 | round: true 458 | }); 459 | eq( 460 | result.data.every(n => n >= 0 && n <= 1), 461 | true 462 | ); 463 | eq(result.read_bands, [0, 1]); 464 | }); 465 | }); 466 | }); 467 | 468 | test("edge case: web mercator tile from UTM", async ({ eq }) => { 469 | const filepath = path.resolve(__dirname, "./test-data/utm.tif"); 470 | const geotiff = await GeoTIFF.fromFile(filepath); 471 | const image = await geotiff.getImage(); 472 | const rasters = await image.readRasters(); 473 | const palette = getPalette(image); 474 | const in_width = image.getWidth(); // 100 475 | const in_height = image.getHeight(); // 100 476 | 477 | const in_data = xdim.transform({ 478 | data: rasters, 479 | from: "[band][row,column]", 480 | to: "[band][row][column]", 481 | sizes: { 482 | band: 1, 483 | row: in_height, 484 | column: in_width 485 | } 486 | }).data; 487 | const in_srs = 32617; 488 | const out_srs = 3857; 489 | 490 | const { inverse, forward } = proj4("EPSG:" + in_srs, "EPSG:" + out_srs); 491 | 492 | // tile x: 1152, y: 1535, z: 12, 493 | const out_bbox = [-8766409.899970293, 5009418.403634399, -8756625.96034979, 5019161.025317816]; 494 | const out_height = 255; 495 | const out_width = 256; 496 | 497 | ["vectorize", "near", "bilinear", "median"].forEach(method => { 498 | console.log("method:", method); 499 | const options = { 500 | debug_level: 0, 501 | inverse, 502 | forward, 503 | 504 | expr: ({ pixel }) => { 505 | return palette[pixel[0]] || [0, 0, 0, 0]; 506 | }, 507 | 508 | // regarding input data 509 | in_bbox: getBoundingBox(image), 510 | in_data, 511 | in_layout: "[band][row][column]", 512 | in_srs, 513 | in_width, 514 | in_height, 515 | 516 | // regarding location to paint 517 | out_array_types: ["Array", "Array", "Array"], 518 | out_bbox, 519 | out_layout: "[band][row][column]", 520 | out_pixel_depth: 4, 521 | out_srs, 522 | out_height, 523 | out_width, 524 | method, 525 | round: true 526 | }; 527 | 528 | const warped = geowarp(options); 529 | 530 | if (process.env.WRITE) { 531 | writePNGSync({ h: out_height, w: out_width, data: warped.data, filepath: "./test-output/edge-case-utm-" + method }); 532 | } 533 | }); 534 | }); 535 | 536 | test("OpenLandMap", async ({ eq }) => { 537 | const filepath = path.resolve(__dirname, "./test-data/lcv_landuse.cropland_hyde_p_10km_s0..0cm_2016_v3.2.tif"); 538 | const geotiff = await GeoTIFF.fromFile(filepath); 539 | const image = await geotiff.getImage(); 540 | const rasters = await image.readRasters(); 541 | const in_width = image.getWidth(); 542 | const in_height = image.getHeight(); 543 | 544 | const in_data = xdim.transform({ 545 | data: rasters, 546 | from: "[band][row,column]", 547 | to: "[band][row][column]", 548 | sizes: { 549 | band: 1, 550 | row: in_height, 551 | column: in_width 552 | } 553 | }).data; 554 | const in_srs = 4326; 555 | const out_srs = 3857; 556 | 557 | const { inverse, forward } = proj4("EPSG:" + in_srs, "EPSG:" + out_srs); 558 | 559 | // tile x: 1152, y: 1535, z: 12, 560 | const tile_bbox = require("@mapbox/tilebelt").tileToBBOX([0, 0, 1]); 561 | const out_bbox = reprojectBoundingBox({ bbox: tile_bbox, from: 4326, to: 3857 }); 562 | 563 | console.log({ tile_bbox, out_bbox }); 564 | // const out_height = 1; 565 | // const out_width = 1; 566 | 567 | const out_height = 512; 568 | const out_width = 512; 569 | 570 | // const methods = ["vectorize", "near", "bilinear", "median"]; 571 | const methods = ["near"]; 572 | for (let m = 0; m < methods.length; m++) { 573 | const method = methods[m]; 574 | console.log("method:", method); 575 | const options = { 576 | debug_level: 0, 577 | inverse, 578 | forward, 579 | expr: ({ pixel }) => 580 | new Promise(res => { 581 | setTimeout(() => res(pixel[0] >= 1 ? [0, 255, 0] : [0, 0, 0]), 1); 582 | }), 583 | // regarding input data 584 | in_bbox: getBoundingBox(image), 585 | in_data, 586 | in_layout: "[band][row][column]", 587 | in_srs, 588 | in_width, 589 | in_height, 590 | 591 | // regarding location to paint 592 | out_array_types: ["Array", "Array", "Array"], 593 | out_pixel_depth: 3, 594 | out_bbox, 595 | out_layout: "[band][row][column]", 596 | out_srs, 597 | out_height, 598 | out_width, 599 | method, 600 | round: true, 601 | cache_process: true 602 | }; 603 | 604 | const warped = await geowarp(options); 605 | 606 | if (process.env.WRITE) { 607 | writePNGSync({ h: out_height, w: out_width, data: warped.data, filepath: "./test-output/openlandmap" }); 608 | } 609 | 610 | const value = warped.data[0][0][out_width - 1]; 611 | eq(value !== null, true); 612 | } 613 | }); 614 | 615 | test("rescale", async ({ eq }) => { 616 | const filename = "gadas.tif"; 617 | const filepath = path.resolve(__dirname, "./test-data", filename); 618 | const geotiff = await GeoTIFF.fromFile(filepath); 619 | const image = await geotiff.getImage(0); 620 | const rasters = await image.readRasters(); 621 | const in_bbox = getBoundingBox(image); 622 | const height = image.getHeight(); 623 | const width = image.getWidth(); 624 | // ProjectedCSTypeGeoKey says 32767, but PCSCitationGeoKey says ESRI PE String = 3857.esriwkt 625 | const in_srs = 3857; 626 | const out_srs = 3857; 627 | const out_height = Math.round(height / 5); 628 | const out_width = Math.round(width / 5); 629 | 630 | const { data } = geowarp({ 631 | debug_level: 0, 632 | in_bbox, 633 | in_data: rasters, 634 | in_layout: "[band][row,column]", 635 | in_srs, 636 | in_height: height, 637 | in_width: width, 638 | out_array_types: ["Array", "Array", "Array"], 639 | out_height, 640 | out_width, 641 | out_layout: "[band][row][column]", 642 | out_srs, 643 | method: "median" 644 | }); 645 | 646 | if (process.env.WRITE) { 647 | writePNGSync({ h: out_height, w: out_width, data, filepath: "./test-output/gadas-rescale" }); 648 | } 649 | eq(data.length, 4); // check band count 650 | eq(data[0][0].constructor.name, "Array"); 651 | }); 652 | 653 | test("auto-detect out_pixel_depth", async ({ eq }) => { 654 | const filename = "gadas.tif"; 655 | const filepath = path.resolve(__dirname, "./test-data", filename); 656 | const geotiff = await GeoTIFF.fromFile(filepath); 657 | const image = await geotiff.getImage(0); 658 | const rasters = await image.readRasters(); 659 | const in_bbox = getBoundingBox(image); 660 | const height = image.getHeight(); 661 | const width = image.getWidth(); 662 | // ProjectedCSTypeGeoKey says 32767, but PCSCitationGeoKey says ESRI PE String = 3857.esriwkt 663 | const in_srs = 3857; 664 | const out_srs = 3857; 665 | const out_height = Math.round(height / 10); 666 | const out_width = Math.round(width / 10); 667 | 668 | const result = await geowarp({ 669 | debug_level: 1, 670 | expr: async ({ pixel: [r, g, b, a] }) => (b > 150 ? [223, 255, 0] : [r, g, b]), 671 | in_bbox, 672 | in_data: rasters, 673 | in_layout: "[band][row,column]", 674 | in_srs, 675 | in_height: height, 676 | in_width: width, 677 | out_height, 678 | out_width, 679 | out_layout: "[band][row][column]", 680 | out_srs, 681 | method: "median" 682 | }); 683 | 684 | const { data } = result; 685 | 686 | if (process.env.WRITE) { 687 | writePNGSync({ h: out_height, w: out_width, data, filepath: "./test-output/out-pixel-depth" }); 688 | } 689 | eq(data.length, 3); // check band count 690 | eq(data[0][0].constructor.name, "Array"); 691 | }); 692 | 693 | test("skew", async ({ eq }) => { 694 | // https://a.tile.openstreetmap.org/18/254460/145575.png 695 | // https://a.tile.openstreetmap.org/14/15903/9098.png 696 | 697 | const filename = "umbra_mount_yasur.tiff"; 698 | const filepath = path.resolve(__dirname, "./test-data", filename); 699 | const geotiff = await GeoTIFF.fromFile(filepath); 700 | const image = await geotiff.getImage(0); 701 | const in_data = await image.readRasters(); 702 | const in_bbox = getBoundingBox(image); 703 | const in_height = image.getHeight(); 704 | const in_width = image.getWidth(); 705 | const fd = image.fileDirectory; 706 | const geokeys = image.getGeoKeys(); 707 | const in_srs = geokeys.ProjectedCSTypeGeoKey; 708 | const mt = fd.ModelTransformation; 709 | const in_geotransform = [mt[3], mt[0], mt[1], mt[7], mt[4], mt[5]]; 710 | 711 | // write unwarped for debugging 712 | if (process.env.WRITE) { 713 | writePNGSync({ h: in_height, w: in_width, data: [in_data[0], in_data[0], in_data[0]], filepath: "./test-output/skew-original" }); 714 | } 715 | 716 | const resized = await geowarp({ 717 | expr: ({ pixel }) => [pixel[0], pixel[0], pixel[0]], 718 | in_bbox: [0, 0, in_width, in_height], 719 | in_data, 720 | in_height, 721 | in_width, 722 | out_height: 64, 723 | out_layout: "[band][row][column]", 724 | out_width: 64, 725 | method: "median" 726 | }); 727 | 728 | if (process.env.WRITE) { 729 | writePNGSync({ h: 64, w: 64, data: resized.data, filepath: "./test-output/skew-resized" }); 730 | } 731 | 732 | const unskewed = await geowarp({ 733 | debug_level: 0, 734 | expr: ({ pixel }) => [pixel[0], pixel[0], pixel[0]], 735 | in_bbox, 736 | in_geotransform, 737 | in_data, 738 | in_layout: "[band][row,column]", 739 | in_srs, 740 | in_height, 741 | in_width, 742 | out_height: 512, 743 | out_width: 512, 744 | out_layout: "[band][row][column]", 745 | out_srs: in_srs, 746 | method: "near" 747 | }); 748 | if (process.env.WRITE) { 749 | writePNGSync({ h: 512, w: 512, data: unskewed.data, filepath: "./test-output/unskewed" }); 750 | } 751 | 752 | const methods = ["near", "bilinear", "median"]; 753 | for (let i = 0; i < methods.length; i++) { 754 | const method = methods[i]; 755 | const out_srs = 3857; 756 | // https://a.tile.openstreetmap.org/14/15903/9098.png 757 | 758 | const bbox4326 = tilebelt.tileToBBOX([15903, 9098, 14]); 759 | const bbox3857 = reprojectBoundingBox({ bbox: bbox4326, from: 4326, to: 3857 }); 760 | const out_size = 512; 761 | const webmercator = await geowarp({ 762 | debug_level: 0, 763 | expr: ({ pixel }) => [pixel[0], pixel[0], pixel[0]], 764 | ...proj4("EPSG:" + in_srs, "EPSG:" + out_srs), 765 | in_bbox, 766 | in_geotransform, 767 | in_data, 768 | in_layout: "[band][row,column]", 769 | in_srs, 770 | in_height, 771 | in_width, 772 | out_bbox: bbox3857, 773 | out_height: out_size, 774 | out_width: out_size, 775 | out_layout: "[band][row][column]", 776 | out_srs: 3857, 777 | method 778 | }); 779 | 780 | if (process.env.WRITE) { 781 | writePNGSync({ h: out_size, w: out_size, data: webmercator.data, filepath: "./test-output/unskewed-webmercator-" + method }); 782 | } 783 | eq(webmercator.data.length, 3); // check band count 784 | eq(webmercator.data[0][0].constructor.name, "Array"); 785 | } 786 | 787 | // zoomed into caldera: https://a.tile.openstreetmap.org/18/254460/145575.png 788 | // but in 4326 projection 789 | { 790 | const out_srs = 4326; 791 | const vectorized = await geowarp({ 792 | expr: ({ pixel }) => { 793 | // console.log(pixel) 794 | return [pixel[0], pixel[0], pixel[0]]; 795 | }, 796 | ...proj4("EPSG:" + in_srs, "EPSG:" + out_srs), 797 | in_bbox, 798 | in_data, 799 | in_geotransform, 800 | in_layout: "[band][row,column]", 801 | in_srs, 802 | in_height, 803 | in_width, 804 | out_bbox: tilebelt.tileToBBOX([508919, 291152, 19]), 805 | out_height: 512, 806 | out_width: 512, 807 | out_layout: "[band][row][column]", 808 | out_srs, 809 | method: "vectorize" 810 | }); 811 | 812 | if (process.env.WRITE) { 813 | writePNGSync({ h: 512, w: 512, data: vectorized.data, filepath: "./test-output/unskewed-vectorized" }); 814 | } 815 | } 816 | }); 817 | 818 | test("antarctica with NaN", async ({ eq }) => { 819 | const filename = "bremen_sea_ice_conc_2022_9_9.tif"; 820 | const filepath = path.resolve(__dirname, "./test-data", filename); 821 | const geotiff = await GeoTIFF.fromFile(filepath); 822 | const image = await geotiff.getImage(0); 823 | const in_data = await image.readRasters(); 824 | // console.log("read data", in_data); 825 | const bbox = getBoundingBox(image); 826 | const in_height = image.getHeight(); 827 | const in_width = image.getWidth(); 828 | // const fd = image.fileDirectory; 829 | const geokeys = image.getGeoKeys(); 830 | const in_srs = geokeys.ProjectedCSTypeGeoKey; // 3031 831 | 832 | const methods = ["near", "bilinear", "median"]; 833 | for (let i = 0; i < methods.length; i++) { 834 | const method = methods[i]; 835 | const result = await geowarp({ 836 | in_bbox: bbox, 837 | in_data, 838 | in_layout: "[band][row,column]", 839 | in_srs, 840 | in_height, 841 | in_width, 842 | out_bbox: bbox, 843 | out_height: 512, 844 | out_no_data: 127, 845 | out_width: 512, 846 | out_layout: "[band][row][column]", 847 | out_srs: 3031, 848 | method 849 | }); 850 | console.log(method + " warped", result.data); 851 | 852 | // check that no NaN values in output 853 | eq( 854 | result.data.flat(3).findIndex(it => isNaN(it)), 855 | -1 856 | ); 857 | 858 | if (process.env.WRITE) { 859 | writePNGSync({ h: 512, w: 512, data: [result.data[0], result.data[0], result.data[0]], filepath: `./test-output/sea-icea-${method}` }); 860 | } 861 | } 862 | }); 863 | 864 | test("issue 27: globe 3857 to 4326", async ({ eq }) => { 865 | const filename = "gadas-world.tif"; 866 | const filepath = path.resolve(__dirname, "./test-data", filename); 867 | const geotiff = await GeoTIFF.fromFile(filepath); 868 | const image = await geotiff.getImage(0); 869 | const in_data = await image.readRasters(); 870 | const bbox = getBoundingBox(image); 871 | const in_height = image.getHeight(); 872 | const in_width = image.getWidth(); 873 | const out_height = 180 * 2; 874 | const out_width = 360 * 2; 875 | 876 | const { forward, inverse } = proj4("EPSG:3857", "EPSG:4326"); 877 | 878 | const methods = ["near-vectorize", "near", "bilinear", "median"]; 879 | const turbos = [true, false]; 880 | for (let i = 0; i < methods.length; i++) { 881 | const method = methods[i]; 882 | for (let ii = 0; ii < 2; ii++) { 883 | const turbo = turbos[ii]; 884 | const result = await geowarp({ 885 | debug_level: 0, 886 | in_bbox: bbox, 887 | in_geotransform: [-20057076.25595305, 39135.75848200009, 0, 12640208.839021027, 0, -39135.75848200009], 888 | in_data, 889 | in_layout: "[band][row,column]", 890 | in_srs: 3857, 891 | in_height, 892 | in_width, 893 | out_bbox: [-180, -90, 180, 90], 894 | out_height, 895 | out_no_data: null, 896 | out_width, 897 | out_layout: "[band][row][column]", 898 | out_srs: 4326, 899 | forward, 900 | inverse, 901 | method, 902 | round: true, 903 | theoretical_max: 255, 904 | theoretical_min: 0, 905 | turbo 906 | }); 907 | // console.log(method + " warped"); 908 | // console.dir(result.data, { maxArrayLength: 3 }); 909 | 910 | // console.dir( 911 | // result.data.map(band => band.map(row => row[0])), 912 | // { maxArrayLength: 500 } 913 | // ); 914 | 915 | // check that no NaN values in output 916 | eq( 917 | result.data.flat(3).findIndex(it => isNaN(it)), 918 | -1 919 | ); 920 | 921 | if (process.env.WRITE) { 922 | writePNGSync({ h: out_height, w: out_width, data: result.data, filepath: `./test-output/gadas-whole-4326-${method}${turbo ? "-turbo" : ""}` }); 923 | } 924 | } 925 | } 926 | }); 927 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { writeFileSync } from "node:fs"; 3 | // @ts-ignore 4 | import findAndRead from "find-and-read"; 5 | // @ts-ignore 6 | import { resolve } from "node:path"; 7 | 8 | import count from "fast-counter"; 9 | import test from "flug"; 10 | // @ts-ignore 11 | import { fromFile } from "geotiff"; 12 | // @ts-ignore 13 | import readBoundingBox from "geotiff-read-bbox"; 14 | // @ts-ignore 15 | import proj4 from "proj4-fully-loaded"; 16 | import reprojectBoundingBox from "reproject-bbox"; 17 | // @ts-ignore 18 | import tilebelt from "@mapbox/tilebelt"; 19 | // @ts-ignore 20 | import { transform } from "xdim"; 21 | // @ts-ignore 22 | import writeImage from "write-image"; 23 | 24 | import geowarp from "./geowarp"; 25 | 26 | const range = (ct: number) => new Array(ct).fill(0).map((_, i) => i); 27 | 28 | const exit = (process as any).exit; 29 | 30 | const writePNGSync = ({ h, w, data, filepath }: { h: number; w: number; data: any; filepath: string }) => { 31 | const { data: buf } = writeImage({ data, height: h, format: "PNG", width: w })!; 32 | writeFileSync(`${filepath}.png`, Buffer.from(buf)); 33 | }; 34 | 35 | ["vectorize", "near", "median", "bilinear"].forEach(method => { 36 | test("cutline " + method, async ({ eq }) => { 37 | const cutline = JSON.parse(findAndRead("sri-lanka-hi-res.geojson", { encoding: "utf-8" })); 38 | const filename = "gadas.tif"; 39 | const filepath = resolve(__dirname, "./test-data", filename); 40 | const geotiff = await fromFile(filepath); 41 | const image = await geotiff.getImage(0); 42 | const rasters = await image.readRasters(); 43 | const in_bbox = image.getBoundingBox(); 44 | const height = image.getHeight(); 45 | const width = image.getWidth(); 46 | // ProjectedCSTypeGeoKey says 32767, but PCSCitationGeoKey says ESRI PE String = 3857.esriwkt 47 | const in_srs = 3857; 48 | const out_srs = "EPSG:5234"; // Kandawala / Sri Lanka Grid 49 | const { forward, inverse } = proj4("EPSG:" + in_srs, out_srs); 50 | 51 | const { data } = geowarp({ 52 | debug_level: 0, 53 | in_bbox, 54 | in_data: rasters, 55 | in_layout: "[band][row,column]", 56 | in_srs, 57 | in_height: height, 58 | in_width: width, 59 | out_array_types: ["Array", "Array", "Uint8ClampedArray"], 60 | out_height: height, 61 | out_width: width, 62 | out_layout: "[band][row][column]", 63 | out_srs, 64 | forward, 65 | inverse, 66 | cutline, 67 | cutline_srs: 4326, 68 | cutline_forward: proj4("EPSG:4326", out_srs).forward, 69 | method 70 | }); 71 | 72 | if (process.env.WRITE) { 73 | writePNGSync({ h: height, w: width, data, filepath: `./test-output/gadas-cutline-` + method }); 74 | } 75 | eq(data.length, 4); // check band count 76 | eq((data as any)[0][0].constructor.name, "Uint8ClampedArray"); 77 | }); 78 | }); 79 | 80 | test("reproject without clipping", async ({ eq }) => { 81 | const filename = "wildfires.tiff"; 82 | const filepath = resolve(__dirname, "./test-data", filename); 83 | const geotiff = await fromFile(filepath); 84 | const image = await geotiff.getImage(0); 85 | const geoKeys = image.getGeoKeys(); 86 | const { GeographicTypeGeoKey, ProjectedCSTypeGeoKey } = geoKeys; 87 | const rasters = await image.readRasters(); 88 | const height = image.getHeight(); 89 | const width = image.getWidth(); 90 | const in_srs = ProjectedCSTypeGeoKey || GeographicTypeGeoKey; 91 | const [xmin, ymax] = image.getOrigin(); 92 | const [resolutionX, resolutionY] = image.getResolution(); 93 | const ymin = ymax - height * Math.abs(resolutionY); 94 | const xmax = xmin + width * Math.abs(resolutionX); 95 | const in_bbox = [xmin, ymin, xmax, ymax]; 96 | const out_srs = "EPSG:26910"; // NAD83 / UTM zone 10N 97 | const { forward, inverse } = proj4("EPSG:" + in_srs, out_srs); 98 | const { data } = geowarp({ 99 | in_bbox, 100 | in_data: rasters, 101 | in_layout: "[band][row,column]", 102 | in_srs, 103 | in_height: height, 104 | in_width: width, 105 | out_height: height, 106 | out_width: width, 107 | out_layout: "[band][row][column]", 108 | out_srs, 109 | forward, 110 | inverse 111 | }); 112 | 113 | if (process.env.WRITE) { 114 | writePNGSync({ h: height, w: width, data, filepath: `./test-output/reproject-without-clipping.tif` }); 115 | } 116 | eq(data.length, 3); // check band count 117 | }); 118 | 119 | test("bug: reprojecting to EPSG:26910", async ({ eq }) => { 120 | const filename = "wildfires.tiff"; 121 | const filepath = resolve(__dirname, "./test-data", filename); 122 | const geotiff = await fromFile(filepath); 123 | const image = await geotiff.getImage(0); 124 | const geoKeys = image.getGeoKeys(); 125 | const { GeographicTypeGeoKey, ProjectedCSTypeGeoKey } = geoKeys; 126 | const rasters = await image.readRasters(); 127 | const height = image.getHeight(); 128 | const width = image.getWidth(); 129 | const in_srs = ProjectedCSTypeGeoKey || GeographicTypeGeoKey; 130 | const [xmin, ymax] = image.getOrigin(); 131 | const [resolutionX, resolutionY] = image.getResolution(); 132 | const ymin = ymax - height * Math.abs(resolutionY); 133 | const xmax = xmin + width * Math.abs(resolutionX); 134 | const in_bbox = [xmin, ymin, xmax, ymax]; 135 | const out_srs = 26910; // NAD83 / UTM zone 10N 136 | const factor = 0.05; 137 | let out_bbox = reprojectBoundingBox({ bbox: [xmin, ymin, xmax, ymax], from: in_srs, to: out_srs }); 138 | // change out_bbox to top left quarter 139 | out_bbox = [ 140 | out_bbox[0], 141 | Math.round(out_bbox[3] - (out_bbox[3] - out_bbox[1]) * factor), 142 | Math.round(out_bbox[0] + (out_bbox[2] - out_bbox[0]) * factor), 143 | out_bbox[3] 144 | ]; 145 | const { inverse } = proj4("EPSG:" + in_srs, "EPSG:" + out_srs); 146 | const { data } = geowarp({ 147 | in_bbox, 148 | in_data: rasters, 149 | in_layout: "[band][row,column]", 150 | in_srs, 151 | in_height: height, 152 | in_width: width, 153 | out_bbox, 154 | out_height: height, 155 | out_width: width, 156 | out_layout: "[band][row][column]", 157 | out_srs, 158 | inverse 159 | }); 160 | 161 | if (process.env.WRITE) { 162 | const filepath = "./test-output/wildfires-26910"; 163 | writePNGSync({ h: height, w: width, data, filepath }); 164 | console.log("wrote:", filepath); 165 | } 166 | eq(data.length, 3); // check band count 167 | }); 168 | 169 | const readTile = async ({ x, y, z, filename }: { x: number; y: number; z: number; filename: string }) => { 170 | const filepath = resolve(__dirname, "./test-data", filename); 171 | const bbox4326 = tilebelt.tileToBBOX([x, y, z]); 172 | const bbox3857 = reprojectBoundingBox({ bbox: bbox4326, from: 4326, to: 3857 }); 173 | const geotiff = await fromFile(filepath); 174 | const { data, read_bbox, height, width, srs_of_geotiff } = await readBoundingBox({ 175 | bbox: bbox3857, 176 | geotiff, 177 | srs: 3857 178 | }); 179 | return { 180 | data, 181 | depth: data.length, // num bands 182 | geotiff_srs: srs_of_geotiff, 183 | height, 184 | layout: "[band][row,column]", 185 | tile_bbox: bbox3857, 186 | geotiff_bbox: read_bbox, 187 | width 188 | }; 189 | }; 190 | 191 | const runTileTests = async ({ 192 | x, 193 | y, 194 | z, 195 | filename, 196 | methods, 197 | debug_level, 198 | out_bands_array, 199 | out_layouts = ["[row][column][band]", "[band][row][column]", "[band][row,column]"], 200 | out_no_data, 201 | sizes = [64, 256, 512], 202 | most_common_pixels, 203 | turbos = [false, true], 204 | out_resolutions = [[1, 1]] 205 | }: { 206 | x: number; 207 | y: number; 208 | z: number; 209 | filename: string; 210 | methods: string[]; 211 | debug_level?: number; 212 | out_bands_array: Array; 213 | out_no_data?: number; 214 | out_layouts?: string[]; 215 | sizes: number[]; 216 | most_common_pixels: string[]; 217 | turbos?: boolean[]; 218 | out_resolutions?: Readonly>>; 219 | }) => { 220 | try { 221 | const info = await readTile({ x, y, z, filename }); 222 | sizes.forEach((size: number) => { 223 | methods.forEach((method: string) => { 224 | out_layouts.forEach((out_layout: string) => { 225 | out_bands_array.forEach((out_bands: number[] | undefined) => { 226 | turbos.forEach((turbo: boolean) => { 227 | out_resolutions.forEach(out_resolution => { 228 | const testName = `${filename.split(".")[0]}-${z}-${x}-${y}-${method}-${size}-${out_layout}-${out_bands}-${out_resolution[0]}${ 229 | turbo ? "-turbo" : "" 230 | }`; 231 | test(testName, async ({ eq }) => { 232 | const in_srs = info.geotiff_srs; 233 | 234 | const { inverse, forward } = proj4("EPSG:" + in_srs, "EPSG:" + 3857); 235 | 236 | const result = geowarp({ 237 | debug_level, 238 | forward, 239 | inverse, 240 | 241 | // regarding input data 242 | in_data: info.data, 243 | in_bbox: info.geotiff_bbox, 244 | in_layout: info.layout, 245 | in_srs: info.geotiff_srs, 246 | in_width: info.width, 247 | in_height: info.height, 248 | 249 | // regarding location to paint 250 | out_bands, 251 | out_bbox: info.tile_bbox, 252 | out_layout, 253 | out_no_data, 254 | out_srs: 3857, 255 | out_height: size, 256 | out_resolution, 257 | out_width: size, 258 | method: method === "first" ? ({ values }) => values[0] : method, 259 | round: true, 260 | turbo 261 | }); 262 | 263 | eq(result.read_bands, out_bands || range(info.depth)); 264 | 265 | const result_data = result.data as any; 266 | 267 | let counts: { [key: string]: number } | undefined; 268 | if (out_layout === "[row][column][band]") { 269 | eq(result_data.length, size); 270 | eq(result_data[0].length, size); 271 | eq(result_data[0][0].length, out_bands?.length ?? 3); 272 | counts = count(result.data, { depth: 2 }); 273 | const sortedCounts = Object.entries(counts).sort((a, b) => Math.sign(b[1] - a[1])); 274 | const top = sortedCounts[0][0]; 275 | if (!["first", "min", "max"].includes(method) && !out_bands) { 276 | try { 277 | eq(most_common_pixels.includes(top), true); 278 | } catch (error) { 279 | console.log("method:", method); 280 | console.log("sortedCounts:", sortedCounts.slice(0, 5)); 281 | console.error("top:", `rgb(${top})`); 282 | throw error; 283 | } 284 | } 285 | } else if (out_layout === "[band][row][column]") { 286 | eq(result_data.length, out_bands?.length ?? 3); 287 | eq(result_data[0].length, size); 288 | eq(result_data[0][0].length, size); 289 | } else if (out_layout === "[band][row,column]") { 290 | eq(result_data.length, out_bands?.length ?? 3); 291 | eq( 292 | result_data.every((b: number[]) => b.length === size * size), 293 | true 294 | ); 295 | counts = count(result_data, { depth: 1 }); 296 | } else if (out_layout === "[row,column,band]") { 297 | eq(result_data.length, 3 * size * size); 298 | eq( 299 | result_data.every((n: number) => typeof n === "number"), 300 | true 301 | ); 302 | } 303 | 304 | if (process.env.WRITE) { 305 | writePNGSync({ h: size, w: size, data: result.data, filepath: `./test-output/${testName}` }); 306 | } 307 | }); 308 | }); 309 | }); 310 | }); 311 | }); 312 | }); 313 | }); 314 | } catch (error) { 315 | console.error(error); 316 | exit(); 317 | } 318 | }; 319 | 320 | [ 321 | { 322 | // tile for Bagley Mountain, ex: https://b.tile.osm.org/13/1319/3071.png 323 | x: 1319, 324 | y: 3071, 325 | z: 13, 326 | sizes: [64, 256, 512], 327 | debug_level: 0, 328 | filename: "wildfires.tiff", 329 | methods: ["near-vectorize", "vectorize", "first", "bilinear", "near", "max", "mean", "median", "min", "mode", "mode-mean", "mode-max", "mode-min"], 330 | out_bands_array: [undefined], 331 | out_no_data: 0, 332 | most_common_pixels: ["0,0,0", "11,16,8", "16,22,12", "16,24,11", "18,26,11", "18,26,12", "13,18,9", "22,30,17", "48,59,61", "218,33,33"], 333 | turbos: [false, true], 334 | out_resolutions: [ 335 | [1, 1], 336 | [0.5, 0.5], 337 | [0.25, 0.25] 338 | ] as const 339 | }, 340 | { 341 | // note: the left edge of the tile is actually west of the left edge of the geotiff, 342 | // thus the resulting image should appear to have a black stripe on the left edge 343 | x: 40, 344 | y: 96, 345 | z: 8, 346 | sizes: [64, 256, 512], 347 | filename: "wildfires.tiff", 348 | methods: ["near-vectorize", "first", "bilinear", "near", "max", "mean", "median", "min", "mode", "mode-mean", "mode-max", "mode-min"], 349 | out_bands_array: [undefined, [0], [2, 1, 0]], 350 | most_common_pixels: [ 351 | "0,0,0", 352 | "11,16,8", 353 | "12,20,7", 354 | "13,18,9", 355 | "14,22,9", 356 | "15,23,10", 357 | "16,22,11", 358 | "16,22,12", 359 | "16,22,13", 360 | "16,23,11", 361 | "16,23,12", 362 | "16,23,13", 363 | "16,24,11", 364 | "16,24,13", 365 | "17,25,12", 366 | "17,25,14", 367 | "18,24,12", 368 | "18,26,11", 369 | "18,26,12", 370 | "18,26,13", 371 | "19,25,13", 372 | "19,27,14", 373 | "20,23,12", 374 | "22,30,17", 375 | "22,30,19", 376 | "24,30,18", 377 | "25,33,20", 378 | "27,35,22", 379 | "28,30,17", 380 | "32,34,21", 381 | "33,43,34", 382 | "36,46,45", 383 | "40,49,47", 384 | "42,49,42", 385 | "42,51,48", 386 | "43,49,42", 387 | "46,53,48", 388 | "46,54,48" 389 | ], 390 | turbos: [false, true], 391 | out_resolutions: [ 392 | [1, 1], 393 | [0.5, 0.5], 394 | [0.25, 0.25], 395 | [0.05, 0.05] 396 | ] as const 397 | }, 398 | { 399 | x: 3853, 400 | y: 6815, 401 | z: 14, 402 | sizes: [64, 256, 512], 403 | filename: "SkySat_Freeport_s03_20170831T162740Z3.tif", 404 | methods: ["near-vectorize", "first", "bilinear", "near", "max", "mean", "median", "min", "mode", "mode-mean", "mode-max", "mode-min"], 405 | out_bands_array: [undefined, [0], [2, 1, 0]], 406 | most_common_pixels: [ 407 | "105,88,75", 408 | "105,90,76", 409 | "106,89,75", 410 | "106,90,77", 411 | "107,90,76", 412 | "107,90,77", 413 | "107,91,79", 414 | "107,92,79", 415 | "121,110,99", 416 | "132,127,125", 417 | "136,130,128", 418 | "136,133,139", 419 | "139,132,128", 420 | "140,133,129", 421 | "141,134,131", 422 | "142,135,131", 423 | "142,135,132", 424 | "142,136,132", 425 | "143,136,133", 426 | "143,137,132", 427 | "146,133,139", 428 | "146,140,135", 429 | "146,140,137", 430 | "146,141,139", 431 | "147,133,139", 432 | "147,140,136", 433 | "147,140,137", 434 | "147,141,137", 435 | "152,146,142", 436 | "152,146,143", 437 | "153,133,143", 438 | "154,147,144", 439 | "157,133,139", 440 | "157,152,150", 441 | "208,205,204", 442 | "208,204,204" 443 | ], 444 | turbos: [false, true], 445 | out_resolutions: [ 446 | [1, 1], 447 | [0.5, 0.5], 448 | [0.25, 0.25] 449 | ] as const 450 | } 451 | ].forEach(runTileTests); 452 | 453 | ["vectorize", "bilinear", "near", "min", "max", "median"].forEach(method => { 454 | test(method + " performance", async ({ eq }) => { 455 | const info = await readTile({ x: 3853, y: 6815, z: 14, filename: "SkySat_Freeport_s03_20170831T162740Z3.tif" }); 456 | 457 | const { forward, inverse } = proj4("EPSG:" + info.geotiff_srs, "EPSG:" + 3857); 458 | 459 | const result = geowarp({ 460 | debug_level: 0, 461 | forward, 462 | inverse, 463 | 464 | // regarding input data 465 | in_data: info.data, 466 | in_bbox: info.geotiff_bbox, 467 | in_srs: info.geotiff_srs, 468 | in_width: info.width, 469 | in_height: info.height, 470 | 471 | // regarding location to paint 472 | out_bbox: info.tile_bbox, 473 | out_layout: "[row][column][band]", 474 | out_srs: 3857, 475 | out_height: 256, 476 | out_width: 256, 477 | method, 478 | round: true 479 | }); 480 | }); 481 | }); 482 | 483 | ["bilinear", "near", "min", "max", "median"].forEach(method => { 484 | test("expr " + method, async ({ eq }) => { 485 | const info = await readTile({ x: 3853, y: 6815, z: 14, filename: "SkySat_Freeport_s03_20170831T162740Z3.tif" }); 486 | 487 | const { forward, inverse } = proj4("EPSG:" + info.geotiff_srs, "EPSG:" + 3857); 488 | const result = geowarp({ 489 | debug_level: 0, 490 | // rescale and add alpha channel 491 | read_bands: [0, 1], // only read the first two bands 492 | expr: ({ pixel }) => pixel.map(v => v / 255).concat([0, 1]), 493 | forward, 494 | inverse, 495 | 496 | // regarding input data 497 | in_data: info.data, 498 | in_bbox: info.geotiff_bbox, 499 | in_srs: info.geotiff_srs, 500 | in_width: info.width, 501 | in_height: info.height, 502 | 503 | // regarding location to paint 504 | out_bbox: info.tile_bbox, 505 | out_layout: "[row,column,band]", 506 | out_pixel_depth: 4, 507 | out_srs: 3857, 508 | out_height: 256, 509 | out_width: 256, 510 | method, 511 | round: true 512 | }); 513 | const result_data = result.data as any; 514 | eq( 515 | result_data.every((n: number) => n >= 0 && n <= 1), 516 | true 517 | ); 518 | eq(result.read_bands, [0, 1]); 519 | }); 520 | }); 521 | 522 | test("georaster-layer-for-leaflet v3 issues", async ({ eq }) => { 523 | const filename = "example_4326.tif"; 524 | const filepath = resolve(__dirname, "./test-data", filename); 525 | const geotiff = await fromFile(filepath); 526 | const image = await geotiff.getImage(0); 527 | const rasters = await image.readRasters(); 528 | const { inverse, forward } = proj4("EPSG:4326", "EPSG:3857"); 529 | const in_height = image.getHeight(); 530 | const in_width = image.getWidth(); 531 | const in_data = transform({ 532 | data: rasters, 533 | from: "[band][row,column]", 534 | to: "[band][row][column]", 535 | sizes: { band: rasters.length, row: in_height, column: in_width } 536 | }).data; 537 | geowarp({ 538 | debug_level: 0, 539 | forward, 540 | inverse, 541 | in_data, 542 | in_bbox: image.getBoundingBox(), 543 | in_layout: "[band][row][column]", 544 | in_srs: 4326, 545 | in_width, 546 | in_height, 547 | out_array_types: ["Array", "Array", "Array"], 548 | out_bbox: [149.99999998948465, 49.99999988790859, 309.99999995583534, 159.99999987996836], 549 | out_layout: "[band][row][column]", 550 | out_srs: 3857, 551 | out_height: 256, 552 | out_width: 256, 553 | method: "near", 554 | round: false 555 | }); 556 | }); 557 | 558 | test("issue #24", async ({ eq }) => { 559 | const filename = "vestfold.tif"; 560 | const filepath = resolve(__dirname, "./test-data", filename); 561 | const geotiff = await fromFile(filepath); 562 | const image = await geotiff.getImage(0); 563 | const rasters = await image.readRasters(); 564 | const in_height = image.getHeight(); 565 | const in_width = image.getWidth(); 566 | const in_data = transform({ 567 | data: rasters, 568 | from: "[band][row,column]", 569 | to: "[band][row][column]", 570 | sizes: { band: rasters.length, row: in_height, column: in_width } 571 | }).data; 572 | 573 | const result = geowarp({ 574 | debug_level: 0, 575 | in_data, 576 | in_bbox: [0, 0, in_width, in_height], 577 | in_layout: "[band][row][column]", 578 | in_width, 579 | in_height, 580 | out_array_types: ["Array", "Array", "Array"], 581 | out_bbox: [256, 256, 512, 512], 582 | out_layout: "[band][row][column]", 583 | out_height: 256, 584 | out_width: 256, 585 | method: "near-vectorize", 586 | round: false, 587 | turbo: undefined 588 | }); 589 | 590 | const out_data = result.data as number[][][]; 591 | 592 | eq(out_data.length, 1); 593 | eq(out_data[0].length, 256); 594 | eq(out_data[0][0].length, 256); 595 | 596 | if (process.env.WRITE) { 597 | writePNGSync({ h: result.out_height, w: result.out_width, data: [result.data[0], result.data[0], result.data[0]], filepath: `./test-output/issue-24` }); 598 | } 599 | }); 600 | --------------------------------------------------------------------------------