@itwin/core-frontend
Version:
iTwin.js frontend components
346 lines • 17.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ArcGISMapLayerImageryProvider = exports.ArcGISIdentifyRequestUrl = void 0;
/*---------------------------------------------------------------------------------------------
* 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
*/
const core_common_1 = require("@itwin/core-common");
const IModelApp_1 = require("../../../../IModelApp");
const internal_1 = require("../../../../tile/internal");
const appui_abstract_1 = require("@itwin/appui-abstract");
const core_geometry_1 = require("@itwin/core-geometry");
const core_bentley_1 = require("@itwin/core-bentley");
const loggerCategory = "MapLayerImageryProvider.ArcGISMapLayerImageryProvider";
/** @internal */
class ArcGISIdentifyRequestUrl {
static fromJSON(baseUrl, json, srFractionDigits) {
const newUrl = new URL(baseUrl);
newUrl.pathname = `${newUrl.pathname}/identify`;
if (json.f) {
newUrl.searchParams.append("f", json.f);
}
const geomPt = core_geometry_1.Point2d.fromJSON(json.geometry);
newUrl.searchParams.append("geometry", `${this.toFixed(geomPt.x, srFractionDigits)},${this.toFixed(geomPt.y, srFractionDigits)}`);
newUrl.searchParams.append("geometryType", json.geometryType);
if (json.sr) {
newUrl.searchParams.append("sr", `${json.sr}`);
}
if (json.layers) {
newUrl.searchParams.append("layers", `${json.layers.prefix}${json.layers.layerIds?.length ? `: ${json.layers.layerIds.join(",")}` : ""}`);
}
newUrl.searchParams.append("tolerance", `${json.tolerance}`);
newUrl.searchParams.append("mapExtent", ArcGISIdentifyRequestUrl.getExtentString(json.mapExtent, srFractionDigits));
newUrl.searchParams.append("imageDisplay", `${json.imageDisplay.width},${json.imageDisplay.height},${json.imageDisplay.dpi}`);
if (json.returnGeometry !== undefined) {
newUrl.searchParams.append("returnGeometry", json.returnGeometry ? "true" : "false");
}
if (json.maxAllowableOffset !== undefined) {
newUrl.searchParams.append("maxAllowableOffset", `${this.toFixed(json.maxAllowableOffset, srFractionDigits)}`);
}
return newUrl;
}
static toFixed(value, srFractionDigits) {
return srFractionDigits === undefined ? value.toString() : value.toFixed(srFractionDigits);
}
static getExtentString(range, srFractionDigits) {
const extent = core_geometry_1.Range2d.fromJSON(range);
const extentStringArray = [];
extent.toFloat64Array().forEach((value) => extentStringArray.push(this.toFixed(value, srFractionDigits)));
return extentStringArray.join(",");
}
}
exports.ArcGISIdentifyRequestUrl = ArcGISIdentifyRequestUrl;
/** @internal */
class ArcGISMapLayerImageryProvider extends internal_1.ArcGISImageryProvider {
_maxDepthFromLod = 0;
_minDepthFromLod = 0;
_copyrightText = "Copyright";
_tileMapSupported = false;
_mapSupported = false;
_tilesOnly = false;
_tileMap;
serviceJson;
constructor(settings) {
super(settings, false);
this._accessClient = IModelApp_1.IModelApp.mapLayerFormatRegistry.getAccessClient(settings.formatId);
}
get _filterByCartoRange() { return false; } // Can't trust footprint ranges (USGS Hydro)
get minimumZoomLevel() { return Math.max(super.minimumZoomLevel, this._minDepthFromLod); }
get maximumZoomLevel() { return this._maxDepthFromLod > 0 ? this._maxDepthFromLod : super.maximumZoomLevel; }
uintToString(uintArray) {
return Buffer.from(uintArray).toJSON();
}
async fetchTile(row, column, zoomLevel) {
const tileUrl = await this.constructUrl(row, column, zoomLevel);
if (tileUrl.length === 0)
return undefined;
return this.fetch(new URL(tileUrl), { method: "GET" });
}
async loadTile(row, column, zoomLevel) {
if ((this.status === internal_1.MapLayerImageryProviderStatus.RequireAuth)) {
return undefined;
}
try {
const tileResponse = await this.fetchTile(row, column, zoomLevel);
if (tileResponse === undefined)
return undefined;
if (!this._hasSuccessfullyFetchedTile) {
this._hasSuccessfullyFetchedTile = true;
}
return await this.getImageFromTileResponse(tileResponse, zoomLevel);
}
catch (error) {
core_bentley_1.Logger.logError(loggerCategory, `Error occurred when loading tile(${row},${column},${zoomLevel}) : ${error}`);
return undefined;
}
}
_generateChildIds(quadId, resolveChildren) {
const childIds = this.getPotentialChildIds(quadId);
if (quadId.level < Math.max(1, this.minimumZoomLevel - 1)) {
resolveChildren(childIds);
return;
}
if (this._tileMap) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._tileMap.getChildrenAvailability(childIds).then((availability) => {
const availableChildIds = new Array();
for (let i = 0; i < availability.length; i++)
if (availability[i])
availableChildIds.push(childIds[i]);
resolveChildren(availableChildIds);
});
}
else if (this._usesCachedTiles && this.cartoRange) {
// Filter children by range
const availableChildIds = new Array();
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < childIds.length; i++) {
const childExtent = this.getEPSG4326Extent(childIds[i].row, childIds[i].column, childIds[i].level);
const childRange = internal_1.MapCartoRectangle.fromDegrees(childExtent.longitudeLeft, childExtent.latitudeBottom, childExtent.longitudeRight, childExtent.latitudeTop);
if (childRange.intersectsRange(this.cartoRange)) {
availableChildIds.push(childIds[i]);
}
}
resolveChildren(availableChildIds);
}
else {
resolveChildren(childIds); // Resolve all children
}
}
async initialize() {
const metadata = await this.getServiceJson();
if (metadata?.content === undefined)
throw new core_common_1.ServerError(core_bentley_1.IModelStatus.ValidationFailed, "");
const json = metadata.content;
if (json?.error?.code === internal_1.ArcGisErrorCode.TokenRequired
|| json?.error?.code === internal_1.ArcGisErrorCode.InvalidToken
|| json?.error?.code === internal_1.ArcGisErrorCode.MissingPermissions) {
// Check again layer status, it might have change during await.
if (this.status === internal_1.MapLayerImageryProviderStatus.Valid) {
this.setStatus(internal_1.MapLayerImageryProviderStatus.RequireAuth);
return; // By returning (i.e not throwing), we ensure the tileTree get created and current provider is preserved to report status.
}
}
this.serviceJson = json;
if (json.capabilities) {
const capabilities = json.capabilities.split(",");
this._querySupported = capabilities.includes("Query");
this._tileMapSupported = capabilities.includes("Tilemap");
this._mapSupported = capabilities.includes("Map");
this._tilesOnly = capabilities.includes("TilesOnly");
}
if (json.copyrightText)
this._copyrightText = json.copyrightText;
this._usesCachedTiles = !!json.tileInfo;
if (this._usesCachedTiles) {
// Only EPSG:3857 is supported with pre-rendered tiles. Fall back to 'Export' queries if possible otherwise throw.
if (!internal_1.ArcGisUtilities.isEpsg3857Compatible(json.tileInfo)) {
if (this._mapSupported && !this._tilesOnly) {
this._usesCachedTiles = false;
}
else {
throw new core_common_1.ServerError(core_bentley_1.IModelStatus.ValidationFailed, "Invalid coordinate system");
}
}
}
if (this._usesCachedTiles) {
// Read max LOD
if (json.maxScale !== undefined && json.maxScale !== 0 && Array.isArray(json.tileInfo.lods)) {
for (; this._maxDepthFromLod < json.tileInfo.lods.length && json.tileInfo.lods[this._maxDepthFromLod].scale > json.maxScale; this._maxDepthFromLod++)
;
}
// Create tile map object only if we are going to request tiles from this server and it support tilemap requests.
if (this._tileMapSupported) {
const fetch = async (url, options) => {
return this.fetch(url, options);
};
this._tileMap = new internal_1.ArcGISTileMap(this._settings.url, this._settings, fetch);
}
}
// Read range using fullextent from service metadata
if (json.fullExtent) {
if (json.fullExtent.spatialReference.latestWkid === 3857 || json.fullExtent.spatialReference.wkid === 102100) {
const range3857 = core_geometry_1.Range2d.createFrom({
low: { x: json.fullExtent.xmin, y: json.fullExtent.ymin },
high: { x: json.fullExtent.xmax, y: json.fullExtent.ymax }
});
const west = this.getEPSG4326Lon(range3857.xLow);
const south = this.getEPSG4326Lat(range3857.yLow);
const east = this.getEPSG4326Lon(range3857.xHigh);
const north = this.getEPSG4326Lat(range3857.yHigh);
this.cartoRange = internal_1.MapCartoRectangle.fromDegrees(west, south, east, north);
}
}
// Read minLOD if available
if (json.minLOD !== undefined) {
const minLod = parseInt(json.minLOD, 10);
if (!Number.isNaN(minLod)) {
this._minDepthFromLod = minLod;
}
}
else if (json.minScale) {
// Read min LOD using minScale
const minScale = json.minScale;
if (json.tileInfo?.lods !== undefined && Array.isArray(json.tileInfo.lods)) {
for (const lod of json.tileInfo.lods) {
if (lod.scale < minScale) {
this._minDepthFromLod = lod.level;
break;
}
}
}
}
}
/** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [addAttributions] instead. */
addLogoCards(cards) {
if (!cards.dataset.arcGisLogoCard) {
cards.dataset.arcGisLogoCard = "true";
cards.appendChild(IModelApp_1.IModelApp.makeLogoCard({ heading: "ArcGIS", notice: this._copyrightText }));
}
}
async addAttributions(cards, _vp) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return Promise.resolve(this.addLogoCards(cards));
}
// Translates the provided Cartographic into a EPSG:3857 point, and retrieve information.
// tolerance is in pixels
async getIdentifyData(quadId, carto, tolerance, returnGeometry, maxAllowableOffset) {
const bbox = this.getEPSG3857Extent(quadId.row, quadId.column, quadId.level);
const layerIds = new Array();
this._settings.subLayers.forEach((subLayer) => {
if (this._settings.isSubLayerVisible(subLayer))
layerIds.push(subLayer.idString);
});
const urlObj = ArcGISIdentifyRequestUrl.fromJSON(this._settings.url, {
f: "json",
geometry: { x: this.getEPSG3857X(carto.longitudeDegrees), y: this.getEPSG3857Y(carto.latitudeDegrees) },
geometryType: "esriGeometryPoint",
tolerance,
mapExtent: { low: { x: bbox.left, y: bbox.bottom }, high: { x: bbox.right, y: bbox.top } },
sr: 3857,
imageDisplay: { width: this.tileSize, height: this.tileSize, dpi: 96 },
layers: { prefix: "top", layerIds },
returnGeometry,
maxAllowableOffset
}, 3 /* 1mm accuracy*/);
const response = await this.fetch(urlObj, { method: "GET" });
return response.json();
}
// Makes an identify request to ESRI MapService server, and return it as a list of formatted strings
async getToolTip(strings, quadId, carto, tree) {
await super.getToolTip(strings, quadId, carto, tree);
if (!this._querySupported)
return;
const stringSet = new Set();
const json = await this.getIdentifyData(quadId, carto, 1);
if (json && Array.isArray(json.results)) {
for (const result of json.results) {
if (result.attributes !== undefined && result.attributes[result.displayFieldName] !== undefined) {
const thisString = `${result.displayFieldName}: ${result.attributes[result.displayFieldName]}`;
if (!stringSet.has(thisString)) {
strings.push(thisString);
stringSet.add(thisString);
}
}
}
}
}
// Makes an identify request to ESRI MapService , and return it as a list MapLayerFeatureInfo object
async getFeatureInfo(featureInfos, quadId, carto, _tree, hit, options) {
if (!this._querySupported)
return;
const tileExtent = this.getEPSG3857Extent(quadId.row, quadId.column, quadId.level);
const toleranceWorld = (tileExtent.top - tileExtent.bottom) / this.tileSize;
const maxAllowableOffsetFactor = 2;
const maxAllowableOffset = maxAllowableOffsetFactor * toleranceWorld;
const tolerancePixel = options?.tolerance ?? 7;
const json = await this.getIdentifyData(quadId, carto, tolerancePixel, true, maxAllowableOffset);
if (json && Array.isArray(json.results)) {
const renderer = new internal_1.FeatureGraphicsRenderer({ viewport: hit.viewport, crs: "webMercator" });
const layerInfo = { layerName: this._settings.name, subLayerInfos: [] };
// The 'identify' service returns us a flat/unordered list of records..
// results may represent features for the a common subLayer.
// For simplicity, we group together features for a given sub-layer.
const subLayers = new Map();
for (const result of json.results) {
let subLayerInfo = subLayers.get(result.layerName);
if (!subLayerInfo) {
subLayerInfo = {
subLayerName: result.layerName ?? "",
displayFieldName: result.displayFieldName,
features: [],
};
subLayers.set(result.layerName, subLayerInfo);
}
const feature = { geometries: [], attributes: [] };
// Read all feature attributes
for (const [key, value] of Object.entries(result.attributes)) {
// Convert everything to string for now
const strValue = String(value);
feature.attributes.push({
value: { valueFormat: appui_abstract_1.PropertyValueFormat.Primitive, value: strValue, displayValue: strValue },
property: { name: key, displayLabel: key, typename: appui_abstract_1.StandardTypeNames.String },
});
}
// Read feature geometries
const geomReader = new internal_1.ArcGisGeometryReaderJSON(result.geometryType, renderer);
await geomReader.readGeometry(result.geometry);
const graphics = renderer.moveGraphics();
feature.geometries = graphics.map((graphic) => {
return { graphic };
});
subLayerInfo.features.push(feature);
}
for (const value of subLayers.values()) {
layerInfo.subLayerInfos.push(value);
}
featureInfos.push(layerInfo);
}
}
getLayerString(prefix = "show") {
const layers = new Array();
this._settings.subLayers.forEach((subLayer) => {
if (this._settings.isSubLayerVisible(subLayer))
layers.push(subLayer.idString);
});
return `${prefix}: ${layers.join(",")} `;
}
// construct the Url from the desired Tile
async constructUrl(row, column, zoomLevel) {
let tmpUrl;
if (this._usesCachedTiles) {
tmpUrl = `${this._settings.url}/tile/${zoomLevel}/${row}/${column} `;
}
else {
const bboxString = `${this.getEPSG3857ExtentString(row, column, zoomLevel)}&bboxSR=3857`;
tmpUrl = `${this._settings.url}/export?bbox=${bboxString}&size=${this.tileSize},${this.tileSize}&layers=${this.getLayerString()}&format=png&transparent=${this.transparentBackgroundString}&f=image&sr=3857&imagesr=3857`;
}
return tmpUrl;
}
}
exports.ArcGISMapLayerImageryProvider = ArcGISMapLayerImageryProvider;
//# sourceMappingURL=ArcGISMapLayerImageryProvider.js.map