diff --git a/examples/entwine_3d_loader.html b/examples/entwine_3d_loader.html
index bc75139d36..923dca6675 100644
--- a/examples/entwine_3d_loader.html
+++ b/examples/entwine_3d_loader.html
@@ -87,8 +87,10 @@
eptSource = new itowns.EntwinePointTileSource({ url });
if (eptLayer) {
- view.removeLayer('ept');
+ debugGui.removeFolder(eptLayer.debugUI);
+ view.removeLayer('Entwine Point Tile');
view.notifyChange();
+ eptLayer.delete();
}
eptLayer = new itowns.EntwinePointTileLayer('Entwine Point Tile', {
diff --git a/examples/entwine_simple_loader.html b/examples/entwine_simple_loader.html
index 15e436e5e2..deb80d8924 100644
--- a/examples/entwine_simple_loader.html
+++ b/examples/entwine_simple_loader.html
@@ -88,7 +88,7 @@
eptSource = new itowns.EntwinePointTileSource({ url });
if (eptLayer) {
- debugGUI.removeFolder(eptLayer.debugUI);
+ debugGui.removeFolder(eptLayer.debugUI);
view.removeLayer('Entwine Point Tile');
view.notifyChange();
eptLayer.delete();
diff --git a/src/Core/Geographic/Crs.ts b/src/Core/Geographic/Crs.ts
index 9580dcfb77..ec92c55b51 100644
--- a/src/Core/Geographic/Crs.ts
+++ b/src/Core/Geographic/Crs.ts
@@ -35,6 +35,10 @@ export const UNIT = {
* Distance unit in meter.
*/
METER: 2,
+ /**
+ * Distance unit in foot.
+ */
+ FOOT: 3,
} as const;
/**
@@ -50,8 +54,10 @@ export function is4326(crs: ProjectionLike) {
function unitFromProj4Unit(proj: ProjectionDefinition) {
if (proj.units === 'degrees') {
return UNIT.DEGREE;
- } else if (proj.units === 'm') {
+ } else if (proj.units === 'm' || proj.units === 'meter') {
return UNIT.METER;
+ } else if (proj.units === 'foot') {
+ return UNIT.FOOT;
} else if (proj.units === undefined && proj.to_meter === undefined) {
// See https://proj.org/en/9.4/usage/projections.html [17/10/2024]
// > The default unit for projected coordinates is the meter.
@@ -65,7 +71,7 @@ function unitFromProj4Unit(proj: ProjectionDefinition) {
* Returns the horizontal coordinates system units associated with this CRS.
*
* @param crs - The CRS to extract the unit from.
- * @returns Either `UNIT.METER`, `UNIT.DEGREE` or `undefined`.
+ * @returns Either `UNIT.METER`, `UNIT.DEGREE`, `UNIT.FOOT` or `undefined`.
*/
export function getUnit(crs: ProjectionLike) {
mustBeString(crs);
diff --git a/src/Layer/PointCloudLayer.js b/src/Layer/PointCloudLayer.js
index 65ce078dc5..fa128c8395 100644
--- a/src/Layer/PointCloudLayer.js
+++ b/src/Layer/PointCloudLayer.js
@@ -328,10 +328,6 @@ class PointCloudLayer extends GeometryLayer {
redraw: true,
earlyDropFunction: cmd => !cmd.requester.visible || !this.visible,
}).then((pts) => {
- if (this.onPointsCreated) {
- this.onPointsCreated(layer, pts);
- }
-
elt.obj = pts;
// store tightbbox to avoid ping-pong (bbox = larger => visible, tight => invisible)
elt.tightbbox = pts.tightbbox;
@@ -340,12 +336,12 @@ class PointCloudLayer extends GeometryLayer {
// be added nor cleaned
this.group.add(elt.obj);
elt.obj.updateMatrixWorld(true);
-
- elt.promise = null;
- }, (err) => {
- if (err.isCancelledCommandException) {
- elt.promise = null;
+ }).catch((err) => {
+ if (!err.isCancelledCommandException) {
+ return err;
}
+ }).finally(() => {
+ elt.promise = null;
});
}
}
diff --git a/src/Source/CopcSource.js b/src/Source/CopcSource.js
index 905c600803..9f91f27f57 100644
--- a/src/Source/CopcSource.js
+++ b/src/Source/CopcSource.js
@@ -1,3 +1,4 @@
+import proj4 from 'proj4';
import { Binary, Info, Las } from 'copc';
import Extent from 'Core/Geographic/Extent';
import Fetcher from 'Provider/Fetcher';
@@ -102,8 +103,21 @@ class CopcSource extends Source {
this.header = metadata.header;
this.info = metadata.info;
this.eb = metadata.eb;
- // TODO: use wkt definition in `metadata.wkt` to infer/define crs
- this.crs = config.crs || 'EPSG:4326';
+
+ proj4.defs('unknown', metadata.wkt);
+ let projCS;
+
+ if (proj4.defs('unknown').type === 'COMPD_CS') {
+ console.warn('CopcSource: compound coordinate system is not yet supported.');
+ projCS = proj4.defs('unknown').PROJCS;
+ } else {
+ projCS = proj4.defs('unknown');
+ }
+
+ this.crs = projCS.title || projCS.name || 'EPSG:4326';
+ if (!(this.crs in proj4.defs)) {
+ proj4.defs(this.crs, projCS);
+ }
const bbox = new THREE.Box3();
bbox.min.fromArray(this.info.cube, 0);
diff --git a/src/Source/EntwinePointTileSource.js b/src/Source/EntwinePointTileSource.js
index 97a83dcb44..ed113bb3ac 100644
--- a/src/Source/EntwinePointTileSource.js
+++ b/src/Source/EntwinePointTileSource.js
@@ -40,12 +40,20 @@ class EntwinePointTileSource extends Source {
this.parse = metadata.dataType === 'laszip' ? LASParser.parse : PotreeBinParser.parse;
this.extension = metadata.dataType === 'laszip' ? 'laz' : 'bin';
- if (metadata.srs && metadata.srs.authority && metadata.srs.horizontal) {
- this.crs = `${metadata.srs.authority}:${metadata.srs.horizontal}`;
- if (!proj4.defs(this.crs)) {
- proj4.defs(this.crs, metadata.srs.wkt);
+ if (metadata.srs) {
+ if (metadata.srs.authority && metadata.srs.horizontal) {
+ this.crs = `${metadata.srs.authority}:${metadata.srs.horizontal}`;
+ if (!proj4.defs(this.crs)) {
+ proj4.defs(this.crs, metadata.srs.wkt);
+ }
+ } else if (metadata.srs.wkt) {
+ proj4.defs('unknown', metadata.srs.wkt);
+ const projCS = proj4.defs('unknown');
+ this.crs = projCS.title || projCS.name;
+ if (!(this.crs in proj4.defs)) {
+ proj4.defs(this.crs, projCS);
+ }
}
-
if (metadata.srs.vertical && metadata.srs.vertical !== metadata.srs.horizontal) {
console.warn('EntwinePointTileSource: Vertical coordinates system code is not yet supported.');
}
@@ -58,6 +66,7 @@ class EntwinePointTileSource extends Source {
+ Math.abs(metadata.boundsConforming[4] - metadata.boundsConforming[1])) / (2 * metadata.span);
this.boundsConforming = metadata.boundsConforming;
+ this.bounds = metadata.bounds;
this.span = metadata.span;
return this;
diff --git a/test/unit/copc.js b/test/unit/copc.js
new file mode 100644
index 0000000000..c4d9286d34
--- /dev/null
+++ b/test/unit/copc.js
@@ -0,0 +1,31 @@
+import assert from 'assert';
+import { HttpsProxyAgent } from 'https-proxy-agent';
+import CopcSource from 'Source/CopcSource';
+
+const copcUrl = 'https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz';
+
+describe('COPC', function () {
+ let source;
+
+ describe('Copc Source', function () {
+ describe('retrieving crs from wkt information', function () {
+ it('wkt.srs.type is COMPD_CS', function (done) {
+ const networkOptions = process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {};
+ source = new CopcSource({
+ url: copcUrl,
+ networkOptions,
+ });
+ source.whenReady
+ .then((headers) => {
+ assert.ok(headers.header.pointCount);
+ assert.ok(headers.info.spacing);
+ assert.ok(Array.isArray(headers.eb));
+ assert.equal(source.crs, 'NAD83 / Oregon GIC Lambert (ft)');
+ // when the proj4 PR will be merged we should change to :
+ // assert.equal(source.crs, 'EPSG:2992');
+ done();
+ }).catch(done);
+ }).timeout(5000);
+ });
+ });
+});
diff --git a/test/unit/entwine.js b/test/unit/entwine.js
index 7a96be6bdd..43fc24237e 100644
--- a/test/unit/entwine.js
+++ b/test/unit/entwine.js
@@ -10,16 +10,23 @@ import sinon from 'sinon';
import Fetcher from 'Provider/Fetcher';
import Renderer from './bootstrap';
-import ept from '../data/entwine/ept.json';
-import eptHierarchy from '../data/entwine/ept-hierarchy/0-0-0-0.json';
+import eptFile from '../data/entwine/ept.json';
+import eptHierarchyFile from '../data/entwine/ept-hierarchy/0-0-0-0.json';
-const baseurl = 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds';
-const urlEpt = `${baseurl}/entwine/ept.json`;
-const urlEptHierarchy = `${baseurl}/entwine/ept-hierarchy/0-0-0-0.json`;
+// LASParser need to be mocked instead of calling it
+LASParser.enableLazPerf('./examples/libs/laz-perf');
+
+const baseurl = 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/entwine';
+
+const eptSsAuthority = JSON.parse(eptFile);
+eptSsAuthority.srs = {
+ wkt: 'PROJCS["RGF93 v1 / Lambert-93",GEOGCS["RGF93 v1",DATUM["Reseau_Geodesique_Francais_1993_v1",SPHEROID["GRS 1980",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4171"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["latitude_of_origin",46.5],PARAMETER["central_meridian",3],PARAMETER["standard_parallel_1",49],PARAMETER["standard_parallel_2",44],PARAMETER["false_easting",700000],PARAMETER["false_northing",6600000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","2154"]]',
+};
const resources = {
- [urlEpt]: ept,
- [urlEptHierarchy]: eptHierarchy,
+ [`${baseurl}/ept.json`]: JSON.parse(eptFile),
+ 'withoutAutority/ept.json': eptSsAuthority,
+ [`${baseurl}/ept-hierarchy/0-0-0-0.json`]: JSON.parse(eptHierarchyFile),
};
describe('Entwine Point Tile', function () {
@@ -29,15 +36,12 @@ describe('Entwine Point Tile', function () {
before(function () {
stubFetcherJson = sinon.stub(Fetcher, 'json')
- .callsFake(url => Promise.resolve(JSON.parse(resources[url])));
+ .callsFake(url => Promise.resolve(resources[url]));
stubFetcherArrayBuf = sinon.stub(Fetcher, 'arrayBuffer')
.callsFake(() => Promise.resolve(new ArrayBuffer()));
// currently no test on data fetched...
LASParser.enableLazPerf('./examples/libs/laz-perf');
- source = new EntwinePointTileSource({
- url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/entwine',
- });
});
after(async function () {
@@ -46,11 +50,32 @@ describe('Entwine Point Tile', function () {
await LASParser.terminate();
});
- it('loads the EPT structure', (done) => {
- source.whenReady
- .then(() => {
- done();
- }).catch(done);
+ describe('Entwine Point Tile Source', function () {
+ describe('data type', function () {
+ // TO DO dataType in [laszip, binary, zstandard]
+ });
+ describe('retrieving crs from srs information', function () {
+ it('No srs authority', (done) => {
+ source = new EntwinePointTileSource({
+ url: 'withoutAutority',
+ });
+ source.whenReady
+ .then(() => {
+ assert.equal(source.crs, 'RGF93 v1 / Lambert-93');
+ done();
+ }).catch(done);
+ });
+ it('With srs authority', (done) => {
+ source = new EntwinePointTileSource({
+ url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/entwine',
+ });
+ source.whenReady
+ .then(() => {
+ assert.equal(source.crs, 'EPSG:3857');
+ done();
+ }).catch(done);
+ });
+ });
});
describe('Layer', function () {
diff --git a/test/unit/fetcher.js b/test/unit/fetcher.js
index b2b60f6083..dc365947fb 100644
--- a/test/unit/fetcher.js
+++ b/test/unit/fetcher.js
@@ -70,7 +70,8 @@ describe('Fetcher', function () {
describe('texture', function () {
// Fetcher.texture always send a texture even with a false url...
const url = 'https://data.geopf.fr/wmts?' +
- 'LAYER=ORTHOIMAGERY.ORTHOPHOTOS&FORMAT=image/jpeg&SERVICE=WMTS&VERSION=1.0.0&' +
+ 'LAYER=ORTHOIMAGERY.ORTHOPHOTOS&FORMAT=image/jpeg' +
+ '&SERVICE=WMTS&VERSION=1.0.0&' +
'REQUEST=GetTile&STYLE=normal&' +
'TILEMATRIXSET=PM&TILEMATRIX=2&TILEROW=1&TILECOL=1';
it('should load a texture', (done) => {
diff --git a/test/unit/lasparser.js b/test/unit/lasparser.js
index 4ce2226eb0..3c91d8b5ed 100644
--- a/test/unit/lasparser.js
+++ b/test/unit/lasparser.js
@@ -10,16 +10,26 @@ const lasUrl = `${baseurl}/data_test.las`;
const url = 'https://github.com/connormanning/copc.js/raw/master/src/test/data';
const lazV14Url = `${url}/ellipsoid-1.4.laz`;
+const copcUrl = `${url}/ellipsoid.copc.laz`;
+
describe('LASParser', function () {
let lasData;
let lazV14Data;
- it('fetch binaries', async function () {
+ let copcData;
+ describe('fetch binaries', function () {
const networkOptions = process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {};
- lasData = await Fetcher.arrayBuffer(lasUrl, networkOptions);
- lazV14Data = await Fetcher.arrayBuffer(lazV14Url, networkOptions);
- }).timeout(4000);
+ it('fetch las data', async function () {
+ lasData = await Fetcher.arrayBuffer(lasUrl, networkOptions);
+ });
+ it('fetch laz data', async function () {
+ lazV14Data = await Fetcher.arrayBuffer(lazV14Url, networkOptions);
+ });
+ it('fetch copc data', async function _it() {
+ copcData = await Fetcher.arrayBuffer(copcUrl, networkOptions);
+ });
+ });
- describe('unit tests', function () {
+ describe('unit tests', function _describe() {
const epsilon = 0.1;
LASParser.enableLazPerf('./examples/libs/laz-perf');
@@ -40,6 +50,7 @@ describe('LASParser', function () {
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x + origin.x, header.max[0], epsilon));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y + origin.y, header.max[1], epsilon));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z + origin.z, header.max[2], epsilon));
+ await LASParser.terminate();
});
it('parses a laz file to a THREE.BufferGeometry', async function () {
@@ -59,9 +70,55 @@ describe('LASParser', function () {
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x + origin.x, header.max[0], epsilon));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y + origin.y, header.max[1], epsilon));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z + origin.z, header.max[2], epsilon));
+ await LASParser.terminate();
});
- afterEach(async function () {
+ it('parses a copc chunk to a THREE.BufferGeometry', async function _it() {
+ if (!copcData) { this.skip(); }
+ const header = {
+ fileSignature: 'LASF',
+ fileSourceId: 0,
+ globalEncoding: 16,
+ projectId: '00000000-0000-0000-0000000000000000',
+ majorVersion: 1,
+ minorVersion: 4,
+ systemIdentifier: '',
+ generatingSoftware: '',
+ fileCreationDayOfYear: 1,
+ fileCreationYear: 1,
+ headerLength: 375,
+ pointDataOffset: 1424,
+ vlrCount: 3,
+ pointDataRecordFormat: 7,
+ pointDataRecordLength: 36,
+ pointCount: 100000,
+ pointCountByReturn: [
+ 50002, 49998, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0,
+ ],
+ scale: [0.01, 0.01, 0.01],
+ offset: [-8242596, 4966606, 0],
+ min: [-8242746, 4966506, -50],
+ max: [-8242446, 4966706, 50],
+ waveformDataOffset: 0,
+ evlrOffset: 630520,
+ evlrCount: 1,
+ };
+ const options = {
+ in: {
+ pointCount: header.pointCount,
+ header,
+ },
+ // eb,
+ };
+ const bufferGeometry = await LASParser.parseChunk(copcData, options);
+
+ assert.strictEqual(bufferGeometry.attributes.position.count, header.pointCount);
+ assert.strictEqual(bufferGeometry.attributes.intensity.count, header.pointCount);
+ assert.strictEqual(bufferGeometry.attributes.classification.count, header.pointCount);
+ assert.strictEqual(bufferGeometry.attributes.color.count, header.pointCount);
await LASParser.terminate();
});
});
diff --git a/test/unit/potree.js b/test/unit/potree.js
index 2d1f3bee10..b744b603a7 100644
--- a/test/unit/potree.js
+++ b/test/unit/potree.js
@@ -65,7 +65,6 @@ describe('Potree', function () {
// Configure Point Cloud layer
potreeLayer = new PotreeLayer('lion_takanawa', {
source,
- onPointsCreated: () => {},
crs: viewer.referenceCrs,
});
diff --git a/test/unit/potree2.js b/test/unit/potree2.js
index ca5ff065cd..ff1b48a0c7 100644
--- a/test/unit/potree2.js
+++ b/test/unit/potree2.js
@@ -27,7 +27,6 @@ describe('Potree2', function () {
url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/potree2.0/lion',
networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {},
}),
- onPointsCreated: () => {},
crs: viewer.referenceCrs,
});
@@ -63,7 +62,7 @@ describe('Potree2', function () {
assert.equal(potreeLayer.group.children.length, 1);
done();
}).catch(done);
- });
+ }).timeout(5000);
it('postUpdate potree2 layer', function () {
potreeLayer.postUpdate(context, potreeLayer);