import lang = require("dojo/_base/lang"); import esriLang = require("esri/core/lang"); import * as typeValidator from "../../typeValidator"; import { RouteProvider } from "../utils/interceptor"; import { ServiceUrlData, ServiceUrlDataInput, PortalServiceArgs, PortalService } from "./interfaces"; import { RouteGenerationArgs, delayRoutes, generateLayerUrl, generateBaseRegex, generateLayerRegex, generateDefault404Route, deepMerge, createJSONUrlRegExp } from "./serviceUtils"; import * as sceneServiceUtils from "./sceneServiceUtils"; import spatialReferenceUtils = require("esri/geometry/support/spatialReferenceUtils"); import earthUtils = require("esri/views/3d/support/earthUtils"); import { SpatialReference as SpatialReferenceJSON, ElevationInfo as ElevationInfoJSON } from "esri/portal/jsonTypes"; // NB: add properties as they become necessary type LayerId = number; type NodeId = string; type PCNodeId = number; const layerRefDefinition = { id: typeValidator.number(), name: typeValidator.string() }; const rootDefinition = { serviceName: typeValidator.string(), serviceVersion: typeValidator.string(), layers: typeValidator.array(layerRefDefinition) }; const layerDefinition = { id: typeValidator.number(), name: typeValidator.string() }; const nodeDefinition = { id: typeValidator.string(), mbs: typeValidator.array(typeValidator.number()) }; const nodeLayerDefinition = { layerId: typeValidator.number(), nodes: typeValidator.array(nodeDefinition) }; const pcPageLayerDefinition = { layerId: typeValidator.number(), pages: typeValidator.array({}) }; interface LayerRef { id: LayerId; name: string; layerType: string; } interface RootData { currentVersion?: number; serviceName?: string; serviceVersion?: string; layers?: LayerRef[]; } interface LayerData { id: LayerId; name?: string; layerType?: string; spatialReference?: SpatialReferenceJSON; elevationInfo?: ElevationInfoJSON; store?: HashMap; fields?: HashMap; attributeStorageInfo?: HashMap; drawingInfo?: HashMap; } interface NodeData { id: NodeId; mbs: number[]; attributeData?: {href: string}[]; featureData?: {href: string}[]; sharedResource?: {href: string}; geometryData?: {href: string}[]; textureData?: {href: string}[]; } interface NodeLayerData { layerId: LayerId; nodes: NodeData[]; } interface NodeGeometryData { id: NodeId; layerId: LayerId; buffer: ArrayBuffer; } interface NodeSharedData { id: NodeId; layerId: LayerId; resource: HashMap; } interface NodeAttributeData { id: NodeId; layerId: LayerId; attributes: { key: string; buffer: ArrayBuffer; }[]; } interface NodeFeatureData { id: NodeId; layerId: LayerId; resource: HashMap; } interface NodeTextureData { id: NodeId; layerId: LayerId; key: string; type: string; buffer: ArrayBuffer; } interface MeshMaterialDefinition { hasTexture: boolean; } interface GenMeshBoxArgs { layerId: LayerId; wkid?: number; size?: number | number[]; boxes?: { size: number | number[]; offset: number[]; color?: [number, number, number, number]; id?: number; }[]; center: number[]; levels?: number; material?: MeshMaterialDefinition; color?: [number, number, number, number]; } interface GenMeshGridArgs { layerType?: string, layerId: LayerId; wkid?: number, size: number | number[]; center: number[]; spacing: number[]; subdivs?: number; forEachLeaf?: (this: SceneService, id: string, i: number) => void; } interface PCPageData { actualCount: number, nodes: { resourceId: PCNodeId; [k: string]: any; }[]; } interface PCPageLayerData { layerId: LayerId; pages: PCPageData[]; } interface PCNodeGeometryData { id: PCNodeId; layerId: LayerId; buffer: ArrayBuffer; } interface PCNodeAttributeData { id: PCNodeId; layerId: LayerId; attributes: { key: string; buffer: ArrayBuffer; }[]; } interface GenPCQuadArgs { layerId: LayerId; wkid: number; position: number[]; size?: number; subdiv?: number; } const serverType: "SceneServer" = "SceneServer"; function getServiceRoutes(root: RootData, args: RouteGenerationArgs) { const baseRegex = generateBaseRegex(args); const regex = createJSONUrlRegExp(baseRegex, { formatJSONOptional: true }); return { match: regex, response: { bodyJSON: root } }; } function getLayerRoutes(layer: LayerData, args: RouteGenerationArgs) { const baseRegex = generateBaseRegex(args); // Make the json parameter mandatory on layer routes, to enforce the correct // handling of tokens in the api. Without the parameter, requests to real // services that require authentication will be redirected to a login page. const regex = createJSONUrlRegExp(baseRegex + "\\/layers\\/" + layer.id); return { match: regex, response: { bodyJSON: layer } } } namespace MeshAndPoint { function getLevel(id: NodeId) { if (id === "root") { return 1; } else { let level = 2; for (const char of id) { if (char === "-") { level++; } } return level; } } function getParentId(id: NodeId) { const split = id.split("-"); if (id === "root") { return null; } else if (split.length === 1) { return "root"; } else { return split.splice(0, split.length - 1).join("-"); } } function getChildIds(id: NodeId, allIds: NodeId[]) { return allIds.filter( childId => (id === "root" || esriLang.startsWith(childId, id + "-")) && getLevel(childId) === getLevel(id) + 1 ); } function getRelationMetadata(node: NodeData) { return { id: node.id, href: "../" + node.id, mbs: node.mbs }; } function getParentChildMetadata( id: NodeId, nodesById: { [id: string]: NodeData } ) { const allIds = Object.keys(nodesById); const parentId = getParentId(id); const childIds = getChildIds(id, allIds); const parentNode = parentId ? getRelationMetadata(nodesById[parentId]) : null; const children = childIds.length > 0 ? childIds.map(childId => getRelationMetadata(nodesById[childId])) : null; const metadata = { parentNode, children }; return metadata; } function generateNodeRelations(nodes: NodeData[]) { let nodesById = {}; nodes.forEach(n => nodesById[n.id] = n); const newNodes = nodes.map( n => lang.mixin({}, n, getParentChildMetadata(n.id, nodesById)) ); return newNodes; } function getNodeLevel(id: NodeId) { return id === "root" ? 1 : id.split("-").length + 1; } function generateNodeLevels(nodes: NodeData[]) { const newNodes = nodes.map( n => lang.mixin({}, n, { level: getNodeLevel(n.id) }) ); return newNodes; } export function getNodeLayerRoutes( nodeLayer: NodeLayerData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, nodeLayer.layerId); const newNodes = generateNodeLevels(generateNodeRelations(nodeLayer.nodes)); const version = "" + Math.random(); // Add unique version to avoid aliasing of cached nodes const nodeRoutes = newNodes.map(node => { const regex = createJSONUrlRegExp( layerRegex + "\\/nodes\\/" + node.id, { formatJSONOptional: true } ); return { match: regex, response: { bodyJSON: lang.mixin({ version }, node) } }; }); return nodeRoutes; } export function getNodeGeometryRoutes( geometry: NodeGeometryData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, geometry.layerId); const nodeRegex = layerRegex + "\\/nodes\\/" + geometry.id; return { match: new RegExp("^" + nodeRegex + "\\/geometries\\/0$"), response: () => ({ body: geometry.buffer.slice(0), // copy buffer, is moved to worker thread headers: {"Content-Type": "application/octet-stream;charset=binary"} }) }; } export function getSharedResourceGeometryRoutes( shared: NodeSharedData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, shared.layerId); const nodeRegex = layerRegex + "\\/nodes\\/" + shared.id; return { match: new RegExp("^" + nodeRegex + "\\/shared$"), response: { bodyJSON: shared.resource } }; } export function getNodeAttributeRoutes( attrs: NodeAttributeData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, attrs.layerId); const nodeRegex = layerRegex + "\\/nodes\\/" + attrs.id; return attrs.attributes.map(attr => ({ match: new RegExp("^" + nodeRegex + "\\/attributes\\/" + attr.key + "\\/0$"), response: { body: attr.buffer, headers: {"Content-Type": "application/octet-stream;charset=binary"} } })); } export function getNodeFeatureRoutes( features: NodeFeatureData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, features.layerId); const nodeRegex = layerRegex + "\\/nodes\\/" + features.id; return { match: new RegExp("^" + nodeRegex + "\\/features\\/0\\/?$"), response: { bodyJSON: features.resource } }; } export function getNodeTextureRoutes( texture: NodeTextureData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, texture.layerId); const nodeRegex = layerRegex + "\\/nodes\\/" + texture.id; return { match: new RegExp("^" + nodeRegex + "\\/textures\\/" + texture.key + "\\/?$"), response: { body: texture.buffer, headers: {"Content-Type": "image/" + texture.type} } }; } } namespace PointCloud { export function getPageLayerRoutes( pageLayer: PCPageLayerData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, pageLayer.layerId); const pageRoutes = pageLayer.pages.map((page, i) => { return { match: new RegExp("^" + layerRegex + "\\/nodepages\\/" + i + "\\?f=json$"), response: { bodyJSON: page } } }); return pageRoutes; } export function getNodeGeometryRoutes( geometry: PCNodeGeometryData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, geometry.layerId); const nodeRegex = layerRegex + "\\/nodes\\/" + geometry.id; return { match: new RegExp("^" + nodeRegex + "\\/geometries\\/0$"), response: () => ({ body: geometry.buffer.slice(0), // copy buffer, is moved to worker thread headers: {"Content-Type": "application/octet-stream;charset=binary"} }) }; } export function getNodeAttributeRoutes( attrs: PCNodeAttributeData, args: RouteGenerationArgs ) { const layerRegex = generateLayerRegex(args, attrs.layerId); const nodeRegex = layerRegex + "\\/nodes\\/" + attrs.id; return attrs.attributes.map(attr => ({ match: new RegExp("^" + nodeRegex + "\\/attributes\\/" + attr.key + "$"), response: () => ({ body: attr.buffer.slice(0), // copy buffer, is moved to worker thread headers: {"Content-Type": "application/octet-stream;charset=binary"} }) })); } } function handleValidation(isValid: boolean) { // validate input types, since most tests won't be written with typescript if (!isValid) { throw new Error("Validation failed for input parameters"); } } class SceneService implements RouteProvider, PortalService { private _name: string; private _urlData: ServiceUrlData; private _rootData: RootData; private _layerData: HashMap; private _nodeLayerData: HashMap; private _nodeGeometryData: NodeGeometryData[]; private _nodeSharedData: NodeSharedData[]; private _nodeAttributeData: NodeAttributeData[]; private _nodeFeatureData: NodeFeatureData[]; private _nodeTextureData: NodeTextureData[]; private _pcPageLayerData: HashMap; private _pcNodeGeometryData: PCNodeGeometryData[]; private _pcNodeAttributeData: PCNodeAttributeData[]; private _delayMs: number; constructor(urlData: ServiceUrlData) { this._urlData = urlData; this._rootData = { layers: [] }; this._layerData = {}; this._nodeLayerData = {}; this._nodeGeometryData = []; this._nodeSharedData = []; this._nodeAttributeData = []; this._nodeFeatureData = []; this._nodeTextureData = []; this._pcPageLayerData = {}; this._pcNodeGeometryData = []; this._pcNodeAttributeData = []; this._delayMs = 0; } /** * Define the name of the service used in generated routes. If not defined, a * default name of 'TestService' will be used. */ setName(input: string) { this._name = input; return this; } /** * Define the metadata returned from the root url of the service. * * @param {Object} input - An object defining the metadata to return. */ setRoot(input: RootData) { handleValidation(typeValidator.check(input, rootDefinition)); lang.mixin(this._rootData, input); return this; } /** * Define the format of the url segment prefixing the server-specific url * segment of routes generated for this service. * * @param {Object} input - An object defining the 'host', 'instance' and * 'folderName' of the service catalog root url, as described * [here](http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#//02r3000000tn000000). */ setUrl(input: ServiceUrlDataInput) { lang.mixin(this._urlData, input); return this; } /** * Delay responses to requests made to this service. * * @param {Object} input - Defines the duration of time to delay responses by. */ setResponseDelay(input: {ms: number}) { this._delayMs = input.ms; return this; } /** * Define routes for a new layer on the service. * A minimal amount of metadata defining suitable defaults for the layer will * be generated. * * @param {Object} input - An object defining layer metadata to be mixed in * with the generated metadata. The input metadata must at least define a * layer id. */ addLayer(input: LayerData) { const defaults = {name: "layer " + input.id}; const data = deepMerge(defaults, input); handleValidation(typeValidator.check(data, layerDefinition)); this._layerData[data.id] = data; this._rootData.layers = this._rootData.layers // replace existing layers .filter(l => l.id !== input.id) .concat([{ id: data.id, name: data.name, layerType: data.layerType }]); return this; } /** * Update metadata for an existing layer on the service. * * @param {Object} input - An object defining layer metadata to be mixed in * with the existing metadata. */ updateLayer(input: LayerData) { const existing = this._layerData[input.id]; if (!existing) { throw new Error("layer does not exist"); } const data = deepMerge(existing, input); handleValidation(typeValidator.check(data, layerDefinition)); return this.addLayer(data); } /** * Define routes for a new mesh layer on the service. * Metadata defining suitable defaults for the layer will be * generated. * * @param {Object} input - An object defining layer metadata to be mixed in * with the generated metadata. The input metadata must at least define a * layer id. */ addMeshLayer(input: LayerData) { handleValidation(typeValidator.check(input, layerDefinition)); const common = sceneServiceUtils.generateMeshLayerData(); this.addLayer(deepMerge(common, input)); return this; } /** * Define routes for a new point layer on the service. * Metadata defining suitable defaults for the layer will be * generated. * * @param {Object} input - An object defining layer metadata to be mixed in * with the generated metadata. The input metadata must at least define a * layer id. */ addPointLayer(input: LayerData) { handleValidation(typeValidator.check(input, layerDefinition)); const common = sceneServiceUtils.generatePointLayerData(); this.addLayer(deepMerge(common, input)); return this; } /** * Define routes for nodes on an existing scene layer. * The 'children' and 'parentNode' properties will be automatically populated, * assuming that the node ids follow the usual naming convention e.g. * '0-1-3' for a node with parent '0-1' and grand-parent '0'. * * @param {Object} input - An object specifying the layer to define routes for * and the data of the nodes. */ addNodes(input: NodeLayerData) { handleValidation(typeValidator.check(input, nodeLayerDefinition)); this._nodeLayerData[input.layerId] = input; return this; } /** * Define routes for binary geometry data of a node on an existing * layer. The node's 'geometryData' property will be replaced to * reflect the change. * * @param {Object} input - An object specifying the node to define the * geometry for, the id of its layer and the geometry array buffer. */ setNodeGeometry(input: NodeGeometryData) { this._nodeGeometryData = this._nodeGeometryData // replace conflicting items .filter(d => !(d.id === input.id && d.layerId === input.layerId)) .concat([input]); for (const node of this._nodeLayerData[input.layerId].nodes) { if (node.id === input.id) { node.geometryData = [{href: `./geometries/0`}]; break; } } return this; } /** * Define routes for the 'shared resource' metadata of a node on an existing * layer. The node's 'shared' property will be replaced to * reflect the change. * * @param {Object} input - An object specifying the node to define the * resource for, the id of its layer and the resource metadata. */ setNodeShared(input: NodeSharedData) { this._nodeSharedData = this._nodeSharedData // replace conflicting items .filter(d => !(d.id === input.id && d.layerId === input.layerId)) .concat([input]); for (const node of this._nodeLayerData[input.layerId].nodes) { if (node.id === input.id) { node.sharedResource = {href: `./shared`}; break; } } return this; } /** * Define routes for binary attribute data of a node on an existing * layer. The node's 'attributeData' property will be replaced to * reflect the change. * * @param {Object} input - An object specifying the node to define the * attributes for, the id of its layer and an array of attribute data. */ setNodeAttributes(input: NodeAttributeData) { this._nodeAttributeData = this._nodeAttributeData // replace conflicting items .filter(d => !(d.id === input.id && d.layerId === input.layerId)) .concat([input]); for (const node of this._nodeLayerData[input.layerId].nodes) { if (node.id === input.id) { node.attributeData = input.attributes.map(a => ({href: `./attributes/${a.key}/0`})) break; } } return this; }; /** * Define routes for the feature resource of a node on an existing * layer. The node's 'featureData' property will be replaced to * reflect the change. * * @param {Object} input - An object specifying the node to define the * attributes for, the id of its layer and the resource metadata. */ setNodeFeatures(input: NodeFeatureData) { this._nodeFeatureData = this._nodeFeatureData // replace conflicting items .filter(d => !(d.id === input.id && d.layerId === input.layerId)) .concat([input]); for (const node of this._nodeLayerData[input.layerId].nodes) { if (node.id === input.id) { node.featureData = [{href: `./features/0`}]; break; } } return this; }; /** * Define routes for the texture resource of a node on an existing * layer. * * @param {Object} input - An object specifying the node to define the * texture for, the id of its layer and the texture data. */ setNodeTexture(input: NodeTextureData) { this._nodeTextureData = this._nodeTextureData // replace conflicting items .filter(d => !(d.id === input.id && d.layerId === input.layerId)) .concat([input]); return this; }; /** * Define routes for a new mesh layer with node, geometry, shared and * attribute resources for a procedurally generated cube. * * @param {Object} args - An object specifying the layer where the cube * should be stored and properties affecting its appearance. */ genMeshBoxLayer(args: GenMeshBoxArgs) { const wkid = args.wkid || 4326; const levels = (args.levels != null ? args.levels : 1); const materialHasTexture = args.material && args.material.hasTexture; const boxes = args.boxes ? args.boxes : [{ size: args.size, color: args.color }]; // compute approximate conversion factor from degree to meters for geographic coordinate systems const radiusScale = spatialReferenceUtils.isGeographic({ wkid }) ? [ 1 / earthUtils.getLonDeltaForDistance(args.center[0], args.center[1], 1), 1 / earthUtils.getLatDeltaForDistance(args.center[0], args.center[1], 1), 1 ] : [1, 1, 1]; const box = sceneServiceUtils.genMeshBox({ center: args.center, boxes, levels: levels, materialHasTexture: materialHasTexture, radiusScale }); let service = this .addMeshLayer({ id: args.layerId, store: { extent: box.extent, indexCRS: "http://www.opengis.net/def/crs/EPSG/0/" + wkid, vertexCRS: "http://www.opengis.net/def/crs/EPSG/0/" + wkid }, spatialReference: { wkid: wkid } }) .addNodes({ layerId: args.layerId, nodes: box.nodes }) .setNodeGeometry({ id: box.id, layerId: args.layerId, buffer: box.geometry.buffer }) .setNodeShared({ id: box.id, layerId: args.layerId, resource: box.sharedResource }) .setNodeAttributes({ id: box.id, layerId: args.layerId, attributes: box.attributes }); if (materialHasTexture) { service = service.setNodeTexture({ id: box.id, layerId: args.layerId, key: box.texture.key, type: box.texture.type, buffer: box.texture.buffer }); } return service; } /** * Define routes for a new mesh layer with node, geometry, shared and * attribute resources for a procedurally generated grid of cubes. * * @param {Object} args - An object specifying the layer where the cubes * should be stored and properties affecting their appearance. */ genMeshGridLayer(args: GenMeshGridArgs) { const wkid = args.wkid || 4326; const subdivs = (args.subdivs != null ? args.subdivs : 0); const layerType = args.layerType || "3DObject"; const grid = sceneServiceUtils.genMeshGrid({ size: args.size, center: args.center, spacing: args.spacing, subdivs: subdivs }); this .addMeshLayer({ layerType: layerType, id: args.layerId, store: { extent: grid.extent, indexCRS: "http://www.opengis.net/def/crs/EPSG/0/" + wkid, vertexCRS: "http://www.opengis.net/def/crs/EPSG/0/" + wkid }, spatialReference: { wkid: wkid } }) .addNodes({ layerId: args.layerId, nodes: grid.nodes }); grid.geometries.forEach(({id, buffer}) => this.setNodeGeometry({ id: id, layerId: args.layerId, buffer: buffer })); grid.attributes.forEach(({id, attributes}) => this.setNodeAttributes({ id: id, layerId: args.layerId, attributes: attributes })); grid.sharedResources.forEach(({id, sharedResource}) => this.setNodeShared({ id: id, layerId: args.layerId, resource: sharedResource })); if (args.forEachLeaf) { grid.leafNodeIds.forEach(args.forEachLeaf.bind(this)); } return this; } /** * Define routes for a new point cloud layer on the service. * Metadata defining suitable defaults for the layer will be * generated. * * @param {Object} input - An object defining layer metadata to be mixed in * with the generated metadata. The input metadata must at least define a * layer id. */ addPointCloudLayer(input: LayerData) { handleValidation(typeValidator.check(input, layerDefinition)); const common = sceneServiceUtils.generatePCLayerData(); this.addLayer(deepMerge(common, input)); return this; } /** * Define routes for 'pages' on an existing point cloud layer. * * @param {Object} input - An object specifying the layer to define pages on * and the data of the pages. */ addPointCloudPages(input: PCPageLayerData) { handleValidation(typeValidator.check(input, pcPageLayerDefinition)); this._pcPageLayerData[input.layerId] = input; return this; } /** * Define routes for binary geometry data of a node on an existing * point cloud layer. * * @param {Object} input - An object specifying the node to define the * geometry for, the id of its layer and the geometry array buffer. */ setPointCloudNodeGeometry(input: PCNodeGeometryData) { this._pcNodeGeometryData = this._pcNodeGeometryData // replace conflicting items .filter(d => !(d.id === input.id && d.layerId === input.layerId)) .concat([input]); return this; } /** * Define routes for binary attribute data of a node on an existing * point cloud layer. * * @param {Object} input - An object specifying the node to define the * attributes for, the id of its layer and an array of attribute data. */ setPointCloudNodeAttributes(input: PCNodeAttributeData) { this._pcNodeAttributeData = this._pcNodeAttributeData // replace conflicting items .filter(d => !(d.id === input.id && d.layerId === input.layerId)) .concat([input]); return this; }; /** * Define routes for a new point cloud layer with page, geometry and * attribute resources for a procedurally generated quad. * * @param {Object} input - An object specifying the layer where the quad * should be stored and properties affecting its appearance. */ genPointCloudQuadLayer(args: GenPCQuadArgs) { const quad = sceneServiceUtils.genPointCloudQuad( args.position, args.subdiv, args.size ); this.addPointCloudLayer({ id: args.layerId, spatialReference: { wkid: args.wkid }, store: { extent: quad.extent } }) .addPointCloudPages({ layerId: args.layerId, pages: quad.pages }); for (const geometry of quad.nodeGeometries) { this.setPointCloudNodeGeometry({ id: geometry.id, layerId: args.layerId, buffer: geometry.buffer }) } for (const attributes of quad.nodeAttributes) { this.setPointCloudNodeAttributes({ id: attributes.id, layerId: args.layerId, attributes: attributes.attributes }); } return this; } getPortalItemType(args: PortalServiceArgs) { return "Scene Service"; } getServiceUrl(args: PortalServiceArgs = {}) { return generateLayerUrl({ url: this._urlData, serviceName: this._name, serverType: serverType }, args.layerId); } get routes() { const args: RouteGenerationArgs = { url: this._urlData, serviceName: this._name, serverType: serverType }; const rootRoute = getServiceRoutes(this._rootData, args); const layerIds = Object.keys(this._layerData); const layerRoutes = layerIds.map( id => getLayerRoutes(this._layerData[id], args) ); // only generate routes for layers that have been added const nodeRoutes = layerIds .map(id => this._nodeLayerData[id]) .filter(l => l != null) .map(l => MeshAndPoint.getNodeLayerRoutes(l, args)); const geometryRoutes = this._nodeGeometryData.map( d => MeshAndPoint.getNodeGeometryRoutes(d, args) ); const sharedResourceRoutes = this._nodeSharedData.map( d => MeshAndPoint.getSharedResourceGeometryRoutes(d, args) ); const attributeRoutes = this._nodeAttributeData.map( d => MeshAndPoint.getNodeAttributeRoutes(d, args) ); const featureRoutes = this._nodeFeatureData.map( d => MeshAndPoint.getNodeFeatureRoutes(d, args) ); const textureRoutes = this._nodeTextureData.map( d => MeshAndPoint.getNodeTextureRoutes(d, args) ); const pcPageRoutes = layerIds .map(id => this._pcPageLayerData[id]) .filter(l => l != null) .map(l => PointCloud.getPageLayerRoutes(l, args)); const pcGeometryRoutes = this._pcNodeGeometryData.map( g => PointCloud.getNodeGeometryRoutes(g, args) ); const pcAttributeRoutes = this._pcNodeAttributeData.map( a => PointCloud.getNodeAttributeRoutes(a, args) ); const default404 = generateDefault404Route(); return delayRoutes([ rootRoute, layerRoutes, nodeRoutes, geometryRoutes, sharedResourceRoutes, attributeRoutes, featureRoutes, textureRoutes, pcPageRoutes, pcGeometryRoutes, pcAttributeRoutes, default404 ], this._delayMs); } } export = SceneService;