|
| 1 | +const forEach = (nums, no_data, cb) => { |
| 2 | + const len = nums.length; |
| 3 | + if (no_data) { |
| 4 | + for (let i = 0; i < len; i++) { |
| 5 | + const n = nums[i]; |
| 6 | + if (n !== no_data) cb(n); |
| 7 | + } |
| 8 | + } else { |
| 9 | + for (let i = 0; i < len; i++) { |
| 10 | + cb(nums[i]); |
| 11 | + } |
| 12 | + } |
| 13 | +}; |
| 14 | + |
| 15 | +const max = (nums, in_no_data, out_no_data) => { |
| 16 | + let result = -Infinity; |
| 17 | + forEach(nums, in_no_data, n => { |
| 18 | + if (n > result) result = n; |
| 19 | + }); |
| 20 | + return result === -Infinity ? out_no_data : result; |
| 21 | +}; |
| 22 | + |
| 23 | +const mean = (nums, in_no_data, out_no_data) => { |
| 24 | + let running_sum = 0; |
| 25 | + let count = 0; |
| 26 | + forEach(nums, in_no_data, n => { |
| 27 | + count++; |
| 28 | + running_sum += n; |
| 29 | + }); |
| 30 | + return count === 0 ? out_no_data : running_sum / count; |
| 31 | +}; |
| 32 | + |
| 33 | +const min = (nums, in_no_data, out_no_data) => { |
| 34 | + let result = Infinity; |
| 35 | + forEach(nums, in_no_data, n => { |
| 36 | + if (n < result) result = n; |
| 37 | + }); |
| 38 | + return result === Infinity ? out_no_data : result; |
| 39 | +}; |
| 40 | + |
| 41 | +const median = (nums, in_no_data, out_no_data) => { |
| 42 | + nums = nums.filter(n => n !== in_no_data).sort(); |
| 43 | + switch (nums.length) { |
| 44 | + case 0: |
| 45 | + return out_no_data; |
| 46 | + case 1: |
| 47 | + return nums[0]; |
| 48 | + default: |
| 49 | + const mid = nums.length / 2; |
| 50 | + if (nums.length % 2 === 0) { |
| 51 | + return (nums[mid - 1] + nums[mid]) / 2; |
| 52 | + } else { |
| 53 | + return nums[Math.floor(mid)]; |
| 54 | + } |
| 55 | + } |
| 56 | +}; |
| 57 | + |
| 58 | +const mode = (nums, no_data) => { |
| 59 | + switch (nums.length) { |
| 60 | + case 0: |
| 61 | + return undefined; |
| 62 | + default: |
| 63 | + const counts = {}; |
| 64 | + if (no_data) { |
| 65 | + for (let i = 0; i < nums.length; i++) { |
| 66 | + const n = nums[i]; |
| 67 | + if (n !== no_data) { |
| 68 | + if (n in counts) counts[n].count++; |
| 69 | + else counts[n] = { n, count: 1 }; |
| 70 | + } |
| 71 | + } |
| 72 | + } else { |
| 73 | + for (let i = 0; i < nums.length; i++) { |
| 74 | + const n = nums[i]; |
| 75 | + if (n in counts) counts[n].count++; |
| 76 | + else counts[n] = { n, count: 1 }; |
| 77 | + } |
| 78 | + } |
| 79 | + const items = Object.values(counts); |
| 80 | + const count = items.sort((a, b) => Math.sign(b.count - a.count))[0].count; |
| 81 | + return items.filter(it => it.count === count).map(it => it.n); |
| 82 | + } |
| 83 | +}; |
| 84 | + |
| 85 | +const geowarp = ({ |
| 86 | + debug_level = 0, |
| 87 | + reproject, // equivalent of proj4(source, target).inverse() |
| 88 | + in_data, |
| 89 | + in_bbox, |
| 90 | + in_srs, |
| 91 | + in_height, |
| 92 | + in_width, |
| 93 | + in_no_data, |
| 94 | + out_bbox, |
| 95 | + out_srs, |
| 96 | + out_width = 256, |
| 97 | + out_height = 256, |
| 98 | + out_no_data = null, |
| 99 | + method = "median", |
| 100 | + round = false, // whether to round output |
| 101 | +}) => { |
| 102 | + if (debug_level) console.log("[geowarp] starting"); |
| 103 | + |
| 104 | + const sameSRS = in_srs === out_srs; |
| 105 | + |
| 106 | + if (!sameSRS && typeof reproject !== "function") { |
| 107 | + throw new Error("[geowarp] you must specify a reproject function"); |
| 108 | + } |
| 109 | + |
| 110 | + if (!in_height) throw new Error("[geowarp] you must provide in_height"); |
| 111 | + if (!in_width) throw new Error("[geowarp] you must provide in_width"); |
| 112 | + |
| 113 | + const num_bands = in_data.length; |
| 114 | + if (debug_level) console.log("[geowarp] number of bands in source data:", num_bands); |
| 115 | + |
| 116 | + const [in_xmin, in_ymin, in_xmax, in_ymax] = in_bbox; |
| 117 | + |
| 118 | + const in_pixel_height = (in_ymax - in_ymin) / in_height; |
| 119 | + const in_pixel_width = (in_xmax - in_xmin) / in_width; |
| 120 | + if (debug_level) console.log("[geowarp] pixel height of source data:", in_pixel_height); |
| 121 | + if (debug_level) console.log("[geowarp] pixel width of source data:", in_pixel_width); |
| 122 | + |
| 123 | + const [out_xmin, out_ymin, out_xmax, out_ymax] = out_bbox; |
| 124 | + |
| 125 | + const out_pixel_height = (out_ymax - out_ymin) / out_height; |
| 126 | + const out_pixel_width = (out_xmax - out_xmin) / out_width; |
| 127 | + |
| 128 | + // iterate over pixels in the out box |
| 129 | + const rows = []; |
| 130 | + let top, left, bottom, right; |
| 131 | + bottom = out_ymax; |
| 132 | + for (let r = 0; r < out_height; r++) { |
| 133 | + const row = []; |
| 134 | + top = bottom; |
| 135 | + bottom = top - out_pixel_height; |
| 136 | + right = out_xmin; |
| 137 | + for (let c = 0; c < out_width; c++) { |
| 138 | + left = right; |
| 139 | + right = left + out_pixel_width; |
| 140 | + // top, left, bottom, right is the sample area in the coordinate system of the output |
| 141 | + |
| 142 | + // convert to bbox of input coordinate system |
| 143 | + const bbox_in_srs = sameSRS ? [left, bottom, right, top] : [...reproject([left, bottom]), ...reproject([right, top])]; |
| 144 | + // console.log({bbox}); |
| 145 | + const [xmin_in_srs, ymin_in_srs, xmax_in_srs, ymax_in_srs] = bbox_in_srs; |
| 146 | + |
| 147 | + // convert bbox in input srs to raster pixels |
| 148 | + const leftInRasterPixels = (xmin_in_srs - in_xmin) / in_pixel_width; |
| 149 | + const rightInRasterPixels = (xmax_in_srs - in_xmin) / in_pixel_width; |
| 150 | + const topInRasterPixels = (in_ymax - ymax_in_srs) / in_pixel_height; |
| 151 | + const bottomInRasterPixels = (in_ymax - ymin_in_srs) / in_pixel_height; |
| 152 | + // console.log({xmin_in_srs, in_xmin, leftInRasterPixels, rightInRasterPixels, topInRasterPixels, bottomInRasterPixels}); |
| 153 | + |
| 154 | + const pixel = []; |
| 155 | + const leftSample = Math.round(leftInRasterPixels); |
| 156 | + const rightSample = Math.round(rightInRasterPixels); |
| 157 | + const topSample = Math.round(topInRasterPixels); |
| 158 | + const bottomSample = Math.round(bottomInRasterPixels); |
| 159 | + for (let b = 0; b < num_bands; b++) { |
| 160 | + const band = in_data[b]; |
| 161 | + const values = []; |
| 162 | + for (let y = topSample; y <= bottomSample; y++) { |
| 163 | + const start = y * in_width; |
| 164 | + for (let x = leftSample; x <= rightSample; x++) { |
| 165 | + // assuming flattened data by band |
| 166 | + const value = band[start + x]; |
| 167 | + values.push(value); |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + let pixelBandValue = null; |
| 172 | + if (method === "max") { |
| 173 | + pixelBandValue = max(values, in_no_data, out_no_data); |
| 174 | + } else if (method === "mean") { |
| 175 | + pixelBandValue = mean(values, in_no_data, out_no_data); |
| 176 | + } else if (method === "median") { |
| 177 | + pixelBandValue = median(values, in_no_data, out_no_data); |
| 178 | + } else if (method === "min") { |
| 179 | + pixelBandValue = min(values, in_no_data, out_no_data); |
| 180 | + } else if (method.startsWith("mode")) { |
| 181 | + const modes = mode(values); |
| 182 | + const len = modes.length; |
| 183 | + if (len === 1) { |
| 184 | + pixelBandValue = modes[0]; |
| 185 | + } else { |
| 186 | + if (method === "mode") { |
| 187 | + pixelBandValue = modes[0]; |
| 188 | + } else if (method === "mode-max") { |
| 189 | + pixelBandValue = max(values); |
| 190 | + } else if (method === "mode-mean") { |
| 191 | + pixelBandValue = mean(values); |
| 192 | + } else if (method === "mode-median") { |
| 193 | + pixelBandValue = median(values); |
| 194 | + } else if (method === "mode-min") { |
| 195 | + pixelBandValue = min(values); |
| 196 | + } |
| 197 | + } |
| 198 | + } |
| 199 | + if (round) pixelBandValue = Math.round(pixelBandValue); |
| 200 | + pixel.push(pixelBandValue); |
| 201 | + } |
| 202 | + row.push(pixel); |
| 203 | + } |
| 204 | + rows.push(row); |
| 205 | + } |
| 206 | + |
| 207 | + if (debug_level) console.log("[geowarp] finishing"); |
| 208 | + return { data: rows }; |
| 209 | +}; |
| 210 | + |
| 211 | +if (typeof module === "object") module.exports = geowarp; |
| 212 | +if (typeof window === "object") window.geowarp = geowarp; |
| 213 | +if (typeof self === "object") self.geowarp = geowarp; |
0 commit comments