cog-tiler-wasm 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cog-tiler.js +30 -24
- package/cog_tiler_wasm_bg.wasm +0 -0
- package/package.json +2 -1
- package/sampling.js +33 -0
package/cog-tiler.js
CHANGED
|
@@ -25,6 +25,7 @@ import initTiler, { colorize, colormap_names } from "./cog_tiler_wasm.js";
|
|
|
25
25
|
import proj4 from "proj4";
|
|
26
26
|
import * as GeoTIFF from "geotiff";
|
|
27
27
|
import geokeysToProj4 from "geotiff-geokeys-to-proj4";
|
|
28
|
+
import { sampleWindowBilinear } from "./sampling.js";
|
|
28
29
|
|
|
29
30
|
const OS = 20037508.342789244; // Web Mercator half-extent (m)
|
|
30
31
|
const TILE = 256; // output tile size (px)
|
|
@@ -224,22 +225,6 @@ function bilin(v00, v10, v01, v11, tx, ty) {
|
|
|
224
225
|
return top + (bot - top) * ty;
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
// Bilinear sample of a row-major window at fractional (fc,fr). Returns NaN when
|
|
228
|
-
// the cell center is outside the window so out-of-raster pixels stay transparent
|
|
229
|
-
// (no edge smear); falls back to nearest at the window edge / next to nodata.
|
|
230
|
-
function sampleWindowBilinear(buf, w, h, fc, fr) {
|
|
231
|
-
const c0 = Math.floor(fc), r0 = Math.floor(fr);
|
|
232
|
-
if (c0 < 0 || c0 >= w || r0 < 0 || r0 >= h) return NaN;
|
|
233
|
-
const c1 = Math.min(c0 + 1, w - 1), r1 = Math.min(r0 + 1, h - 1);
|
|
234
|
-
const v00 = buf[r0 * w + c0];
|
|
235
|
-
if (Number.isNaN(v00)) return NaN;
|
|
236
|
-
const v10 = buf[r0 * w + c1], v01 = buf[r1 * w + c0], v11 = buf[r1 * w + c1];
|
|
237
|
-
if (Number.isNaN(v10) || Number.isNaN(v01) || Number.isNaN(v11)) return v00; // edge/nodata
|
|
238
|
-
const tx = fc - c0, ty = fr - r0;
|
|
239
|
-
const top = v00 + (v10 - v00) * tx, bot = v01 + (v11 - v01) * tx;
|
|
240
|
-
return top + (bot - top) * ty;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
228
|
// Build a byte reader from a URL string, a Blob/File, or an in-memory source
|
|
244
229
|
// (ArrayBuffer, Uint8Array). `range(a,b)` yields bytes [a..b]; `openTiff()`
|
|
245
230
|
// returns a geotiff.js GeoTIFF for reading the CRS / color table from the header.
|
|
@@ -290,6 +275,11 @@ async function makeReader(source) {
|
|
|
290
275
|
export async function openCog(source) {
|
|
291
276
|
await init();
|
|
292
277
|
const { range, openTiff, label } = await makeReader(source);
|
|
278
|
+
// whitebox-wasm's streaming decoder currently materializes numeric tile
|
|
279
|
+
// samples as little-endian. Detect Motorola TIFFs up front so their pixels
|
|
280
|
+
// can be decoded by geotiff.js, which honors the header byte order.
|
|
281
|
+
const byteOrder = await range(0, 1);
|
|
282
|
+
const bigEndian = byteOrder[0] === 0x4d && byteOrder[1] === 0x4d;
|
|
293
283
|
// Parse the COG header; grow the prefix and retry for large COGs whose IFDs
|
|
294
284
|
// exceed 64 KB (many overviews / huge tile-offset arrays).
|
|
295
285
|
let stream;
|
|
@@ -301,7 +291,7 @@ export async function openCog(source) {
|
|
|
301
291
|
if (len >= 1 << 25) throw e; // give up past ~32 MB
|
|
302
292
|
}
|
|
303
293
|
}
|
|
304
|
-
|
|
294
|
+
let gt = stream.geo_transform(); // [x0, px_w, rot, y0, rot, px_h]
|
|
305
295
|
const levels = JSON.parse(stream.levels_json());
|
|
306
296
|
if (!Array.isArray(levels) || levels.length === 0) {
|
|
307
297
|
throw new Error("levels_json() returned no levels");
|
|
@@ -312,12 +302,27 @@ export async function openCog(source) {
|
|
|
312
302
|
// through geotiff.js instead (see _assembleWindow / point).
|
|
313
303
|
const multiBand = levels[0].bands > 1;
|
|
314
304
|
let tiff = null, img = null, planar = false;
|
|
315
|
-
if (stream.epsg !== 3857 || multiBand) {
|
|
305
|
+
if (stream.epsg !== 3857 || multiBand || bigEndian) {
|
|
316
306
|
tiff = await openTiff();
|
|
317
307
|
img = await tiff.getImage();
|
|
318
308
|
planar = multiBand && (await readTiffTag(img, "PlanarConfiguration")) === 2;
|
|
309
|
+
if (bigEndian && gt.length !== 6) {
|
|
310
|
+
const [x, y] = img.getOrigin();
|
|
311
|
+
const [pixelWidth, pixelHeight] = img.getResolution();
|
|
312
|
+
gt = [x, pixelWidth, 0, y, 0, -pixelHeight];
|
|
313
|
+
}
|
|
319
314
|
}
|
|
320
|
-
const base = {
|
|
315
|
+
const base = {
|
|
316
|
+
url: label,
|
|
317
|
+
range,
|
|
318
|
+
stream,
|
|
319
|
+
levels,
|
|
320
|
+
gt,
|
|
321
|
+
nodata: stream.nodata,
|
|
322
|
+
tiff,
|
|
323
|
+
planar,
|
|
324
|
+
bigEndian,
|
|
325
|
+
};
|
|
321
326
|
|
|
322
327
|
if (stream.epsg === 3857) {
|
|
323
328
|
return new CogSource({
|
|
@@ -425,10 +430,11 @@ export class CogSource {
|
|
|
425
430
|
|
|
426
431
|
// Fetch + decode band `band` (0-based) over a level pixel window into a
|
|
427
432
|
// row-major buffer. Chunky COGs go through whitebox (cached, NaN for gaps);
|
|
428
|
-
//
|
|
429
|
-
//
|
|
433
|
+
// Planar (INTERLEAVE=BAND) and big-endian COGs are read per-band via
|
|
434
|
+
// geotiff.js. Whitebox's streaming decoder cannot address planar tiles and
|
|
435
|
+
// currently interprets multi-byte tile samples as little-endian.
|
|
430
436
|
async _assembleWindow(level, x, y, w, h, band = 0) {
|
|
431
|
-
if (this.planar) {
|
|
437
|
+
if (this.planar || this.bigEndian) {
|
|
432
438
|
const img = await this._tiffImage(level);
|
|
433
439
|
const rasters = await img.readRasters({ window: [x, y, x + w, y + h], samples: [band] });
|
|
434
440
|
return rasters[0]; // typed array, length w*h, row-major
|
|
@@ -569,7 +575,7 @@ export class CogSource {
|
|
|
569
575
|
// RGB composite: bilinear-sample each band, rescale -> curve -> gamma.
|
|
570
576
|
let ok = true;
|
|
571
577
|
for (let k = 0; k < 3; k++) {
|
|
572
|
-
const v = sampleWindowBilinear(bufs[k], ww, hh, fcol, frow);
|
|
578
|
+
const v = sampleWindowBilinear(bufs[k], ww, hh, fcol, frow, ndSet ? nodata : undefined);
|
|
573
579
|
if (!isFinite(v) || (ndSet && v === nodata)) { ok = false; break; }
|
|
574
580
|
const [mn, mx] = rescales[k] || rescales[0];
|
|
575
581
|
const t = transferCurve(Math.max(0, Math.min(1, (v - mn) / ((mx - mn) || 1))), stretch, gamma);
|
|
@@ -579,7 +585,7 @@ export class CogSource {
|
|
|
579
585
|
else { out[o] = out[o + 1] = out[o + 2] = 0; }
|
|
580
586
|
} else {
|
|
581
587
|
// Continuous single band: bilinear; colormap applied below.
|
|
582
|
-
const v = sampleWindowBilinear(bufs[0], ww, hh, fcol, frow);
|
|
588
|
+
const v = sampleWindowBilinear(bufs[0], ww, hh, fcol, frow, ndSet ? nodata : undefined);
|
|
583
589
|
if (!isFinite(v) || (ndSet && v === nodata)) continue;
|
|
584
590
|
grid[py * outW + px] = v;
|
|
585
591
|
}
|
package/cog_tiler_wasm_bg.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"Qiusheng Wu <giswqs@gmail.com>"
|
|
6
6
|
],
|
|
7
7
|
"description": "Serverless Cloud Optimized GeoTIFF (COG) dynamic tiling in WebAssembly. TiTiler-style XYZ tiles, no backend.",
|
|
8
|
-
"version": "0.3.
|
|
8
|
+
"version": "0.3.2",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"cog_tiler_wasm.js",
|
|
17
17
|
"cog_tiler_wasm.d.ts",
|
|
18
18
|
"cog-tiler.js",
|
|
19
|
+
"sampling.js",
|
|
19
20
|
"cog-tiler.d.ts",
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE"
|
package/sampling.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resampling helpers, kept dependency-free so they can be unit-tested
|
|
3
|
+
* without loading the wasm/geotiff stack that `cog-tiler.js` pulls in.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Bilinear sample of a row-major window at fractional (fc,fr). Returns NaN when
|
|
7
|
+
// the cell center is outside the window so out-of-raster pixels stay transparent
|
|
8
|
+
// (no edge smear); falls back to nearest at the window edge / next to nodata.
|
|
9
|
+
//
|
|
10
|
+
// `nodata` is the declared sentinel, when the dataset has one. It must be tested
|
|
11
|
+
// here rather than only on the result: interpolating across the boundary blends
|
|
12
|
+
// the sentinel into the value, and the product matches neither the sentinel nor
|
|
13
|
+
// NaN, so a caller's `v === nodata` check cannot recognize it. With a large
|
|
14
|
+
// negative sentinel (-9999 is the GDAL convention) the blend lands far below the
|
|
15
|
+
// rescale window, clamps to its low end, and paints a ring of the colormap's
|
|
16
|
+
// first color around every nodata boundary -- e.g. a blue border along a swath
|
|
17
|
+
// edge under `jet`. Treating a sentinel neighbor exactly like a NaN one keeps
|
|
18
|
+
// the boundary pixel at its own valid value instead.
|
|
19
|
+
export function sampleWindowBilinear(buf, w, h, fc, fr, nodata) {
|
|
20
|
+
const c0 = Math.floor(fc), r0 = Math.floor(fr);
|
|
21
|
+
if (c0 < 0 || c0 >= w || r0 < 0 || r0 >= h) return NaN;
|
|
22
|
+
const c1 = Math.min(c0 + 1, w - 1), r1 = Math.min(r0 + 1, h - 1);
|
|
23
|
+
// `nodata` is undefined when the dataset declares none; `v === undefined` is
|
|
24
|
+
// false for every numeric sample, so the test costs nothing in that case.
|
|
25
|
+
const isVoid = (v) => Number.isNaN(v) || v === nodata;
|
|
26
|
+
const v00 = buf[r0 * w + c0];
|
|
27
|
+
if (isVoid(v00)) return NaN;
|
|
28
|
+
const v10 = buf[r0 * w + c1], v01 = buf[r1 * w + c0], v11 = buf[r1 * w + c1];
|
|
29
|
+
if (isVoid(v10) || isVoid(v01) || isVoid(v11)) return v00; // edge/nodata
|
|
30
|
+
const tx = fc - c0, ty = fr - r0;
|
|
31
|
+
const top = v00 + (v10 - v00) * tx, bot = v01 + (v11 - v01) * tx;
|
|
32
|
+
return top + (bot - top) * ty;
|
|
33
|
+
}
|