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 = cropped.planarDimensions();
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
- * Creates a texture from the pixel buffer(s).
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 | THREE.TypedArray[]} buffers The buffers (one buffer per band)
94
- * @param {number} buffers.width
95
- * @param {number} buffers.height
96
- * @param {number} buffers.byteLength
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, buffers) {
100
- const { width, height, byteLength } = buffers;
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
- let texture;
106
- let data;
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 (!data) {
116
- // We convert RGB buffer to RGBA
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
- texture = new THREE.DataTexture(data, width, height, format, THREE.UnsignedByteType);
121
- break;
158
+ return new THREE.DataTexture(tmpBuffer, width, height, format, THREE.UnsignedByteType);
122
159
  }
123
160
  case THREE.FloatType: {
124
- if (!data) {
125
- // We convert RGB buffer to RGBA
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
- texture = new THREE.DataTexture(data, width, height, format, THREE.FloatType);
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
- function convertToRGBA(buffers, newBuffers, defaultAlpha) {
140
- const { width, height } = buffers;
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
- newBuffers[index + 0] = buffers[oldIndex + 0]; // R
147
- newBuffers[index + 1] = buffers[oldIndex + 1]; // G
148
- newBuffers[index + 2] = buffers[oldIndex + 2]; // B
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
- newBuffers[index + 3] = defaultAlpha; // A
190
+ newBuffer[index + 3] = defaultAlpha; // A
151
191
  }
152
192
 
153
- return newBuffers;
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 nodeExtent = data.extent.as(source.crs);
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 texture = createTexture(source, buffers);
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 = data.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.tileWidth = this.firstImage.getTileWidth();
64
- this.tileHeight = this.firstImage.getTileHeight();
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.push(await Promise.all(promises));
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/bloc-in-bloc/iTowns2-sample-data@add-cog-sample/cog/orvault.tif';
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 loadCOG(url, crs) {
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
- var view = new itowns.PlanarView(viewerDiv, cogSource.extent, { disableSkirt: true, placement: { tilt: 90 } });
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itowns",
3
- "version": "2.43.2-next.4",
3
+ "version": "2.43.2-next.5",
4
4
  "description": "A JS/WebGL framework for 3D geospatial data visualization",
5
5
  "type": "module",
6
6
  "main": "lib/Main.js",