-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
GeoTIFFManager.ts
280 lines (236 loc) · 8.23 KB
/
GeoTIFFManager.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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import * as geokeysToProj4 from "geotiff-geokeys-to-proj4";
import proj4 from "proj4";
import {Cartesian3, Color, CustomHeightmapTerrainProvider, GeographicTilingScheme, Rectangle, Viewer} from "cesium";
import {polygon} from "@turf/helpers";
import {ModalImagesSelector} from "../components/ModalImagesSelector";
import LoadingWindow from "../components/LoadingWindow";
import bbox from "@turf/bbox";
const tilingScheme = new GeographicTilingScheme();
/**
* File statistics and bounding rectangle
*/
export interface FileData {
name: string,
min: number,
max: number,
mean: number,
/**
* Bounding rectangle of all images in this file. Use it to zoom to the file.
*/
rectangle: Rectangle
}
interface FileEntry {
proj4projection: proj4.Converter,
originX: number, originY: number,
imageData: Array<number>,
boundingRect: Rectangle,
rightX: number, bottomY: number, width: number, height: number,
heightToMeters: number,
heightMultiplier: number,
}
interface FileEntries {
[id: string]: FileEntry[]
}
/**
* Manages GeoTIFF files. Use its exported instance.
*/
class GeoTIFFManager {
private readonly size = 32;
private readonly width = this.size;
private readonly height = this.size;
private readonly fileEntries: FileEntries;
public readonly terrainProvider: CustomHeightmapTerrainProvider;
public viewer: Viewer | undefined
public imageSelector: ModalImagesSelector | null = null;
public loadingWindow: LoadingWindow | null = null;
constructor() {
this.fileEntries = {};
this.terrainProvider = new CustomHeightmapTerrainProvider({
width: this.width,
height: this.height,
tilingScheme,
callback: (x, y, level) => {
return this.getTerrainData(x, y, level);
}
});
}
/**
* Reads the file and adds it to the {@link GeoTIFFManager#fileEntries}
* @param file A file to add
* @param geoTiff GeoTIFF instance
* @param indices Image indices to add
* @throws {Error} When image is not georeferenced
* @return Data to pass to the menu item
*/
addFile = async (file: File, geoTiff: any, indices: Iterable<number>): Promise<FileData> => {
let min = +Infinity,
max = -Infinity,
mean = 0,
pixelCount = 0,
rectangle: Rectangle | null = null,
images: FileEntry[] = [];
for (let i of indices) {
const image = await geoTiff.getImage(i);
const projectionParameters = geokeysToProj4.toProj4(image.getGeoKeys()), // Convert geokeys to proj4 string
proj4projection = proj4(projectionParameters.proj4, "WGS84"); // Get projection to WGS84
// Get dimensions to determine pixel position in source CRS
let [originX, originY] = image.getOrigin(),
width = image.getWidth(),
height = image.getHeight(),
[xSize, ySize] = image.getResolution(),
nodata = image.getGDALNoData();
// Rest of the coordinates for the top and left sides. Needed to determine pixel position.
const rightX = originX + width * xSize, bottomY = originY + height * ySize;
// Build bounding rect, image polygon and data for further math
// Original image rectangle
const origPoints = [
[originX, originY],
[rightX, originY],
[originX + width * xSize, originY + height * ySize],
[originX, bottomY],
[originX, originY],
];
// Projected image rectangle
let projPoints = [];
for (let point of origPoints)
projPoints.push(proj4projection.forward(point));
// Bounding rect to detect whether image should be read later
const boundingRect = Rectangle.fromDegrees(...bbox(polygon([projPoints])));
if (rectangle === null)
rectangle = boundingRect;
else
Rectangle.union(rectangle, boundingRect, rectangle);
// Output useful data and show image outline in dev mode.
if (process.env.NODE_ENV !== "production") {
console.log(projectionParameters);
const pts = [...projPoints.slice(0, projPoints.length - 1)];
const newPts = [];
for (let pt of pts) {
for (let c of pt)
newPts.push(c);
}
this.viewer?.entities.add({
polygon: {
// @ts-ignore
hierarchy: Cartesian3.fromDegreesArray(newPts),
height: 1000,
material: Color.RED.withAlpha(0.5),
outline: true,
outlineColor: Color.RED
}
});
}
// Read image and get optimal TypedArray instance to save some RAM
const imageData = new ((await image.readRasters([0, 1, 0, 1]))[0]).constructor(width * height);
// Convert image to grayscale and collect statistics
for (let y = 0; y < height; y++) {
// Read one row of pixels. Easier to deal with coordinates, takes less RAM.
const rasters = await image.readRasters({window: [0, y, width, y + 1]});
let rasterLength = rasters[0].length;
for (let x = 0; x < rasterLength; x++) {
// Convert pixel to grayscale
let pixel = 0;
for (let raster of rasters)
pixel += raster[x];
pixel = (pixel === nodata) ? 0 : pixel / rasters.length;
imageData.set([pixel], y * width + x);
// Calculate statistics
if (pixel < min)
min = pixel;
if (pixel > max)
max = pixel;
mean += pixel;
pixelCount++;
}
}
images.push({
proj4projection,
imageData,
boundingRect,
rightX,
bottomY,
originX,
originY,
width,
height,
heightToMeters: projectionParameters.coordinatesConversionParameters.z,
heightMultiplier: 1,
});
}
this.fileEntries[file.name] = images;
return {
name: file.name,
min,
max,
mean: mean / pixelCount,
rectangle: rectangle as Rectangle,
}
}
getTerrainData = (tileX: number, tileY: number, level: number): Float32Array => {
const tileRect = tilingScheme.tileXYToRectangle(tileX, tileY, level); // Tile rectangle to detect whether image should be read
// Get tile rect coordinates for coordinate conversion and to find how much degrees is in tile pixel for X and Y
const {west, east, north, south} = tileRect,
toDeg = 180 / Math.PI,
topLeftX = west * toDeg,
topLeftY = north * toDeg,
bottomRightX = east * toDeg,
bottomRightY = south * toDeg,
degInPxX = Math.abs(bottomRightX - topLeftX) / this.width,
degInPxY = Math.abs(bottomRightY - topLeftY) / this.height,
buffer = new Float32Array(this.width * this.height); // Buffer to return to Cesium
// For each GeoTIFF file and each image in it
for (let id in this.fileEntries) {
let entry = this.fileEntries[id];
for (let img of entry) {
// Optimize performance by not reading files outside of tile
if (!Rectangle.simpleIntersection(tileRect, img.boundingRect))
continue;
// Read current image
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
// Convert pixel position to lon lat and project it to the image CRS
const globalX = topLeftX + x * degInPxX, globalY = topLeftY - y * degInPxY,
[projX, projY] = img.proj4projection.inverse([globalX, globalY]);
// Get pixel position by finding at which % of the side lies current position. Then multiply it by image width/height to convert % to px.
const imageX = Math.floor(img.width * (projX - img.originX) / (img.rightX - img.originX)),
imageY = Math.floor(img.height * (projY - img.originY) / (img.bottomY - img.originY));
// Some debug data in case something will go wrong
// if (level === 12 && tileX === 2635 && tileY === 944)
// console.log(imageX, imageY, projX, projY);
// Don't add points outside of image
if (imageX < 0 || imageX >= img.width || imageY < 0 || imageY >= img.height)
continue;
// Set pixel value. Both Cesium and our program uses row-major order.
buffer.set([img.imageData[imageY * img.width + imageX] * img.heightToMeters * img.heightMultiplier], y * this.width + x);
}
}
}
}
return buffer;
}
/**
* Removes a file with given name from the manager
* @param name Filename to remove
*/
removeFile = (name: string) => {
delete this.fileEntries[name];
}
/**
* Sets a height multiplier for a given file name
* @param fileName
* @param multiplier
*/
setHeightMultiplier = (fileName: string, multiplier: number) => {
if (multiplier < 1)
multiplier = 1;
else if (multiplier > 1000)
multiplier = 1000;
let entries = this.fileEntries[fileName];
for (let entry of entries)
entry.heightMultiplier = multiplier;
}
}
/**
* Manager singleton
*/
export default new GeoTIFFManager();