itowns 2.43.2-next.0 → 2.43.2-next.1

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.
@@ -27,7 +27,8 @@
27
27
  "potree_3d_map": "Potree 3D map",
28
28
  "laz_dragndrop": "LAS/LAZ viewer",
29
29
  "entwine_simple_loader": "Entwine loader",
30
- "entwine_3d_loader": "Entwine 3D loader"
30
+ "entwine_3d_loader": "Entwine 3D loader",
31
+ "copc_simple_loader": "COPC loader"
31
32
  },
32
33
 
33
34
  "Vector tiles": {
@@ -0,0 +1,128 @@
1
+ <html>
2
+ <head>
3
+ <meta charset="UTF-8">
4
+
5
+ <title>Itowns - COPC loader</title>
6
+
7
+ <link rel="stylesheet" type="text/css" href="css/example.css">
8
+ <link rel="stylesheet" type="text/css" href="css/LoadingScreen.css">
9
+
10
+ <style>
11
+ #description {
12
+ z-index: 2;
13
+ left: 10px;
14
+ }
15
+ </style>
16
+
17
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
18
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
19
+ </head>
20
+ <body>
21
+ <div id="description">Specify the URL of a COPC file to load:
22
+ <input type="text" id="url" />
23
+ <button onclick="readURL()">Load</button>
24
+ <div>
25
+ <button onClick="loadAutzen()">Load Autzen Stadium (80mb)</button>
26
+ <button onClick="loadSofi()">Load SoFI Stadium (2.3gb)</button>
27
+ <button onClick="loadMillsite()">Load Millsite (1.9gb)</button>
28
+ <div id="share"></div>
29
+ </div>
30
+ </div>
31
+ <div id="viewerDiv"></div>
32
+
33
+ <script src="../dist/itowns.js"></script>
34
+ <script src="js/GUI/LoadingScreen.js"></script>
35
+ <script src="../dist/debug.js"></script>
36
+ <script type="text/javascript">
37
+ let layer; // COPCLayer
38
+
39
+ const uri = new URL(location);
40
+
41
+ const gui = new dat.GUI();
42
+
43
+ const viewerDiv = document.getElementById('viewerDiv');
44
+ const view = new itowns.View('EPSG:4326', viewerDiv);
45
+ const controls = new itowns.PlanarControls(view);
46
+ view.mainLoop.gfxEngine.renderer.setClearColor(0xdddddd);
47
+
48
+ setUrl(uri.searchParams.get('copc'));
49
+
50
+ function onLayerReady(layer) {
51
+ const camera = view.camera.camera3D;
52
+
53
+ const lookAt = new itowns.THREE.Vector3();
54
+ const size = new itowns.THREE.Vector3();
55
+ layer.root.bbox.getSize(size);
56
+ layer.root.bbox.getCenter(lookAt);
57
+
58
+ camera.far = 2.0 * size.length();
59
+
60
+ controls.groundLevel = layer.root.bbox.min.z;
61
+ const position = layer.root.bbox.min.clone().add(
62
+ size.multiply({ x: 1, y: 1, z: size.x / size.z }),
63
+ );
64
+
65
+ camera.position.copy(position);
66
+ camera.lookAt(lookAt);
67
+ camera.updateProjectionMatrix();
68
+
69
+ view.notifyChange(camera);
70
+ }
71
+
72
+
73
+ function readURL() {
74
+ const url = document.getElementById('url').value;
75
+
76
+ if (url) {
77
+ setUrl(url);
78
+ }
79
+ }
80
+
81
+ function setUrl(url) {
82
+ if (!url) return;
83
+
84
+ const input_url = document.getElementById('url');
85
+ if (!input_url) return;
86
+
87
+ uri.searchParams.set('copc', url);
88
+ history.replaceState(null, null, `?${uri.searchParams.toString()}`);
89
+
90
+ input_url.value = url;
91
+ load(url);
92
+ }
93
+
94
+
95
+ function load(url) {
96
+ const source = new itowns.CopcSource({ url });
97
+
98
+ if (layer) {
99
+ gui.removeFolder(layer.debugUI);
100
+ view.removeLayer('COPC');
101
+ view.notifyChange();
102
+ layer.delete();
103
+ }
104
+
105
+ layer = new itowns.CopcLayer('COPC', {
106
+ source,
107
+ crs: view.referenceCrs,
108
+ sseThreshold: 2,
109
+ pointBudget: 3000000,
110
+ });
111
+ view.addLayer(layer).then(onLayerReady);
112
+ debug.PotreeDebug.initTools(view, layer, gui);
113
+ }
114
+
115
+ function loadAutzen() {
116
+ setUrl("https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz");
117
+ }
118
+
119
+ function loadSofi() {
120
+ setUrl("https://s3.amazonaws.com/hobu-lidar/sofi.copc.laz");
121
+ }
122
+
123
+ function loadMillsite() {
124
+ setUrl("https://s3.amazonaws.com/data.entwine.io/millsite.copc.laz");
125
+ }
126
+ </script>
127
+ </body>
128
+ </html>
@@ -0,0 +1,174 @@
1
+ import * as THREE from 'three';
2
+ import { Hierarchy } from 'copc';
3
+ import PointCloudNode from "./PointCloudNode.js";
4
+ const size = new THREE.Vector3();
5
+ const position = new THREE.Vector3();
6
+ const translation = new THREE.Vector3();
7
+ function buildId(depth, x, y, z) {
8
+ return `${depth}-${x}-${y}-${z}`;
9
+ }
10
+ class CopcNode extends PointCloudNode {
11
+ /**
12
+ * Constructs a new instance of a COPC Octree node
13
+ *
14
+ * @param {number} depth - Depth within the octree
15
+ * @param {number} x - X position within the octree
16
+ * @param {number} y - Y position within the octree
17
+ * @param {number} z - Z position with the octree
18
+ * @param {number} entryOffset - Offset from the beginning of the file of
19
+ * the node entry
20
+ * @param {number} entryLength - Size of the node entry
21
+ * @param {CopcLayer} layer - Parent COPC layer
22
+ * @param {number} [numPoints=0] - Number of points given by this entry
23
+ */
24
+ constructor(depth, x, y, z, entryOffset, entryLength, layer) {
25
+ let numPoints = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 0;
26
+ super(numPoints, layer);
27
+ this.isCopcNode = true;
28
+ this.entryOffset = entryOffset;
29
+ this.entryLength = entryLength;
30
+ this.layer = layer;
31
+ this.depth = depth;
32
+ this.x = x;
33
+ this.y = y;
34
+ this.z = z;
35
+ }
36
+ get id() {
37
+ return buildId(this.depth, this.x, this.y, this.z);
38
+ }
39
+ get octreeIsLoaded() {
40
+ return this.numPoints >= 0;
41
+ }
42
+
43
+ /**
44
+ * @param {number} offset
45
+ * @param {number} size
46
+ */
47
+ async _fetch(offset, size) {
48
+ return this.layer.source.fetcher(this.layer.source.url, {
49
+ ...this.layer.source.networkOptions,
50
+ headers: {
51
+ ...this.layer.source.networkOptions.headers,
52
+ range: `bytes=${offset}-${offset + size - 1}`
53
+ }
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Create an (A)xis (A)ligned (B)ounding (B)ox for the given node given
59
+ * `this` is its parent.
60
+ * @param {CopcNode} node - The child node
61
+ */
62
+ createChildAABB(node) {
63
+ // factor to apply, based on the depth difference (can be > 1)
64
+ const f = 2 ** (node.depth - this.depth);
65
+
66
+ // size of the child node bbox (Vector3), based on the size of the
67
+ // parent node, and divided by the factor
68
+ this.bbox.getSize(size).divideScalar(f);
69
+
70
+ // initialize the child node bbox at the location of the parent node bbox
71
+ node.bbox.min.copy(this.bbox.min);
72
+
73
+ // position of the parent node, if it was at the same depth as the
74
+ // child, found by multiplying the tree position by the factor
75
+ position.copy(this).multiplyScalar(f);
76
+
77
+ // difference in position between the two nodes, at child depth, and
78
+ // scale it using the size
79
+ translation.subVectors(node, position).multiply(size);
80
+
81
+ // apply the translation to the child node bbox
82
+ node.bbox.min.add(translation);
83
+
84
+ // use the size computed above to set the max
85
+ node.bbox.max.copy(node.bbox.min).add(size);
86
+ }
87
+
88
+ /**
89
+ * Create a CopcNode from the provided subtree and add it as child
90
+ * of the current node.
91
+ * @param {number} depth - Child node depth in the octree
92
+ * @param {number} x - Child node x position in the octree
93
+ * @param {number} y - Child node y position in the octree
94
+ * @param {number} z - Child node z position in the octree
95
+ * @param {Hierarchy.Subtree} hierarchy - Octree's subtree
96
+ * @param {CopcNode[]} stack - Stack of node candidates for traversal
97
+ */
98
+ findAndCreateChild(depth, x, y, z, hierarchy, stack) {
99
+ const id = buildId(depth, x, y, z);
100
+ let pointCount;
101
+ let offset;
102
+ let byteSize;
103
+ const node = hierarchy.nodes[id];
104
+ if (node) {
105
+ pointCount = node.pointCount;
106
+ offset = node.pointDataOffset;
107
+ byteSize = node.pointDataLength;
108
+ } else {
109
+ const page = hierarchy.pages[id];
110
+ if (!page) {
111
+ return;
112
+ }
113
+ pointCount = -1;
114
+ offset = page.pageOffset;
115
+ byteSize = page.pageLength;
116
+ }
117
+ const child = new CopcNode(depth, x, y, z, offset, byteSize, this.layer, pointCount);
118
+ this.add(child);
119
+ stack.push(child);
120
+ }
121
+ async loadOctree() {
122
+ // Load hierarchy
123
+ const buffer = await this._fetch(this.entryOffset, this.entryLength);
124
+ const hierarchy = await Hierarchy.parse(new Uint8Array(buffer));
125
+
126
+ // Update current node entry from loaded subtree
127
+ const node = hierarchy.nodes[this.id];
128
+ if (!node) {
129
+ return Promise.reject('[CopcNode]: Ill-formed data, entry not found in hierarchy.');
130
+ }
131
+ this.numPoints = node.pointCount;
132
+ this.entryOffset = node.pointDataOffset;
133
+ this.entryLength = node.pointDataLength;
134
+
135
+ // Load subtree entries
136
+ const stack = [];
137
+ stack.push(this);
138
+ while (stack.length) {
139
+ const node = stack.shift();
140
+ const depth = node.depth + 1;
141
+ const x = node.x * 2;
142
+ const y = node.y * 2;
143
+ const z = node.z * 2;
144
+ node.findAndCreateChild(depth, x, y, z, hierarchy, stack);
145
+ node.findAndCreateChild(depth, x + 1, y, z, hierarchy, stack);
146
+ node.findAndCreateChild(depth, x, y + 1, z, hierarchy, stack);
147
+ node.findAndCreateChild(depth, x + 1, y + 1, z, hierarchy, stack);
148
+ node.findAndCreateChild(depth, x, y, z + 1, hierarchy, stack);
149
+ node.findAndCreateChild(depth, x + 1, y, z + 1, hierarchy, stack);
150
+ node.findAndCreateChild(depth, x, y + 1, z + 1, hierarchy, stack);
151
+ node.findAndCreateChild(depth, x + 1, y + 1, z + 1, hierarchy, stack);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Load the COPC Buffer geometry for this node.
157
+ * @returns {Promise<THREE.BufferGeometry>}
158
+ */
159
+ async load() {
160
+ if (!this.octreeIsLoaded) {
161
+ await this.loadOctree();
162
+ }
163
+ const buffer = await this._fetch(this.entryOffset, this.entryLength);
164
+ const geometry = await this.layer.source.parser(buffer, {
165
+ in: {
166
+ ...this.layer.source,
167
+ pointCount: this.numPoints
168
+ },
169
+ out: this.layer
170
+ });
171
+ return geometry;
172
+ }
173
+ }
174
+ export default CopcNode;
@@ -0,0 +1,59 @@
1
+ import * as THREE from 'three';
2
+ import CopcNode from "../Core/CopcNode.js";
3
+ import PointCloudLayer from "./PointCloudLayer.js";
4
+
5
+ /**
6
+ * @classdesc
7
+ * A layer for [Cloud Optimised Point Cloud](https://copc.io) (COPC) datasets.
8
+ * See {@link PointCloudLayer} class for documentation on base properties.
9
+ *
10
+ * @extends {PointCloudLayer}
11
+ *
12
+ * @example
13
+ * // Create a new COPC layer
14
+ * const copcSource = new CopcSource({
15
+ * url: 'https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz',
16
+ * crs: 'EPSG:4978',
17
+ * colorDepth: 16, // bit-depth of 'color' attribute (either 8 or 16 bits)
18
+ * });
19
+ *
20
+ * const copcLayer = new CopcLayer('COPC', {
21
+ * source: copcSource,
22
+ * });
23
+ *
24
+ * View.prototype.addLayer.call(view, copcLayer);
25
+ */
26
+ class CopcLayer extends PointCloudLayer {
27
+ /**
28
+ * @param {string} id - Unique id of the layer.
29
+ * @param {Object} config - See {@link PointCloudLayer} for base pointcloud
30
+ * options.
31
+ */
32
+ constructor(id, config) {
33
+ super(id, config);
34
+ this.isCopcLayer = true;
35
+ const resolve = () => this;
36
+ this.whenReady = this.source.whenReady.then(( /** @type {CopcSource} */source) => {
37
+ const {
38
+ cube,
39
+ rootHierarchyPage
40
+ } = source.info;
41
+ const {
42
+ pageOffset,
43
+ pageLength
44
+ } = rootHierarchyPage;
45
+ this.root = new CopcNode(0, 0, 0, 0, pageOffset, pageLength, this, -1);
46
+ this.root.bbox.min.fromArray(cube, 0);
47
+ this.root.bbox.max.fromArray(cube, 3);
48
+ this.minElevationRange = source.header.min[2];
49
+ this.maxElevationRange = source.header.max[2];
50
+ this.scale = new THREE.Vector3(1.0, 1.0, 1.0);
51
+ this.offset = new THREE.Vector3(0.0, 0.0, 0.0);
52
+ return this.root.loadOctree().then(resolve);
53
+ });
54
+ }
55
+ get spacing() {
56
+ return this.source.info.spacing;
57
+ }
58
+ }
59
+ export default CopcLayer;
package/lib/Main.js CHANGED
@@ -60,6 +60,7 @@ export { default as GlobeLayer } from "./Core/Prefab/Globe/GlobeLayer.js";
60
60
  export { default as PlanarLayer } from "./Core/Prefab/Planar/PlanarLayer.js";
61
61
  export { default as LabelLayer } from "./Layer/LabelLayer.js";
62
62
  export { default as EntwinePointTileLayer } from "./Layer/EntwinePointTileLayer.js";
63
+ export { default as CopcLayer } from "./Layer/CopcLayer.js";
63
64
  export { default as GeoidLayer } from "./Layer/GeoidLayer.js";
64
65
 
65
66
  // Sources provided by default in iTowns
@@ -78,6 +79,7 @@ export { default as C3DTilesSource } from "./Source/C3DTilesSource.js";
78
79
  export { default as C3DTilesIonSource } from "./Source/C3DTilesIonSource.js";
79
80
  export { default as C3DTilesGoogleSource } from "./Source/C3DTilesGoogleSource.js";
80
81
  export { default as EntwinePointTileSource } from "./Source/EntwinePointTileSource.js";
82
+ export { default as CopcSource } from "./Source/CopcSource.js";
81
83
 
82
84
  // Parsers provided by default in iTowns
83
85
  // Custom parser can be implemented as wanted, as long as the main function
@@ -15,6 +15,10 @@ import { Las } from 'copc';
15
15
  * xOffset, zOffset]`) added to the scaled X, Y, Z point record values.
16
16
  */
17
17
 
18
+ function defaultColorEncoding(header) {
19
+ return header.majorVersion === 1 && header.minorVersion <= 2 ? 8 : 16;
20
+ }
21
+
18
22
  /**
19
23
  * @classdesc
20
24
  * Loader for LAS and LAZ (LASZip) point clouds. It uses the copc.js library and
@@ -117,6 +121,46 @@ class LASLoader {
117
121
  this._wasmPromise = null;
118
122
  }
119
123
 
124
+ /**
125
+ * Parses a LAS or LAZ (LASZip) chunk. Note that this function is
126
+ * **CPU-bound** and shall be parallelised in a dedicated worker.
127
+ * @param {Uint8Array} data - File chunk data.
128
+ * @param {Object} options - Parsing options.
129
+ * @param {Header} options.header - Partial LAS header.
130
+ * @param {number} options.pointCount - Number of points encoded in this
131
+ * data chunk.
132
+ * @param {Las.ExtraBytes[]} [options.eb] - Extra bytes LAS VLRs
133
+ * headers.
134
+ * @param {8 | 16} [options.colorDepth] - Color depth encoding (in bits).
135
+ * Either 8 or 16 bits. Defaults to 8 bits for LAS 1.2 and 16 bits for later
136
+ * versions (as mandatory by the specification).
137
+ */
138
+ async parseChunk(data, options) {
139
+ const {
140
+ header,
141
+ eb,
142
+ pointCount
143
+ } = options;
144
+ const {
145
+ pointDataRecordFormat,
146
+ pointDataRecordLength
147
+ } = header;
148
+ const colorDepth = options.colorDepth ?? defaultColorEncoding(header);
149
+ const bytes = new Uint8Array(data);
150
+ const pointData = await Las.PointData.decompressChunk(bytes, {
151
+ pointCount,
152
+ pointDataRecordFormat,
153
+ pointDataRecordLength
154
+ }, this._initDecoder());
155
+ const view = Las.View.create(pointData, header, eb);
156
+ const attributes = this._parseView(view, {
157
+ colorDepth
158
+ });
159
+ return {
160
+ attributes
161
+ };
162
+ }
163
+
120
164
  /**
121
165
  * Parses a LAS or LAZ (LASZip) file. Note that this function is
122
166
  * **CPU-bound** and shall be parallelised in a dedicated worker.
@@ -131,7 +175,7 @@ class LASLoader {
131
175
  const bytes = new Uint8Array(data);
132
176
  const pointData = await Las.PointData.decompressFile(bytes, this._initDecoder());
133
177
  const header = Las.Header.parse(bytes);
134
- const colorDepth = options.colorDepth ?? (header.majorVersion === 1 && header.minorVersion <= 2 ? 8 : 16);
178
+ const colorDepth = options.colorDepth ?? defaultColorEncoding(header);
135
179
  const getter = async (begin, end) => bytes.slice(begin, end);
136
180
  const vlrs = await Las.Vlr.walk(getter, header);
137
181
  const ebVlr = Las.Vlr.find(vlrs, 'LASF_Spec', 4);
@@ -1,6 +1,29 @@
1
1
  import * as THREE from 'three';
2
2
  import LASLoader from "./LASLoader.js";
3
3
  const lasLoader = new LASLoader();
4
+ function buildBufferGeometry(attributes) {
5
+ const geometry = new THREE.BufferGeometry();
6
+ const positionBuffer = new THREE.BufferAttribute(attributes.position, 3);
7
+ geometry.setAttribute('position', positionBuffer);
8
+ const intensityBuffer = new THREE.BufferAttribute(attributes.intensity, 1);
9
+ geometry.setAttribute('intensity', intensityBuffer);
10
+ const returnNumber = new THREE.BufferAttribute(attributes.returnNumber, 1);
11
+ geometry.setAttribute('returnNumber', returnNumber);
12
+ const numberOfReturns = new THREE.BufferAttribute(attributes.numberOfReturns, 1);
13
+ geometry.setAttribute('numberOfReturns', numberOfReturns);
14
+ const classBuffer = new THREE.BufferAttribute(attributes.classification, 1);
15
+ geometry.setAttribute('classification', classBuffer);
16
+ const pointSourceID = new THREE.BufferAttribute(attributes.pointSourceID, 1);
17
+ geometry.setAttribute('pointSourceID', pointSourceID);
18
+ if (attributes.color) {
19
+ const colorBuffer = new THREE.BufferAttribute(attributes.color, 4, true);
20
+ geometry.setAttribute('color', colorBuffer);
21
+ }
22
+ const scanAngle = new THREE.BufferAttribute(attributes.scanAngle, 1);
23
+ geometry.setAttribute('scanAngle', scanAngle);
24
+ geometry.userData.origin = new THREE.Vector3().fromArray(attributes.origin);
25
+ return geometry;
26
+ }
4
27
 
5
28
  /** The LASParser module provides a [parse]{@link
6
29
  * module:LASParser.parse} method that takes a LAS or LAZ (LASZip) file in, and
@@ -21,6 +44,36 @@ export default {
21
44
  }
22
45
  lasLoader.lazPerf = path;
23
46
  },
47
+ /**
48
+ * Parses a chunk of a LAS or LAZ (LASZip) and returns the corresponding
49
+ * `THREE.BufferGeometry`.
50
+ *
51
+ * @param {ArrayBuffer} data - The file content to parse.
52
+ * @param {Object} options
53
+ * @param {Object} options.in - Options to give to the parser.
54
+ * @param {number} options.in.pointCount - Number of points encoded in this
55
+ * data chunk.
56
+ * @param {Object} options.in.header - Partial LAS file header.
57
+ * @param {number} options.in.header.pointDataRecordFormat - Type of Point
58
+ * Data Record contained in the LAS file.
59
+ * @param {number} options.in.header.pointDataRecordLength - Size (in bytes)
60
+ * of the Point Data Record.
61
+ * @param {Object} [options.eb] - Extra bytes LAS VLRs headers.
62
+ * @param { 8 | 16 } [options.in.colorDepth] - Color depth (in bits).
63
+ * Defaults to 8 bits for LAS 1.2 and 16 bits for later versions
64
+ * (as mandatory by the specification)
65
+ *
66
+ * @return {Promise<THREE.BufferGeometry>} A promise resolving with a
67
+ * `THREE.BufferGeometry`.
68
+ */
69
+ parseChunk(data) {
70
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
71
+ return lasLoader.parseChunk(data, options.in).then(parsedData => {
72
+ const geometry = buildBufferGeometry(parsedData.attributes);
73
+ geometry.computeBoundingBox();
74
+ return geometry;
75
+ });
76
+ },
24
77
  /**
25
78
  * Parses a LAS file or a LAZ (LASZip) file and return the corresponding
26
79
  * `THREE.BufferGeometry`.
@@ -44,29 +97,9 @@ export default {
44
97
  return lasLoader.parseFile(data, {
45
98
  colorDepth: (_options$in = options.in) === null || _options$in === void 0 ? void 0 : _options$in.colorDepth
46
99
  }).then(parsedData => {
47
- const geometry = new THREE.BufferGeometry();
48
- const attributes = parsedData.attributes;
49
- geometry.userData = parsedData.header;
50
- const positionBuffer = new THREE.BufferAttribute(attributes.position, 3);
51
- geometry.setAttribute('position', positionBuffer);
52
- const intensityBuffer = new THREE.BufferAttribute(attributes.intensity, 1);
53
- geometry.setAttribute('intensity', intensityBuffer);
54
- const returnNumber = new THREE.BufferAttribute(attributes.returnNumber, 1);
55
- geometry.setAttribute('returnNumber', returnNumber);
56
- const numberOfReturns = new THREE.BufferAttribute(attributes.numberOfReturns, 1);
57
- geometry.setAttribute('numberOfReturns', numberOfReturns);
58
- const classBuffer = new THREE.BufferAttribute(attributes.classification, 1);
59
- geometry.setAttribute('classification', classBuffer);
60
- const pointSourceID = new THREE.BufferAttribute(attributes.pointSourceID, 1);
61
- geometry.setAttribute('pointSourceID', pointSourceID);
62
- if (attributes.color) {
63
- const colorBuffer = new THREE.BufferAttribute(attributes.color, 4, true);
64
- geometry.setAttribute('color', colorBuffer);
65
- }
66
- const scanAngle = new THREE.BufferAttribute(attributes.scanAngle, 1);
67
- geometry.setAttribute('scanAngle', scanAngle);
100
+ const geometry = buildBufferGeometry(parsedData.attributes);
101
+ geometry.userData.header = parsedData.header;
68
102
  geometry.computeBoundingBox();
69
- geometry.userData.origin = new THREE.Vector3().fromArray(attributes.origin);
70
103
  return geometry;
71
104
  });
72
105
  }
@@ -0,0 +1,118 @@
1
+ import { Binary, Info, Las } from 'copc';
2
+ import Extent from "../Core/Geographic/Extent.js";
3
+ import Fetcher from "../Provider/Fetcher.js";
4
+ import LASParser from "../Parser/LASParser.js";
5
+ import Source from "./Source.js";
6
+ import * as THREE from 'three';
7
+
8
+ /**
9
+ * @param {function(number, number):Promise<Uint8Array>} fetcher
10
+ */
11
+ async function getHeaders(fetcher) {
12
+ const header = Las.Header.parse(await fetcher(0, Las.Constants.minHeaderLength));
13
+ const vlrs = await Las.Vlr.walk(fetcher, header);
14
+
15
+ // info VLR: required by COPC
16
+ const infoVlr = Las.Vlr.find(vlrs, 'copc', 1);
17
+ if (!infoVlr) {
18
+ return Promise.reject('COPC info VLR is required');
19
+ }
20
+ const info = Info.parse(await Las.Vlr.fetch(fetcher, infoVlr));
21
+
22
+ // OGC Coordinate System WKT: required by LAS1.4
23
+ const wktVlr = Las.Vlr.find(vlrs, 'LASF_Projection', 2112);
24
+ if (!wktVlr) {
25
+ return Promise.reject('LAS1.4 WKT VLR is required');
26
+ }
27
+ const wkt = Binary.toCString(await Las.Vlr.fetch(fetcher, wktVlr));
28
+
29
+ // Extra bytes: optional by LAS1.4
30
+ const ebVlr = Las.Vlr.find(vlrs, 'LASF_Spec', 4);
31
+ const eb = ebVlr ? Las.ExtraBytes.parse(await Las.Vlr.fetch(fetcher, ebVlr)) : [];
32
+ return {
33
+ header,
34
+ info,
35
+ wkt,
36
+ eb
37
+ };
38
+ }
39
+
40
+ /**
41
+ * @classdesc
42
+ * A source for [Cloud Optimised Point Cloud](https://copc.io/) (COPC) data.
43
+ * Such data consists of a [LAZ 1.4](https://www.ogc.org/standard/las/) file
44
+ * that stores compressed points data organized in a clustered octree.
45
+ *
46
+ * A freshly created source fetches and parses portions of the file
47
+ * corresponding to the LAS 1.4 header, all the Variable Length Record (VLR)
48
+ * headers as well the following VLRs:
49
+ * - COPC [`info`](https://copc.io/#info-vlr) record (mandatory)
50
+ * - LAS 1.4 `OGC Coordinate System WKT` record (mandatory, see [Las 1.4
51
+ * spec](https://portal.ogc.org/files/?artifact_id=74523))
52
+ * - LAS 1.4 `Extra Bytes` record (optional, see [Las 1.4
53
+ * spec](https://portal.ogc.org/files/?artifact_id=74523))
54
+ *
55
+ * @extends {Source}
56
+ *
57
+ * @property {boolean} isCopcSource - Read-only flag to check that a given
58
+ * object is of type CopcSource.
59
+ * @property {Object} header - LAS header of the source.
60
+ * @property {Object[]} eb - List of headers of each Variable Length Records
61
+ * (VLRs).
62
+ * @property {Object} info - COPC `info` VLR.
63
+ * @property {number[]} info.cube - Bounding box of the octree as a 6-elements.
64
+ * tuple `[minX, minY, minZ, maxX, maxY, maxZ]`. Computed from `center_x`,
65
+ * `center_y`, `center_z` and `halfSize` properties.
66
+ * @property {Object} info.rootHierarchyPage - Hierarchy page of the root node.
67
+ * @property {number} info.rootHierarchyPage.pageOffset - Absolute Offset to the
68
+ * root node data chunk.
69
+ * @property {number} info.rootHierarchyPage.pageOffset - Size (in bytes) of the
70
+ * root node data chunk.
71
+ * @property {number[]} gpsTimeRange - A 2-element tuple denoting the minimum
72
+ * and maximum values of attribute `gpsTime`.
73
+ */
74
+ class CopcSource extends Source {
75
+ /**
76
+ * @param {Object} config - Source configuration
77
+ * @param {string} config.url - URL of the COPC resource.
78
+ * @param {8 | 16} [config.colorDepth=16] - Encoding of the `color`
79
+ * attribute. Either `8` or `16` bits.
80
+ * @param {string} [config._lazPerfBaseUrl] - (experimental) Overrides base
81
+ * url of the `las-zip.wasm` file of the `laz-perf` library.
82
+ * @param {string} [config.crs='EPSG:4326'] - Native CRS of the COPC
83
+ * ressource. Note that this is not for now inferred from the COPC header.
84
+ * @param {RequestInit} [config.networkOptions] - Fetch options (passed
85
+ * directly to `fetch()`), see [the syntax for more information]{@link
86
+ * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax}.
87
+ * @param {Object} [config.attribution] - Attribution of the data.
88
+ *
89
+ * @constructor
90
+ */
91
+ constructor(config) {
92
+ super(config);
93
+ this.isCopcSource = true;
94
+ this.parser = LASParser.parseChunk;
95
+ this.fetcher = Fetcher.arrayBuffer;
96
+ this.colorDepth = config.colorDepth ?? 16;
97
+ const get = ( /** @type {number} */begin, /** @type {number} */end) => this.fetcher(this.url, {
98
+ ...this.networkOptions,
99
+ headers: {
100
+ ...this.networkOptions.headers,
101
+ range: `bytes=${begin}-${end - 1}`
102
+ }
103
+ }).then(buffer => new Uint8Array(buffer));
104
+ this.whenReady = getHeaders(get).then(metadata => {
105
+ this.header = metadata.header;
106
+ this.info = metadata.info;
107
+ this.eb = metadata.eb;
108
+ // TODO: use wkt definition in `metadata.wkt` to infer/define crs
109
+ this.crs = config.crs || 'EPSG:4326';
110
+ const bbox = new THREE.Box3();
111
+ bbox.min.fromArray(this.info.cube, 0);
112
+ bbox.max.fromArray(this.info.cube, 3);
113
+ this.extent = Extent.fromBox3(this.crs, bbox);
114
+ return this;
115
+ });
116
+ }
117
+ }
118
+ export default CopcSource;