diff --git a/modules/i3s/src/index.ts b/modules/i3s/src/index.ts index 78ea82b813..9f5930a34f 100644 --- a/modules/i3s/src/index.ts +++ b/modules/i3s/src/index.ts @@ -29,7 +29,6 @@ export type { Histogram, ValueCount, BuildingSceneSublayer, - DATA_TYPE, OperationalLayer, TextureSetDefinitionFormats } from './types'; diff --git a/modules/i3s/src/lib/parsers/constants.ts b/modules/i3s/src/lib/parsers/constants.ts index 7ff2377c82..102c690b5d 100644 --- a/modules/i3s/src/lib/parsers/constants.ts +++ b/modules/i3s/src/lib/parsers/constants.ts @@ -1,17 +1,16 @@ import GL from '@luma.gl/constants'; -import {DATA_TYPE} from '../../types'; export function getConstructorForDataFormat(dataType: string) { switch (dataType) { - case DATA_TYPE.UInt8: + case 'UInt8': return Uint8Array; - case DATA_TYPE.UInt16: + case 'UInt16': return Uint16Array; - case DATA_TYPE.UInt32: + case 'UInt32': return Uint32Array; - case DATA_TYPE.Float32: + case 'Float32': return Float32Array; - case DATA_TYPE.UInt64: + case 'UInt64': return Float64Array; default: throw new Error(`parse i3s tile content: unknown type of data: ${dataType}`); @@ -32,18 +31,18 @@ export const GL_TYPE_MAP: {[key: string]: number} = { */ export function sizeOf(dataType: string): number { switch (dataType) { - case DATA_TYPE.UInt8: + case 'UInt8': return 1; - case DATA_TYPE.UInt16: - case DATA_TYPE.Int16: + case 'UInt16': + case 'Int16': return 2; - case DATA_TYPE.UInt32: - case DATA_TYPE.Int32: - case DATA_TYPE.Float32: + case 'UInt32': + case 'Int32': + case 'Float32': return 4; - case DATA_TYPE.UInt64: - case DATA_TYPE.Int64: - case DATA_TYPE.Float64: + case 'UInt64': + case 'Int64': + case 'Float64': return 8; default: throw new Error(`parse i3s tile content: unknown size of data: ${dataType}`); diff --git a/modules/i3s/src/lib/parsers/parse-slpk/slpk-archieve.ts b/modules/i3s/src/lib/parsers/parse-slpk/slpk-archieve.ts index e588e8dd09..a84dacf1a1 100644 --- a/modules/i3s/src/lib/parsers/parse-slpk/slpk-archieve.ts +++ b/modules/i3s/src/lib/parsers/parse-slpk/slpk-archieve.ts @@ -26,7 +26,7 @@ const PATH_DESCRIPTIONS: {test: RegExp; extensions: string[]}[] = [ extensions: ['.json.gz'] }, { - test: /^nodes\/\d+$/, + test: /^nodes\/(\d+|root)$/, extensions: ['/3dNodeIndexDocument.json.gz'] }, { diff --git a/modules/i3s/src/types.ts b/modules/i3s/src/types.ts index ef74a7ce54..f0e840ffb7 100644 --- a/modules/i3s/src/types.ts +++ b/modules/i3s/src/types.ts @@ -2,18 +2,6 @@ import type {Matrix4, Quaternion, Vector3} from '@math.gl/core'; import type {TypedArray, MeshAttribute, TextureLevel} from '@loaders.gl/schema'; import {Tile3D, Tileset3D} from '@loaders.gl/tiles'; -export enum DATA_TYPE { - UInt8 = 'UInt8', - UInt16 = 'UInt16', - UInt32 = 'UInt32', - UInt64 = 'UInt64', - Int16 = 'Int16', - Int32 = 'Int32', - Int64 = 'Int64', - Float32 = 'Float32', - Float64 = 'Float64' -} - export type COLOR = [number, number, number, number]; /** @@ -690,20 +678,24 @@ type Domain = { * spec - https://github.com/Esri/i3s-spec/blob/master/docs/1.8/store.cmn.md */ type Store = { - id: string | number; + id?: string | number; profile: string; version: number | string; - resourcePattern: string[]; - rootNode: string; - extent: number[]; - indexCRS: string; - vertexCRS: string; - normalReferenceFrame: string; - attributeEncoding: string; - textureEncoding: string[]; - lodType: string; - lodModel: string; + resourcePattern?: string[]; + rootNode?: string; + extent?: number[]; + indexCRS?: string; + vertexCRS?: string; + normalReferenceFrame?: string; + lodType?: string; + lodModel?: string; defaultGeometrySchema: DefaultGeometrySchema; + nidEncoding?: string; + textureEncoding?: string[]; + featureEncoding?: string; + geometryEncoding?: string; + attributeEncoding?: string; + indexingScheme?: string; }; /** * Spec - https://github.com/Esri/i3s-spec/blob/master/docs/1.8/defaultGeometrySchema.cmn.md @@ -726,15 +718,15 @@ type DefaultGeometrySchema = { export type HeaderAttribute = { property: HeaderAttributeProperty.vertexCount | HeaderAttributeProperty.featureCount | string; type: - | DATA_TYPE.UInt8 - | DATA_TYPE.UInt16 - | DATA_TYPE.UInt32 - | DATA_TYPE.UInt64 - | DATA_TYPE.Int16 - | DATA_TYPE.Int32 - | DATA_TYPE.Int64 - | DATA_TYPE.Float32 - | DATA_TYPE.Float64; + | 'UInt8' + | 'UInt16' + | 'UInt32' + | 'UInt64' + | 'Int16' + | 'Int32' + | 'Int64' + | 'Float32' + | 'Float64'; }; export enum HeaderAttributeProperty { vertexCount = 'vertexCount', @@ -749,14 +741,7 @@ export type VertexAttribute = { }; export type GeometryAttribute = { byteOffset?: number; - valueType: - | DATA_TYPE.UInt8 - | DATA_TYPE.UInt16 - | DATA_TYPE.Int16 - | DATA_TYPE.Int32 - | DATA_TYPE.Int64 - | DATA_TYPE.Float32 - | DATA_TYPE.Float64; + valueType: 'UInt8' | 'UInt16' | 'Int16' | 'Int32' | 'Int64' | 'Float32' | 'Float64'; valuesPerElement: number; }; export type I3SMeshAttributes = { diff --git a/modules/tile-converter/src/i3s-server/app.ts b/modules/tile-converter/src/i3s-server/app.ts index 510a986bf5..24753af161 100644 --- a/modules/tile-converter/src/i3s-server/app.ts +++ b/modules/tile-converter/src/i3s-server/app.ts @@ -2,10 +2,14 @@ import express from 'express'; import path from 'path'; import logger from 'morgan'; import cors from 'cors'; +import {loadArchive} from './controllers/slpk-controller'; + +const I3S_LAYER_PATH = process.env.I3sLayerPath || ''; // eslint-disable-line no-process-env, no-undef +const FULL_LAYER_PATH = path.join(process.cwd(), I3S_LAYER_PATH); // eslint-disable-line no-undef +loadArchive(FULL_LAYER_PATH); const indexRouter = require('./routes/index'); -const I3S_LAYER_PATH = process.env.I3sLayerPath || ''; // eslint-disable-line no-process-env, no-undef export const app = express(); app.use(logger('dev')); diff --git a/modules/tile-converter/src/i3s-server/bin/www.ts b/modules/tile-converter/src/i3s-server/bin/www.ts index 9485d1a2a4..ad98d2ec35 100755 --- a/modules/tile-converter/src/i3s-server/bin/www.ts +++ b/modules/tile-converter/src/i3s-server/bin/www.ts @@ -1,29 +1,25 @@ #!/usr/bin/env node -/** - * Module dependencies. - */ - import {app} from '../app'; -import debugFactory from 'debug'; import https from 'https'; import http from 'http'; import fs from 'fs'; import path from 'path'; +import {formErrorHandler, formListeningHandler, normalizePort} from '../utils/server-utils'; -const debug = debugFactory('i3s-server:server'); - -/** - * Get port from environment and store in Express. - */ - -const httpPort = normalizePort(process.env.PORT || '80'); // eslint-disable-line no-process-env, no-undef -const httpsPort = normalizePort(process.env.HTTPS_PORT || '443'); // eslint-disable-line no-process-env, no-undef - -/** - * Create HTTP server. - */ +/** Get port from environment and store in Express. */ +const httpPort = normalizePort(process.env.PORT || '80'); +if (httpPort === false) { + console.error(`Incorrect HTTP port`); + process.exit(1); +} +const httpsPort = normalizePort(process.env.HTTPS_PORT || '443'); +if (httpsPort === false) { + console.error(`Incorrect HTTPs port`); + process.exit(1); +} +/** Create HTTP server. */ const options = { key: fs.readFileSync(path.join(__dirname, '../certs/key.pem')), cert: fs.readFileSync(path.join(__dirname, '../certs/cert.pem')) @@ -32,10 +28,7 @@ const options = { const httpServer = http.createServer(app); const httpsServer = https.createServer(options, app); -/** - * Listen on provided port, on all network interfaces. - */ - +/** Listen on provided port, on all network interfaces. */ httpServer.listen(httpPort); httpServer.on('error', formErrorHandler(httpPort)); httpServer.on('listening', formListeningHandler(httpServer)); @@ -43,62 +36,3 @@ httpServer.on('listening', formListeningHandler(httpServer)); httpsServer.listen(httpsPort); httpsServer.on('error', formErrorHandler(httpsPort)); httpsServer.on('listening', formListeningHandler(httpsServer)); - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - const chkPort = parseInt(val, 10); - - if (isNaN(chkPort)) { - // named pipe - return val; - } - - if (chkPort >= 0) { - // port number - return chkPort; - } - - return false; -} - -/** - * Event listener for HTTP/HTTPS server "error" event. - */ - -function formErrorHandler(optionalPort) { - return function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - const bind = typeof global.port === 'string' ? `Pipe ${optionalPort}` : `Port ${optionalPort}`; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(`${bind} requires elevated privileges`); // eslint-disable-line no-console, no-undef - process.exit(1); // eslint-disable-line no-process-exit, no-undef - break; - case 'EADDRINUSE': - console.error(`${bind} is already in use`); // eslint-disable-line no-console, no-undef - process.exit(1); // eslint-disable-line no-process-exit, no-undef - break; - default: - throw error; - } - }; -} - -/** - * Event listener for HTTP/HTTPS server "listening" event. - */ -function formListeningHandler(optionalServer) { - return function onListening() { - const addr = optionalServer.address(); - const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`; - debug(`Listening on ${bind}`); - }; -} diff --git a/modules/tile-converter/src/i3s-server/controllers/index-controller.ts b/modules/tile-converter/src/i3s-server/controllers/index-controller.ts index 61d9d150d8..fdc4ade617 100644 --- a/modules/tile-converter/src/i3s-server/controllers/index-controller.ts +++ b/modules/tile-converter/src/i3s-server/controllers/index-controller.ts @@ -3,11 +3,16 @@ import fs from 'fs'; const {promises} = fs; -const I3S_LAYER_PATH = process.env.I3sLayerPath || ''; // eslint-disable-line no-process-env, no-undef -const FULL_LAYER_PATH = path.join(process.cwd(), I3S_LAYER_PATH); // eslint-disable-line no-undef +const I3S_LAYER_PATH = process.env.I3sLayerPath || ''; -export async function getFileNameByUrl(url) { +/** + * Get local file name by input HTTP URL + * @param url - I3S HTTP service url + * @returns - local file name + */ +export async function getFileNameByUrl(url: string): Promise { const extensions = ['json', 'bin', 'jpg', 'jpeg', 'png', 'bin.dds', 'ktx2']; + const FULL_LAYER_PATH = path.join(process.cwd(), I3S_LAYER_PATH); for (const ext of extensions) { const fileName = `${FULL_LAYER_PATH}${url}/index.${ext}`; try { diff --git a/modules/tile-converter/src/i3s-server/controllers/slpk-controller.ts b/modules/tile-converter/src/i3s-server/controllers/slpk-controller.ts index 3ae6d0f0e1..679ad70e25 100644 --- a/modules/tile-converter/src/i3s-server/controllers/slpk-controller.ts +++ b/modules/tile-converter/src/i3s-server/controllers/slpk-controller.ts @@ -1,22 +1,25 @@ import '@loaders.gl/polyfills'; import {parseSLPK} from '@loaders.gl/i3s'; import {FileHandleProvider} from '@loaders.gl/tile-converter'; -import path from 'path'; let slpkArchive; -const loadArchive = async (fullLayerPath) => { +/** + * Open SLPK file for reading and load HASH file + * @param fullLayerPath - full path to SLPK file + */ +export const loadArchive = async (fullLayerPath: string): Promise => { slpkArchive = await parseSLPK(await FileHandleProvider.from(fullLayerPath)); }; -const I3S_LAYER_PATH = process.env.I3sLayerPath || ''; // eslint-disable-line no-process-env, no-undef -const FULL_LAYER_PATH = path.join(process.cwd(), I3S_LAYER_PATH); // eslint-disable-line no-undef - -loadArchive(FULL_LAYER_PATH); - -export async function getFileByUrl(url) { +/** + * Get a file from SLPK + * @param url - I3S HTTP URL + * @returns - file content + */ +export async function getFileByUrl(url: string) { const trimmedPath = /^\/?(.*)\/?$/.exec(url); - let uncompressedFile; + let uncompressedFile: Buffer | null = null; if (trimmedPath) { try { uncompressedFile = Buffer.from(await slpkArchive.getFile(trimmedPath[1], 'http')); diff --git a/modules/tile-converter/src/i3s-server/utils/create-scene-server.ts b/modules/tile-converter/src/i3s-server/utils/create-scene-server.ts index 3110a8c818..40675fe69f 100644 --- a/modules/tile-converter/src/i3s-server/utils/create-scene-server.ts +++ b/modules/tile-converter/src/i3s-server/utils/create-scene-server.ts @@ -1,6 +1,13 @@ +import {SceneLayer3D} from '@loaders.gl/i3s'; import {v4 as uuidv4} from 'uuid'; -export const createSceneServer = (name, layer) => { +/** + * Create `/SceneServer` response + * @param name - service name, custom user-friendly name of the service + * @param layer - I3S layer JSON + * @returns reponse JSON for `/SceneServer` route + */ +export const createSceneServer = (name: string, layer: SceneLayer3D) => { return { serviceItemId: uuidv4().replace(/-/gi, ''), serviceName: name, diff --git a/modules/tile-converter/src/i3s-server/utils/server-utils.ts b/modules/tile-converter/src/i3s-server/utils/server-utils.ts new file mode 100644 index 0000000000..e3b2acbb66 --- /dev/null +++ b/modules/tile-converter/src/i3s-server/utils/server-utils.ts @@ -0,0 +1,70 @@ +import type {Server as HttpsServer} from 'https'; +import type {Server as HttpServer} from 'http'; + +import debugFactory from 'debug'; +const debug = debugFactory('i3s-server:server'); + +/** + * Normalize a port into a number, string, or false. + * @param val - port value from env variables + * @returns - `number` for port, `string` for a named pipe, or `false` if the port number is not correct + */ +export function normalizePort(val: string): number | string | false { + const chkPort = parseInt(val, 10); + + if (Number.isNaN(chkPort)) { + // named pipe + return val; + } + + if (chkPort >= 0) { + // port number + return chkPort; + } + + return false; +} + +/** + * Event listener creator for HTTP/HTTPS server "error" event. + * @param optionalPort - the port/named pipe the server is started on + * @return callback to handle server errors + */ +export function formErrorHandler( + optionalPort: string | number +): (error: NodeJS.ErrnoException) => void { + return function onError(error: NodeJS.ErrnoException) { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof optionalPort === 'string' ? `Pipe ${optionalPort}` : `Port ${optionalPort}`; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(`${bind} requires elevated privileges`); // eslint-disable-line no-console, no-undef + process.exit(1); // eslint-disable-line no-process-exit, no-undef + break; + case 'EADDRINUSE': + console.error(`${bind} is already in use`); // eslint-disable-line no-console, no-undef + process.exit(1); // eslint-disable-line no-process-exit, no-undef + break; + default: + throw error; + } + }; +} + +/** + * Event listener for HTTP/HTTPS server "listening" event. + * @param optionalServer - http or https NodeJS server + * @return callback that is triggered when the server has started + */ +export function formListeningHandler(optionalServer: HttpsServer | HttpServer): () => void { + return function onListening() { + const addr = optionalServer.address(); + const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr?.port}`; + debug(`Listening on ${bind}`); + }; +} diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/index.json b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/index.json new file mode 100644 index 0000000000..58ff5b1405 --- /dev/null +++ b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/index.json @@ -0,0 +1 @@ +{"serviceItemId":"82a6203820c740e88bbafb94132f6b03","serviceName":"Frankfurt-md-2","name":"Frankfurt-md-2","currentVersion":10.7,"serviceVersion":"1.8","supportedBindings":["REST"],"layers":[{"version":"{1F518F6A-9621-4476-8194-DD034FF34614}","id":0,"name":"Frankfurt-md-2","href":"./layers/0","layerType":"IntegratedMesh","spatialReference":{"wkid":4326,"latestWkid":4326,"vcsWkid":5773,"latestVcsWkid":5773},"capabilities":["View","Query"],"store":{"id":"{F23F2D74-EE90-4C42-AFF3-56CE04640CE6}","profile":"meshpyramids","version":"1.8","resourcePattern":["3dNodeIndexDocument","Attributes","SharedResource","Geometry"],"rootNode":"./nodes/root","extent":[8.638486093840738,50.10162577927056,8.714439334060673,50.11520548628318],"indexCRS":"http://www.opengis.net/def/crs/EPSG/0/4326","vertexCRS":"http://www.opengis.net/def/crs/EPSG/0/4326","normalReferenceFrame":"east-north-up","attributeEncoding":"application/octet-stream; version=1.6","textureEncoding":["image/jpeg","image/ktx2"],"lodType":"MeshPyramid","lodModel":"node-switching","defaultGeometrySchema":{"geometryType":"triangles","header":[{"property":"vertexCount","type":"UInt32"},{"property":"featureCount","type":"UInt32"}],"topology":"PerAttributeArray","ordering":["position","normal","uv0","color"],"vertexAttributes":{"position":{"valueType":"Float32","valuesPerElement":3},"normal":{"valueType":"Float32","valuesPerElement":3},"uv0":{"valueType":"Float32","valuesPerElement":2},"color":{"valueType":"UInt8","valuesPerElement":4}},"featureAttributeOrder":["id","faceRange"],"featureAttributes":{"id":{"valueType":"UInt64","valuesPerElement":1},"faceRange":{"valueType":"UInt32","valuesPerElement":2}}}},"fullExtent":{"xmin":8.638486093840738,"ymin":50.10162577927056,"xmax":8.714439334060673,"ymax":50.11520548628318,"zmin":-4668.025435449932,"zmax":5048.2198269797045},"heightModelInfo":{"heightModel":"gravity_related_height","vertCRS":"EGM96_Geoid","heightUnit":"meter"},"nodePages":{"nodesPerPage":64,"lodSelectionMetricType":"maxScreenThresholdSQ"},"materialDefinitions":[{"doubleSided":true,"emissiveFactor":[255,255,255],"alphaMode":"opaque","pbrMetallicRoughness":{"roughnessFactor":1,"metallicFactor":1,"baseColorTexture":{"textureSetDefinitionId":0}}}],"textureSetDefinitions":[{"formats":[{"name":"0","format":"jpg"},{"name":"1","format":"ktx2"}]},{"formats":[{"name":"0","format":"jpg"},{"name":"1","format":"ktx2"}],"atlas":true}],"geometryDefinitions":[{"geometryBuffers":[{"offset":8,"position":{"type":"Float32","component":3},"normal":{"type":"Float32","component":3},"uv0":{"type":"Float32","component":2},"color":{"type":"UInt8","component":4},"featureId":{"binding":"per-feature","type":"UInt64","component":1},"faceRange":{"binding":"per-feature","type":"UInt32","component":2}},{"compressedAttributes":{"encoding":"draco","attributes":["position","normal","uv0","color","feature-index"]}}]}],"attributeStorageInfo":[],"fields":[]}]} \ No newline at end of file diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/index.json b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/index.json new file mode 100644 index 0000000000..0750502d87 --- /dev/null +++ b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/index.json @@ -0,0 +1 @@ +{"version":"{1F518F6A-9621-4476-8194-DD034FF34614}","id":0,"name":"Frankfurt-md-2","href":"./layers/0","layerType":"IntegratedMesh","spatialReference":{"wkid":4326,"latestWkid":4326,"vcsWkid":5773,"latestVcsWkid":5773},"capabilities":["View","Query"],"store":{"id":"{F23F2D74-EE90-4C42-AFF3-56CE04640CE6}","profile":"meshpyramids","version":"1.8","resourcePattern":["3dNodeIndexDocument","Attributes","SharedResource","Geometry"],"rootNode":"./nodes/root","extent":[8.638486093840738,50.10162577927056,8.714439334060673,50.11520548628318],"indexCRS":"http://www.opengis.net/def/crs/EPSG/0/4326","vertexCRS":"http://www.opengis.net/def/crs/EPSG/0/4326","normalReferenceFrame":"east-north-up","attributeEncoding":"application/octet-stream; version=1.6","textureEncoding":["image/jpeg","image/ktx2"],"lodType":"MeshPyramid","lodModel":"node-switching","defaultGeometrySchema":{"geometryType":"triangles","header":[{"property":"vertexCount","type":"UInt32"},{"property":"featureCount","type":"UInt32"}],"topology":"PerAttributeArray","ordering":["position","normal","uv0","color"],"vertexAttributes":{"position":{"valueType":"Float32","valuesPerElement":3},"normal":{"valueType":"Float32","valuesPerElement":3},"uv0":{"valueType":"Float32","valuesPerElement":2},"color":{"valueType":"UInt8","valuesPerElement":4}},"featureAttributeOrder":["id","faceRange"],"featureAttributes":{"id":{"valueType":"UInt64","valuesPerElement":1},"faceRange":{"valueType":"UInt32","valuesPerElement":2}}}},"fullExtent":{"xmin":8.638486093840738,"ymin":50.10162577927056,"xmax":8.714439334060673,"ymax":50.11520548628318,"zmin":-4668.025435449932,"zmax":5048.2198269797045},"heightModelInfo":{"heightModel":"gravity_related_height","vertCRS":"EGM96_Geoid","heightUnit":"meter"},"nodePages":{"nodesPerPage":64,"lodSelectionMetricType":"maxScreenThresholdSQ"},"materialDefinitions":[{"doubleSided":true,"emissiveFactor":[255,255,255],"alphaMode":"opaque","pbrMetallicRoughness":{"roughnessFactor":1,"metallicFactor":1,"baseColorTexture":{"textureSetDefinitionId":0}}}],"textureSetDefinitions":[{"formats":[{"name":"0","format":"jpg"},{"name":"1","format":"ktx2"}]},{"formats":[{"name":"0","format":"jpg"},{"name":"1","format":"ktx2"}],"atlas":true}],"geometryDefinitions":[{"geometryBuffers":[{"offset":8,"position":{"type":"Float32","component":3},"normal":{"type":"Float32","component":3},"uv0":{"type":"Float32","component":2},"color":{"type":"UInt8","component":4},"featureId":{"binding":"per-feature","type":"UInt64","component":1},"faceRange":{"binding":"per-feature","type":"UInt32","component":2}},{"compressedAttributes":{"encoding":"draco","attributes":["position","normal","uv0","color","feature-index"]}}]}],"attributeStorageInfo":[],"fields":[]} \ No newline at end of file diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodepages/0/index.json b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodepages/0/index.json new file mode 100644 index 0000000000..f908de3a64 --- /dev/null +++ b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodepages/0/index.json @@ -0,0 +1 @@ +{"nodes":[{"index":0,"lodThreshold":0,"obb":{"center":[8.676496951388435,50.10841667136257,141.39774178531218],"halfSize":[2311.620410505966,2062.4371056037326,959.9614393664732],"quaternion":[0.2227952683502938,0.25834836931465127,0.7144546321993598,0.6109373295678506]},"children":[1]},{"index":1,"obb":{"center":[8.676496951388435,50.108416671362576,141.39774178662847],"halfSize":[2311.620410505966,2062.4371056037326,959.9614393664732],"quaternion":[0.2227952683502938,0.25834836931465127,0.7144546321993598,0.6109373295678506]},"lodThreshold":217717.37806230574,"children":[],"mesh":{"geometry":{"definition":0,"resource":1,"vertexCount":15312,"featureCount":1},"attribute":{"resource":1},"material":{"definition":0,"resource":1,"texelCountHint":524288}}}]} \ No newline at end of file diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/geometries/0/index.bin b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/geometries/0/index.bin new file mode 100644 index 0000000000..39930c2449 Binary files /dev/null and b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/geometries/0/index.bin differ diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/geometries/1/index.bin b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/geometries/1/index.bin new file mode 100644 index 0000000000..06e1051096 Binary files /dev/null and b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/geometries/1/index.bin differ diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/index.json b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/index.json new file mode 100644 index 0000000000..7d2b60e1eb --- /dev/null +++ b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/index.json @@ -0,0 +1 @@ +{"version":"{9CF428F9-E6D2-47C1-86C7-20589A1510B2}","id":"1","level":1,"mbs":[8.676496951388435,50.108416671362576,141.39774178662847,3243.2640505992454],"obb":{"center":[8.676496951388435,50.108416671362576,141.39774178662847],"halfSize":[2311.620410505966,2062.4371056037326,959.9614393664732],"quaternion":[0.2227952683502938,0.25834836931465127,0.7144546321993598,0.6109373295678506]},"lodSelection":[{"metricType":"maxScreenThreshold","maxError":526.503917672968},{"metricType":"maxScreenThresholdSQ","maxError":217717.37806230574}],"children":[],"neighbors":[],"parentNode":{"id":"root","href":"../root","mbs":[8.676496951388435,50.10841667136257,141.39774178531218,3243.2640505992454],"obb":{"center":[8.676496951388435,50.10841667136257,141.39774178531218],"halfSize":[2311.620410505966,2062.4371056037326,959.9614393664732],"quaternion":[0.2227952683502938,0.25834836931465127,0.7144546321993598,0.6109373295678506]}},"sharedResource":{"href":"./shared"},"geometryData":[{"href":"./geometries/0"}],"textureData":[{"href":"./textures/0"},{"href":"./textures/1"}]} \ No newline at end of file diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/shared/index.json b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/shared/index.json new file mode 100644 index 0000000000..5051cb2d18 --- /dev/null +++ b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/shared/index.json @@ -0,0 +1 @@ +{"materialDefinitions":{"Mat10":{"name":"standard","type":"standard","params":{"renderMode":"solid","shininess":1,"reflectivity":0,"ambient":[1,1,1],"diffuse":[1,1,1],"specular":[0,0,0],"useVertexColorAlpha":false,"vertexRegions":false,"vertexColors":true}}},"textureDefinitions":{"10":{"encoding":["image/jpeg"],"wrap":["none"],"atlas":false,"uvSet":"uv0","channels":"rgb","images":[{"id":"1170920505658572802","size":1024,"href":["../textures/0"],"length":[2097152]}]}}} \ No newline at end of file diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/textures/0/index.jpg b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/textures/0/index.jpg new file mode 100644 index 0000000000..cf7f57c119 Binary files /dev/null and b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/textures/0/index.jpg differ diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/textures/1/index.ktx2 b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/textures/1/index.ktx2 new file mode 100644 index 0000000000..a1f87635f4 Binary files /dev/null and b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/1/textures/1/index.ktx2 differ diff --git a/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/root/index.json b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/root/index.json new file mode 100644 index 0000000000..02ec05e642 --- /dev/null +++ b/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0/nodes/root/index.json @@ -0,0 +1 @@ +{"version":"{9CF428F9-E6D2-47C1-86C7-20589A1510B2}","id":"root","level":0,"mbs":[8.676496951388435,50.10841667136257,141.39774178531218,3243.2640505992454],"obb":{"center":[8.676496951388435,50.10841667136257,141.39774178531218],"halfSize":[2311.620410505966,2062.4371056037326,959.9614393664732],"quaternion":[0.2227952683502938,0.25834836931465127,0.7144546321993598,0.6109373295678506]},"lodSelection":[{"metricType":"maxScreenThresholdSQ","maxError":0},{"metricType":"maxScreenThreshold","maxError":0}],"children":[{"id":"1","href":"../1","obb":{"center":[8.676496951388435,50.108416671362576,141.39774178662847],"halfSize":[2311.620410505966,2062.4371056037326,959.9614393664732],"quaternion":[0.2227952683502938,0.25834836931465127,0.7144546321993598,0.6109373295678506]},"mbs":[8.676496951388435,50.108416671362576,141.39774178662847,3243.2640505992454]}],"parentNode":{}} \ No newline at end of file diff --git a/modules/tile-converter/test/i3s-server/controllers/index-controller.spec.ts b/modules/tile-converter/test/i3s-server/controllers/index-controller.spec.ts new file mode 100644 index 0000000000..7e79f70f7c --- /dev/null +++ b/modules/tile-converter/test/i3s-server/controllers/index-controller.spec.ts @@ -0,0 +1,33 @@ +import test from 'tape-promise/tape'; +import {isBrowser} from '@loaders.gl/core'; +import {getFileNameByUrl} from '../../../src/i3s-server/controllers/index-controller'; + +const URL_PREFIX = + '/modules/tile-converter/test/data/i3s-server/Frankfurt-md-2/SceneServer/layers/0'; +const TEST_CASES = [ + {input: '', output: 'index.json'}, + {input: '/nodepages/0', output: 'nodepages/0/index.json'}, + {input: '/nodes/root', output: 'nodes/root/index.json'}, + {input: '/nodes/1', output: 'nodes/1/index.json'}, + {input: '/nodes/1/geometries/0', output: 'nodes/1/geometries/0/index.bin'}, + {input: '/nodes/1/geometries/1', output: 'nodes/1/geometries/1/index.bin'}, + {input: '/nodes/1/shared', output: 'nodes/1/shared/index.json'}, + {input: '/nodes/1/textures/0', output: 'nodes/1/textures/0/index.jpg'}, + {input: '/nodes/1/textures/1', output: 'nodes/1/textures/1/index.ktx2'} +]; + +test('tile-converter(i3s-server)#getFileNameByUrl', async (t) => { + if (isBrowser) { + t.end(); + return; + } + + const cwd = process.cwd(); + + for (const testCase of TEST_CASES) { + const result = await getFileNameByUrl(`${URL_PREFIX}${testCase.input}`); + t.equals(result, `${cwd}${URL_PREFIX}/${testCase.output}`); + } + + t.end(); +}); diff --git a/modules/tile-converter/test/i3s-server/controllers/slpk-controller.spec.ts b/modules/tile-converter/test/i3s-server/controllers/slpk-controller.spec.ts new file mode 100644 index 0000000000..324b7783ac --- /dev/null +++ b/modules/tile-converter/test/i3s-server/controllers/slpk-controller.spec.ts @@ -0,0 +1,45 @@ +import test from 'tape-promise/tape'; +import {isBrowser} from '@loaders.gl/core'; +import path from 'path'; +import {getFileByUrl, loadArchive} from '../../../src/i3s-server/controllers/slpk-controller'; + +const URL_PREFIX = ''; +const SLPK_URL = './modules/i3s/test/data/DA12_subset.slpk'; +const TEST_CASES = [ + {input: '', output: 4780}, + {input: 'nodepages/0', output: 16153}, + {input: 'nodes/root', output: 11550}, + {input: 'nodes/1', output: 1175}, + {input: 'nodes/1/geometries/0', output: 25620}, + {input: 'nodes/1/geometries/1', output: 1767}, + {input: 'nodes/1/shared', output: 333} +]; + +test('tile-converter(i3s-server)#getFileByUrl return null if file is not loaded', async (t) => { + if (isBrowser) { + t.end(); + return; + } + + const result = await getFileByUrl('layers/0'); + t.equals(result, null); + + t.end(); +}); + +test('tile-converter(i3s-server)#getFileByUrl return files content', async (t) => { + if (isBrowser) { + t.end(); + return; + } + + const FULL_LAYER_PATH = path.join(process.cwd(), SLPK_URL); // eslint-disable-line no-undef + await loadArchive(FULL_LAYER_PATH); + + for (const testCase of TEST_CASES) { + const result = await getFileByUrl(`${URL_PREFIX}${testCase.input}`); + t.equals(result?.byteLength, testCase.output); + } + + t.end(); +}); diff --git a/modules/tile-converter/test/i3s-server/utils/create-scene-server.spec.ts b/modules/tile-converter/test/i3s-server/utils/create-scene-server.spec.ts new file mode 100644 index 0000000000..2f4691dc30 --- /dev/null +++ b/modules/tile-converter/test/i3s-server/utils/create-scene-server.spec.ts @@ -0,0 +1,164 @@ +import test from 'tape-promise/tape'; +import {isBrowser} from '@loaders.gl/core'; +import {SceneLayer3D} from '@loaders.gl/i3s'; +import {createSceneServer} from '../../../src/i3s-server/utils/create-scene-server'; + +test('tile-converter(i3s-server)#createSceneServer', async (t) => { + if (isBrowser) { + t.end(); + return; + } + + const result = await createSceneServer('Buildings_3D_Multipatch_DA12_Subset', LAYER); + t.equals(JSON.stringify(result).length, 4898); + + t.end(); +}); + +const LAYER: SceneLayer3D = { + id: 0, + version: 'ECB1F245-BAB6-4CF3-86CE-9CF6047E9239', + name: 'Buildings_3D_Multipatch_DA12_Subset', + serviceUpdateTimeStamp: {lastUpdate: 1570746568000}, + href: './layers/0', + layerType: '3DObject', + spatialReference: {wkid: 4326, latestWkid: 4326, vcsWkid: 5773, latestVcsWkid: 5773}, + heightModelInfo: { + heightModel: 'gravity_related_height', + vertCRS: 'EGM96_Geoid', + heightUnit: 'meter' + }, + ZFactor: 0.30480060960121924, + alias: 'Buildings_3D_Multipatch_DA12_Subset', + description: 'Buildings_3D_Multipatch_DA12_Subset', + capabilities: ['View', 'Query'], + cachedDrawingInfo: {color: false}, + popupInfo: { + title: '{BIN}', + mediaInfos: [], + fieldInfos: [ + {fieldName: 'OBJECTID', visible: true, isEditable: false, label: 'OBJECTID'}, + {fieldName: 'BIN', visible: true, isEditable: true, label: 'BIN'}, + {fieldName: 'DOITT_ID', visible: true, isEditable: true, label: 'DOITT_ID'}, + {fieldName: 'SOURCE_ID', visible: true, isEditable: true, label: 'SOURCE_ID'} + ], + popupElements: [ + { + fieldInfos: [ + {fieldName: 'OBJECTID', visible: true, isEditable: false, label: 'OBJECTID'}, + {fieldName: 'BIN', visible: true, isEditable: true, label: 'BIN'}, + {fieldName: 'DOITT_ID', visible: true, isEditable: true, label: 'DOITT_ID'}, + {fieldName: 'SOURCE_ID', visible: true, isEditable: true, label: 'SOURCE_ID'} + ], + type: 'fields' + } + ], + expressionInfos: [] + }, + disablePopup: false, + fields: [ + {name: 'OBJECTID', type: 'esriFieldTypeOID', alias: 'OBJECTID'}, + {name: 'BIN', type: 'esriFieldTypeInteger', alias: 'BIN'}, + {name: 'DOITT_ID', type: 'esriFieldTypeInteger', alias: 'DOITT_ID'}, + {name: 'SOURCE_ID', type: 'esriFieldTypeDouble', alias: 'SOURCE_ID'} + ], + statisticsInfo: [ + {key: 'f_1', name: 'BIN', href: './statistics/f_1/0'}, + {key: 'f_2', name: 'DOITT_ID', href: './statistics/f_2/0'}, + {key: 'f_3', name: 'SOURCE_ID', href: './statistics/f_3/0'} + ], + attributeStorageInfo: [ + { + key: 'f_0', + name: 'OBJECTID', + header: [{property: 'count', valueType: 'UInt32'}], + ordering: ['attributeValues'], + attributeValues: {valueType: 'Oid32', valuesPerElement: 1} + }, + { + key: 'f_1', + name: 'BIN', + header: [{property: 'count', valueType: 'UInt32'}], + ordering: ['attributeValues'], + attributeValues: {valueType: 'Int32', valuesPerElement: 1} + }, + { + key: 'f_2', + name: 'DOITT_ID', + header: [{property: 'count', valueType: 'UInt32'}], + ordering: ['attributeValues'], + attributeValues: {valueType: 'Int32', valuesPerElement: 1} + }, + { + key: 'f_3', + name: 'SOURCE_ID', + header: [{property: 'count', valueType: 'UInt32'}], + ordering: ['attributeValues'], + attributeValues: {valueType: 'Float64', valuesPerElement: 1} + } + ], + store: { + id: 'D20A5DA0-623B-4359-880D-474D634F7158', + profile: 'meshpyramids', + resourcePattern: ['3dNodeIndexDocument', 'Attributes', 'SharedResource', 'Geometry'], + rootNode: './nodes/root', + extent: [ + -74.01610254118118348, 40.70883303940371434, -74.00660086904034074, 40.71625626184324886 + ], + indexCRS: 'http://www.opengis.net/def/crs/EPSG/0/4326', + vertexCRS: 'http://www.opengis.net/def/crs/EPSG/0/4326', + normalReferenceFrame: 'earth-centered', + nidEncoding: 'application/vnd.esri.i3s.json+gzip; version=1.7', + featureEncoding: 'application/vnd.esri.i3s.json+gzip; version=1.7', + geometryEncoding: 'application/octet-stream; version=1.7', + attributeEncoding: 'application/octet-stream; version=1.7', + lodType: 'MeshPyramid', + lodModel: 'node-switching', + defaultGeometrySchema: { + geometryType: 'triangles', + header: [ + {property: 'vertexCount', type: 'UInt32'}, + {property: 'featureCount', type: 'UInt32'} + ], + topology: 'PerAttributeArray', + ordering: ['position', 'normal', 'uv0', 'color'], + vertexAttributes: { + position: {valueType: 'Float32', valuesPerElement: 3}, + normal: {valueType: 'Float32', valuesPerElement: 3}, + uv0: {valueType: 'Float32', valuesPerElement: 2}, + color: {valueType: 'UInt8', valuesPerElement: 4} + }, + featureAttributeOrder: ['id', 'faceRange'], + featureAttributes: { + id: {valueType: 'UInt64', valuesPerElement: 1}, + faceRange: {valueType: 'UInt32', valuesPerElement: 2} + } + }, + textureEncoding: ['image/jpeg', 'image/vnd-ms.dds'], + version: '1.7' + }, + nodePages: {nodesPerPage: 64, lodSelectionMetricType: 'maxScreenThresholdSQ'}, + materialDefinitions: [], + geometryDefinitions: [ + { + topology: 'PerAttributeArray', + geometryBuffers: [ + { + offset: 8, + position: {type: 'Float32', component: 3, binding: 'per-vertex'}, + normal: {type: 'Float32', component: 3, binding: 'per-vertex'}, + uv0: {type: 'Float32', component: 2, binding: 'per-vertex'}, + color: {type: 'UInt8', component: 4, binding: 'per-vertex'}, + featureId: {type: 'UInt64', component: 1, binding: 'per-feature'}, + faceRange: {type: 'UInt32', component: 2, binding: 'per-feature'} + }, + { + compressedAttributes: { + encoding: 'draco', + attributes: ['position', 'uv0', 'color', 'feature-index'] + } + } + ] + } + ] +}; diff --git a/modules/tile-converter/test/i3s-server/utils/server-utils.spec.ts b/modules/tile-converter/test/i3s-server/utils/server-utils.spec.ts new file mode 100644 index 0000000000..8da19052b9 --- /dev/null +++ b/modules/tile-converter/test/i3s-server/utils/server-utils.spec.ts @@ -0,0 +1,33 @@ +import test from 'tape-promise/tape'; +import {isBrowser} from '@loaders.gl/core'; +import {formErrorHandler, normalizePort} from '../../../src/i3s-server/utils/server-utils'; + +test('tile-converter(i3s-server)#normalizePort', async (t) => { + if (isBrowser) { + t.end(); + return; + } + + const result1 = normalizePort('8080'); + t.strictEquals(result1, 8080); + + const result2 = normalizePort('\\\\.\\pipe\\PIPE_NAME'); + t.strictEquals(result2, '\\\\.\\pipe\\PIPE_NAME'); + + const result3 = normalizePort('-1000'); + t.strictEquals(result3, false); + + t.end(); +}); + +test('tile-converter(i3s-server)#formErrorHandler', async (t) => { + if (isBrowser) { + t.end(); + return; + } + + const func = formErrorHandler(8080); + t.ok(func); + + t.end(); +}); diff --git a/modules/tile-converter/test/index.js b/modules/tile-converter/test/index.js index d70c66b0ac..021d676e56 100644 --- a/modules/tile-converter/test/index.js +++ b/modules/tile-converter/test/index.js @@ -25,3 +25,8 @@ import './3d-tiles-converter/3d-tiles-converter.spec'; import './3d-tiles-converter/helpers/geometry-attributes.spec'; import './deps-installer/deps-installer.spec'; + +import './i3s-server/controllers/index-controller.spec'; +import './i3s-server/controllers/slpk-controller.spec'; +import './i3s-server/utils/create-scene-server.spec.ts'; +import './i3s-server/utils/server-utils.spec';