UNPKG

@itwin/core-frontend

Version:
452 lines • 20.3 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module MapLayers */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MapLayerImageryProvider = exports.MapLayerImageryProviderStatus = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const core_geometry_1 = require("@itwin/core-geometry"); const IModelApp_1 = require("../../IModelApp"); const NotificationManager_1 = require("../../NotificationManager"); const internal_1 = require("../internal"); const utils_1 = require("../../request/utils"); /** @internal */ const tileImageSize = 256, untiledImageSize = 256; const earthRadius = 6378137; const doDebugToolTips = false; /** The status of the map layer imagery provider that lets you know if authentication is needed to request tiles. * @public */ var MapLayerImageryProviderStatus; (function (MapLayerImageryProviderStatus) { MapLayerImageryProviderStatus[MapLayerImageryProviderStatus["Valid"] = 0] = "Valid"; MapLayerImageryProviderStatus[MapLayerImageryProviderStatus["RequireAuth"] = 1] = "RequireAuth"; })(MapLayerImageryProviderStatus || (exports.MapLayerImageryProviderStatus = MapLayerImageryProviderStatus = {})); /** Abstract class for map layer imagery providers. * Map layer imagery providers request and provide tile images and other data. Each map layer from a separate source needs its own imagery provider object. * @beta */ class MapLayerImageryProvider { _settings; _usesCachedTiles; _hasSuccessfullyFetchedTile = false; onStatusChanged = new core_bentley_1.BeEvent(); /** @internal */ _mercatorTilingScheme = new internal_1.WebMercatorTilingScheme(); /** @internal */ _geographicTilingScheme = new internal_1.GeographicTilingScheme(); /** @internal */ _status = MapLayerImageryProviderStatus.Valid; /** @internal */ _includeUserCredentials = false; /** @internal */ onFirstRequestCompleted = new core_bentley_1.BeEvent(); /** @internal */ _firstRequestPromise; /** * The status of the map layer imagery provider. * @public @preview */ get status() { return this._status; } /** Determine if this provider supports map feature info. * For example, this can be used to show the map feature info tool only when a provider is registered to support it. * @returns true if provider supports map feature info else return false. * @public */ get supportsMapFeatureInfo() { return false; } resetStatus() { this.setStatus(MapLayerImageryProviderStatus.Valid); } /** @internal */ get tileSize() { return this._usesCachedTiles ? tileImageSize : untiledImageSize; } /** @internal */ get maximumScreenSize() { return 2 * this.tileSize; } get minimumZoomLevel() { return this.defaultMinimumZoomLevel; } get maximumZoomLevel() { return this.defaultMaximumZoomLevel; } /** @internal */ get usesCachedTiles() { return this._usesCachedTiles; } get mutualExclusiveSubLayer() { return false; } /** @internal */ get useGeographicTilingScheme() { return false; } _cartoRange; /** Validates a cartographic range for NaN and infinite values. * @param range The cartographic range to validate. * @returns true if the range is valid, false otherwise. * @internal */ static isRangeValid(range) { if (!range) { return false; } return !Number.isNaN(range.low.x) && !Number.isNaN(range.low.y) && !Number.isNaN(range.high.x) && !Number.isNaN(range.high.y) && Number.isFinite(range.low.x) && Number.isFinite(range.low.y) && Number.isFinite(range.high.x) && Number.isFinite(range.high.y); } /** Gets or sets the cartographic range for this provider. * When setting, if the range is invalid (contains NaN or infinite values), it will be stored as undefined. * When getting, returns undefined if the range was set to an invalid value. */ get cartoRange() { return this._cartoRange; } set cartoRange(range) { this._cartoRange = MapLayerImageryProvider.isRangeValid(range) ? range : undefined; } /** * This value is used internally for various computations, this should not get overriden. * @internal */ defaultMinimumZoomLevel = 0; /** * This value is used internally for various computations, this should not get overriden. * @internal */ defaultMaximumZoomLevel = 22; /** @internal */ get _filterByCartoRange() { return true; } constructor(_settings, _usesCachedTiles) { this._settings = _settings; this._usesCachedTiles = _usesCachedTiles; this._mercatorTilingScheme = new internal_1.WebMercatorTilingScheme(); this._geographicTilingScheme = new internal_1.GeographicTilingScheme(2, 1, true); } /** * Initialize the provider by loading the first tile at its default maximum zoom level. * @beta */ async initialize() { this.loadTile(0, 0, this.defaultMaximumZoomLevel).then((tileData) => { if (tileData !== undefined) this._missingTileData = tileData.data; }); } get tilingScheme() { return this.useGeographicTilingScheme ? this._geographicTilingScheme : this._mercatorTilingScheme; } /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [addAttributions] instead. */ addLogoCards(_cards, _viewport) { } /** * Add attribution logo cards for the data supplied by this provider to the [[Viewport]]'s logo div. * @param _cards Logo cards HTML element that may contain custom data attributes. * @param _viewport Viewport to add logo cards to. * @beta */ async addAttributions(cards, vp) { // eslint-disable-next-line @typescript-eslint/no-deprecated return Promise.resolve(this.addLogoCards(cards, vp)); } /** @internal */ _missingTileData; /** @internal */ get transparentBackgroundString() { return this._settings.transparentBackground ? "true" : "false"; } /** @internal */ async _areChildrenAvailable(_tile) { return true; } /** @internal */ getPotentialChildIds(quadId) { const childLevel = quadId.level + 1; return quadId.getChildIds(this.tilingScheme.getNumberOfXChildrenAtLevel(childLevel), this.tilingScheme.getNumberOfYChildrenAtLevel(childLevel)); } /** * Get child IDs of a quad and generate tiles based on these child IDs. * See [[ImageryTileTree._loadChildren]] for the definition of `resolveChildren` where this function is commonly called. * @param quadId quad to generate child IDs for. * @param resolveChildren Function that creates tiles from child IDs. * @beta */ _generateChildIds(quadId, resolveChildren) { resolveChildren(this.getPotentialChildIds(quadId)); } /** @internal */ generateChildIds(tile, resolveChildren) { if (tile.depth >= this.maximumZoomLevel || (undefined !== this.cartoRange && this._filterByCartoRange && !this.cartoRange.intersectsRange(tile.rectangle))) { tile.setLeaf(); return; } this._generateChildIds(tile.quadId, resolveChildren); } /** * Get tooltip text for a specific quad and cartographic position. * @param strings List of strings to contain tooltip text. * @param quadId Quad ID to get tooltip for. * @param _carto Cartographic that may be used to retrieve and/or format tooltip text. * @param tree Tree associated with the quad to get the tooltip for. * @internal */ async getToolTip(strings, quadId, _carto, tree) { if (doDebugToolTips) { const range = quadId.getLatLongRangeDegrees(tree.tilingScheme); strings.push(`QuadId: ${quadId.debugString}, Lat: ${range.low.x} - ${range.high.x} Long: ${range.low.y} - ${range.high.y}`); } } /** @internal */ async getFeatureInfo(featureInfos, _quadId, _carto, _tree, _hit, _options) { // default implementation; simply return an empty feature info featureInfos.push({ layerName: this._settings.name }); } /** @internal */ decorate(_context) { } /** @internal */ async getImageFromTileResponse(tileResponse, zoomLevel) { const arrayBuffer = await tileResponse.arrayBuffer(); const byteArray = new Uint8Array(arrayBuffer); if (!byteArray || (byteArray.length === 0)) return undefined; if (this.matchesMissingTile(byteArray) && zoomLevel > 8) return undefined; const contentType = tileResponse.headers.get("content-type")?.toLowerCase(); let imageFormat; if (contentType) { // Note: 'includes' is used here instead of exact comparison because we encountered // some servers that would give content type such as 'image/png;charset=UTF-8'. if (contentType.includes("image/jpeg")) imageFormat = core_common_1.ImageSourceFormat.Jpeg; else if (contentType.includes("image/png")) imageFormat = core_common_1.ImageSourceFormat.Png; } if (imageFormat !== undefined) return new core_common_1.ImageSource(byteArray, imageFormat); (0, core_bentley_1.assert)(false, "Invalid tile content type"); return undefined; } /** * Change the status of this provider. * Sub-classes should override 'onStatusUpdated' instead of this method. * @internal */ setStatus(status) { if (this._status !== status) { this.onStatusUpdated(status); this._status = status; this.onStatusChanged.raiseEvent(this); } } /** Method called whenever the status changes, giving the opportunity to sub-classes to have a custom behavior. * @internal */ onStatusUpdated(_newStatus) { } /** @internal */ setRequestAuthorization(headers) { if (this._settings.userName && this._settings.password) { (0, utils_1.setBasicAuthorization)(headers, this._settings.userName, this._settings.password); } } /** @internal */ async makeTileRequest(url, timeoutMs, authorization) { // We want to complete the first request before letting other requests go; // this done to avoid flooding server with requests missing credentials if (!this._firstRequestPromise) this._firstRequestPromise = new Promise((resolve) => this.onFirstRequestCompleted.addOnce(() => resolve())); else await this._firstRequestPromise; let response; try { response = await this.makeRequest(url, timeoutMs, authorization); } finally { this.onFirstRequestCompleted.raiseEvent(); } if (response === undefined) throw new Error("fetch call failed"); return response; } /** @internal */ async makeRequest(url, timeoutMs, authorization) { let response; let headers; let hasCreds = false; if (authorization) { headers = new Headers(); headers.set("Authorization", authorization); } else if (this._settings.userName && this._settings.password) { hasCreds = true; headers = new Headers(); this.setRequestAuthorization(headers); } const opts = { method: "GET", headers, credentials: this._includeUserCredentials ? "include" : undefined, }; if (timeoutMs !== undefined) (0, utils_1.setRequestTimeout)(opts, timeoutMs); response = await fetch(url, opts); if (response.status === 401 && (0, utils_1.headersIncludeAuthMethod)(response.headers, ["ntlm", "negotiate"]) && !this._includeUserCredentials && !hasCreds) { // Removed the previous headers and make sure "include" credentials is set opts.headers = undefined; opts.credentials = "include"; // We got a http 401 challenge, lets try again with SSO enabled (i.e. Windows Authentication) response = await fetch(url, opts); if (response.status === 200) { this._includeUserCredentials = true; // avoid going through 401 challenges over and over } } return response; } /** Returns a map layer tile at the specified settings. */ async loadTile(row, column, zoomLevel) { try { const tileUrl = await this.constructUrl(row, column, zoomLevel); if (tileUrl.length === 0) return undefined; const tileResponse = await this.makeTileRequest(tileUrl); if (!this._hasSuccessfullyFetchedTile) { this._hasSuccessfullyFetchedTile = true; } return await this.getImageFromTileResponse(tileResponse, zoomLevel); } catch (error) { if (error?.status === 401) { this.setStatus(MapLayerImageryProviderStatus.RequireAuth); // Only report error to end-user if we were previously able to fetch tiles // and then encountered an error, otherwise I assume an error was already reported // through the source validation process. if (this._hasSuccessfullyFetchedTile) { const msg = IModelApp_1.IModelApp.localization.getLocalizedString("iModelJs:MapLayers.Messages.LoadTileTokenError", { layerName: this._settings.name }); IModelApp_1.IModelApp.notifications.outputMessage(new NotificationManager_1.NotifyMessageDetails(NotificationManager_1.OutputMessagePriority.Warning, msg)); } } return undefined; } } /** @internal */ async toolTipFromUrl(strings, url) { const headers = new Headers(); this.setRequestAuthorization(headers); try { const response = await fetch(url, { method: "GET", headers, credentials: this._includeUserCredentials ? "include" : undefined, }); const text = await response.text(); if (undefined !== text) { strings.push(text); } } catch { } } /** @internal */ matchesMissingTile(tileData) { if (!this._missingTileData) return false; if (tileData.length !== this._missingTileData.length) return false; for (let i = 0; i < tileData.length; i += 10) { if (this._missingTileData[i] !== tileData[i]) { return false; } } return true; } /** * Calculates the projected x cartesian coordinate in EPSG:3857 from the longitude in EPSG:4326 (WGS84) * @param longitude Longitude in EPSG:4326 (WGS84) * @internal */ getEPSG3857X(longitude) { return longitude * 20037508.34 / 180.0; } /** * Calculates the projected y cartesian coordinate in EPSG:3857 from the latitude in EPSG:4326 (WGS84) * @param latitude Latitude in EPSG:4326 (WGS84) * @internal */ getEPSG3857Y(latitude) { const y = Math.log(Math.tan((90.0 + latitude) * Math.PI / 360.0)) / (Math.PI / 180.0); return y * 20037508.34 / 180.0; } /** * Calculates the longitude in EPSG:4326 (WGS84) from the projected x cartesian coordinate in EPSG:3857 * @param x3857 Projected x cartesian coordinate in EPSG:3857 * @internal */ getEPSG4326Lon(x3857) { return core_geometry_1.Angle.radiansToDegrees(x3857 / earthRadius); } /** * Calculates the latitude in EPSG:4326 (WGS84) from the projected y cartesian coordinate in EPSG:3857 * @param y3857 Projected y cartesian coordinate in EPSG:3857 * @internal */ getEPSG4326Lat(y3857) { const y = 2 * Math.atan(Math.exp(y3857 / earthRadius)) - (Math.PI / 2); return core_geometry_1.Angle.radiansToDegrees(y); } /** * Get the bounding box/extents of a tile in EPSG:4326 (WGS84) format. * Map tile providers like Bing and Mapbox allow the URL to be constructed directly from the zoom level and tile coordinates. * However, WMS-based servers take a bounding box instead. This method can help get that bounding box from a tile. * @param row Row of the tile * @param column Column of the tile * @param zoomLevel Desired zoom level of the tile * @internal */ getEPSG4326Extent(row, column, zoomLevel) { // Shift left (this.tileSize << zoomLevel) overflow when using 512 pixels tile at higher resolution, // so use Math.pow instead (I assume the performance lost to be minimal) const mapSize = this.tileSize * Math.pow(2, zoomLevel); const leftGrid = this.tileSize * column; const topGrid = this.tileSize * row; const longitudeLeft = 360 * ((leftGrid / mapSize) - 0.5); const y0 = 0.5 - ((topGrid + this.tileSize) / mapSize); const latitudeBottom = 90.0 - 360.0 * Math.atan(Math.exp(-y0 * 2 * Math.PI)) / Math.PI; const longitudeRight = 360 * (((leftGrid + this.tileSize) / mapSize) - 0.5); const y1 = 0.5 - (topGrid / mapSize); const latitudeTop = 90.0 - 360.0 * Math.atan(Math.exp(-y1 * 2 * Math.PI)) / Math.PI; return { longitudeLeft, longitudeRight, latitudeTop, latitudeBottom }; } /** * Get the bounding box/extents of a tile in EPSG:3857 format. * @param row Row of the tile * @param column Column of the tile * @param zoomLevel Desired zoom level of the tile * @internal */ getEPSG3857Extent(row, column, zoomLevel) { const epsg4326Extent = this.getEPSG4326Extent(row, column, zoomLevel); const left = this.getEPSG3857X(epsg4326Extent.longitudeLeft); const right = this.getEPSG3857X(epsg4326Extent.longitudeRight); const bottom = this.getEPSG3857Y(epsg4326Extent.latitudeBottom); const top = this.getEPSG3857Y(epsg4326Extent.latitudeTop); return { left, right, bottom, top }; } /** @internal */ getEPSG3857ExtentString(row, column, zoomLevel) { const tileExtent = this.getEPSG3857Extent(row, column, zoomLevel); return `${tileExtent.left.toFixed(2)},${tileExtent.bottom.toFixed(2)},${tileExtent.right.toFixed(2)},${tileExtent.top.toFixed(2)}`; } /** @internal */ getEPSG4326TileExtentString(row, column, zoomLevel, latLongAxisOrdering) { const tileExtent = this.getEPSG4326Extent(row, column, zoomLevel); return this.getEPSG4326ExtentString(tileExtent, latLongAxisOrdering); } /** @internal */ getEPSG4326ExtentString(tileExtent, latLongAxisOrdering) { if (latLongAxisOrdering) { return `${tileExtent.latitudeBottom.toFixed(8)},${tileExtent.longitudeLeft.toFixed(8)},${tileExtent.latitudeTop.toFixed(8)},${tileExtent.longitudeRight.toFixed(8)}`; } else { return `${tileExtent.longitudeLeft.toFixed(8)},${tileExtent.latitudeBottom.toFixed(8)},${tileExtent.longitudeRight.toFixed(8)},${tileExtent.latitudeTop.toFixed(8)}`; } } /** Append custom parameters for settings to provided URL object. * @internal */ appendCustomParams(url) { if (!this._settings.savedQueryParams && !this._settings.unsavedQueryParams) return url; let tmpUrl = (0, internal_1.appendQueryParams)(url, this._settings.savedQueryParams); tmpUrl = (0, internal_1.appendQueryParams)(tmpUrl, this._settings.unsavedQueryParams); return tmpUrl; } } exports.MapLayerImageryProvider = MapLayerImageryProvider; //# sourceMappingURL=MapLayerImageryProvider.js.map