UNPKG

@itwin/core-frontend

Version:
232 lines • 13.4 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 Tiles */ import { assert, compareStrings, Dictionary, Logger } from "@itwin/core-bentley"; import { QuadId } from "../../../tile/internal"; const loggerCategory = "ArcGISTileMap"; const nonVisibleChildren = [false, false, false, false]; export class ArcGISTileMap { // For similar reasons as the corner offset, we need to keep the tile map size not too big to avoid covering multiple bundles. tileMapRequestSize = 8; static maxLod = 30; // We want to query a tile map that covers an area all around the top-lef missing tile, we offset the top-left corner position of the tilemap. // We used to create a 32x32 tiles area around the missing tiles, but this was causing the tilemap top-left position // to fall outside the dataset bundle of the remote server, thus giving invalid response. get tileMapOffset() { return (this.tileMapRequestSize * 0.5); } fallbackTileMapRequestSize = 2; _callQueues; _tilesCache = new Dictionary((lhs, rhs) => compareStrings(lhs, rhs)); _restBaseUrl; _fetchFunc; _settings; constructor(restBaseUrl, settings, fetchFunc) { this._restBaseUrl = restBaseUrl; this._fetchFunc = fetchFunc; this._settings = settings; this._callQueues = new Array(ArcGISTileMap.maxLod).fill(Promise.resolve(nonVisibleChildren)); } async fetchTileMapFromServer(level, row, column, width, height) { const tmpUrl = `${this._restBaseUrl}/tilemap/${level}/${row}/${column}/${width}/${height}?f=json`; const response = await this._fetchFunc(new URL(tmpUrl)); return response.json(); } getAvailableTilesFromCache(tiles) { let allTilesFound = true; // Check children visibility from cache const available = tiles.map((tileId) => { const avail = this._tilesCache.get(tileId.contentId); if (undefined === avail) { allTilesFound = false; } return avail ?? false; }); return { allTilesFound, available }; } async getChildrenAvailability(childIds) { if (!childIds.length) return []; // Before entering the queue for a backend request, // let check if cache doesn't already contain what we are looking for. const cacheInfo = this.getAvailableTilesFromCache(childIds); if (cacheInfo.allTilesFound) { if (cacheInfo.available.includes(false)) return cacheInfo.available; return cacheInfo.available; } // If we never encountered this tile level before, then a tilemap request must be made to get tiles visibility. // However, we dont want several overlapping large tilemap request being made simultaneously for tiles on the same level. // To avoid this from happening, we 'serialize' async calls so that we wait until the first tilemap request has completed // before making another one. const childLevel = childIds[0].level + 1; if (this._callQueues && childLevel < this._callQueues.length) { const res = this._callQueues[childLevel].then(async () => { return this.getChildrenAvailabilityFromServer(childIds); }); this._callQueues[childLevel] = res.catch(() => nonVisibleChildren); return res; } else { // We should not be in this case, probably because server info is missing LODs in the capabilities?! Logger.logWarning(loggerCategory, `Skipped request queue for child level ${childLevel}`); return this.getChildrenAvailabilityFromServer(childIds); } } isCacheMissingTile(level, startRow, startColumn, endRow, endColumn) { let missingTileFound = false; if (endRow <= startRow || endColumn <= startColumn) return missingTileFound; for (let j = startColumn; j <= endColumn && !missingTileFound; j++) { for (let i = startRow; i <= endRow && !missingTileFound; i++) { if (j >= 0 && i >= 0) { const contentId = QuadId.getTileContentId(level, j, i); if (this._tilesCache.get(contentId) === undefined) { missingTileFound = true; } } } } return missingTileFound; } collectTilesMissingFromCache(missingQueryTiles) { const missingTiles = []; for (const quad of missingQueryTiles) { const contentId = QuadId.getTileContentId(quad.level, quad.column, quad.row); const avail = this._tilesCache.get(contentId); if (avail === undefined) missingTiles.push(quad); } return missingTiles; } // Query tiles are tiles that we need to check availability // The array is assumed to be in in row major orientation, i.e.: [TileRow0Col0, TileRow0Col1, TileRow1Col0, TileRow1Col1,] async fetchAndReadTilemap(queryTiles, reqWidth, reqHeight) { let available = queryTiles.map(() => false); if (queryTiles.length === 0) { return available; } // console.log(`queryTiles: ${queryTiles.map((quad) => quad.contentId)}`); // Find the top-left most corner of the extent covering the query tiles. const getTopLeftCorner = (tiles) => { let row; let column; for (const quad of tiles) { if (row === undefined || quad.row <= row) row = quad.row; if (column === undefined || quad.column <= column) { column = quad.column; } } return { row, column }; }; const level = queryTiles[0].level; // We assume all tiles to be on the same level let missingQueryTiles = this.collectTilesMissingFromCache(queryTiles); let gotAdjusted = false; let nbAttempt = 0; // Safety: We should never be making more requests than the number of queries tiles (otherwise something is wrong) while (missingQueryTiles.length > 0 && (nbAttempt++ < queryTiles.length)) { const tileMapTopLeft = getTopLeftCorner(missingQueryTiles); if (tileMapTopLeft.row === undefined || tileMapTopLeft.column === undefined) return available; // Should not occurs since missingQueryTiles is non empty let tileMapRow = tileMapTopLeft.row; let tileMapColumn = tileMapTopLeft.column; const logLocationOffset = (newRow, newCol) => `[Row:${newRow !== tileMapTopLeft.row ? `${tileMapTopLeft.row}->${newRow}` : `${newRow}`} Column:${newCol !== tileMapTopLeft.column ? `${tileMapTopLeft.column}->${newCol}` : `${newCol}`}]`; // Position the top-left missing tile in the middle of the tilemap; minimizing requests if sibling tiles are requested right after // If previous response got adjusted, don't try to optimize tile map location if (queryTiles.length < this.tileMapRequestSize && !gotAdjusted) { const tileMapOffset = this.tileMapOffset - Math.floor(Math.sqrt(queryTiles.length) * 0.5); const missingTileBufferSize = Math.ceil(tileMapOffset * 0.5); if (this.isCacheMissingTile(level, tileMapRow - missingTileBufferSize, tileMapColumn - missingTileBufferSize, tileMapRow - 1, tileMapColumn - 1)) { tileMapRow = Math.max(tileMapRow - tileMapOffset, 0); tileMapColumn = Math.max(tileMapColumn - tileMapOffset, 0); Logger.logTrace(loggerCategory, `Offset applied to location in top-left direction: ${logLocationOffset(tileMapRow, tileMapColumn)}`); } else { const leftMissingTiles = this.isCacheMissingTile(level, tileMapRow, tileMapColumn - missingTileBufferSize, tileMapRow + missingTileBufferSize, tileMapColumn - 1); const topMissingTiles = this.isCacheMissingTile(level, tileMapRow - missingTileBufferSize, tileMapColumn, tileMapRow - 1, tileMapColumn + missingTileBufferSize); if (leftMissingTiles && topMissingTiles) { tileMapRow = Math.max(tileMapRow - tileMapOffset, 0); tileMapColumn = Math.max(tileMapColumn - tileMapOffset, 0); Logger.logTrace(loggerCategory, `Offset applied to location in top-left direction. ${logLocationOffset(tileMapRow, tileMapColumn)}`); } else if (leftMissingTiles) { tileMapColumn = Math.max(tileMapColumn - tileMapOffset, 0); Logger.logTrace(loggerCategory, `Offset applied to location in left direction. ${logLocationOffset(tileMapRow, tileMapColumn)}`); } else if (topMissingTiles) { tileMapRow = Math.max(tileMapRow - tileMapOffset, 0); Logger.logTrace(loggerCategory, `Offset applied to location in top direction: ${logLocationOffset(tileMapRow, tileMapColumn)}`); } else Logger.logTrace(loggerCategory, `No offset applied to location: ${logLocationOffset(tileMapRow, tileMapColumn)}`); } } const json = await this.fetchTileMapFromServer(level, tileMapRow, tileMapColumn, reqWidth, reqHeight); let tileMapWidth = reqWidth; let tileMapHeight = reqHeight; if (Array.isArray(json.data)) { // The response width and height might be different than the requested dimensions. // Ref: https://developers.arcgis.com/rest/services-reference/enterprise/tile-map.htm if (json.adjusted) { gotAdjusted = true; // If tilemap size got adjusted, I'm expecting to get adjusted size... // otherwise there is something really odd with this server. assert(json.location?.width !== undefined && json.location?.height !== undefined); if (json.location?.width !== undefined && json.location?.height !== undefined) { tileMapWidth = json.location?.width; tileMapHeight = json.location?.height; } } // Build cache from tile map response for (let j = 0; j < tileMapHeight; j++) { for (let i = 0; i < tileMapWidth; i++) { const avail = json.data[(j * tileMapWidth) + i] !== 0; const curColumn = tileMapColumn + i; const curRow = tileMapRow + j; this._tilesCache.set(QuadId.getTileContentId(level, curColumn, curRow), avail); } } // Collect tile missing from the cache // There are 2 reasons why the tile map response would not cover all the missing tiles: // 1. The requested tile map size is not large enough to cover all tiles // 2. The tile map size has been adjusted by the server (i.e. data bundle limits) missingQueryTiles = this.collectTilesMissingFromCache(missingQueryTiles); if (missingQueryTiles.length > 0) Logger.logTrace(loggerCategory, `There are ${missingQueryTiles.length} missing tiles from previous request`); } else { missingQueryTiles = []; // Mark all tilemap tiles to non-available in the cache too. for (let j = 0; j < tileMapWidth; j++) { for (let i = 0; i < tileMapHeight; i++) { this._tilesCache.set(QuadId.getTileContentId(level, tileMapColumn + i, tileMapRow + j), false); } } } } // end loop missing tiles if (nbAttempt > queryTiles.length) { Logger.logError(loggerCategory, `Request loop was terminated; unable to get missing tiles; `); } // Create final output array from cache available = queryTiles.map((quad) => this._tilesCache.get(quad.contentId) ?? false); if (available.includes(false)) return available; return available; } async getChildrenAvailabilityFromServer(childIds) { let available; try { available = await this.fetchAndReadTilemap(childIds, this.tileMapRequestSize, this.tileMapRequestSize); } catch (err) { // if any error occurs, we assume tiles not to be visible Logger.logError(loggerCategory, `Error while fetching tile map data : ${err}`); available = childIds.map(() => false); } return available; } } //# sourceMappingURL=ArcGISTileMap.js.map