UNPKG

@itwin/core-frontend

Version:
596 lines 29.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module IModelApp */ // @ts-expect-error package.json will resolve from the lib/{cjs,esm} dir without copying it into the build output we deliver // eslint-disable-next-line @itwin/import-within-package import packageJson from "../../package.json"; /** @public */ export const ITWINJS_CORE_VERSION = packageJson.version; const COPYRIGHT_NOTICE = `Copyright © 2017-${new Date().getFullYear()} <a href="https://www.bentley.com" target="_blank" rel="noopener noreferrer">Bentley Systems, Inc.</a>`; import { UiAdmin } from "@itwin/appui-abstract"; import { BeDuration, BeEvent, BentleyStatus, DbResult, dispose, expectDefined, Guid, IModelStatus, Logger, ProcessDetector } from "@itwin/core-bentley"; import { RpcConfiguration } from "@itwin/core-common"; import { ITwinLocalization } from "@itwin/core-i18n"; import { queryRenderCompatibility } from "@itwin/webgl-compatibility"; import { AccuDraw } from "./AccuDraw"; import { AccuSnap } from "./AccuSnap"; import * as auxCoordState from "./AuxCoordSys"; import * as categorySelectorState from "./CategorySelectorState"; import { ExtensionAdmin } from "./extension/ExtensionAdmin"; import * as displayStyleState from "./DisplayStyleState"; import * as drawingViewState from "./DrawingViewState"; import { ElementLocateManager } from "./ElementLocateManager"; import { ElementState, EntityState } from "./EntityState"; import { FrontendLoggerCategory } from "./common/FrontendLoggerCategory"; import * as modelselector from "./ModelSelectorState"; import * as modelState from "./ModelState"; import { NotificationManager } from "./NotificationManager"; import { FormatsProviderManager, QuantityFormatter, QuantityTypeFormatsProvider } from "./quantity-formatting/QuantityFormatter"; import { RenderSystem } from "./render/RenderSystem"; import { System } from "./internal/render/webgl/System"; import * as sheetState from "./SheetViewState"; import * as spatialViewState from "./SpatialViewState"; import { TentativePoint } from "./TentativePoint"; import { RealityDataSourceProviderRegistry } from "./RealityDataSource"; import { MapLayerFormatRegistry, TerrainProviderRegistry, TileAdmin } from "./tile/internal"; import * as accudrawTool from "./tools/AccuDrawTool"; import * as clipViewTool from "./tools/ClipViewTool"; import * as idleTool from "./tools/IdleTool"; import * as measureTool from "./tools/MeasureTool"; import * as selectTool from "./tools/SelectTool"; import { ToolRegistry } from "./tools/Tool"; import { ToolAdmin } from "./tools/ToolAdmin"; import * as viewTool from "./tools/ViewTool"; import { ViewManager } from "./ViewManager"; import * as viewState from "./ViewState"; import "./IModeljs-css"; /** * Global singleton that connects the user interface with the iTwin.js services. There can be only one IModelApp active in a session. All * members of IModelApp are static, and it serves as a singleton object for gaining access to session information. * * Before any interactive operations may be performed by the `@itwin/core-frontend package`, [[IModelApp.startup]] must be called and awaited. * Applications may customize the frontend behavior of iTwin.js by supplying options to [[IModelApp.startup]]. * * @public */ export class IModelApp { static _initialized = false; static _accuDraw; static _accuSnap; static _applicationId; static _applicationVersion; static _localization; static _locateManager; static _notifications; static _quantityFormatter; static _renderSystem; static _userPreferences; static _tentativePoint; static _tileAdmin; static _toolAdmin; static _viewManager; static _uiAdmin; static _noRender; static _wantEventLoop = false; static _animationRequested = false; static _animationInterval = BeDuration.fromSeconds(1); static _animationIntervalId; static _securityOptions; static _mapLayerFormatRegistry; static _terrainProviderRegistry; static _realityDataSourceProviders; static _hubAccess; static _realityDataAccess; static _publicPath; static _formatsProviderManager; static _incrementalSchemaLoading; // No instances of IModelApp may be created. All members are static and must be on the singleton object IModelApp. constructor() { } /** Event raised just before the frontend IModelApp is to be [[shutdown]]. */ static onBeforeShutdown = new BeEvent(); /** Event raised after IModelApp [[startup]] completes. */ static onAfterStartup = new BeEvent(); /** The AuthorizationClient used to obtain [AccessToken]($bentley)s. */ static authorizationClient; /** The [[ToolRegistry]] for this session. */ static tools = new ToolRegistry(); /** A uniqueId for this session */ static sessionId; /** The [[MapLayerFormatRegistry]] for this session. */ static get mapLayerFormatRegistry() { return this._mapLayerFormatRegistry; } /** The [[TerrainProviderRegistry]] for this session. */ static get terrainProviderRegistry() { return this._terrainProviderRegistry; } /** The [[RealityDataSourceProviderRegistry]] for this session. * @beta */ static get realityDataSourceProviders() { return this._realityDataSourceProviders; } /** The [[RenderSystem]] for this session. */ static get renderSystem() { return expectDefined(this._renderSystem); } /** The [[ViewManager]] for this session. */ static get viewManager() { return this._viewManager; } /** The [[NotificationManager]] for this session. */ static get notifications() { return this._notifications; } /** The [[TileAdmin]] for this session. */ static get tileAdmin() { return this._tileAdmin; } /** The [[QuantityFormatter]] for this session. */ static get quantityFormatter() { return this._quantityFormatter; } /** The [[ToolAdmin]] for this session. */ static get toolAdmin() { return this._toolAdmin; } /** The [[AccuDraw]] for this session. */ static get accuDraw() { return this._accuDraw; } /** The [[AccuSnap]] for this session. */ static get accuSnap() { return this._accuSnap; } static get locateManager() { return this._locateManager; } /** The [[TentativePoint]] for this session]]. */ static get tentativePoint() { return this._tentativePoint; } /** The [[Localization]] for this session. */ static get localization() { return this._localization; } /** The [[UserPreferencesAccess]] for this session. * @beta */ static get userPreferences() { return this._userPreferences; } /** The Id of this application. Applications must set this to the Global Product Registry ID (GPRID) for usage logging. */ static get applicationId() { return this._applicationId; } /** The version of this application. Must be set for usage logging. */ static get applicationVersion() { return this._applicationVersion; } /** True after [[startup]] has been called, until [[shutdown]] is called. */ static get initialized() { return this._initialized; } /** Provides access to IModelHub services. */ static get hubAccess() { return this._hubAccess; } /** Provides access to the RealityData service implementation for this IModelApp * @beta */ static get realityDataAccess() { return this._realityDataAccess; } /** * Indicates whether incremental schema loading is enabled. * If not further specified, incremental schema loading is currently disabled by default. * @beta */ static get isIncrementalSchemaLoadingEnabled() { return this._incrementalSchemaLoading === "enabled"; } ; /** Whether the [renderSystem[]] has been successfully initialized. * This will always be `false` before calling [[startup]] and after calling [[shutdown]]. * In rare circumstances (e.g., while executing in a headless test environment) it may remain `false` due to a failure to * obtain a [WebGL rendering context](https://www.google.com/search?channel=fs&client=ubuntu-sn&q=mdn+webglrenderingcontext). * As long as you have called [[startup]], you can generally assume it to be `true`. */ static get hasRenderSystem() { return this._renderSystem !== undefined && this._renderSystem.isValid; } /** The [[UiAdmin]] for this session. */ static get uiAdmin() { return this._uiAdmin; } /** The requested security options for the frontend. */ static get securityOptions() { return this._securityOptions; } /** If present, overrides where public assets are fetched. The default is to fetch assets relative to the current URL. * The path should always end with a trailing `/`. */ static get publicPath() { return this._publicPath; } /** The [[FormatsProvider]] for this session. * @param provider The provider to use for formatting quantities. * @beta */ static get formatsProvider() { return this._formatsProviderManager; } static set formatsProvider(provider) { this._formatsProviderManager.formatsProvider = provider; } /** @alpha */ static extensionAdmin = this._createExtensionAdmin(); /** Map of classFullName to EntityState class */ static _entityClasses = new Map(); /** Register all of the subclasses of EntityState from a module. * @internal */ static registerModuleEntities(moduleObj) { for (const thisMember in moduleObj) { // eslint-disable-line guard-for-in const thisEntityState = moduleObj[thisMember]; if (thisEntityState.prototype instanceof EntityState) { this.registerEntityState(thisEntityState.classFullName, thisEntityState); } } } /** Register an EntityState class by its classFullName * @internal */ static registerEntityState(classFullName, classType) { const lowerName = classFullName.toLowerCase(); if (this._entityClasses.has(lowerName)) { const errMsg = `Class ${classFullName} is already registered. Make sure static schemaName and className members are correct on class ${classType.name}`; Logger.logError(FrontendLoggerCategory.IModelConnection, errMsg); throw new Error(errMsg); } this._entityClasses.set(lowerName, classType); } /** @internal */ static lookupEntityClass(classFullName) { return this._entityClasses.get(classFullName.toLowerCase()); } /** * Obtain WebGL rendering compatibility information for the client system. This information describes whether the client meets the * minimum rendering capabilities. It also describes whether the system lacks any optional capabilities that could improve quality * and/or performance. * @note As of 4.x, iTwin.js requires WebGL 2. If the client does not support WebGL 2, the `status` field of the returned compatibility info will be [WebGLRenderCompatibilityStatus.CannotCreateContext]($webgl-compatibility). */ static queryRenderCompatibility() { return queryRenderCompatibility(true, (canvas, useWebGL2, inputContextAttributes) => System.createContext(canvas, useWebGL2, inputContextAttributes)); } /** * This method must be called before any other `@itwin/core-frontend` methods are used. * Somewhere in your startup code, call [[IModelApp.startup]]. E.g.: * ``` ts * await IModelApp.startup( {applicationId: myAppId} ); * ``` * @param opts The options for configuring IModelApp */ static async startup(opts) { if (this._initialized) return; // we're already initialized, do nothing. this._initialized = true; opts = opts ?? {}; this._securityOptions = opts.security ?? {}; if (process.env.NODE_ENV === "development") { // Make IModelApp globally accessible for debugging purposes. We'll remove it on shutdown. window.iModelAppForDebugger = this; } this.sessionId = opts.sessionId ?? Guid.createValue(); this._applicationId = opts.applicationId ?? "2686"; // Default to product id of iTwin.js this._applicationVersion = opts.applicationVersion ?? "1.0.0"; this.authorizationClient = opts.authorizationClient; this._hubAccess = opts.hubAccess; this._noRender = opts.noRender ?? false; this._setupRpcRequestContext(); this._localization = opts.localization ?? new ITwinLocalization(); const toolsNs = "CoreTools"; await this.localization.initialize(["iModelJs", toolsNs]); [ selectTool, idleTool, viewTool, clipViewTool, measureTool, accudrawTool, ].forEach((tool) => this.tools.registerModule(tool, toolsNs)); this.registerEntityState(EntityState.classFullName, EntityState); this.registerEntityState(ElementState.classFullName, ElementState); [ modelState, sheetState, viewState, drawingViewState, spatialViewState, displayStyleState, modelselector, categorySelectorState, auxCoordState, ].forEach((module) => this.registerModuleEntities(module)); this._renderSystem = (opts.renderSys instanceof RenderSystem) ? opts.renderSys : this.createRenderSys(opts.renderSys); if (opts.userPreferences) this._userPreferences = opts.userPreferences; this._viewManager = opts.viewManager ?? new ViewManager(); this._tileAdmin = await TileAdmin.create(opts.tileAdmin); this._notifications = opts.notifications ?? new NotificationManager(); this._toolAdmin = opts.toolAdmin ?? new ToolAdmin(); this._accuDraw = opts.accuDraw ?? new AccuDraw(); this._accuSnap = opts.accuSnap ?? new AccuSnap(); this._locateManager = opts.locateManager ?? new ElementLocateManager(); this._tentativePoint = opts.tentativePoint ?? new TentativePoint(); this._quantityFormatter = opts.quantityFormatter ?? new QuantityFormatter(); this._uiAdmin = opts.uiAdmin ?? new UiAdmin(); this._mapLayerFormatRegistry = new MapLayerFormatRegistry(opts.mapLayerOptions); this._terrainProviderRegistry = new TerrainProviderRegistry(); this._realityDataSourceProviders = new RealityDataSourceProviderRegistry(); this._realityDataAccess = opts.realityDataAccess; this._formatsProviderManager = new FormatsProviderManager(opts.formatsProvider ?? new QuantityTypeFormatsProvider()); this._incrementalSchemaLoading = opts.incrementalSchemaLoading ?? "disabled"; this._publicPath = opts.publicPath ?? ""; if (this._publicPath !== "" && !this._publicPath.endsWith("/")) { this._publicPath += "/"; } [ this.renderSystem, this.viewManager, this.toolAdmin, this.accuDraw, this.accuSnap, this.locateManager, this.tentativePoint, this.uiAdmin, ].forEach((sys) => sys.onInitialized()); await this.quantityFormatter.onInitialized(); this.onAfterStartup.raiseEvent(); } /** Must be called before the application exits to release any held resources. */ static async shutdown() { if (!this._initialized) return; // notify listeners that this IModelApp is about to be shut down. this.onBeforeShutdown.raiseEvent(); this.onBeforeShutdown.clear(); if (process.env.NODE_ENV === "development") { window.iModelAppForDebugger = undefined; } this._wantEventLoop = false; window.removeEventListener("resize", () => IModelApp.requestNextAnimation()); this.clearIntervalAnimation(); [this.toolAdmin, this.viewManager, this.tileAdmin].forEach((sys) => sys.onShutDown()); this.tools.shutdown(); this._renderSystem = dispose(this._renderSystem); this._entityClasses.clear(); this.authorizationClient = undefined; this._initialized = false; this.quantityFormatter[Symbol.dispose](); this.resetFormatsProvider(); this.onAfterStartup.clear(); } /** Controls how frequently the application polls for changes that may require a new animation frame to be requested. * Such changes include resizing a Viewport or changing the device pixel ratio by zooming in or out in the browser. * The default interval is 1 second. It may be desirable to override the default for specific apps and/or devices. * - Increasing the interval can conserve battery life on battery-powered devices at the expense of slower response to resize events. * - An application that only displays a single Viewport whose dimensions only change when the dimensions of the application window change, and which does not support changing application zoom level, could disable the interval altogether. * @param interval The interval at which to poll for changes. If undefined (or negative), the application will never poll. If zero, the application will poll as frequently as possible. * @beta */ static get animationInterval() { return IModelApp._animationInterval; } static set animationInterval(interval) { if (undefined !== interval && interval.isTowardsPast) interval = undefined; if (interval !== IModelApp._animationInterval) { IModelApp._animationInterval = interval; if (IModelApp._wantEventLoop) IModelApp.requestIntervalAnimation(); } } /** Request that the event loop execute on the next [animation frame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). * There is generally no reason for applications to invoke this method directly. */ static requestNextAnimation() { // Only want to call requestAnimationFrame if it is defined. Need to check whether current iModelApp is a NoRenderApp. if (IModelApp._noRender) return; if (!IModelApp._animationRequested) { IModelApp._animationRequested = true; requestAnimationFrame(() => IModelApp.eventLoop()); } } /** @internal */ static clearIntervalAnimation() { if (undefined !== IModelApp._animationIntervalId) { window.clearInterval(IModelApp._animationIntervalId); IModelApp._animationIntervalId = undefined; } } /** @internal */ static requestIntervalAnimation() { IModelApp.clearIntervalAnimation(); if (undefined !== IModelApp.animationInterval) IModelApp._animationIntervalId = window.setInterval(() => { IModelApp.requestNextAnimation(); }, IModelApp.animationInterval.milliseconds); } /** Return true if the main event processing loop has been started. * @internal */ static get isEventLoopStarted() { return IModelApp._wantEventLoop; } /** @internal */ static startEventLoop() { if (!IModelApp._wantEventLoop) { IModelApp._wantEventLoop = true; window.addEventListener("resize", () => IModelApp.requestNextAnimation()); IModelApp.requestIntervalAnimation(); IModelApp.requestNextAnimation(); } } /** Strictly for tests. @internal */ static stopEventLoop() { this._wantEventLoop = false; } /** The main event processing loop for Tools and rendering. */ static eventLoop() { IModelApp._animationRequested = false; if (!IModelApp._wantEventLoop) // flag turned on at startup return; try { IModelApp.toolAdmin.processEvent(); // eslint-disable-line @typescript-eslint/no-floating-promises IModelApp.viewManager.renderLoop(); IModelApp.tileAdmin.process(); } catch (exception) { ToolAdmin.exceptionHandler(exception); // eslint-disable-line @typescript-eslint/no-floating-promises IModelApp._wantEventLoop = false; IModelApp._animationRequested = true; // unrecoverable after exception, don't request any further frames. window.removeEventListener("resize", () => IModelApp.requestNextAnimation()); } } /** Get the user's access token for this IModelApp, or a blank string if none is available. * @note Access tokens expire periodically and are automatically refreshed, if possible. Therefore tokens should not be saved, and the value * returned by this method may change over time throughout the course of a session. */ static async getAccessToken() { try { return (await this.authorizationClient?.getAccessToken()) ?? ""; } catch { return ""; } } /** @internal */ static createRenderSys(opts) { return System.create(opts); } static _setupRpcRequestContext() { RpcConfiguration.requestContext.getId = (_request) => { return Guid.createValue(); }; RpcConfiguration.requestContext.serialize = async (_request) => { const id = _request.id; const serialized = { id, applicationId: this.applicationId, applicationVersion: this.applicationVersion, sessionId: this.sessionId, authorization: ProcessDetector.isMobileAppFrontend ? "" : await this.getAccessToken(), }; const csrf = IModelApp.securityOptions.csrfProtection; if (csrf && csrf.enabled) { const cookieName = csrf.cookieName || "XSRF-TOKEN"; const cookieValue = document.cookie.split("; ").find((r) => r.startsWith(`${cookieName}=`)); if (cookieValue) { const headerName = csrf.headerName || "X-XSRF-TOKEN"; const headerValue = cookieValue.split("=")[1]; serialized.csrfToken = { headerName, headerValue }; } } return serialized; }; } /** Shortcut for creating an HTMLElement with optional parent, className, id, innerHTML, innerText. */ static makeHTMLElement(type, opt) { const el = document.createElement(type); if (undefined !== opt) { if (undefined !== opt.className) el.className = opt.className; if (undefined !== opt.id) el.id = opt.id; if (undefined !== opt.innerHTML) el.innerHTML = opt.innerHTML; if (undefined !== opt.innerText) el.innerText = opt.innerText; if (undefined !== opt.parent) opt.parent.appendChild(el); } return el; } /** Shortcut for making a modal dialog on top of the root of the application. The returned HTMLDivElement will be placed topmost, all other application * windows will be covered with a semi-transparent background that intercepts all key/mouse/touch events until the modal is dismissed. * @param options The options that describe how the modal should work. */ static makeModalDiv(options) { const root = options.rootDiv ? options.rootDiv : document.body; // create the overlay div to "black out" the application to indicate everything is inactive until the modal has been dismissed. const overlay = IModelApp.makeHTMLElement("div", { parent: root, className: "imodeljs-modal-overlay" }); overlay.tabIndex = -1; // so we can catch keystroke events // function to remove modal dialog const stop = (ev) => { root.removeChild(overlay); ev.stopPropagation(); }; if (options.autoClose) { overlay.onclick = overlay.oncontextmenu = stop; overlay.onkeydown = overlay.onkeyup = (ev) => { switch (ev.key) { case "Enter": case "Escape": stop(ev); return; } ev.stopPropagation(); }; overlay.focus(); } const modal = IModelApp.makeHTMLElement("div", { parent: overlay, className: "imodeljs-modal" }); if (undefined !== options.width) { modal.style.width = `${options.width}px`; // allow the dialog to be smaller than the width modal.style.maxWidth = `min(100% - (2 * var(--width-border)), ${options.width}px)`; } if (options.closeBox) { const close = IModelApp.makeHTMLElement("p", { parent: modal, className: "imodeljs-modal-close" }); close.innerText = "\u00d7"; // unicode "times" symbol close.onclick = stop; } return { modal, stop }; } /** Applications may implement this method to supply a Logo Card. * @beta */ static applicationLogoCard; /** Make a new Logo Card. Call this method from your implementation of [[IModelApp.applicationLogoCard]] * @param opts Options for Logo Card * @beta */ static makeLogoCard(opts) { const card = IModelApp.makeHTMLElement("tr"); const iconCell = IModelApp.makeHTMLElement("td", { parent: card, className: "logo-card-logo" }); if (undefined !== opts.iconSrc) { if (typeof opts.iconSrc === "string") { const logo = IModelApp.makeHTMLElement("img"); logo.src = opts.iconSrc; logo.width = opts.iconWidth ? opts.iconWidth : 64; opts.iconSrc = logo; } iconCell.appendChild(opts.iconSrc); } const noticeCell = IModelApp.makeHTMLElement("td", { parent: card, className: "logo-card-message" }); if (undefined !== opts.heading) { if (typeof opts.heading === "string") IModelApp.makeHTMLElement("h2", { parent: noticeCell, innerHTML: opts.heading, className: "logo-card-header" }); else noticeCell.appendChild(opts.heading); } if (undefined !== opts.notice) { if (typeof opts.notice === "string") IModelApp.makeHTMLElement("p", { parent: noticeCell, innerHTML: opts.notice, className: "logo-cards" }); else noticeCell.appendChild(opts.notice); } return card; } /** Make the logo card for the library itself. This card gets placed at the top of the stack. * @internal */ static makeIModelJsLogoCard() { return this.makeLogoCard({ iconSrc: `${this.publicPath}images/about-imodeljs.svg`, heading: `<span class="itwinjs-header-lead">${this.localization.getLocalizedString("iModelJs:Notices.PoweredBy")}</span>&nbsp;iTwin.js`, notice: `${ITWINJS_CORE_VERSION}<br>${COPYRIGHT_NOTICE}`, }); } /** Format the tooltip strings returned by [[IModelConnection.getToolTipMessage]]. * @alpha */ static formatElementToolTip(msg) { let out = ""; msg.forEach((line) => out += `${IModelApp.localization?.getLocalizedKeys(line)}<br>`); const div = document.createElement("div"); div.innerHTML = out; return div; } /** Localize an error status * @param status one of the status values from [BentleyStatus]($core-bentley), [IModelStatus]($core-bentley) or [DbResult]($core-bentley) * @returns a localized error message * @beta */ static translateStatus(status) { let key; if (typeof status !== "number") { key = { scope: "Errors", val: "IllegalValue" }; } else { key = { scope: "BentleyStatus", val: BentleyStatus[status] }; if (!key.val) key = { scope: "IModelStatus", val: IModelStatus[status] }; if (!key.val) key = { scope: "DbResult", val: DbResult[status] }; if (!key.val) key = { scope: "Errors", val: "Status", status: status.toString() }; } return this.localization.getLocalizedString(`iModelJs:${key.scope}.${key.val}`, key); } /** * Resets the formatsProvider back to the default [[QuantityTypeFormatsProvider]]. * @beta */ static resetFormatsProvider() { this.formatsProvider = new QuantityTypeFormatsProvider(); } /** * Creates an instance of the ExtensionAdmin * and registers an event to execute after startup is complete * @returns an instance of ExtensionAdmin */ static _createExtensionAdmin() { const extensionAdmin = new ExtensionAdmin(); IModelApp.onAfterStartup.addListener(extensionAdmin.onStartup); return extensionAdmin; } } //# sourceMappingURL=IModelApp.js.map