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);