Skip to content

Commit 46cdf4c

Browse files
committed
support no out_bbox
1 parent e67bfdd commit 46cdf4c

File tree

4 files changed

+220
-67
lines changed

4 files changed

+220
-67
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ const result = geowarp({
1616
// set debug_level to zero to turn off console logging
1717
debug_level: 2,
1818

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+
1923
// reproject from an [x, y] point in the output spatial reference system
2024
// to an [x, y] point in the input spatial reference system
21-
reproject: proj4("EPSG:" + 3857, "EPSG:" + in_srs).forward,
25+
inverse: proj4("EPSG:" + in_srs, "EPSG:3857").inverse,
2226

2327
// two-dimensional array of pixel data organized by band
2428
// usually [ r, g, b ] or [ r, g, b, a ]

geowarp.js

Lines changed: 121 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const getTheoreticalMin = require("typed-array-ranges/get-min");
66
const fasterMedian = require("faster-median");
77
const xdim = require("xdim");
88

9+
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
10+
911
const uniq = arr => Array.from(new Set(arr)).sort((a, b) => b - a);
1012

1113
const range = ct => new Array(ct).fill(0).map((_, i) => i);
@@ -55,7 +57,7 @@ const mode = (nums, no_data) => {
5557
return undefined;
5658
default:
5759
const counts = {};
58-
if (no_data) {
60+
if (no_data) {q
5961
for (let i = 0; i < nums.length; i++) {
6062
const n = nums[i];
6163
if (n !== no_data) {
@@ -76,11 +78,28 @@ const mode = (nums, no_data) => {
7678
}
7779
};
7880

79-
const geowarp = ({
81+
// taken from reproject-bbox
82+
// because don't want proj4 dependency
83+
const reprojectBoundingBox = ({ bbox, forward }) => {
84+
const [xmin, ymin, xmax, ymax] = bbox;
85+
86+
const topleft = forward([xmin, ymax]);
87+
const topright = forward([xmax, ymax]);
88+
const bottomleft = forward([xmin, ymin]);
89+
const bottomright = forward([xmax, ymin]);
90+
91+
const corners = [topleft, topright, bottomleft, bottomright];
92+
93+
const xs = corners.map(corner => corner[0]);
94+
const ys = corners.map(corner => corner[1]);
95+
96+
return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
97+
};
98+
99+
const geowarp = function geowarp({
80100
debug_level = 0,
81-
reproject, // equivalent of proj4(source, target).inverse()
82101
in_data,
83-
in_bbox,
102+
in_bbox = undefined,
84103
in_layout = "[band][row,column]",
85104
in_srs,
86105
in_height,
@@ -93,7 +112,7 @@ const geowarp = ({
93112
out_pixel_depth, // number of output bands
94113
out_pixel_height, // optional, automatically calculated from out_bbox
95114
out_pixel_width, // optional, automatically calculated from out_bbox
96-
out_bbox,
115+
out_bbox = null,
97116
out_layout,
98117
out_srs,
99118
out_width = 256,
@@ -104,14 +123,27 @@ const geowarp = ({
104123
expr = undefined, // band expression function
105124
round = false, // whether to round output
106125
theoretical_min, // minimum theoretical value (e.g., 0 for unsigned integer arrays)
107-
theoretical_max // maximum values (e.g., 255 for 8-bit unsigned integer arrays)
108-
}) => {
126+
theoretical_max, // maximum values (e.g., 255 for 8-bit unsigned integer arrays),
127+
inverse, // function to reproject [x, y] point from out_srs back to in_srs
128+
forward // function to reproject [x, y] point from in_srs to out_srs
129+
}) {
109130
if (debug_level >= 1) console.log("[geowarp] starting");
110131

111132
const same_srs = in_srs === out_srs;
112133
if (debug_level >= 1) console.log("[geowarp] input and output srs are the same:", same_srs);
113134

114-
if (!same_srs && typeof reproject !== "function") {
135+
// support for deprecated alias of inverse
136+
inverse ??= arguments[0].reproject;
137+
138+
if (!same_srs) {
139+
if (!in_bbox) throw new Error("[geowarp] can't reproject without in_bbox");
140+
if (!out_bbox) {
141+
if (forward) out_bbox = reprojectBoundingBox({ bbox: in_bbox, forward });
142+
else throw new Error("[geowarp] must specify out_bbox or forward");
143+
}
144+
}
145+
146+
if (!same_srs && typeof inverse !== "function") {
115147
throw new Error("[geowarp] you must specify a reproject function");
116148
}
117149

@@ -161,7 +193,7 @@ const geowarp = ({
161193
out_pixel_depth ??= out_bands?.length ?? read_bands?.length ?? in_pixel_depth;
162194

163195
// just resizing an image without reprojection
164-
if (same_srs && !in_bbox && !out_bbox) {
196+
if (same_srs && eq(in_bbox, out_bbox)) {
165197
out_srs = in_srs = null;
166198
in_bbox = [0, 0, in_width, in_height];
167199
out_bbox = [0, 0, out_width, out_height];
@@ -190,8 +222,8 @@ const geowarp = ({
190222
try {
191223
const data_constructor = in_data[0].constructor.name;
192224
if (debug_level >= 1) console.log("[geowarp] data_constructor:", data_constructor);
193-
if (theoretical_min === undefined) theoretical_min = getTheoreticalMin(data_constructor);
194-
if (theoretical_max === undefined) theoretical_max = getTheoreticalMax(data_constructor);
225+
theoretical_min ??= getTheoreticalMin(data_constructor);
226+
theoretical_max ??= getTheoreticalMax(data_constructor);
195227
if (debug_level >= 1) console.log("[geowarp] theoretical_min:", theoretical_min);
196228
if (debug_level >= 1) console.log("[geowarp] theoretical_max:", theoretical_max);
197229
} catch (error) {
@@ -214,7 +246,12 @@ const geowarp = ({
214246
column: out_width
215247
};
216248

217-
const { data: out_data } = xdim.prepareData({ layout: out_layout, sizes: out_sizes });
249+
// can we later on skip inserting no data pixels
250+
const { data: out_data } = xdim.prepareData({
251+
fill: out_no_data,
252+
layout: out_layout,
253+
sizes: out_sizes
254+
});
218255

219256
const update = xdim.prepareUpdate({ data: out_data, layout: out_layout, sizes: out_sizes });
220257

@@ -234,7 +271,7 @@ const geowarp = ({
234271
for (let c = 0; c < out_width; c++) {
235272
const x = out_xmin + out_pixel_width * c;
236273
const pt_out_srs = [x, y];
237-
const [x_in_srs, y_in_srs] = same_srs ? pt_out_srs : reproject(pt_out_srs);
274+
const [x_in_srs, y_in_srs] = same_srs ? pt_out_srs : inverse(pt_out_srs);
238275
const xInRasterPixels = Math.round((x_in_srs - in_xmin) / in_pixel_width);
239276
const yInRasterPixels = Math.round((in_ymax - y_in_srs) / in_pixel_height);
240277
let pixel = [];
@@ -266,7 +303,7 @@ const geowarp = ({
266303
for (let c = 0; c < out_width; c++) {
267304
const x = out_xmin + out_pixel_width * c;
268305
const pt_out_srs = [x, y];
269-
const [x_in_srs, y_in_srs] = same_srs ? pt_out_srs : reproject(pt_out_srs);
306+
const [x_in_srs, y_in_srs] = same_srs ? pt_out_srs : inverse(pt_out_srs);
270307

271308
const xInRasterPixels = (x_in_srs - in_xmin) / in_pixel_width;
272309
const yInRasterPixels = (in_ymax - y_in_srs) / in_pixel_height;
@@ -347,7 +384,7 @@ const geowarp = ({
347384
// top, left, bottom, right is the sample area in the coordinate system of the output
348385

349386
// convert to bbox of input coordinate system
350-
const bbox_in_srs = same_srs ? [left, bottom, right, top] : [...reproject([left, bottom]), ...reproject([right, top])];
387+
const bbox_in_srs = same_srs ? [left, bottom, right, top] : [...inverse([left, bottom]), ...inverse([right, top])];
351388
if (debug_level >= 3) console.log("[geowarp] bbox_in_srs:", bbox_in_srs);
352389
const [xmin_in_srs, ymin_in_srs, xmax_in_srs, ymax_in_srs] = bbox_in_srs;
353390

@@ -361,68 +398,88 @@ const geowarp = ({
361398
const bottomInRasterPixels = (in_ymax - ymin_in_srs) / in_pixel_height;
362399
if (debug_level >= 4) console.log("[geowarp] bottomInRasterPixels:", bottomInRasterPixels);
363400

364-
const leftSample = Math.round(leftInRasterPixels);
365-
const rightSample = Math.round(rightInRasterPixels);
366-
const topSample = Math.round(topInRasterPixels);
367-
const bottomSample = Math.round(bottomInRasterPixels);
368-
let pixel = [];
369-
for (let i = 0; i < read_bands.length; i++) {
370-
const read_band = read_bands[i];
371-
const { data: values } = xdim.clip({
372-
data: in_data,
373-
flat: true,
374-
layout: in_layout,
375-
sizes: in_sizes,
376-
rect: {
377-
band: [read_band, read_band],
378-
row: [topSample, Math.max(topSample, bottomSample - 1)],
379-
column: [leftSample, Math.max(leftSample, rightSample - 1)]
380-
}
381-
});
401+
// round and make sure leftSample isn't less than zero
402+
let leftSample = Math.round(leftInRasterPixels);
403+
let rightSample = Math.round(rightInRasterPixels);
404+
let topSample = Math.round(topInRasterPixels);
405+
let bottomSample = Math.round(bottomInRasterPixels);
382406

383-
let pixelBandValue = null;
384-
if (typeof method === "function") {
385-
pixelBandValue = method({ values });
386-
} else if (method === "max") {
387-
pixelBandValue = max({ nums: values, in_no_data, out_no_data, theoretical_max: undefined });
388-
} else if (method === "mean") {
389-
pixelBandValue = mean(values, in_no_data, out_no_data);
390-
} else if (method === "median") {
391-
pixelBandValue = median({ nums: values, in_no_data, out_no_data });
392-
} else if (method === "min") {
393-
pixelBandValue = min({ nums: values, in_no_data, out_no_data, theoretical_min: undefined });
394-
} else if (method.startsWith("mode")) {
395-
const modes = mode(values);
396-
const len = modes.length;
397-
if (len === 1) {
398-
pixelBandValue = modes[0];
399-
} else {
400-
if (method === "mode") {
407+
let pixel = [];
408+
if (leftSample >= in_width || rightSample < 0 || topSample < 0 || bottomSample >= in_height) {
409+
pixel = new Array(read_bands.length).fill(in_no_data);
410+
} else {
411+
// clamp edges to prevent clipping outside bounds
412+
leftSample = Math.max(0, leftSample);
413+
rightSample = Math.min(rightSample, in_width);
414+
topSample = Math.max(0, topSample);
415+
bottomSample = Math.min(bottomSample, in_height);
416+
417+
for (let i = 0; i < read_bands.length; i++) {
418+
const read_band = read_bands[i];
419+
const { data: values } = xdim.clip({
420+
data: in_data,
421+
flat: true,
422+
layout: in_layout,
423+
sizes: in_sizes,
424+
rect: {
425+
band: [read_band, read_band],
426+
row: [topSample, Math.max(topSample, bottomSample - 1)],
427+
column: [leftSample, Math.max(leftSample, rightSample - 1)]
428+
}
429+
});
430+
431+
let pixelBandValue = null;
432+
if (typeof method === "function") {
433+
pixelBandValue = method({ values });
434+
} else if (method === "max") {
435+
pixelBandValue = max({ nums: values, in_no_data, out_no_data, theoretical_max: undefined });
436+
} else if (method === "mean") {
437+
pixelBandValue = mean(values, in_no_data, out_no_data);
438+
} else if (method === "median") {
439+
pixelBandValue = median({ nums: values, in_no_data, out_no_data });
440+
} else if (method === "min") {
441+
pixelBandValue = min({ nums: values, in_no_data, out_no_data, theoretical_min: undefined });
442+
} else if (method.startsWith("mode")) {
443+
const modes = mode(values);
444+
const len = modes.length;
445+
if (len === 1) {
401446
pixelBandValue = modes[0];
402-
} else if (method === "mode-max") {
403-
pixelBandValue = max({ nums: values });
404-
} else if (method === "mode-mean") {
405-
pixelBandValue = mean(values);
406-
} else if (method === "mode-median") {
407-
pixelBandValue = median({ nums: values });
408-
} else if (method === "mode-min") {
409-
pixelBandValue = min({ nums: values });
447+
} else {
448+
if (method === "mode") {
449+
pixelBandValue = modes[0];
450+
} else if (method === "mode-max") {
451+
pixelBandValue = max({ nums: values });
452+
} else if (method === "mode-mean") {
453+
pixelBandValue = mean(values);
454+
} else if (method === "mode-median") {
455+
pixelBandValue = median({ nums: values });
456+
} else if (method === "mode-min") {
457+
pixelBandValue = min({ nums: values });
458+
}
410459
}
460+
} else {
461+
throw new Error(`[geowarp] unknown method "${method}"`);
411462
}
412-
} else {
413-
throw new Error(`[geowarp] unknown method "${method}"`);
463+
if (round) pixelBandValue = Math.round(pixelBandValue);
464+
pixel.push(pixelBandValue);
414465
}
415-
if (round) pixelBandValue = Math.round(pixelBandValue);
416-
pixel.push(pixelBandValue);
417466
}
467+
418468
if (process) pixel = process({ pixel });
419469
insert({ row: r, column: c, pixel });
420470
}
421471
}
422472
}
423473

424474
if (debug_level >= 1) console.log("[geowarp] finishing");
425-
return { data: out_data, out_bands, out_layout, read_bands };
475+
return {
476+
data: out_data,
477+
out_bands,
478+
out_layout,
479+
out_pixel_height,
480+
out_pixel_width,
481+
read_bands
482+
};
426483
};
427484

428485
if (typeof module === "object") module.exports = geowarp;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@
5353
"faster-median": "^0.1.0",
5454
"get-depth": "^0.0.3",
5555
"typed-array-ranges": "^0.0.0",
56-
"xdim": "^1.5.1"
56+
"xdim": "^1.5.2"
5757
}
5858
}

0 commit comments

Comments
 (0)