itowns 2.43.2-next.4 → 2.43.2-next.5
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.
|
@@ -22,10 +22,8 @@
|
|
|
22
22
|
* @returns {GeoTIFFLevel} The selected zoom level.
|
|
23
23
|
*/
|
|
24
24
|
function selectLevel(source, requestExtent, requestWidth, requestHeight) {
|
|
25
|
-
// Number of images = original + overviews if any
|
|
26
|
-
const cropped = requestExtent.clone().intersect(source.extent);
|
|
27
25
|
// Dimensions of the requested extent
|
|
28
|
-
const extentDimension =
|
|
26
|
+
const extentDimension = requestExtent.clone().planarDimensions();
|
|
29
27
|
|
|
30
28
|
const targetResolution = Math.min(
|
|
31
29
|
extentDimension.x / requestWidth,
|
|
@@ -87,70 +85,112 @@ function makeWindowFromExtent(source, extent, resolution) {
|
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
/**
|
|
90
|
-
*
|
|
88
|
+
* Reads raster data from the image as RGB.
|
|
89
|
+
* The result is always an interleaved typed array.
|
|
90
|
+
* Colorspaces other than RGB will be transformed to RGB, color maps expanded.
|
|
91
|
+
*
|
|
92
|
+
* @param {Source} source The COGSource.
|
|
93
|
+
* @param {GeoTIFFLevel} level The GeoTIFF level to read
|
|
94
|
+
* @param {number[]} viewport The image region to read.
|
|
95
|
+
* @returns {Promise<TypedArray[]>} The raster data
|
|
96
|
+
*/
|
|
97
|
+
async function readRGB(source, level, viewport) {
|
|
98
|
+
try {
|
|
99
|
+
// TODO possible optimization: instead of letting geotiff.js crop and resample
|
|
100
|
+
// the tiles into the desired region, we could use image.getTileOrStrip() to
|
|
101
|
+
// read individual tiles (aka blocks) and make a texture per block. This way,
|
|
102
|
+
// there would not be multiple concurrent reads for the same block, and we would not
|
|
103
|
+
// waste time resampling the blocks since resampling is already done in the composer.
|
|
104
|
+
// We would create more textures, but it could be worth it.
|
|
105
|
+
return await level.image.readRGB({
|
|
106
|
+
window: viewport,
|
|
107
|
+
pool: source.pool,
|
|
108
|
+
width: source.tileWidth,
|
|
109
|
+
height: source.tileHeight,
|
|
110
|
+
resampleMethod: source.resampleMethod,
|
|
111
|
+
enableAlpha: true,
|
|
112
|
+
interleave: true,
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error.toString() === 'AggregateError: Request failed') {
|
|
116
|
+
// Problem with the source that is blocked by another fetch
|
|
117
|
+
// (request failed in readRasters). See the conversations in
|
|
118
|
+
// https://github.com/geotiffjs/geotiff.js/issues/218
|
|
119
|
+
// https://github.com/geotiffjs/geotiff.js/issues/221
|
|
120
|
+
// https://github.com/geotiffjs/geotiff.js/pull/224
|
|
121
|
+
// Retry until it is not blocked.
|
|
122
|
+
// TODO retry counter
|
|
123
|
+
await new Promise((resolve) => {
|
|
124
|
+
setTimeout(resolve, 100);
|
|
125
|
+
});
|
|
126
|
+
return readRGB(level, viewport, source);
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Creates a texture from the pixel buffer
|
|
91
134
|
*
|
|
92
135
|
* @param {Object} source The COGSource
|
|
93
|
-
* @param {THREE.TypedArray
|
|
94
|
-
* @param {number}
|
|
95
|
-
* @param {number}
|
|
96
|
-
* @param {number}
|
|
136
|
+
* @param {THREE.TypedArray[]} buffer The pixel buffer
|
|
137
|
+
* @param {number} buffer.width
|
|
138
|
+
* @param {number} buffer.height
|
|
139
|
+
* @param {number} buffer.byteLength
|
|
97
140
|
* @returns {THREE.DataTexture} The generated texture.
|
|
98
141
|
*/
|
|
99
|
-
function createTexture(source,
|
|
100
|
-
const {
|
|
142
|
+
function createTexture(source, buffer) {
|
|
143
|
+
const { byteLength } = buffer;
|
|
144
|
+
const width = source.tileWidth;
|
|
145
|
+
const height = source.tileHeight;
|
|
101
146
|
const pixelCount = width * height;
|
|
102
147
|
const targetDataType = source.dataType;
|
|
103
148
|
const format = THREE.RGBAFormat;
|
|
104
149
|
const channelCount = 4;
|
|
105
|
-
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
// Check if it's a RGBA buffer
|
|
109
|
-
if (pixelCount * channelCount === byteLength) {
|
|
110
|
-
data = buffers;
|
|
111
|
-
}
|
|
150
|
+
const isRGBA = pixelCount * channelCount === byteLength;
|
|
151
|
+
let tmpBuffer = buffer;
|
|
112
152
|
|
|
113
153
|
switch (targetDataType) {
|
|
114
154
|
case THREE.UnsignedByteType: {
|
|
115
|
-
if (!
|
|
116
|
-
|
|
117
|
-
const newBuffers = new Uint8ClampedArray(pixelCount * channelCount);
|
|
118
|
-
data = convertToRGBA(buffers, newBuffers, source.defaultAlpha);
|
|
155
|
+
if (!isRGBA) {
|
|
156
|
+
tmpBuffer = convertToRGBA(tmpBuffer, new Uint8ClampedArray(pixelCount * channelCount), source.defaultAlpha);
|
|
119
157
|
}
|
|
120
|
-
|
|
121
|
-
break;
|
|
158
|
+
return new THREE.DataTexture(tmpBuffer, width, height, format, THREE.UnsignedByteType);
|
|
122
159
|
}
|
|
123
160
|
case THREE.FloatType: {
|
|
124
|
-
if (!
|
|
125
|
-
|
|
126
|
-
const newBuffers = new Float32Array(pixelCount * channelCount);
|
|
127
|
-
data = convertToRGBA(buffers, newBuffers, source.defaultAlpha / 255);
|
|
161
|
+
if (!isRGBA) {
|
|
162
|
+
tmpBuffer = convertToRGBA(tmpBuffer, new Float32Array(pixelCount * channelCount), source.defaultAlpha / 255);
|
|
128
163
|
}
|
|
129
|
-
|
|
130
|
-
break;
|
|
164
|
+
return new THREE.DataTexture(tmpBuffer, width, height, format, THREE.FloatType);
|
|
131
165
|
}
|
|
132
166
|
default:
|
|
133
167
|
throw new Error('unsupported data type');
|
|
134
168
|
}
|
|
135
|
-
|
|
136
|
-
return texture;
|
|
137
169
|
}
|
|
138
170
|
|
|
139
|
-
|
|
140
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Convert RGB pixel buffer to RGBA pixel buffer
|
|
173
|
+
*
|
|
174
|
+
* @param {THREE.TypedArray[]} buffer The RGB pixel buffer
|
|
175
|
+
* @param {THREE.TypedArray[]} newBuffer The empty RGBA pixel buffer
|
|
176
|
+
* @param {number} defaultAlpha Default alpha value
|
|
177
|
+
* @returns {THREE.DataTexture} The generated texture.
|
|
178
|
+
*/
|
|
179
|
+
function convertToRGBA(buffer, newBuffer, defaultAlpha) {
|
|
180
|
+
const { width, height } = buffer;
|
|
141
181
|
|
|
142
182
|
for (let i = 0; i < width * height; i++) {
|
|
143
183
|
const oldIndex = i * 3;
|
|
144
184
|
const index = i * 4;
|
|
145
185
|
// Copy RGB from original buffer
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
186
|
+
newBuffer[index + 0] = buffer[oldIndex + 0]; // R
|
|
187
|
+
newBuffer[index + 1] = buffer[oldIndex + 1]; // G
|
|
188
|
+
newBuffer[index + 2] = buffer[oldIndex + 2]; // B
|
|
149
189
|
// Add alpha to new buffer
|
|
150
|
-
|
|
190
|
+
newBuffer[index + 3] = defaultAlpha; // A
|
|
151
191
|
}
|
|
152
192
|
|
|
153
|
-
return
|
|
193
|
+
return newBuffer;
|
|
154
194
|
}
|
|
155
195
|
|
|
156
196
|
/**
|
|
@@ -195,20 +235,14 @@ const COGParser = (function _() {
|
|
|
195
235
|
*/
|
|
196
236
|
parse: async function _(data, options) {
|
|
197
237
|
const source = options.in;
|
|
198
|
-
const
|
|
199
|
-
const level = selectLevel(source, nodeExtent, source.tileWidth, source.tileHeight);
|
|
200
|
-
const viewport = makeWindowFromExtent(source, nodeExtent, level.resolution);
|
|
201
|
-
|
|
202
|
-
const buffers = await level.image.readRGB({
|
|
203
|
-
window: viewport,
|
|
204
|
-
pool: source.pool,
|
|
205
|
-
enableAlpha: true,
|
|
206
|
-
interleave: true,
|
|
207
|
-
});
|
|
238
|
+
const tileExtent = options.extent.as(source.crs);
|
|
208
239
|
|
|
209
|
-
const
|
|
240
|
+
const level = selectLevel(source, tileExtent, source.tileWidth, source.tileHeight);
|
|
241
|
+
const viewport = makeWindowFromExtent(source, tileExtent, level.resolution);
|
|
242
|
+
const rgbBuffer = await readRGB(source, level, viewport);
|
|
243
|
+
const texture = createTexture(source, rgbBuffer);
|
|
210
244
|
texture.flipY = true;
|
|
211
|
-
texture.extent =
|
|
245
|
+
texture.extent = options.extent;
|
|
212
246
|
texture.needsUpdate = true;
|
|
213
247
|
texture.magFilter = THREE.LinearFilter;
|
|
214
248
|
texture.minFilter = THREE.LinearFilter;
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* @property {string} url - The URL of the COG.
|
|
16
16
|
* @property {GeoTIFF.Pool} pool - Pool use to decode GeoTiff.
|
|
17
17
|
* @property {number} defaultAlpha - Alpha byte value used if no alpha is present in COG. Default value is 255.
|
|
18
|
+
* @property {number} tileWidth - Tile width in pixels. Default value use 'geotiff.getTileWidth()'.
|
|
19
|
+
* @property {number} tileHeight - Tile height in pixels. Default value use 'geotiff.getTileHeight()'.
|
|
20
|
+
* @property {number} resampleMethod - The desired resampling method. Default is 'nearest'.
|
|
18
21
|
*
|
|
19
22
|
* @example
|
|
20
23
|
* // Create the source
|
|
@@ -59,9 +62,9 @@ class COGSource extends itowns.Source {
|
|
|
59
62
|
this.firstImage = await geotiff.getImage();
|
|
60
63
|
this.origin = this.firstImage.getOrigin();
|
|
61
64
|
this.dataType = this.selectDataType(this.firstImage.getSampleFormat(), this.firstImage.getBitsPerSample());
|
|
62
|
-
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
65
|
+
this.tileWidth = source.tileWidth || this.firstImage.getTileWidth();
|
|
66
|
+
this.tileHeight = source.tileHeight || this.firstImage.getTileHeight();
|
|
67
|
+
this.resampleMethod = source.resampleMethod || 'nearest';
|
|
65
68
|
|
|
66
69
|
// Compute extent
|
|
67
70
|
const [minX, minY, maxX, maxY] = this.firstImage.getBoundingBox();
|
|
@@ -80,7 +83,7 @@ class COGSource extends itowns.Source {
|
|
|
80
83
|
.then(image => this.makeLevel(image, image.getResolution(this.firstImage)));
|
|
81
84
|
promises.push(promise);
|
|
82
85
|
}
|
|
83
|
-
this.levels.
|
|
86
|
+
this.levels = this.levels.concat(await Promise.all(promises));
|
|
84
87
|
});
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
itowns.proj4.defs('EPSG:2154', '+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs');
|
|
37
37
|
|
|
38
38
|
var viewerDiv = document.getElementById('viewerDiv');
|
|
39
|
+
var view;
|
|
39
40
|
|
|
40
41
|
function readCOGURL() {
|
|
41
42
|
var url = document.getElementById('cog_url').value || new URLSearchParams(window.location.search).get('geotiff');
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function loadRGBSample() {
|
|
50
|
-
document.getElementById('cog_url').value = 'https://cdn.jsdelivr.net/gh/
|
|
51
|
+
document.getElementById('cog_url').value = 'https://cdn.jsdelivr.net/gh/iTowns/iTowns2-sample-data/cog/nantes/nantes.tif';
|
|
51
52
|
readCOGURL();
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -56,17 +57,33 @@
|
|
|
56
57
|
readCOGURL();
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function
|
|
60
|
+
function removeAllChildNodes(parent) {
|
|
61
|
+
while (parent.firstChild) {
|
|
62
|
+
parent.removeChild(parent.firstChild);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadCOG(url) {
|
|
60
67
|
// create a source from a Cloud Optimized GeoTiff
|
|
61
68
|
var cogSource = new COGSource({
|
|
62
69
|
url: url,
|
|
63
|
-
crs: "EPSG:2154"
|
|
70
|
+
crs: "EPSG:2154",
|
|
71
|
+
// Default resample method is 'nearest', 'bilinear' looks better when zoomed
|
|
72
|
+
resampleMethod: 'bilinear'
|
|
64
73
|
});
|
|
65
74
|
|
|
66
75
|
cogSource.whenReady.then(() => {
|
|
67
|
-
|
|
76
|
+
if (view !== undefined) {
|
|
77
|
+
view.dispose(true);
|
|
78
|
+
removeAllChildNodes(viewerDiv);
|
|
79
|
+
}
|
|
80
|
+
view = new itowns.PlanarView(viewerDiv, cogSource.extent, {
|
|
81
|
+
// Default maxSubdivisionLevel is 5, so with huge geotiff it's not enough to see details when zoomed
|
|
82
|
+
maxSubdivisionLevel: 10,
|
|
83
|
+
disableSkirt: true,
|
|
84
|
+
placement: { tilt: 90 }
|
|
85
|
+
});
|
|
68
86
|
setupLoadingScreen(viewerDiv, view);
|
|
69
|
-
new itowns.PlanarControls(view, {});
|
|
70
87
|
var cogLayer = new itowns.ColorLayer('cog', {
|
|
71
88
|
source: cogSource,
|
|
72
89
|
});
|