/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2021, Open Design Alliance (the "Alliance"). // All rights reserved. // // This software and its documentation and related materials are owned by // the Alliance. The software may only be incorporated into application // programs owned by members of the Alliance, subject to a signed // Membership Agreement and Supplemental Software License Agreement with the // Alliance. The structure and organization of this software are the valuable // trade secrets of the Alliance and its suppliers. The software is also // protected by copyright law and international treaty provisions. Application // programs incorporating this software must include the following statement // with their copyright notices: // // This application incorporates Open Design Alliance software pursuant to a // license agreement with Open Design Alliance. // Open Design Alliance Copyright (C) 2002-2021 by Open Design Alliance. // All rights reserved. // // By use of this software, its documentation or related materials, you // acknowledge and accept the above terms. /////////////////////////////////////////////////////////////////////////////// import { BaseLoader } from "./BaseLoader"; import { UpdaterController, UpdateType } from "./UpdaterController"; export class VsfXPartialLoader extends BaseLoader { override async load(): Promise { if (!this.viewer.visualizeJs) return; const visLib = this.viewer.visLib(); const visViewer = visLib.getViewer(); const abortController = new AbortController(); const abortControllerForRequestMap = new Map(); let servicePartAborted = false; const pendingRequestsMap = new Map(); const PENDING_REQUESTS_SIZE = 50; const PENDING_REQUESTS_TIMEOUT = 250; let pendingRequestsTimerId = 0; const pendingRequestsAbortHandler = () => clearTimeout(pendingRequestsTimerId); const pendingRequestsAbortController = new AbortController(); abortControllerForRequestMap.set(0, pendingRequestsAbortController); const updaterController = new UpdaterController(); updaterController.initialize(this.viewer); this.viewer._abortController = abortController; this.viewer._abortControllerForRequestMap = abortControllerForRequestMap; visViewer.memoryLimit = this.options.memoryLimit; const requestLoadHandler = (progress, data, requestId) => { if (!this.viewer.visualizeJs) return; const state = visViewer.parseVsfxInPartialMode(requestId, data); updaterController.update(UpdateType.kDelay); this.viewer.emitEvent({ type: "geometryprogress", data: progress, model: this.model }); if (state) { updaterController.update(UpdateType.kForce); this.viewer.syncOpenCloudVisualStyle(false); this.viewer.syncOptions(); this.viewer.resize(); this.viewer.emitEvent({ type: "databasechunk", data, model: this.model }); } else { this.viewer.emitEvent({ type: "geometrychunk", data, model: this.model }); } }; const downloadPartOfFile = async (requestId, records, dataId, isMultipleParts = false) => { const abortCtrl = new AbortController(); abortControllerForRequestMap.set(requestId, abortCtrl); try { await this.model.downloadFileRange(requestId, records, dataId, requestLoadHandler, abortCtrl.signal); } catch (error) { this.viewer.emitEvent({ type: "geometryerror", data: error, model: this.model }); } finally { const requests = isMultipleParts ? [...new Set(records.map((item) => item.reqId))] : [requestId]; requests.forEach((requestId) => visViewer.onRequestResponseComplete(requestId)); abortControllerForRequestMap.delete(requestId); updaterController.update(UpdateType.kNormal); } }; const recordsToArray = (requestId, records) => { const res = []; for (let i = 0; i < records.size(); i++) { const record = records.get(i); res.push({ reqId: requestId, begin: record.begin, end: record.end, size: parseInt(record.end, 10) - parseInt(record.begin, 10), }); record.delete(); } return res; }; const objectHandler = { onServicePartReceived: (bHasIndex) => { if (bHasIndex) { servicePartAborted = true; abortController.abort(); } }, onRequest: (requestId, records) => { downloadPartOfFile(requestId, records, this.model.database); }, onFullLoaded: () => { updaterController.update(UpdateType.kNormal); console.timeEnd("File load time"); }, onRequestResponseParsed: (requestId) => { abortControllerForRequestMap.delete(requestId); updaterController.update(UpdateType.kNormal); }, onRequestAborted: (requestId) => { const abortCtrl = abortControllerForRequestMap.get(requestId); if (abortCtrl) abortCtrl.abort(); }, onRequestResourceFile: async (requestId, _, records) => { const dataId = `${this.model.fileId}${this.model.file.type}`; let pendingRequests = []; let recNumber = 0; const pendingRequestsRecord = pendingRequestsMap.get(dataId); if (pendingRequestsRecord) { pendingRequests = pendingRequestsRecord.array; recNumber = pendingRequestsRecord.number; } // first several records of each file are processed without grouping (they usually require to be processed sequentially) if (recNumber < 6) { pendingRequestsMap.set(dataId, { array: pendingRequests, number: recNumber + 1 }); await downloadPartOfFile(requestId, records, dataId); return; } // group requests to each file to launch a combined server request if (pendingRequests.length >= PENDING_REQUESTS_SIZE) { if (pendingRequestsTimerId) { window.clearTimeout(pendingRequestsTimerId); pendingRequestsTimerId = 0; } downloadPartOfFile(requestId, pendingRequests, dataId, true); pendingRequests = [...recordsToArray(requestId, records)]; } else { pendingRequests = [...pendingRequests, ...recordsToArray(requestId, records)]; } pendingRequestsMap.set(dataId, { array: pendingRequests, number: recNumber + 1 }); // set timeout to wait for the new requests, after that process the remaining requests if (pendingRequestsTimerId === 0) { pendingRequestsTimerId = window.setTimeout(() => { pendingRequestsAbortController.signal.removeEventListener("abort", pendingRequestsAbortHandler); pendingRequestsTimerId = 0; pendingRequestsMap.forEach((requestsRecord, keyFileName) => { const array = requestsRecord.array; if (array.length > 0) { downloadPartOfFile(requestId, array, keyFileName, true); pendingRequestsMap.set(keyFileName, { array: [], number: requestsRecord.number + 1 }); } }); }, PENDING_REQUESTS_TIMEOUT); pendingRequestsAbortController.signal.addEventListener("abort", pendingRequestsAbortHandler, { once: true }); } }, }; visViewer.attachPartialResolver(objectHandler); console.time("File load time"); try { this.viewer.emitEvent({ type: "geometrystart", model: this.model }); await this.model .downloadFileRange(0, null, this.model.database, requestLoadHandler, abortController.signal) .catch((error) => { if (!servicePartAborted) throw error; }); this.viewer.emitEvent({ type: "geometryend", model: this.model }); } catch (error) { if (pendingRequestsTimerId) { window.clearTimeout(pendingRequestsTimerId); pendingRequestsTimerId = 0; } this.viewer.emitEvent({ type: "geometryerror", data: error, model: this.model }); throw error; } } }