-
Notifications
You must be signed in to change notification settings - Fork 16
/
height-tile.ts
166 lines (157 loc) · 5.17 KB
/
height-tile.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import type { DemTile } from "./types";
const MIN_VALID_M = -12000;
const MAX_VALID_M = 9000;
function defaultIsValid(number: number): boolean {
return !isNaN(number) && number >= MIN_VALID_M && number <= MAX_VALID_M;
}
/** A tile containing elevation values aligned to a grid. */
export class HeightTile {
get: (x: number, y: number) => number;
width: number;
height: number;
constructor(
width: number,
height: number,
get: (x: number, y: number) => number,
) {
this.get = get;
this.width = width;
this.height = height;
}
/** Construct a height tile from raw DEM pixel values */
static fromRawDem(demTile: DemTile): HeightTile {
return new HeightTile(demTile.width, demTile.height, (x, y) => {
const value = demTile.data[y * demTile.width + x];
return defaultIsValid(value) ? value : NaN;
});
}
/**
* Construct a height tile from a DEM tile plus it's 8 neighbors, so that
* you can request `x` or `y` outside the bounds of the original tile.
*
* @param neighbors An array containing tiles: `[nw, n, ne, w, c, e, sw, s, se]`
*/
static combineNeighbors(
neighbors: (HeightTile | undefined)[],
): HeightTile | undefined {
if (neighbors.length !== 9) {
throw new Error("Must include a tile plus 8 neighbors");
}
const mainTile = neighbors[4];
if (!mainTile) {
return undefined;
}
const width = mainTile.width;
const height = mainTile.height;
return new HeightTile(width, height, (x, y) => {
let gridIdx = 0;
if (y < 0) {
y += height;
} else if (y < height) {
gridIdx += 3;
} else {
y -= height;
gridIdx += 6;
}
if (x < 0) {
x += width;
} else if (x < width) {
gridIdx += 1;
} else {
x -= width;
gridIdx += 2;
}
const grid = neighbors[gridIdx];
return grid ? grid.get(x, y) : NaN;
});
}
/**
* Splits this tile into a `1<<subz` x `1<<subz` grid and returns the tile at coordinates `subx, suby`.
*/
split = (subz: number, subx: number, suby: number): HeightTile => {
if (subz === 0) return this;
const by = 1 << subz;
const dx = (subx * this.width) / by;
const dy = (suby * this.height) / by;
return new HeightTile(this.width / by, this.height / by, (x, y) =>
this.get(x + dx, y + dy),
);
};
/**
* Returns a new tile scaled up by `factor` with pixel values that are subsampled using
* bilinear interpolation between the original height tile values.
*
* The original and result tile are assumed to represent values taken at the center of each pixel.
*/
subsamplePixelCenters = (factor: number): HeightTile => {
const lerp = (a: number, b: number, f: number) =>
isNaN(a) ? b : isNaN(b) ? a : a + (b - a) * f;
if (factor <= 1) return this;
const sub = 0.5 - 1 / (2 * factor);
const blerper = (x: number, y: number) => {
const dx = x / factor - sub;
const dy = y / factor - sub;
const ox = Math.floor(dx);
const oy = Math.floor(dy);
const a = this.get(ox, oy);
const b = this.get(ox + 1, oy);
const c = this.get(ox, oy + 1);
const d = this.get(ox + 1, oy + 1);
const fx = dx - ox;
const fy = dy - oy;
const top = lerp(a, b, fx);
const bottom = lerp(c, d, fx);
return lerp(top, bottom, fy);
};
return new HeightTile(this.width * factor, this.height * factor, blerper);
};
/**
* Assumes the input tile represented measurements taken at the center of each pixel, and
* returns a new tile where values are the height at the top-left of each pixel by averaging
* the 4 adjacent pixel values.
*/
averagePixelCentersToGrid = (radius: number = 1): HeightTile =>
new HeightTile(this.width + 1, this.height + 1, (x, y) => {
let sum = 0,
count = 0,
v = 0;
for (let newX = x - radius; newX < x + radius; newX++) {
for (let newY = y - radius; newY < y + radius; newY++) {
if (!isNaN((v = this.get(newX, newY)))) {
count++;
sum += v;
}
}
}
return count === 0 ? NaN : sum / count;
});
/** Returns a new tile with elevation values scaled by `multiplier`. */
scaleElevation = (multiplier: number): HeightTile =>
multiplier === 1
? this
: new HeightTile(
this.width,
this.height,
(x, y) => this.get(x, y) * multiplier,
);
/**
* Precompute every value from `-bufer, -buffer` to `width + buffer, height + buffer` and serve them
* out of a `Float32Array`. Until this method is called, all `get` requests are lazy and call all previous
* methods in the chain up to the root DEM tile.
*/
materialize = (buffer: number = 2): HeightTile => {
const stride = this.width + 2 * buffer;
const data = new Float32Array(stride * (this.height + 2 * buffer));
let idx = 0;
for (let y = -buffer; y < this.height + buffer; y++) {
for (let x = -buffer; x < this.width + buffer; x++) {
data[idx++] = this.get(x, y);
}
}
return new HeightTile(
this.width,
this.height,
(x, y) => data[(y + buffer) * stride + x + buffer],
);
};
}