From f2b9c2c67e0409cf1b29dd2806c87eacab8ba891 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Fri, 24 Jan 2025 13:44:34 -0500 Subject: [PATCH 01/44] wip --- core/bentley/src/Compare.ts | 44 +++++ core/common/src/MapLayerSettings.ts | 35 ++++ core/frontend/src/Viewport.ts | 3 +- core/frontend/src/tile/map/ImageryTileTree.ts | 35 +++- .../src/tile/map/MapLayerImageryProvider.ts | 6 + .../src/tile/map/MapLayerTileTreeReference.ts | 5 + core/frontend/src/tile/map/MapTileTree.ts | 6 +- .../src/GoogleMaps/GoogleMapsImageryFormat.ts | 15 ++ .../GoogleMaps/GoogleMapsImageryProvider.ts | 159 ++++++++++++++++++ .../src/map-layers-formats.ts | 2 + .../src/mapLayersFormats.ts | 2 + .../src/common/DtaConfiguration.ts | 4 + .../display-test-app/src/frontend/App.ts | 3 + 13 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryFormat.ts create mode 100644 extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts diff --git a/core/bentley/src/Compare.ts b/core/bentley/src/Compare.ts index 4aed7c2132f1..8973f7cc8e64 100644 --- a/core/bentley/src/Compare.ts +++ b/core/bentley/src/Compare.ts @@ -92,3 +92,47 @@ export function areEqualPossiblyUndefined(t: T | undefined, u: U | undefin else return areEqual(t, u); } + +export type PrimitiveType = number | string | boolean; +export function comparePrimitives(lhs: PrimitiveType, rhs: PrimitiveType): number { + // Make sure the types are the same + if (typeof lhs !== typeof rhs) { + return 1; + } + + let cmp = 0; + // Compare actual values + switch (typeof lhs) { + case "number": + cmp = compareNumbers(lhs, rhs as number); + break; + case "string": + cmp = compareStrings(lhs, rhs as string); + break; + case "boolean": + cmp = compareBooleans(lhs, rhs as boolean); + break; + } + return cmp; +} + +export function comparePrimitiveArrays (lhs?: Array, rhs?: Array ) { + if (undefined === lhs) + return undefined === rhs ? 0 : -1; + else if (undefined === rhs) + return 1; + else if (lhs.length === 0 && rhs.length === 0) { + return 0; + } else if (lhs.length !== rhs.length) { + return Math.abs(lhs.length - rhs.length); + } + + let cmp = 0; + for (let i = 0; i < lhs.length; i++) { + cmp = comparePrimitives(lhs[i], rhs[i]); + if (cmp !== 0) { + break; + } + } + return cmp; +} \ No newline at end of file diff --git a/core/common/src/MapLayerSettings.ts b/core/common/src/MapLayerSettings.ts index 19c6123a552c..d2e054442be6 100644 --- a/core/common/src/MapLayerSettings.ts +++ b/core/common/src/MapLayerSettings.ts @@ -19,6 +19,10 @@ export type ImageryMapLayerFormatId = "ArcGIS" | "BingMaps" | "MapboxImagery" | /** @public */ export type SubLayerId = string | number; +/** @public */ +export type PropertyBagArrayProperty = Array; +export interface PropertyBag { [key: string]: number | string | boolean | PropertyBagArrayProperty }; + /** JSON representation of the settings associated with a map sublayer included within a [[MapLayerProps]]. * A map sub layer represents a set of objects within the layer that can be controlled separately. These * are produced only from map servers that produce images on demand and are not supported by tiled (cached) servers. @@ -169,6 +173,11 @@ export interface ImageMapLayerProps extends CommonMapLayerProps { */ queryParams?: { [key: string]: string }; + /** Data specific to each imagery format. + * @beta + */ + properties?: PropertyBag; + } /** JSON representation of a [[ModelMapLayerSettings]]. @@ -204,6 +213,11 @@ export interface MapLayerKey { value: string; } +// export interface MapLayerProviderData { +// compare(rhs: MapLayerProviderData): number; +// clone(): MapLayerProviderData; +// } + /** Abstract base class for normalized representation of a [[MapLayerProps]] for which values have been validated and default values have been applied where explicit values not defined. * This class is extended by [[ImageMapLayerSettings]] and [ModelMapLayerSettings]] to create the settings for image and model based layers. * One or more map layers may be included within [[MapImagerySettings]] object. @@ -287,6 +301,7 @@ export class ImageMapLayerSettings extends MapLayerSettings { public password?: string; public accessKey?: MapLayerKey; + /** List of query parameters to append to the settings URL and persisted as part of the JSON representation. * @note Sensitive information like user credentials should be provided in [[unsavedQueryParams]] to ensure it is never persisted. * @beta @@ -297,6 +312,12 @@ export class ImageMapLayerSettings extends MapLayerSettings { * @beta */ public unsavedQueryParams?: { [key: string]: string }; + + /** TODO + * @beta + */ + public readonly properties?: PropertyBag; + public readonly subLayers: MapSubLayerSettings[]; public override get source(): string { return this.url; } @@ -311,6 +332,11 @@ export class ImageMapLayerSettings extends MapLayerSettings { if (props.queryParams) { this.savedQueryParams = {...props.queryParams}; } + + if (props.properties) { + this.properties = {...props.properties} + } + this.subLayers = []; if (!props.subLayers) return; @@ -338,6 +364,10 @@ export class ImageMapLayerSettings extends MapLayerSettings { if (this.savedQueryParams) props.queryParams = {...this.savedQueryParams}; + if (this.properties) { + props.properties = {...this.properties}; + } + return props; } @@ -374,6 +404,11 @@ export class ImageMapLayerSettings extends MapLayerSettings { props.queryParams = {...this.savedQueryParams}; } + if (changedProps.properties) { + props.properties = {...changedProps.properties} + } else if (this.properties) { + props.properties = {...this.properties} + } return props; } diff --git a/core/frontend/src/Viewport.ts b/core/frontend/src/Viewport.ts index 066f9c15d583..49a3b537708b 100644 --- a/core/frontend/src/Viewport.ts +++ b/core/frontend/src/Viewport.ts @@ -3430,7 +3430,8 @@ export class ScreenViewport extends Viewport { this._decorationCache.prohibitRemoval = true; context.addFromDecorator(this.view); - this.forEachTiledGraphicsProviderTree((ref) => context.addFromDecorator(ref)); + // this.forEachTiledGraphicsProviderTree((ref) => context.addFromDecorator(ref)); + this.forEachTileTreeRef((ref) => context.addFromDecorator(ref)); for (const decorator of IModelApp.viewManager.decorators) context.addFromDecorator(decorator); diff --git a/core/frontend/src/tile/map/ImageryTileTree.ts b/core/frontend/src/tile/map/ImageryTileTree.ts index 15e3d7e0fefc..a2f258dffe9a 100644 --- a/core/frontend/src/tile/map/ImageryTileTree.ts +++ b/core/frontend/src/tile/map/ImageryTileTree.ts @@ -6,9 +6,9 @@ * @module Tiles */ -import { assert, compareBooleans, compareNumbers, compareStrings, compareStringsOrUndefined, dispose, Logger} from "@itwin/core-bentley"; +import { assert, compareBooleans, compareNumbers, comparePrimitiveArrays, comparePrimitives, compareStrings, compareStringsOrUndefined, dispose, Logger, PrimitiveType} from "@itwin/core-bentley"; import { Angle, Range3d, Transform } from "@itwin/core-geometry"; -import { Cartographic, ImageMapLayerSettings, ImageSource, MapLayerSettings, RenderTexture, ViewFlagOverrides } from "@itwin/core-common"; +import { Cartographic, ImageMapLayerSettings, ImageSource, MapLayerSettings, PropertyBag, PropertyBagArrayProperty, RenderTexture, ViewFlagOverrides } from "@itwin/core-common"; import { IModelApp } from "../../IModelApp"; import { IModelConnection } from "../../IModelConnection"; import { RenderMemory } from "../../render/RenderMemory"; @@ -333,12 +333,33 @@ class ImageryMapLayerTreeSupplier implements TileTreeSupplier { if (0 === cmp) { cmp = compareBooleans(lhs.settings.transparentBackground, rhs.settings.transparentBackground); if (0 === cmp) { - cmp = compareNumbers(lhs.settings.subLayers.length, rhs.settings.subLayers.length); - if (0 === cmp) { - for (let i = 0; i < lhs.settings.subLayers.length && 0 === cmp; i++) { - cmp = compareStrings(lhs.settings.subLayers[i].name, rhs.settings.subLayers[i].name); + if (lhs.settings.properties && rhs.settings.properties) { + for (const key of Object.keys(lhs.settings.properties)) { + const lhsProp = lhs.settings.properties[key]; + const rhsProp = rhs.settings.properties[key]; + if (typeof lhsProp !== typeof rhsProp) { + cmp = 1; + break; + } + if (Array.isArray(lhsProp)) { + cmp = comparePrimitiveArrays(lhsProp, rhsProp as PropertyBagArrayProperty); + if (0 !== cmp) + break; + } else { + cmp = comparePrimitives(lhsProp as PrimitiveType, rhsProp as PrimitiveType); + if (0 !== cmp) + break; + } + } + if (0 === cmp) { + cmp = compareNumbers(lhs.settings.subLayers.length, rhs.settings.subLayers.length); if (0 === cmp) { - cmp = compareBooleans(lhs.settings.subLayers[i].visible, rhs.settings.subLayers[i].visible); + for (let i = 0; i < lhs.settings.subLayers.length && 0 === cmp; i++) { + cmp = compareStrings(lhs.settings.subLayers[i].name, rhs.settings.subLayers[i].name); + if (0 === cmp) { + cmp = compareBooleans(lhs.settings.subLayers[i].visible, rhs.settings.subLayers[i].visible); + } + } } } } diff --git a/core/frontend/src/tile/map/MapLayerImageryProvider.ts b/core/frontend/src/tile/map/MapLayerImageryProvider.ts index 0e2229f683d9..00e4db1cc228 100644 --- a/core/frontend/src/tile/map/MapLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/MapLayerImageryProvider.ts @@ -15,6 +15,7 @@ import { ScreenViewport } from "../../Viewport"; import { appendQueryParams, GeographicTilingScheme, ImageryMapTile, ImageryMapTileTree, MapCartoRectangle, MapFeatureInfoOptions, MapLayerFeatureInfo, MapTilingScheme, QuadId, WebMercatorTilingScheme } from "../internal"; import { HitDetail } from "../../HitDetail"; import { headersIncludeAuthMethod, setBasicAuthorization, setRequestTimeout } from "../../request/utils"; +import { DecorateContext } from "../../ViewContext"; /** @internal */ const tileImageSize = 256, untiledImageSize = 256; @@ -195,6 +196,11 @@ export abstract class MapLayerImageryProvider { featureInfos.push({ layerName: this._settings.name }); } + /** @internal */ + public decorate(_context: DecorateContext): void { + console.log ("MapLayerImageryProvider.Decorate called"); + } + /** @internal */ protected async getImageFromTileResponse(tileResponse: Response, zoomLevel: number) { const arrayBuffer = await tileResponse.arrayBuffer(); diff --git a/core/frontend/src/tile/map/MapLayerTileTreeReference.ts b/core/frontend/src/tile/map/MapLayerTileTreeReference.ts index ea8485efa313..78b7be1b1bac 100644 --- a/core/frontend/src/tile/map/MapLayerTileTreeReference.ts +++ b/core/frontend/src/tile/map/MapLayerTileTreeReference.ts @@ -12,6 +12,7 @@ import { HitDetail } from "../../HitDetail"; import { IModelApp } from "../../IModelApp"; import { IModelConnection } from "../../IModelConnection"; import { createModelMapLayerTileTreeReference, MapLayerImageryProvider, TileTreeReference } from "../internal"; +import { DecorateContext } from "../../ViewContext"; /** * A [[TileTreeReference]] to be used specifically for [[MapTileTree]]s. @@ -84,6 +85,10 @@ export abstract class MapLayerTileTreeReference extends TileTreeReference { div.innerHTML = strings.join("
"); return div; } + + public override decorate(_context: DecorateContext): void { + this.imageryProvider?.decorate(_context); + } } /** diff --git a/core/frontend/src/tile/map/MapTileTree.ts b/core/frontend/src/tile/map/MapTileTree.ts index a278b8c2b60b..25dd9db37c41 100644 --- a/core/frontend/src/tile/map/MapTileTree.ts +++ b/core/frontend/src/tile/map/MapTileTree.ts @@ -21,7 +21,7 @@ import { IModelApp } from "../../IModelApp"; import { PlanarClipMaskState } from "../../PlanarClipMaskState"; import { FeatureSymbology } from "../../render/FeatureSymbology"; import { RenderPlanarClassifier } from "../../render/RenderPlanarClassifier"; -import { SceneContext } from "../../ViewContext"; +import { DecorateContext, SceneContext } from "../../ViewContext"; import { MapLayerScaleRangeVisibility, ScreenViewport } from "../../Viewport"; import { BingElevationProvider, createDefaultViewFlagOverrides, createMapLayerTreeReference, DisclosedTileTreeSet, EllipsoidTerrainProvider, GeometryTileTreeReference, @@ -1241,6 +1241,10 @@ export class MapTileTreeReference extends TileTreeReference { } } } + + public override decorate(context: DecorateContext): void { + this.forEachLayerTileTreeRef((ref) => ref.decorate(context)); + } } /** Returns whether a GCS converter is available. diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryFormat.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryFormat.ts new file mode 100644 index 000000000000..6ee4a8e92a02 --- /dev/null +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryFormat.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ + +import { ImageMapLayerSettings } from "@itwin/core-common"; +import { ImageryMapLayerFormat, MapLayerImageryProvider } from "@itwin/core-frontend"; +import { GoogleMapsImageryProvider } from "./GoogleMapsImageryProvider"; + +export class GoogleMapsMapLayerFormat extends ImageryMapLayerFormat { + public static override formatId = "GoogleMaps"; + public static override createImageryProvider(settings: ImageMapLayerSettings): MapLayerImageryProvider | undefined { + return new GoogleMapsImageryProvider(settings); + } +} diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts new file mode 100644 index 000000000000..78e06ae62971 --- /dev/null +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -0,0 +1,159 @@ +import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; +import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, ScreenViewport, TileUrlImageryProvider } from "@itwin/core-frontend"; +import { CreateGoogleMapsSessionOptions, GoogleMaps, GoogleMapsMapType } from "./GoogleMaps"; +import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; +const loggerCategory = "MapLayersFormats.GoogleMaps"; +const levelToken = "{level}"; +const rowToken = "{row}"; +const columnToken = "{column}"; + +/** Number of load tile requests before we have to refresh the attributions data */ +const attributionsRefreshCount = 40; + +export class GoogleMapsImageryProvider extends MapLayerImageryProvider { + + private static _sessions: {[layerName: string]: string} = {}; + private _attributions: string[]|undefined; + private _loadTileCounter = 0; + private _subLayerName = ""; + constructor(settings: ImageMapLayerSettings) { + super(settings, true); + } + public static validateUrlTemplate(template: string): MapLayerSourceValidation { + return { status: (template.indexOf(levelToken) > 0 && template.indexOf(columnToken) > 0 && template.indexOf(rowToken) > 0) ? MapLayerSourceStatus.Valid : MapLayerSourceStatus.InvalidUrl }; + } + + public override async initialize(): Promise { + + const layerPropertyKeys = this._settings.properties ? Object.keys(this._settings.properties) : undefined; + if (layerPropertyKeys === undefined || + !layerPropertyKeys.includes("mapType") || + !layerPropertyKeys.includes("language") || + !layerPropertyKeys.includes("region")) { + const msg = "Missing session options"; + Logger.logError(loggerCategory, msg); + throw new BentleyError(BentleyStatus.ERROR, msg); + } + + const sessionOptions = this.createCreateSessionOptions(); + if (this._settings.accessKey ) { + // Create session and store in query parameters + const sessionObj = await GoogleMaps.createSession(this._settings.accessKey.value, sessionOptions); + this._settings.unsavedQueryParams = {session: sessionObj.session}; + } else { + Logger.logError(loggerCategory, `Missing GoogleMaps api key/`); + } + } + + private createCreateSessionOptions(): CreateGoogleMapsSessionOptions { + const layerPropertyKeys = this._settings.properties ? Object.keys(this._settings.properties) : undefined; + if (layerPropertyKeys === undefined || + !layerPropertyKeys.includes("mapType") || + !layerPropertyKeys.includes("language") || + !layerPropertyKeys.includes("region")) { + const msg = "Missing session options"; + Logger.logError(loggerCategory, msg); + throw new BentleyError(BentleyStatus.ERROR, msg); + } + + const createSessionOptions: CreateGoogleMapsSessionOptions = { + + mapType: this._settings.properties!.mapType as GoogleMapsMapType, + region: this._settings.properties!.region as string, + language: this._settings.properties!.language as string, + } + if (this._settings.properties?.orientation !== undefined) { + createSessionOptions.orientation = this._settings.properties!.orientation as number; + } + + if (this._settings.properties?.layerTypes !== undefined) { + createSessionOptions.layerTypes = this._settings.properties!.layerTypes as string[]; + } + return createSessionOptions; + + } + + // construct the Url from the desired Tile + public async constructUrl(row: number, column: number, level: number): Promise { + let url = this._settings.url; + if (TileUrlImageryProvider.validateUrlTemplate(url).status !== MapLayerSourceStatus.Valid) { + if (url.lastIndexOf("/") !== url.length - 1) + url = `${url}/`; + url = `${url}{level}/{column}/{row}.png`; + } + + const tmpUrl = url.replace(levelToken, level.toString()).replace(columnToken, column.toString()).replace(rowToken, row.toString()); + const obj = new URL(tmpUrl); + if (this._settings.accessKey ) { + obj.searchParams.append("key", this._settings.accessKey.value); + } + + // We 'session' param to be already part of the query parameters + return this.appendCustomParams(obj.toString()); + } + + private async getAttributions(row: number, column: number, zoomLevel: number): Promise { + let attributions: string[] = []; + const session = this._settings.collectQueryParams().session; + const key = this._settings.accessKey?.value; + if (!session || !key) { + return attributions; + } + + const extent = this.getEPSG4326Extent(row, column, zoomLevel); + const range = MapCartoRectangle.fromDegrees(extent.longitudeLeft, extent.latitudeBottom, extent.longitudeRight, extent.latitudeTop); + + try { + const viewportInfo = await GoogleMaps.getViewportInfo(range, zoomLevel, session, key); + if (viewportInfo) { + attributions = viewportInfo.copyright.split(","); + } + } catch { + + } + return attributions; + } + + public override async loadTile(row: number, column: number, zoomLevel: number): Promise { + // This is a hack until 'addLogoCards' is made async + if ((this._loadTileCounter++%attributionsRefreshCount === 0)) { + this._attributions = await this.getAttributions(row, column, zoomLevel); + } + + return super.loadTile(row, column, zoomLevel); + } + + public override decorate(context: DecorateContext): void { + context.viewport.invalidateDecorations(); + console.log("GoogleMapsImageryProvider.decorate"); + } + + public override addLogoCards(cards: HTMLTableElement, _vp: ScreenViewport): void { + // const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; + // const matchingAttributions = this.getMatchingAttributions(tiles); + // const copyrights: string[] = []; + // for (const match of matchingAttributions) + // copyrights.push(match.copyrightMessage); + + // let copyrightMsg = ""; + // for (let i = 0; i < copyrights.length; ++i) { + // if (i > 0) + // copyrightMsg += "
"; + // copyrightMsg += copyrights[i]; + // } + // const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; + // const attributions = await this.getAttributions(tiles); + if (!this._attributions) + return; + + const attributions = this._attributions; + let copyrightMsg = ""; + for (let i = 0; i < attributions.length; ++i) { + if (i > 0) + copyrightMsg += "
"; + copyrightMsg += attributions[i]; + } + + cards.appendChild(IModelApp.makeLogoCard({ iconSrc: `${IModelApp.publicPath}images/google_on_white_hdpi.png`, heading: "Google Maps", notice: copyrightMsg })); + } +} diff --git a/extensions/map-layers-formats/src/map-layers-formats.ts b/extensions/map-layers-formats/src/map-layers-formats.ts index a4d05f36a888..cbb94179613b 100644 --- a/extensions/map-layers-formats/src/map-layers-formats.ts +++ b/extensions/map-layers-formats/src/map-layers-formats.ts @@ -6,6 +6,8 @@ export * from "./mapLayersFormats"; export * from "./ArcGisFeature/ArcGisFeatureProvider"; export * from "./Tools/MapFeatureInfoTool"; +export * from "./GoogleMaps/GoogleMaps"; + /** @docs-package-description * This package provides support for additional map layer formats that are not included in the @itwin/core-frontend package. diff --git a/extensions/map-layers-formats/src/mapLayersFormats.ts b/extensions/map-layers-formats/src/mapLayersFormats.ts index 25568d7f38b3..6a6fdbf8f3e4 100644 --- a/extensions/map-layers-formats/src/mapLayersFormats.ts +++ b/extensions/map-layers-formats/src/mapLayersFormats.ts @@ -12,6 +12,7 @@ import { ArcGisFeatureMapLayerFormat } from "./ArcGisFeature/ArcGisFeatureFormat import { MapFeatureInfoTool } from "./Tools/MapFeatureInfoTool"; import { Localization } from "@itwin/core-common"; import { OgcApiFeaturesMapLayerFormat } from "./OgcApiFeatures/OgcApiFeaturesFormat"; +import { GoogleMapsMapLayerFormat } from "./GoogleMaps/GoogleMapsImageryFormat"; /** Configuration options. * @beta @@ -37,6 +38,7 @@ export class MapLayersFormats { if (IModelApp.initialized) { IModelApp.mapLayerFormatRegistry.register(ArcGisFeatureMapLayerFormat); IModelApp.mapLayerFormatRegistry.register(OgcApiFeaturesMapLayerFormat); + IModelApp.mapLayerFormatRegistry.register(GoogleMapsMapLayerFormat); } // register namespace containing localized strings for this package diff --git a/test-apps/display-test-app/src/common/DtaConfiguration.ts b/test-apps/display-test-app/src/common/DtaConfiguration.ts index 3d3699151c55..39a2a3ac4ad9 100644 --- a/test-apps/display-test-app/src/common/DtaConfiguration.ts +++ b/test-apps/display-test-app/src/common/DtaConfiguration.ts @@ -46,6 +46,7 @@ export interface DtaStringConfiguration { iTwinId?: GuidString; // default is undefined, used by spatial classification to query reality data from context share, and by iModel download mapBoxKey?: string; // default undefined bingMapsKey?: string; // default undefined + googleMapsKey?: string; // default undefined cesiumIonKey?: string; // default undefined logLevel?: string; // default undefined windowSize?: string; // default undefined @@ -141,6 +142,9 @@ export const getConfig = (): DtaConfiguration => { if (undefined !== process.env.IMJS_BING_MAPS_KEY) configuration.bingMapsKey = process.env.IMJS_BING_MAPS_KEY; + if (undefined !== process.env.IMJS_GOOGLE_MAPS_KEY) + configuration.googleMapsKey = process.env.IMJS_GOOGLE_MAPS_KEY; + if (undefined !== process.env.IMJS_MAPBOX_KEY) configuration.mapBoxKey = process.env.IMJS_MAPBOX_KEY; diff --git a/test-apps/display-test-app/src/frontend/App.ts b/test-apps/display-test-app/src/frontend/App.ts index f93abe220bf2..816995f8b8d6 100644 --- a/test-apps/display-test-app/src/frontend/App.ts +++ b/test-apps/display-test-app/src/frontend/App.ts @@ -263,6 +263,9 @@ export class DisplayTestApp { BingMaps: configuration.bingMapsKey ? { key: "key", value: configuration.bingMapsKey } : undefined, + GoogleMaps: configuration.googleMapsKey + ? { key: "key", value: configuration.googleMapsKey } + : undefined, }, /* eslint-enable @typescript-eslint/naming-convention */ hubAccess: createHubAccess(configuration), From 038ca9c8bf2346f4a707416df981b23fcb1320a3 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Fri, 24 Jan 2025 13:47:53 -0500 Subject: [PATCH 02/44] wip --- core/common/src/MapLayerSettings.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/common/src/MapLayerSettings.ts b/core/common/src/MapLayerSettings.ts index d2e054442be6..0e565732e6eb 100644 --- a/core/common/src/MapLayerSettings.ts +++ b/core/common/src/MapLayerSettings.ts @@ -213,11 +213,6 @@ export interface MapLayerKey { value: string; } -// export interface MapLayerProviderData { -// compare(rhs: MapLayerProviderData): number; -// clone(): MapLayerProviderData; -// } - /** Abstract base class for normalized representation of a [[MapLayerProps]] for which values have been validated and default values have been applied where explicit values not defined. * This class is extended by [[ImageMapLayerSettings]] and [ModelMapLayerSettings]] to create the settings for image and model based layers. * One or more map layers may be included within [[MapImagerySettings]] object. @@ -301,7 +296,6 @@ export class ImageMapLayerSettings extends MapLayerSettings { public password?: string; public accessKey?: MapLayerKey; - /** List of query parameters to append to the settings URL and persisted as part of the JSON representation. * @note Sensitive information like user credentials should be provided in [[unsavedQueryParams]] to ensure it is never persisted. * @beta From 41e88af8f4e99488389a5d88c8594871e66b8014 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Mon, 27 Jan 2025 07:42:40 -0500 Subject: [PATCH 03/44] removed absolute in comparaison --- core/bentley/src/Compare.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/bentley/src/Compare.ts b/core/bentley/src/Compare.ts index 8973f7cc8e64..589c977f64f9 100644 --- a/core/bentley/src/Compare.ts +++ b/core/bentley/src/Compare.ts @@ -124,7 +124,7 @@ export function comparePrimitiveArrays (lhs?: Array, rhs? else if (lhs.length === 0 && rhs.length === 0) { return 0; } else if (lhs.length !== rhs.length) { - return Math.abs(lhs.length - rhs.length); + return lhs.length - rhs.length; } let cmp = 0; From 344b4c4066eff720d9a7878d703520e5afdd6391 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 29 Jan 2025 08:36:31 -0500 Subject: [PATCH 04/44] wip --- .../src/GoogleMaps/GoogleMapDecorator.ts | 68 +++++++ .../src/GoogleMaps/GoogleMaps.ts | 188 ++++++++++++++++++ .../GoogleMaps/GoogleMapsImageryProvider.ts | 68 ++----- .../images/google_on_non_white_hdpi.png | Bin 0 -> 11448 bytes .../public/images/google_on_white_hdpi.png | Bin 0 -> 11368 bytes 5 files changed, 278 insertions(+), 46 deletions(-) create mode 100644 extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts create mode 100644 extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts create mode 100644 extensions/map-layers-formats/src/public/images/google_on_non_white_hdpi.png create mode 100644 extensions/map-layers-formats/src/public/images/google_on_white_hdpi.png diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts new file mode 100644 index 000000000000..6b1efda01f11 --- /dev/null +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts @@ -0,0 +1,68 @@ +import { CanvasDecoration, DecorateContext, Decorator, IconSprites, IModelApp, ScreenViewport, Sprite } from "@itwin/core-frontend"; +import { Point3d } from "@itwin/core-geometry"; +import { MapTypesType } from "./GoogleMaps"; + +// Similar to 'SprintLocation' but uses a viewport pixel position instead of world position +class ImagePixelLocationDecoration implements CanvasDecoration { + private _viewport?: ScreenViewport; + private _sprite?: Sprite; + private _alpha?: number; + public readonly position = new Point3d(); + // public get isActive(): boolean { return this._viewport !== undefined; } + private _isSpriteLoaded = false; + public async activate(sprite: Sprite): Promise { + this._sprite = sprite; + return new Promise((resolve, _reject) => { + sprite.loadPromise.then(() => { + resolve(true); + }).catch(() => { + resolve (false); + }); + }); + + + } + + public deactivate() { + + } + + /** Draw this sprite onto the supplied canvas. + * @see [[CanvasDecoration.drawDecoration]] + */ + public drawDecoration(ctx: CanvasRenderingContext2D): void { + const sprite = this._sprite!; + if (undefined === sprite.image) + return; + + if (undefined !== this._alpha) + ctx.globalAlpha = this._alpha; + + ctx.drawImage(sprite.image, -sprite.offset.x, -sprite.offset.y); + } + + /** If this SpriteLocation is active and the supplied DecorateContext is for its Viewport, add the Sprite to decorations. */ + public decorate(context: DecorateContext) { + this._viewport = context.viewport; + const vpHeight = context.viewport.parentDiv.clientHeight; + this.position.setFrom({x: 120, y: vpHeight - 25, z: 0}); + context.addCanvasDecoration(this); + } +} + +export class GoogleMapsDecorator implements Decorator { + public readonly logo = new ImagePixelLocationDecoration(); + private _sprite: Sprite|undefined; + public constructor() { + } + + public async activate(mapType: MapTypesType): Promise { + const imageName = mapType === "roadmap" ? "google_on_white_hdpi" : "google_on_non_white_hdpi"; + this._sprite = IconSprites.getSpriteFromUrl(`${IModelApp.publicPath}images/${imageName}.png`); + return this.logo.activate(this._sprite); + }; + + public decorate = (context: DecorateContext) => { + this.logo.decorate(context); + }; +} diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts new file mode 100644 index 000000000000..05b2d097b66e --- /dev/null +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -0,0 +1,188 @@ +import { BackgroundMapType, BaseMapLayerSettings, ImageMapLayerProps, ImageMapLayerSettings, PropertyBag } from "@itwin/core-common"; +import { IModelApp, MapCartoRectangle } from "@itwin/core-frontend"; +import { GoogleMapsMapLayerFormat } from "./GoogleMapsImageryFormat"; +import { Angle } from "@itwin/core-geometry"; +import { Logger } from "@itwin/core-bentley"; + +const loggerCategory = "MapLayersFormats.GoogleMaps"; + +export type LayerTypesType = "layerRoadmap" | "layerStreetview" | "layerTraffic"; +export type MapTypesType = "roadmap"|"satellite"|"terrain"; +export type ImageFormatsType = "jpeg" | "png"; + +/** +* Represents the options to create a Google Maps session. +*/ +export interface CreateGoogleMapsSessionOptions { + /** + * The type of base map. + * + * `roadmap`: The standard Google Maps painted map tiles. + * + * `satellite`: Satellite imagery. + * + * `terrain`: Terrain imagery. When selecting `terrain` as the map type, you must also include the `layerRoadmap` layer type. + * @beta + * */ + mapType: MapTypesType, + /** + * An {@link https://en.wikipedia.org/wiki/IETF_language_tag | IETF language tag} that specifies the language used to display information on the tiles. For example, `en-US` specifies the English language as spoken in the United States. + */ + language: string; + /** + * A {@link https://cldr.unicode.org/ | Common Locale Data Repository} region identifier (two uppercase letters) that represents the physical location of the user. For example, `US`. + */ + region: string; + + /** + * An array of values that specifies the layer types added to the map. + * + * `layerRoadmap`: Required if you specify terrain as the map type. Can also be optionally overlaid on the satellite map type. Has no effect on roadmap tiles. + * + * `layerStreetview`: Shows Street View-enabled streets and locations using blue outlines on the map. + * + * `layerTraffic`: Displays current traffic conditions. + * @beta + * */ + layerTypes?: LayerTypesType[]; + /** + * Specifies the file format to return. Valid values are either jpeg or png. JPEG files don't support transparency, therefore they aren't recommended for overlay tiles. If you don't specify an imageFormat, then the best format for the tile is chosen automatically. + */ + imageFormat?: ImageFormatsType; +}; + +/** +* Structure representing a Google Maps session. +* @beta +*/ +export interface GoogleMapsSession { + /** A session token value that you must include in all of your Map Tiles API requests. */ + session: string; + + /** string that contains the time (in seconds since the epoch) at which the token expires. A session token is valid for two weeks from its creation time, but this policy might change without notice. */ + expiry: number; + + /** The width of the tiles measured in pixels. */ + tileWidth: number; + + /** he height of the tiles measured in pixels. */ + tileHeight: number; + + /** The image format, which can be either `png` or `jpeg`. */ + imageFormat: string; +}; + +/** + * Represents the maximum zoom level available within a bounding rectangle. +* @beta +*/ +export interface GoogleMapsMaxZoomRect { + maxZoom: number; + north: number; + south: number; + east: number; + west: number; +} + +/** + * Indicate which areas of given viewport have imagery, and at which zoom levels. + * @beta +*/ +export interface GoogleMapsViewportInfo { + /** Attribution string that you must display on your map when you display roadmap and satellite tiles. */ + copyright: string; + + /** Array of bounding rectangles that overlap with the current viewport. Also contains the maximum zoom level available within each rectangle.. */ + maxZoomRects: GoogleMapsMaxZoomRect[]; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const GoogleMaps = { +/** + * Creates a Google Maps session. + * @param apiKey Google Cloud API key + * @param opts Options to create the session + * @beta +*/ + createSession: async (apiKey: string, opts: CreateGoogleMapsSessionOptions): Promise => { + const url = `https://tile.googleapis.com/v1/createSession?key=${apiKey}`; + const request = new Request(url, {method: "POST", body: JSON.stringify(opts)}); + const response = await fetch (request); + if (!response.ok) { + throw new Error(`CreateSession request failed: ${response.status} - ${response.statusText}`); + } + Logger.logInfo(loggerCategory, `Session created successfully`); + return response.json(); + }, + + /** + * Converts the session options to a property bag. + * @param opts Options to create the session + * @beta +*/ + createPropertyBagFromSessionOptions: (opts: CreateGoogleMapsSessionOptions) => { + const properties: PropertyBag = { + mapType: opts.mapType, + language: opts.mapType, + } + + if (opts.layerTypes !== undefined) { + properties.layerTypes = opts.layerTypes; + } + if (opts.region !== undefined) { + properties.region = opts.region; + } + return properties + }, + +/** + * Creates a Google Maps layer settings. + * @param name Name of the layer (Defaults to "GoogleMaps") + * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) + * @beta +*/ + createMapLayerSettings: (name?: string, opts?: CreateGoogleMapsSessionOptions): ImageMapLayerSettings => { + return ImageMapLayerSettings.fromJSON(GoogleMaps.createMapLayerProps(name, opts)); + }, + + /** + * Creates a Google Maps layer props. + * @param name Name of the layer (Defaults to "GoogleMaps") + * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) + * @beta +*/ + createMapLayerProps: (name: string = "GoogleMaps", opts?: CreateGoogleMapsSessionOptions): ImageMapLayerProps => { + if (!IModelApp.mapLayerFormatRegistry.isRegistered(GoogleMapsMapLayerFormat.formatId)) { + IModelApp.mapLayerFormatRegistry.register(GoogleMapsMapLayerFormat); + } + return { + formatId: GoogleMapsMapLayerFormat.formatId, + url: "", + name, + properties: GoogleMaps.createPropertyBagFromSessionOptions(opts ?? {mapType: "satellite", language: "en-US", region: "US:", layerTypes: ["layerRoadmap"]}), + }; + }, + + /** + * Retrieves the maximum zoom level available within a bounding rectangle. + * @param rectangle The bounding rectangle + * @param session The session token + * @param key The Google Cloud API key + * @returns The maximum zoom level available within the bounding rectangle. + * @beta + */ + getViewportInfo: async (rectangle: MapCartoRectangle, zoom: number, session: string, key: string): Promise=> { + const north = Angle.radiansToDegrees(rectangle.north); + const south = Angle.radiansToDegrees(rectangle.south); + const east = Angle.radiansToDegrees(rectangle.east); + const west = Angle.radiansToDegrees(rectangle.west); + const url = `https://tile.googleapis.com/tile/v1/viewport?session=${session}&key=${key}&zoom=${zoom}&north=${north}&south=${south}&east=${east}&west=${west}`; + const request = new Request(url, {method: "GET"}); + const response = await fetch (request); + if (!response.ok) { + return undefined; + } + const json = await response.json(); + return json as GoogleMapsViewportInfo;; + }, +}; diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 78e06ae62971..2801133310b3 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -1,23 +1,25 @@ import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, ScreenViewport, TileUrlImageryProvider } from "@itwin/core-frontend"; -import { CreateGoogleMapsSessionOptions, GoogleMaps, GoogleMapsMapType } from "./GoogleMaps"; +import { CreateGoogleMapsSessionOptions, GoogleMaps, LayerTypesType, MapTypesType } from "./GoogleMaps"; import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; +import { GoogleMapsDecorator } from "./GoogleMapDecorator"; const loggerCategory = "MapLayersFormats.GoogleMaps"; const levelToken = "{level}"; const rowToken = "{row}"; const columnToken = "{column}"; -/** Number of load tile requests before we have to refresh the attributions data */ -const attributionsRefreshCount = 40; +const urlTemplate = `https://tile.googleapis.com/v1/2dtiles/${levelToken}/${columnToken}/${rowToken}`; +/* +* Google Maps imagery provider +* @internal +*/ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { - private static _sessions: {[layerName: string]: string} = {}; - private _attributions: string[]|undefined; - private _loadTileCounter = 0; - private _subLayerName = ""; + private _decorator: GoogleMapsDecorator; constructor(settings: ImageMapLayerSettings) { super(settings, true); + this._decorator = new GoogleMapsDecorator(); } public static validateUrlTemplate(template: string): MapLayerSourceValidation { return { status: (template.indexOf(levelToken) > 0 && template.indexOf(columnToken) > 0 && template.indexOf(rowToken) > 0) ? MapLayerSourceStatus.Valid : MapLayerSourceStatus.InvalidUrl }; @@ -43,6 +45,13 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { } else { Logger.logError(loggerCategory, `Missing GoogleMaps api key/`); } + + const isActivated = await this._decorator.activate(this._settings.properties!.mapType as MapTypesType); + if (!isActivated) { + const msg = `Failed to activate decorator`; + Logger.logError(loggerCategory, msg); + throw new BentleyError(BentleyStatus.ERROR, msg); + } } private createCreateSessionOptions(): CreateGoogleMapsSessionOptions { @@ -58,16 +67,13 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { const createSessionOptions: CreateGoogleMapsSessionOptions = { - mapType: this._settings.properties!.mapType as GoogleMapsMapType, + mapType: this._settings.properties!.mapType as MapTypesType, region: this._settings.properties!.region as string, language: this._settings.properties!.language as string, } - if (this._settings.properties?.orientation !== undefined) { - createSessionOptions.orientation = this._settings.properties!.orientation as number; - } if (this._settings.properties?.layerTypes !== undefined) { - createSessionOptions.layerTypes = this._settings.properties!.layerTypes as string[]; + createSessionOptions.layerTypes = this._settings.properties!.layerTypes as LayerTypesType[]; } return createSessionOptions; @@ -75,20 +81,13 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { // construct the Url from the desired Tile public async constructUrl(row: number, column: number, level: number): Promise { - let url = this._settings.url; - if (TileUrlImageryProvider.validateUrlTemplate(url).status !== MapLayerSourceStatus.Valid) { - if (url.lastIndexOf("/") !== url.length - 1) - url = `${url}/`; - url = `${url}{level}/{column}/{row}.png`; - } - - const tmpUrl = url.replace(levelToken, level.toString()).replace(columnToken, column.toString()).replace(rowToken, row.toString()); + const tmpUrl = urlTemplate.replace(levelToken, level.toString()).replace(columnToken, column.toString()).replace(rowToken, row.toString()); const obj = new URL(tmpUrl); if (this._settings.accessKey ) { obj.searchParams.append("key", this._settings.accessKey.value); } - // We 'session' param to be already part of the query parameters + // We assume the 'session' param to be already part of the query parameters (checked in initialize) return this.appendCustomParams(obj.toString()); } @@ -115,38 +114,15 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { } public override async loadTile(row: number, column: number, zoomLevel: number): Promise { - // This is a hack until 'addLogoCards' is made async - if ((this._loadTileCounter++%attributionsRefreshCount === 0)) { - this._attributions = await this.getAttributions(row, column, zoomLevel); - } - return super.loadTile(row, column, zoomLevel); } public override decorate(context: DecorateContext): void { - context.viewport.invalidateDecorations(); - console.log("GoogleMapsImageryProvider.decorate"); + this._decorator.decorate(context); } public override addLogoCards(cards: HTMLTableElement, _vp: ScreenViewport): void { - // const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; - // const matchingAttributions = this.getMatchingAttributions(tiles); - // const copyrights: string[] = []; - // for (const match of matchingAttributions) - // copyrights.push(match.copyrightMessage); - - // let copyrightMsg = ""; - // for (let i = 0; i < copyrights.length; ++i) { - // if (i > 0) - // copyrightMsg += "
"; - // copyrightMsg += copyrights[i]; - // } - // const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; - // const attributions = await this.getAttributions(tiles); - if (!this._attributions) - return; - - const attributions = this._attributions; + const attributions: string[] = []; let copyrightMsg = ""; for (let i = 0; i < attributions.length; ++i) { if (i > 0) diff --git a/extensions/map-layers-formats/src/public/images/google_on_non_white_hdpi.png b/extensions/map-layers-formats/src/public/images/google_on_non_white_hdpi.png new file mode 100644 index 0000000000000000000000000000000000000000..393d03005d5bd544bd8655319844fca20fc10fc2 GIT binary patch literal 11448 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 literal 0 HcmV?d00001 diff --git a/extensions/map-layers-formats/src/public/images/google_on_white_hdpi.png b/extensions/map-layers-formats/src/public/images/google_on_white_hdpi.png new file mode 100644 index 0000000000000000000000000000000000000000..5424b56d5da6453745deb5a7bd09a5e3929db554 GIT binary patch literal 11368 zcmeHrcTiNz*6$!7N=9-Xk|1#yat6scOGbuafFTVqgds|fA}A<9as~+!B?pO0kR%`= zIYJ@;cH;*p$G8>y1FB5;Z881hmR`^2t&bb0RYrYL8_V8s(6LM zjaZBX)(?yXIzq-pQ>jpf>#x~k(uUGSGNrS;65|h|Zw1RL$dd0Y+=wsToPM-W!Ia>L?upFFR)acxBCyo0711YHX>KVS5UpIxr_qI3AMF=jjZVYGk zAUEIl)Q;<2v(jIzr7#9=q|XDYZ}Jmx5S;C2p`0Teb1v)!8ocpe`({_CcAoy?39Cu_PIx3I~MYcfBa#7N!$=xH4- z@eGywb&8yl>6HzW;e39rUK?Y*xxO}+-&M?$zC?#zAN{_hDIUKMn-Z^3h}kUi-LEv> z^UwA5ZTKTQ7@tJSv@X&i!m0QYPKLTqB?tt1=u zs#Q@uI%Dsi{_0>{eMqxrtY%+QxOMnqav(85%^ z)O&@);A#|zQQq#%_=Q0dmvL4XcVW-cecQmJ06W1;xNW#)BCZ-1{nF^cwB3&cJR?> zC^PdLt}508%NqJtYlFAO8t%(R-yiyNZN0q6+M;kcD|gv!FE`&GyMnA;TfTczI^#uc z`pz0b>8qEeagxe#qxo#O>+Chr*~JJTLk*im{0M%>JEPCE1xM8~n0Y)UgJ^n_&T>+q z;Oxhc+I3S{IxN=5kwDMuC2LzHSm1(Nb#oF+J!9s$II&vJu-ZF#$CTDAclCbB2kF*K z^1~OSm;>(v+5L{UzNNCrcFp(q>hF2QcpZ=SPP=s_z&7-Mt~zjSRgIlL4Zbq?j2$iv zl_7&(;yL(@g>O$)dtum~oPaM3rYv`3d1#9_|Ob5XVe@VS1I3X8^vMcALIh zGA=Wt7b>gPA8OXbiWgTUAG)YzgfCu+`g;V*P&pU569vagA!k{ikGb})Oey*`O;gyE zvz0a$2h`qrwp=mDJuMXCy^G9dBbGpBR}!A zWt_kn89vOJxSxcL%G{w=M-fWZ7aHB&8*8$lqUX;PJJUCBCsgANH7FO_<(pQX+>euF z!?jGsk=DU-r8ZD&$%%#a z7RySk*{}&GWOOrZA|BS@WH+iW$vo7NUjs-tWvj-y!9YwtE0^4&_6yK61E_{uXUBee z#9mfXh5hD0L!reyl$xAsmN!)?VIIPy^gwP(73CXinX_RhXoPcJ79#4+dSF%_4J-}5;^Fl95 zm{z%YY8usNqXX~v=LhkZW@J~>O<-QouJ1Bari61`%ww_MEP+jQoo#?#P*T^QZ6xZC(Dfs0K zWKbp0ADJ1(OvBp3E?So$(ESwQ%u4Ih8GPsc%ZwTE)+wURGT&o$8!C3Y!vpR_RW5uK zv615o%>A%0*-6i;tigdo5SS(u+!TQ3PpzgPh0u1{Zaq3Se z(w>(2NsY>o4-o@fZ`%+BY7J`JnU_DBdLNVR$f2}#J9;XPXf~L(wson_lKRJXjhz#Y zZtxvWSB9y87Ap=!=Cb+&WS)5pGx8utz+=llr zD-qmC$_V=|+a0x-yKDy1N}Wlh#S;&iYj+42cAK>oo3QM{+d4zz^GKf`ofIoD*N@+K0&PtAt#E=VbFxcvJAneT69EdsDgOerg@>wYnbTiPo40)YY{ZauCce{t zSQa6ZVOM)*5=zhT7$ep;Iq+d*_7#DG>g!nnqz7fNMOw|?xjAM8Q@w?2LxGqK&C&FL zz5hEHihhREK;t}@c>mV!TdrOz(a!?`Xxh`1$+Md zy1J8fXpWa-^l*Y*0Mo>+Aa82I&>>T9t3z`%rq{~EyYqqEd4z(U)!jfPb7@A%i>g-! zq?#+Xq?cV!v^y5Q%gx>mC0wH)qg4$s*bp#UN+Nvic^;t)my>ZAWh0=WO%Py{f5N^+ zXM6~by9m5Zjp<<}(Zx)r{`#}6bOaDP+E_yVQi)?o&EjlR%9>IqoFeqhrta!_I+Av7 zrlygukk1{ntcu+saSftz?i(3lo|~ge(?1Pi3)-p>mQ%yj0xS4?n>5IsKD~3|jxE;? zky*c8s2mD-!^?QqNM9YSjMqeyNI#g$apfiiYh5Fnu^&6~1^3^|av&o#T5};#1Js0j zvX4_Rl-7ebpV#H+6@SiJUlm6rLx)rD;Dt=H#GH-4A3?22_)Jp6m?+nqIYX%lj0foM`2!-ViEkO>o!NQp zxl&LsKejY0k|wc@B4sV$0PB%U5y(Br^WC-m|<$oUDIomqF zEX*A#G{?bGE)+yJxT+2EPTQNHPHE$Ne%Bv4c`^T}$>WCcnP$IzlXjj}(OU8*=B1`W zZ#wY_WP&c7^E2e__zy_H(%V+Esg>&Y{@>+c-98N!*^@tbYZ;S{D~k6FM-S$q+Rktpocr6wKBpZKcDUBmN>Z1bl+RspRMpAumAH?_3S@*x4E zkU7pgiqEe~O7`hQ7+bW+c(8D63X%T| zYA2E9dQ*Pb2@Rgl`G^VOMpR|85bvQ?woV}sVfK7EbB|-aoM3}Ds8dUI;ncCLbh{iukuV#q z5!oaxO6ndso0wlBy6C1f07PJ(%1ly$#x z?!|VvE{}Y~lw8a{(FJRM&9D2(>)974i-M>H83A)j|}DAGhc?pV!vi z-cDL80gCBZ@;{$k)PsFFC|G2jiMvG#xBwvk!4yr<6#v z4%}hgMSUe`W0|GOS`v(-#Tk)aqg0?bg5UHFR^R7e2yT@L4+-BM{>3viE?E)7tLf5+hh#^&3BSfBVMhMejO$8kM_OfIRcMQU+Ii};bD{|Nt$zUtdyX)8~&Y+YTmJJAaS+gT}H3*-KNT!`X8vjD0$fexZ zDC;LU9nb~a>3J5?wtRDjH}2@X+5HsPI8&Y|GkEZv<0%A6+zYf<$1f=c*XB`9hp>HB zz6##7)SSM0e-`TLZopsqIctuHQS`j`OVnAOo(5K0;(o~prhwV|k4uk9H858ApZD!( zF9V*+9L-YO&||DMxGM$M-RInlplMc;IK|Za78@;wDJvXYlXgu9%31&U+NBa&u@oR0 zjAs+cKc)L=x3~|18zw+~uph)?`QMbLtrAh?dF=o3!g2~Ul ztYD#k=S+fd6+Lr0?5U7Y##lofN;?aBb8NYN%!WN}_w22P) zfrw|YX@vUDq`lI6?-KjpuK4+?T@R;FMzyl8cA8}59f7&|J3$yx!yLx#O#ClN77tYP zImm)e1kMZTh#RqeAGOY>U5Ew%z!QdBw?##0PJGhG!^Img1GZ1bg z#cU*`3DR_xhuOnbeBELCzFG!QUq`5z4YQ0it|Uqv4d4v(fB;d>PA*7sloa!CTygaK zFEJl8@VAPGqZG5TrVdaZ;SK`|@(S{Tcob1^FEFz-E>P0l##UTULFrEjbW4ia-owLH zoR81j+nd*0fEVFz$Hy-wCdLN>^MS!UXbm2ukBbKc#p8lx`33O@h5`%;b%(opz!5IM zUziYUgr|oTGc&p$_%HvQT{SiTf_Fjw$pV@WJ`}{2kDnLB=j_b)cMGJ4q8A$EPlx_T z3#0-1h>A}ShD3O}Lt%%Bq?= ze_8yZzz**0`r8Uk_J3%4z-|9c)_?f+tLJw(e|H3J{ul0lX#Xqr-^yq$O-*qH1l04F zd#Vaj%)iDLw?RPRHsXK0+6q8HHljj2w)~|1~e-=xET)LVhJG z8sN7a9gMiVI}GB1a5q38oTQk4`2_rB`B%3ldOg`dJRk}X4;UI01O|(PM8v^h1Aef$ zps2WjAP>3;`kOw&25#%~|I+@odVrFD%()62i5}nQx9E>0r4MuaqxDDA3I2O60fE0) zg*XKIM+iuW7tH3jpJ=Q;D^Fxc@GwU@qv- zAa8WJB%C-pgf6viTdON80B(NW*{wy1Xvtky6%!-?fJgf4!2qPCQ=x@89;%v(IO{kR zq#%~!nYnIs{YI*)AZLJ@`EKUtVX#(r^F3f)2j8h|a%LLAwk30#oPnk}$=O;}I)GO=X)xEKlow9gYN_?M*dBW>n7Uiq#@DmD(E}o0@P5AlFa6*F0GW479t0nXs&R?`?TKmkK{Y3?5mU zoI+8~sgCT;#=9~piGfIluIzN9oNIIQhN|yxw))kA&ASHSB^9J3k*S$Rolhpq2Fe%& z5A9OsMk1y~!!JXk+P*Ac8+b|tBT9J*{6Y4+0^qBYsQ@#BC&|EId!H);ZJFZ}%SgYY z$(TuIrhouocc!5~hRS1}Qc<5ZU^%@K#M(A*MLT*k(Ky2I$Fw8)IX7lkD}-_g5J*@M zt}v`{JSrNwb64TCsdS#5lkvV>oJR`IhpVKXQdv!`ZO(eBXe08?uP@lT1s&t6C^O{wD2i$&Ti45G?U{+J!K zp|;X8V{97~UukLaUcQz*Tj=1ii>68XB+&S*T)iX1hl2BHNho*$KW&Q<$j!BCdf+1ykpV}gsB5>wQnESTnFZhoZP zG_=j?gch-W1!m2=nlLx~VEEqk_A}DaHd2;b5?jfMLfi9&8Z|?9>5bwpQ?KG<2q!4^ zDA%8X6f5r+Rsv|k#@sNNe+aLf#ajAq8f5kJ%(G%}F=VVee@M5J*3|PHWqqmhL zR06=D6|IXGd9T#iJFgsJ%LJHYg_`hb@| zUeODyJ=O82)%}EutuJ=lvHB@o)LM?!;mWM05n>QL*!yka;Y;>*96X};pY%UXaNF_( zRUS=IC~vG?B^sNSguuF2wFfi4Y4mG;a!sR=Z*;6FWr_E+!y#(alJw#awQ%85e(%^m zGWYr(l5>WaZ2D%7u_sb<@N#^zXSHGB0w`l=qc0#OuOKig|M=4&Dd6iE@y7u3jHhb` zW{ySslh5m+(()aHnMGKVa)$4wAuDd>Zm;w)c6OViBCN(cr^g2k1|N9l@nJOSN3Hl1 zzP>cp9aj7?KFn*IDO(bB#{|IO%$c5?(JddYoa=7?1ye+YTudtqo^?elvzEhcEEt_ZsDP{m()g% z>;m^u--uh(JDA=+#W)314u}YmuLlOX+Q?_I8$8U`=DfV8@kvSTlk3MX@@2JLu8taD zrDvtltFXxJA~E4!j>XN?Xw+t3xY(4(1N_-RchKYAw9>jaTjhaWG;j(NAKNN{A%rP3 zTV<=$r^Ik=FJ#J}!xt}V!^7oXG#A8BNfOz;>)BcInXQwrX2ty0%Zv>NTGiNv4;>0q z5(I#@=N6>YSr()Zh6$&%ZC_H~@>vkFYC5#%KMV9$?c|%BtoLq_si?pHR_tn1aBmXj^048dMI`WmyBdpEQkJT^#p$-Vvp7@kxZwk>xrE1 z^Suhq3Ecgi9ao!XiJf}!&Z)aCLoZA5BWnfO8Y5)(je=F!8~q{0)t&~Gip08c_Z}Vn zWKT$L4X4Ra;UE#I#9=8v6p$46HDXP7Xx-srKEXIh+vY74B(s!mTq*c|V)5x)H?%Wv zosOL?^DH-jaUvWi^Ij#G6h&QRjhABaAoj=X*Md$NuSi2Pt@@mGO&5t{Wv8qs(jD^R z$;+Bwmilv^O);*0qFOK(y_yFoFR)t`^c99|(;Tp4HxD%$z7eIEaXla>cE8vC;4$Xn z^GvdtR^fd+z?0q0G#%LX(QXDJA1A)SeS;ez5plCSx;?-|fc+DTzwo|r;aFL>pa%ay zhE7tgQ41<16ZpNGWl~FW#8B1e;z#Cw=_G+qQ8u`VNVc6w9V`k_VO6OMWu`s+ zMg^eBfYz*4*-A*p+XY9mrVesdN!0zF`5y)+e)@Xs3@UsFPHhd|-*+8xr9eRF-CmU2 zQ-n}!ieEy|Mid;Qd7Nk;oD`VdHol@1&capWHQD}Ug}6)a@+0tpIvow@z6ia81>xdS z@C(P+Z3P-2^UTH-q|lZwN=^N7+OhbAZurT1gdF6o}1OrC11GR1i^#%{iWc3F#NjKuOHUbW*^n@DxjHD4=%Ym;-hztW{vc*{l; zH+{YYE|=+r5bVL+w0ku19u`Z@IH;-BR&&2DpSW411m_~~lYW`v?f%A5Qu?strWZSw zd~Jd4$q!!1$=x4K;zm{BOkFZ?KnC zka__R_lDEO@;|Nano6}OP8f4D7g->PJdL|@2-ro|lIa;|5(Cd*TU^t`X6wZy7~`lZ zW!>T$@Y(rzb`d~q?I4$pAiVKVk5Kj*y`}b|YDN0h*0;3&;9+y96kCXXMq_lcv7wTh zq+d!S!_NEAmMtQayOPoGuH>+IDlS{{t)L!SrGAvN^Miw!rTX1(Gf5Z2Vsl676(7H= zzq3}ZMkI<5j5w&+x}RV4DkQEdd{uN-vPi*BRqtg{lszR0Bi0T@0*Ov!FXk|}F1M|% zHVHE`VWOL}Cp>%B#8V#P!iM2!F`got&9)qiV9-DE#Rt%ASV}ALaI!BKSjP5W?xdaD zN#!(rEW`3#zDvwLb}!2rvECSk5hNMO{|uglsfuDf9D3LnN;yM(#ee6dVRu82w0!t& zNW90-CTwzc-bd>=)R#y3%q8`{79f(NLYcGjJ;1r25r4zawLPbc$}pZ7cca8*m#LFk z*#;g*g&mRFBUw6tH0QNLJ=3u`KqRYGhOOsQ^&6IV-eKF8`hgI|?J2Ta?5AZI+U7uK zgUfE|Bi|Lt);H3w_K?a+B+_9qX@;?b z*_R$LfMd&FHy^4T_my&-0c2OnsBp;XfT+H3Og1B=`(Q-QbkE|K4_a&?@1oc=!}(yl z_k5;c%}NQa!}VR;U~dWdDK3`T?MI9%QesI{q*Z|Zo|zf1c8EF8h?Ht73;kk=Mnf%L zRq5-Ri1_yM*$KlSudb?!2&;JSxA7jt+SsRKyxq562lSQC>JhR(JryCJ&hXF!VJ*TL ztQEByyHM@pE&;7;?A!*brg(S2E06C=K1emj-u;NH%y}VcrZqSH1WTsElJc@XFX>GP zyMo{(&Z83y->xWiu-!!2r@4h>&xl?B&1nt^;#;fAb{i!n)}4WR7AY1M??UQbJoO&M z9=ZlY58z zOU+wm)ogiG9p3@Zj;&&+uuLU6=^m(8Wa3OjuO-99&&=#V_G!2l#bp>lGpeI1*&=e) zl})TQOX;Qq>2x&^s!z6~)Ib`pQbNo8-HX67=DJ`GK*r(N1+h6_NmfJBoMVOXIV^}^@4D)OQGUR|DB69sj# zN*m2j)C-N4yv_%~2rHZ!&wmE>?8KSMe1kh!qRuHBp;9;ew8#cq+~ zye^rC`Cst2FIbpg4N zFTXtxB1Zvx;@56}xRUID>77NjPMKL|gAY}namw2JKJ$4~dALvh^6Q5i%nu|bWy?*) St>}sgpsJ{)Q1Q?z Date: Thu, 30 Jan 2025 14:48:41 -0500 Subject: [PATCH 05/44] wip + recover expired session --- core/common/src/MapLayerSettings.ts | 1 - .../GoogleMaps/GoogleMapsImageryProvider.ts | 75 +++++++++++++++++-- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/core/common/src/MapLayerSettings.ts b/core/common/src/MapLayerSettings.ts index 0e565732e6eb..09652c0c2ec3 100644 --- a/core/common/src/MapLayerSettings.ts +++ b/core/common/src/MapLayerSettings.ts @@ -375,7 +375,6 @@ export class ImageMapLayerSettings extends MapLayerSettings { // Clone members not part of MapLayerProps clone.userName = this.userName; clone.password = this.password; - clone.accessKey = this.accessKey; if (this.unsavedQueryParams) clone.unsavedQueryParams = {...this.unsavedQueryParams}; if (this.savedQueryParams) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 2801133310b3..25c2ee3f485e 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -17,6 +17,7 @@ const urlTemplate = `https://tile.googleapis.com/v1/2dtiles/${levelToken}/${colu export class GoogleMapsImageryProvider extends MapLayerImageryProvider { private _decorator: GoogleMapsDecorator; + private _hadUnrecoverableError = false; constructor(settings: ImageMapLayerSettings) { super(settings, true); this._decorator = new GoogleMapsDecorator(); @@ -25,6 +26,19 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { return { status: (template.indexOf(levelToken) > 0 && template.indexOf(columnToken) > 0 && template.indexOf(rowToken) > 0) ? MapLayerSourceStatus.Valid : MapLayerSourceStatus.InvalidUrl }; } + protected async createSession() : Promise { + const sessionOptions = this.createCreateSessionOptions(); + if (this._settings.accessKey ) { + // Create session and store in query parameters + const sessionObj = await GoogleMaps.createSession(this._settings.accessKey.value, sessionOptions); + this._settings.unsavedQueryParams = {session: sessionObj.session}; + return true; + } else { + Logger.logError(loggerCategory, `Missing GoogleMaps api key/`); + return false; + } + } + public override async initialize(): Promise { const layerPropertyKeys = this._settings.properties ? Object.keys(this._settings.properties) : undefined; @@ -37,13 +51,11 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { throw new BentleyError(BentleyStatus.ERROR, msg); } - const sessionOptions = this.createCreateSessionOptions(); - if (this._settings.accessKey ) { - // Create session and store in query parameters - const sessionObj = await GoogleMaps.createSession(this._settings.accessKey.value, sessionOptions); - this._settings.unsavedQueryParams = {session: sessionObj.session}; - } else { - Logger.logError(loggerCategory, `Missing GoogleMaps api key/`); + const isSessionCreated = await this.createSession(); + if (!isSessionCreated) { + const msg = `Failed to create session`; + Logger.logError(loggerCategory, msg); + throw new BentleyError(BentleyStatus.ERROR, msg); } const isActivated = await this._decorator.activate(this._settings.properties!.mapType as MapTypesType); @@ -113,8 +125,55 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { return attributions; } + private async logJsonError(tileResponse: Response) { + try { + const error = await tileResponse.json(); + Logger.logError(loggerCategory, `Error while loading tile: ${error?.message}`); + } catch { + Logger.logError(loggerCategory, `Error while loading tile: ${tileResponse.statusText}`); + } + } + public override async loadTile(row: number, column: number, zoomLevel: number): Promise { - return super.loadTile(row, column, zoomLevel); + if (this._hadUnrecoverableError) + return undefined; + + try { + let tileUrl: string = await this.constructUrl(row, column, zoomLevel); + let tileResponse: Response = await this.makeTileRequest(tileUrl); + if (!tileResponse.ok) { + if (tileResponse.headers.get("content-type")?.includes("application/json")) { + // Session might have expired, lets try to re-new it. + const isSessionCreated = await this.createSession(); + if (isSessionCreated) { + tileUrl = await this.constructUrl(row, column, zoomLevel); + tileResponse = await this.makeTileRequest(tileUrl); + if (!tileResponse.ok) { + if (tileResponse.headers.get("content-type")?.includes("application/json")) { + await this.logJsonError(tileResponse); + } else { + Logger.logError(loggerCategory, `Error while loading tile: ${tileResponse.statusText}`); + } + this._hadUnrecoverableError = true; // Prevent from doing more invalid requests + return undefined; + } + } else { + await this.logJsonError(tileResponse); + } + } else { + Logger.logError(loggerCategory, `Error while loading tile: ${tileResponse.statusText}`); + return undefined; + } + } + return await this.getImageFromTileResponse(tileResponse, zoomLevel); + } catch (error: any) { + if (error?.code === 401) { + Logger.logError(loggerCategory, `Authorize to load tile: ${error.message}`); + } else { + Logger.logError(loggerCategory, `Error while loading tile: ${error.message}`); + } + return undefined; + } } public override decorate(context: DecorateContext): void { From 6292d559b63ee21ce8f4cd1a28963d203b3c5055 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Fri, 31 Jan 2025 14:40:50 -0500 Subject: [PATCH 06/44] wip --- core/frontend/src/Viewport.ts | 9 +- .../frontend/src/tile/RealityModelTileTree.ts | 8 ++ core/frontend/src/tile/TileTreeReference.ts | 7 +- .../src/tile/map/CesiumTerrainProvider.ts | 16 ++++ .../ArcGISMapLayerImageryProvider.ts | 9 ++ .../AzureMapsLayerImageryProvider.ts | 9 ++ .../ImageryProviders/BingImageryProvider.ts | 19 +++- .../MapBoxLayerImageryProvider.ts | 9 ++ core/frontend/src/tile/map/ImageryTileTree.ts | 13 +++ .../src/tile/map/MapLayerImageryProvider.ts | 5 +- core/frontend/src/tile/map/MapTileTree.ts | 22 ++++- .../src/tile/map/TerrainMeshProvider.ts | 7 +- .../src/GoogleMaps/GoogleMaps.ts | 24 ++++- .../GoogleMaps/GoogleMapsImageryProvider.ts | 95 ++++++++++++------- 14 files changed, 205 insertions(+), 47 deletions(-) diff --git a/core/frontend/src/Viewport.ts b/core/frontend/src/Viewport.ts index 11a981b53df8..754a9bbdec9a 100644 --- a/core/frontend/src/Viewport.ts +++ b/core/frontend/src/Viewport.ts @@ -3179,7 +3179,7 @@ export class ScreenViewport extends Viewport { logo.src = `${IModelApp.publicPath}images/imodeljs-icon.svg`; logo.alt = ""; - const showLogos = (ev: Event) => { + const showLogos = async (ev: Event) => { const aboutBox = IModelApp.makeModalDiv({ autoClose: true, width: 460, closeBox: true, rootDiv: this.vpDiv.ownerDocument.body }).modal; aboutBox.className += " imodeljs-about"; // only added so the CSS knows this is the about dialog const logos = IModelApp.makeHTMLElement("table", { parent: aboutBox, className: "logo-cards" }); @@ -3187,12 +3187,13 @@ export class ScreenViewport extends Viewport { if (undefined !== IModelApp.applicationLogoCard) { logos.appendChild(IModelApp.applicationLogoCard()); } - + logos.appendChild(IModelApp.makeIModelJsLogoCard()); + const promises = new Array>(); for (const ref of this.getTileTreeRefs()) { - ref.addLogoCards(logos, this); + promises.push(ref.addAttributions(logos, this)); } - + await Promise.all(promises); ev.stopPropagation(); }; diff --git a/core/frontend/src/tile/RealityModelTileTree.ts b/core/frontend/src/tile/RealityModelTileTree.ts index a8f148c6cfcf..bca0672e35b5 100644 --- a/core/frontend/src/tile/RealityModelTileTree.ts +++ b/core/frontend/src/tile/RealityModelTileTree.ts @@ -940,11 +940,19 @@ export class RealityTreeReference extends RealityModelTileTree.Reference { return div; } + /** @deprecated in 5.0 Use [addAttributions] instead. */ public override addLogoCards(cards: HTMLTableElement): void { if (this._rdSourceKey.provider === RealityDataProvider.CesiumIonAsset && !cards.dataset.openStreetMapLogoCard) { cards.dataset.openStreetMapLogoCard = "true"; cards.appendChild(IModelApp.makeLogoCard({ heading: "OpenStreetMap", notice: `©OpenStreetMap ${IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap:OpenStreetMapContributors")}` })); } } + + public override async addAttributions(cards: HTMLTableElement): Promise { + if (this._rdSourceKey.provider === RealityDataProvider.CesiumIonAsset && !cards.dataset.openStreetMapLogoCard) { + cards.dataset.openStreetMapLogoCard = "true"; + cards.appendChild(IModelApp.makeLogoCard({ heading: "OpenStreetMap", notice: `©OpenStreetMap ${IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap:OpenStreetMapContributors")}` })); + } + } } diff --git a/core/frontend/src/tile/TileTreeReference.ts b/core/frontend/src/tile/TileTreeReference.ts index 3eb98b4304a2..8f019940b44e 100644 --- a/core/frontend/src/tile/TileTreeReference.ts +++ b/core/frontend/src/tile/TileTreeReference.ts @@ -261,9 +261,14 @@ export abstract class TileTreeReference /* implements RenderMemory.Consumer */ { */ public get planarClipMaskPriority(): number { return PlanarClipMaskPriority.DesignModel; } - /** Add attribution logo cards for the tile tree source logo cards to the viewport's logo div. */ + /** @deprecated in 5.0 Use [addAttributions] instead. */ public addLogoCards(_cards: HTMLTableElement, _vp: ScreenViewport): void { } + /** Add attribution logo cards for the tile tree source logo cards to the viewport's logo div. + * @beta + */ + public async addAttributions(_cards: HTMLTableElement, _vp: ScreenViewport): Promise { } + /** Create a tile tree reference equivalent to this one that also supplies an implementation of [[GeometryTileTreeReference.collectTileGeometry]]. * Return `undefined` if geometry collection is not supported. * @see [[createGeometryTreeReference]]. diff --git a/core/frontend/src/tile/map/CesiumTerrainProvider.ts b/core/frontend/src/tile/map/CesiumTerrainProvider.ts index f7afc80db3ca..36274ad212a0 100644 --- a/core/frontend/src/tile/map/CesiumTerrainProvider.ts +++ b/core/frontend/src/tile/map/CesiumTerrainProvider.ts @@ -18,6 +18,7 @@ import { GeographicTilingScheme, MapTile, MapTilingScheme, QuadId, ReadMeshArgs, RequestMeshDataArgs, TerrainMeshProvider, TerrainMeshProviderOptions, Tile, TileAvailability, } from "../internal"; +import { ScreenViewport } from "../../Viewport"; /** @internal */ enum QuantizedMeshExtensionIds { @@ -199,6 +200,7 @@ class CesiumTerrainProvider extends TerrainMeshProvider { this._tokenTimeOut = BeTimePoint.now().plus(CesiumTerrainProvider._tokenTimeoutInterval); } + /** @deprecated in 5.0 Use [addAttributions] instead. */ public override addLogoCards(cards: HTMLTableElement): void { if (cards.dataset.cesiumIonLogoCard) return; @@ -212,6 +214,20 @@ class CesiumTerrainProvider extends TerrainMeshProvider { cards.appendChild(card); } + public override async addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise { + if (cards.dataset.cesiumIonLogoCard) + return; + + cards.dataset.cesiumIonLogoCard = "true"; + let notice = IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.CesiumWorldTerrainAttribution"); + if (this._assetId === CesiumTerrainAssetId.Bathymetry) + notice = `${notice}\n${IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.CesiumBathymetryAttribution")}`; + + const card = IModelApp.makeLogoCard({ iconSrc: `${IModelApp.publicPath}images/cesium-ion.svg`, heading: "Cesium Ion", notice }); + cards.appendChild(card); + } + + public get maxDepth(): number { return this._maxDepth; } public get tilingScheme(): MapTilingScheme { return this._tilingScheme; } diff --git a/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts index 070eac48e6fb..7c6d56285ffb 100644 --- a/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts @@ -16,6 +16,7 @@ import { PropertyValueFormat, StandardTypeNames } from "@itwin/appui-abstract"; import { Point2d, Range2d, Range2dProps, XYProps } from "@itwin/core-geometry"; import { IModelStatus, Logger } from "@itwin/core-bentley"; import { HitDetail } from "../../../HitDetail"; +import { ScreenViewport } from "../../../Viewport"; const loggerCategory = "MapLayerImageryProvider.ArcGISMapLayerImageryProvider"; @@ -316,6 +317,7 @@ export class ArcGISMapLayerImageryProvider extends ArcGISImageryProvider { } + /** @deprecated in 5.0 Use [addAttributions] instead. */ public override addLogoCards(cards: HTMLTableElement): void { if (!cards.dataset.arcGisLogoCard) { cards.dataset.arcGisLogoCard = "true"; @@ -323,6 +325,13 @@ export class ArcGISMapLayerImageryProvider extends ArcGISImageryProvider { } } + public override async addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise { + if (!cards.dataset.arcGisLogoCard) { + cards.dataset.arcGisLogoCard = "true"; + cards.appendChild(IModelApp.makeLogoCard({ heading: "ArcGIS", notice: this._copyrightText })); + } + } + // Translates the provided Cartographic into a EPSG:3857 point, and retrieve information. // tolerance is in pixels private async getIdentifyData(quadId: QuadId, carto: Cartographic, tolerance: number, returnGeometry?: boolean, maxAllowableOffset?: number): Promise { diff --git a/core/frontend/src/tile/map/ImageryProviders/AzureMapsLayerImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/AzureMapsLayerImageryProvider.ts index f148c2fb6ebc..98570b5b3145 100644 --- a/core/frontend/src/tile/map/ImageryProviders/AzureMapsLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/AzureMapsLayerImageryProvider.ts @@ -9,6 +9,7 @@ import { ImageMapLayerSettings } from "@itwin/core-common"; import { IModelApp } from "../../../IModelApp"; import { MapLayerImageryProvider } from "../../internal"; +import { ScreenViewport } from "../../../Viewport"; /** @internal */ export class AzureMapsLayerImageryProvider extends MapLayerImageryProvider { @@ -21,10 +22,18 @@ export class AzureMapsLayerImageryProvider extends MapLayerImageryProvider { return `${this._settings.url}&${this._settings.accessKey.key}=${this._settings.accessKey.value}&api-version=2.0&zoom=${zoom}&x=${x}&y=${y}`; } + /** @deprecated in 5.0 Use [addAttributions] instead. */ public override addLogoCards(cards: HTMLTableElement): void { if (!cards.dataset.azureMapsLogoCard) { cards.dataset.azureMapsLogoCard = "true"; cards.appendChild(IModelApp.makeLogoCard({ heading: "Azure Maps", notice: IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.AzureMapsCopyright") })); } } + + public override async addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise { + if (!cards.dataset.azureMapsLogoCard) { + cards.dataset.azureMapsLogoCard = "true"; + cards.appendChild(IModelApp.makeLogoCard({ heading: "Azure Maps", notice: IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.AzureMapsCopyright") })); + } + } } diff --git a/core/frontend/src/tile/map/ImageryProviders/BingImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/BingImageryProvider.ts index 100acfea0a7e..e7aeb67f8bfb 100644 --- a/core/frontend/src/tile/map/ImageryProviders/BingImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/BingImageryProvider.ts @@ -148,7 +148,7 @@ export class BingMapsImageryLayerProvider extends MapLayerImageryProvider { } return matchingAttributions; } - + /** @deprecated in 5.0 Use [addAttributions] instead. */ public override addLogoCards(cards: HTMLTableElement, vp: ScreenViewport): void { const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; const matchingAttributions = this.getMatchingAttributions(tiles); @@ -166,6 +166,23 @@ export class BingMapsImageryLayerProvider extends MapLayerImageryProvider { cards.appendChild(IModelApp.makeLogoCard({ iconSrc: `${IModelApp.publicPath}images/bing.svg`, heading: "Microsoft Bing", notice: copyrightMsg })); } + public override async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; + const matchingAttributions = this.getMatchingAttributions(tiles); + const copyrights: string[] = []; + for (const match of matchingAttributions) + copyrights.push(match.copyrightMessage); + + let copyrightMsg = ""; + for (let i = 0; i < copyrights.length; ++i) { + if (i > 0) + copyrightMsg += "
"; + copyrightMsg += copyrights[i]; + } + + cards.appendChild(IModelApp.makeLogoCard({ iconSrc: `${IModelApp.publicPath}images/bing.svg`, heading: "Microsoft Bing", notice: copyrightMsg })); + } + // initializes the BingImageryProvider by reading the templateUrl, logo image, and attribution list. public override async initialize(): Promise { // get the template url diff --git a/core/frontend/src/tile/map/ImageryProviders/MapBoxLayerImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/MapBoxLayerImageryProvider.ts index 31601708dbc9..0dadd6bbda43 100644 --- a/core/frontend/src/tile/map/ImageryProviders/MapBoxLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/MapBoxLayerImageryProvider.ts @@ -9,6 +9,7 @@ import { ImageMapLayerSettings } from "@itwin/core-common"; import { IModelApp } from "../../../IModelApp"; import { MapLayerImageryProvider } from "../../internal"; +import { ScreenViewport } from "../../../Viewport"; /** Base class imagery map layer formats. Subclasses should override formatId and [[MapLayerFormat.createImageryProvider]]. * @internal @@ -45,6 +46,7 @@ export class MapBoxLayerImageryProvider extends MapLayerImageryProvider { return url; } + /** @deprecated in 5.0 Use [addAttributions] instead. */ public override addLogoCards(cards: HTMLTableElement): void { if (!cards.dataset.mapboxLogoCard) { cards.dataset.mapboxLogoCard = "true"; @@ -52,6 +54,13 @@ export class MapBoxLayerImageryProvider extends MapLayerImageryProvider { } } + public override async addAttributions (cards: HTMLTableElement, _vp: ScreenViewport): Promise { + if (!cards.dataset.mapboxLogoCard) { + cards.dataset.mapboxLogoCard = "true"; + cards.appendChild(IModelApp.makeLogoCard({ heading: "Mapbox", notice: IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.MapBoxCopyright") })); + } + } + // no initialization needed for MapBoxImageryProvider. public override async initialize(): Promise { } } diff --git a/core/frontend/src/tile/map/ImageryTileTree.ts b/core/frontend/src/tile/map/ImageryTileTree.ts index be91d5d76145..c716d7fe44f8 100644 --- a/core/frontend/src/tile/map/ImageryTileTree.ts +++ b/core/frontend/src/tile/map/ImageryTileTree.ts @@ -208,10 +208,17 @@ export class ImageryMapTileTree extends RealityTileTree { this._rootTile = new ImageryMapTile(params.rootTile, this, rootQuadId, this.getTileRectangle(rootQuadId)); } public get tilingScheme(): MapTilingScheme { return this._imageryLoader.imageryProvider.tilingScheme; } + + /** @deprecated in 5.0 Use [addAttributions] instead. */ public addLogoCards(cards: HTMLTableElement, vp: ScreenViewport): void { + // eslint-disable-next-line @typescript-eslint/no-deprecated this._imageryLoader.addLogoCards(cards, vp); } + public async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + return this._imageryLoader.addAttributions(cards, vp); + } + public getTileRectangle(quadId: QuadId): MapCartoRectangle { return this.tilingScheme.tileXYToRectangle(quadId.column, quadId.row, quadId.level); } @@ -258,10 +265,16 @@ class ImageryTileLoader extends RealityTileLoader { public get maxDepth(): number { return this._imageryProvider.maximumZoomLevel; } public get minDepth(): number { return this._imageryProvider.minimumZoomLevel; } public get priority(): TileLoadPriority { return TileLoadPriority.Map; } + + /** @deprecated in 5.0 Use [addAttributions] instead. */ public addLogoCards(cards: HTMLTableElement, vp: ScreenViewport): void { + // eslint-disable-next-line @typescript-eslint/no-deprecated this._imageryProvider.addLogoCards(cards, vp); } + public async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + await this._imageryProvider.addAttributions(cards, vp); + } public get maximumScreenSize(): number { return this._imageryProvider.maximumScreenSize; } public get imageryProvider(): MapLayerImageryProvider { return this._imageryProvider; } public async getToolTip(strings: string[], quadId: QuadId, carto: Cartographic, tree: ImageryMapTileTree): Promise { await this._imageryProvider.getToolTip(strings, quadId, carto, tree); } diff --git a/core/frontend/src/tile/map/MapLayerImageryProvider.ts b/core/frontend/src/tile/map/MapLayerImageryProvider.ts index 00e4db1cc228..ed81cdfba65f 100644 --- a/core/frontend/src/tile/map/MapLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/MapLayerImageryProvider.ts @@ -132,13 +132,16 @@ export abstract class MapLayerImageryProvider { public get tilingScheme(): MapTilingScheme { return this.useGeographicTilingScheme ? this._geographicTilingScheme : this._mercatorTilingScheme; } + /** @deprecated in 5.0 Use [addAttributions] instead. */ + public addLogoCards(_cards: HTMLTableElement, _viewport: ScreenViewport): void { } + /** * 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 */ - public addLogoCards(_cards: HTMLTableElement, _viewport: ScreenViewport): void { } + public async addAttributions(_cards: HTMLTableElement, _viewport: ScreenViewport): Promise { } /** @internal */ protected _missingTileData?: Uint8Array; diff --git a/core/frontend/src/tile/map/MapTileTree.ts b/core/frontend/src/tile/map/MapTileTree.ts index 25dd9db37c41..11800cd5fe4f 100644 --- a/core/frontend/src/tile/map/MapTileTree.ts +++ b/core/frontend/src/tile/map/MapTileTree.ts @@ -1227,21 +1227,41 @@ export class MapTileTreeReference extends TileTreeReference { return info; } - /** Add logo cards to logo div. */ + /** @deprecated in 5.0 Use [addAttributions] instead. */ public override addLogoCards(cards: HTMLTableElement, vp: ScreenViewport): void { const tree = this.treeOwner.tileTree as MapTileTree; if (tree) { + // eslint-disable-next-line @typescript-eslint/no-deprecated tree.mapLoader.terrainProvider.addLogoCards(cards, vp); for (const imageryTreeRef of this._layerTrees) { if (imageryTreeRef?.layerSettings.visible) { const imageryTree = imageryTreeRef.treeOwner.tileTree; if (imageryTree instanceof ImageryMapTileTree) + // eslint-disable-next-line @typescript-eslint/no-deprecated imageryTree.addLogoCards(cards, vp); } } } } + /** Add logo cards to logo div. */ + public override async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + const tree = this.treeOwner.tileTree as MapTileTree; + + if (tree) { + const promises = [tree.mapLoader.terrainProvider.addAttributions(cards, vp)]; + + for (const imageryTreeRef of this._layerTrees) { + if (imageryTreeRef?.layerSettings.visible) { + const imageryTree = imageryTreeRef.treeOwner.tileTree; + if (imageryTree instanceof ImageryMapTileTree) + promises.push(imageryTree.addAttributions(cards, vp)); + } + } + await Promise.all(promises); + } + } + public override decorate(context: DecorateContext): void { this.forEachLayerTileTreeRef((ref) => ref.decorate(context)); } diff --git a/core/frontend/src/tile/map/TerrainMeshProvider.ts b/core/frontend/src/tile/map/TerrainMeshProvider.ts index 6749e7e6edb3..3e5464def47f 100644 --- a/core/frontend/src/tile/map/TerrainMeshProvider.ts +++ b/core/frontend/src/tile/map/TerrainMeshProvider.ts @@ -89,11 +89,14 @@ export abstract class TerrainMeshProvider { */ public abstract readMesh(args: ReadMeshArgs): Promise; - /** Add attribution logo cards for the terrain data supplied by this provider to the [[Viewport]]'s logo div. + /** @deprecated in 5.0 Use [addAttributions] instead. */ + public addLogoCards(_cards: HTMLTableElement, _vp: ScreenViewport): void { } + + /** Add attribution logo cards for the terrain data supplied by this provider to the [[Viewport]]'s logo div. * For example, a provider that produces meshes from [Bing Maps](https://docs.microsoft.com/en-us/bingmaps/rest-services/elevations/) would be required to * disclose any copyrighted data used in the production of those meshes. */ - public addLogoCards(_cards: HTMLTableElement, _vp: ScreenViewport): void { } + public async addAttributions(_cards: HTMLTableElement, _vp: ScreenViewport): Promise {} /** Return whether terrain data can be obtained for the [[MapTile]] specified by `quadId`. If it returns false, a terrain mesh will instead be produced for * that tile by up-sampling the terrain mesh provided by its parent tile. diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index 05b2d097b66e..dec083471b5e 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -96,6 +96,25 @@ export interface GoogleMapsViewportInfo { maxZoomRects: GoogleMapsMaxZoomRect[]; } +/** + * Request parameters for the getViewportInfo method. + * @beta +*/ +export interface ViewportInfoRequestParams { + /** Bounding rectangle */ + rectangle: MapCartoRectangle; + + /** Session token */ + session: string; + + /** The Google Cloud API key */ + key: string; + + /** Zoom level of the viewport */ + zoom: number; +} + + // eslint-disable-next-line @typescript-eslint/naming-convention export const GoogleMaps = { /** @@ -166,12 +185,11 @@ export const GoogleMaps = { /** * Retrieves the maximum zoom level available within a bounding rectangle. * @param rectangle The bounding rectangle - * @param session The session token - * @param key The Google Cloud API key * @returns The maximum zoom level available within the bounding rectangle. * @beta */ - getViewportInfo: async (rectangle: MapCartoRectangle, zoom: number, session: string, key: string): Promise=> { + getViewportInfo: async (params: ViewportInfoRequestParams): Promise=> { + const {rectangle, session, key, zoom} = params; const north = Angle.radiansToDegrees(rectangle.north); const south = Angle.radiansToDegrees(rectangle.south); const east = Angle.radiansToDegrees(rectangle.east); diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 25c2ee3f485e..e49076c15d5c 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -1,6 +1,6 @@ import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; -import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, ScreenViewport, TileUrlImageryProvider } from "@itwin/core-frontend"; -import { CreateGoogleMapsSessionOptions, GoogleMaps, LayerTypesType, MapTypesType } from "./GoogleMaps"; +import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile, TileUrlImageryProvider } from "@itwin/core-frontend"; +import { CreateGoogleMapsSessionOptions, GoogleMaps, GoogleMapsSession, LayerTypesType, MapTypesType } from "./GoogleMaps"; import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; import { GoogleMapsDecorator } from "./GoogleMapDecorator"; const loggerCategory = "MapLayersFormats.GoogleMaps"; @@ -26,16 +26,16 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { return { status: (template.indexOf(levelToken) > 0 && template.indexOf(columnToken) > 0 && template.indexOf(rowToken) > 0) ? MapLayerSourceStatus.Valid : MapLayerSourceStatus.InvalidUrl }; } - protected async createSession() : Promise { + protected async createSession() : Promise { const sessionOptions = this.createCreateSessionOptions(); if (this._settings.accessKey ) { // Create session and store in query parameters const sessionObj = await GoogleMaps.createSession(this._settings.accessKey.value, sessionOptions); this._settings.unsavedQueryParams = {session: sessionObj.session}; - return true; + return sessionObj; } else { - Logger.logError(loggerCategory, `Missing GoogleMaps api key/`); - return false; + Logger.logError(loggerCategory, `Missing GoogleMaps api key`); + return undefined; } } @@ -51,8 +51,8 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { throw new BentleyError(BentleyStatus.ERROR, msg); } - const isSessionCreated = await this.createSession(); - if (!isSessionCreated) { + const session = await this.createSession(); + if (!session) { const msg = `Failed to create session`; Logger.logError(loggerCategory, msg); throw new BentleyError(BentleyStatus.ERROR, msg); @@ -103,28 +103,43 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { return this.appendCustomParams(obj.toString()); } - private async getAttributions(row: number, column: number, zoomLevel: number): Promise { - let attributions: string[] = []; - const session = this._settings.collectQueryParams().session; - const key = this._settings.accessKey?.value; - if (!session || !key) { - return attributions; - } - - const extent = this.getEPSG4326Extent(row, column, zoomLevel); - const range = MapCartoRectangle.fromDegrees(extent.longitudeLeft, extent.latitudeBottom, extent.longitudeRight, extent.latitudeTop); - - try { - const viewportInfo = await GoogleMaps.getViewportInfo(range, zoomLevel, session, key); - if (viewportInfo) { - attributions = viewportInfo.copyright.split(","); + private async fetchAttributions(tiles: Set): Promise { + const zooms = new Set(); + const matchingAttributions: string[] = []; + + // Viewport info requests must be made for a specific zoom level + tiles.forEach((tile) => zooms.add(tile.depth)); + for (const zoom of zooms) { + + let cartoRect: MapCartoRectangle|undefined; + for (const tile of tiles) { + if (tile.depth === zoom && tile instanceof MapTile) { + const extent = this.getEPSG4326Extent(tile.quadId.row, tile.quadId.column, tile.depth); + const rect = MapCartoRectangle.fromDegrees(extent.longitudeLeft, extent.latitudeBottom, extent.longitudeRight, extent.latitudeTop) + if (cartoRect) + cartoRect.union(rect); + else + cartoRect = rect; + } + } + if (cartoRect) { + try { + const viewportInfo = await GoogleMaps.getViewportInfo({ + rectangle: cartoRect, + session: this._settings.collectQueryParams().session, + key: this._settings.accessKey!.value, + zoom}); + if (viewportInfo?.copyright) { + matchingAttributions.push(viewportInfo.copyright); + } + } catch (error:any) { + Logger.logError(loggerCategory, `Error while loading viewport info: ${error?.message??"Unknown error"}`); + } } - } catch { - } - return attributions; - } + return matchingAttributions; + } private async logJsonError(tileResponse: Response) { try { const error = await tileResponse.json(); @@ -180,15 +195,27 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { this._decorator.decorate(context); } - public override addLogoCards(cards: HTMLTableElement, _vp: ScreenViewport): void { - const attributions: string[] = []; + public override async addAttributions (cards: HTMLTableElement, vp: ScreenViewport): Promise { let copyrightMsg = ""; - for (let i = 0; i < attributions.length; ++i) { - if (i > 0) - copyrightMsg += "
"; - copyrightMsg += attributions[i]; + const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; + if (tiles) { + try { + const attrList = await this.fetchAttributions(tiles); + for (const attr of attrList) { + attr.split(",").forEach((line) => { + copyrightMsg += `${copyrightMsg.length===0 ? "": " Date: Fri, 31 Jan 2025 15:17:16 -0500 Subject: [PATCH 07/44] wip --- core/bentley/src/Compare.ts | 7 +++---- core/common/src/MapLayerSettings.ts | 10 +++++----- core/frontend/src/tile/map/ImageryTileTree.ts | 9 +++++---- .../map-layers-formats/src/GoogleMaps/GoogleMaps.ts | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/core/bentley/src/Compare.ts b/core/bentley/src/Compare.ts index 589c977f64f9..a8e84acc2ad6 100644 --- a/core/bentley/src/Compare.ts +++ b/core/bentley/src/Compare.ts @@ -93,8 +93,7 @@ export function areEqualPossiblyUndefined(t: T | undefined, u: U | undefin return areEqual(t, u); } -export type PrimitiveType = number | string | boolean; -export function comparePrimitives(lhs: PrimitiveType, rhs: PrimitiveType): number { +export function compareSimpleTypes(lhs: number | string | boolean, rhs: number | string | boolean): number { // Make sure the types are the same if (typeof lhs !== typeof rhs) { return 1; @@ -116,7 +115,7 @@ export function comparePrimitives(lhs: PrimitiveType, rhs: PrimitiveType): numbe return cmp; } -export function comparePrimitiveArrays (lhs?: Array, rhs?: Array ) { +export function compareSimpleArrays (lhs?: Array, rhs?: Array ) { if (undefined === lhs) return undefined === rhs ? 0 : -1; else if (undefined === rhs) @@ -129,7 +128,7 @@ export function comparePrimitiveArrays (lhs?: Array, rhs? let cmp = 0; for (let i = 0; i < lhs.length; i++) { - cmp = comparePrimitives(lhs[i], rhs[i]); + cmp = compareSimpleTypes(lhs[i], rhs[i]); if (cmp !== 0) { break; } diff --git a/core/common/src/MapLayerSettings.ts b/core/common/src/MapLayerSettings.ts index 09652c0c2ec3..03356e00faf3 100644 --- a/core/common/src/MapLayerSettings.ts +++ b/core/common/src/MapLayerSettings.ts @@ -20,8 +20,8 @@ export type ImageryMapLayerFormatId = "ArcGIS" | "BingMaps" | "MapboxImagery" | export type SubLayerId = string | number; /** @public */ -export type PropertyBagArrayProperty = Array; -export interface PropertyBag { [key: string]: number | string | boolean | PropertyBagArrayProperty }; +export type MapLayerProviderArrayProperty = Array; +export interface MapLayerProviderProperties { [key: string]: number | string | boolean | MapLayerProviderArrayProperty }; /** JSON representation of the settings associated with a map sublayer included within a [[MapLayerProps]]. * A map sub layer represents a set of objects within the layer that can be controlled separately. These @@ -173,10 +173,10 @@ export interface ImageMapLayerProps extends CommonMapLayerProps { */ queryParams?: { [key: string]: string }; - /** Data specific to each imagery format. + /** Data specific to each map layer provider. * @beta */ - properties?: PropertyBag; + properties?: MapLayerProviderProperties; } @@ -310,7 +310,7 @@ export class ImageMapLayerSettings extends MapLayerSettings { /** TODO * @beta */ - public readonly properties?: PropertyBag; + public readonly properties?: MapLayerProviderProperties; public readonly subLayers: MapSubLayerSettings[]; public override get source(): string { return this.url; } diff --git a/core/frontend/src/tile/map/ImageryTileTree.ts b/core/frontend/src/tile/map/ImageryTileTree.ts index c716d7fe44f8..99bd69aacbba 100644 --- a/core/frontend/src/tile/map/ImageryTileTree.ts +++ b/core/frontend/src/tile/map/ImageryTileTree.ts @@ -6,9 +6,9 @@ * @module Tiles */ -import { assert, compareBooleans, compareNumbers, comparePrimitiveArrays, comparePrimitives, compareStrings, compareStringsOrUndefined, dispose, Logger, PrimitiveType} from "@itwin/core-bentley"; +import { assert, compareBooleans, compareNumbers, compareSimpleArrays, compareSimpleTypes, compareStrings, compareStringsOrUndefined, dispose, Logger,} from "@itwin/core-bentley"; import { Angle, Range3d, Transform } from "@itwin/core-geometry"; -import { Cartographic, ImageMapLayerSettings, ImageSource, MapLayerSettings, PropertyBagArrayProperty, RenderTexture, ViewFlagOverrides } from "@itwin/core-common"; +import { Cartographic, ImageMapLayerSettings, ImageSource, MapLayerSettings, RenderTexture, ViewFlagOverrides } from "@itwin/core-common"; import { IModelApp } from "../../IModelApp"; import { IModelConnection } from "../../IModelConnection"; import { RenderMemory } from "../../render/RenderMemory"; @@ -275,6 +275,7 @@ class ImageryTileLoader extends RealityTileLoader { public async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { await this._imageryProvider.addAttributions(cards, vp); } + public get maximumScreenSize(): number { return this._imageryProvider.maximumScreenSize; } public get imageryProvider(): MapLayerImageryProvider { return this._imageryProvider; } public async getToolTip(strings: string[], quadId: QuadId, carto: Cartographic, tree: ImageryMapTileTree): Promise { await this._imageryProvider.getToolTip(strings, quadId, carto, tree); } @@ -355,11 +356,11 @@ class ImageryMapLayerTreeSupplier implements TileTreeSupplier { break; } if (Array.isArray(lhsProp)) { - cmp = comparePrimitiveArrays(lhsProp, rhsProp as PropertyBagArrayProperty); + cmp = compareSimpleArrays(lhsProp, rhsProp as (number | string | boolean)[]); if (0 !== cmp) break; } else { - cmp = comparePrimitives(lhsProp as PrimitiveType, rhsProp as PrimitiveType); + cmp = compareSimpleTypes(lhsProp as number | string | boolean, rhsProp as number | string | boolean); if (0 !== cmp) break; } diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index dec083471b5e..8cdf867f78b8 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -1,4 +1,4 @@ -import { BackgroundMapType, BaseMapLayerSettings, ImageMapLayerProps, ImageMapLayerSettings, PropertyBag } from "@itwin/core-common"; +import { ImageMapLayerProps, ImageMapLayerSettings, MapLayerProviderProperties } from "@itwin/core-common"; import { IModelApp, MapCartoRectangle } from "@itwin/core-frontend"; import { GoogleMapsMapLayerFormat } from "./GoogleMapsImageryFormat"; import { Angle } from "@itwin/core-geometry"; @@ -135,12 +135,12 @@ export const GoogleMaps = { }, /** - * Converts the session options to a property bag. + * Converts the session options to provider properties * @param opts Options to create the session * @beta */ - createPropertyBagFromSessionOptions: (opts: CreateGoogleMapsSessionOptions) => { - const properties: PropertyBag = { + createPropertiesFromSessionOptions: (opts: CreateGoogleMapsSessionOptions): MapLayerProviderProperties => { + const properties: MapLayerProviderProperties = { mapType: opts.mapType, language: opts.mapType, } @@ -178,7 +178,7 @@ export const GoogleMaps = { formatId: GoogleMapsMapLayerFormat.formatId, url: "", name, - properties: GoogleMaps.createPropertyBagFromSessionOptions(opts ?? {mapType: "satellite", language: "en-US", region: "US:", layerTypes: ["layerRoadmap"]}), + properties: GoogleMaps.createPropertiesFromSessionOptions(opts ?? {mapType: "satellite", language: "en-US", region: "US:", layerTypes: ["layerRoadmap"]}), }; }, From 1169ed8602f01a1f815ab33b939816b93adb6fc0 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Fri, 31 Jan 2025 15:55:40 -0500 Subject: [PATCH 08/44] Honor session's tile size --- .../src/GoogleMaps/GoogleMapsImageryProvider.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index e49076c15d5c..5d33bb883936 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -18,6 +18,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { private _decorator: GoogleMapsDecorator; private _hadUnrecoverableError = false; + private _tileSize = 256 constructor(settings: ImageMapLayerSettings) { super(settings, true); this._decorator = new GoogleMapsDecorator(); @@ -38,6 +39,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { return undefined; } } + public override get tileSize(): number { return this._tileSize; } public override async initialize(): Promise { @@ -57,6 +59,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { Logger.logError(loggerCategory, msg); throw new BentleyError(BentleyStatus.ERROR, msg); } + this._tileSize = session.tileWidth; // assuming here tiles are square const isActivated = await this._decorator.activate(this._settings.properties!.mapType as MapTypesType); if (!isActivated) { From ad3909026c71e229ab083b0dc38e562a691cacf5 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Tue, 4 Feb 2025 15:28:51 -0500 Subject: [PATCH 09/44] Improved deprecation of `addLogoCards` --- core/frontend/src/tile/RealityModelTileTree.ts | 6 ++---- core/frontend/src/tile/TileTreeReference.ts | 5 ++++- .../src/tile/map/CesiumTerrainProvider.ts | 12 ++---------- .../ArcGISMapLayerImageryProvider.ts | 8 +++----- .../AzureMapsLayerImageryProvider.ts | 6 ++---- .../map/ImageryProviders/BingImageryProvider.ts | 16 ++-------------- .../MapBoxLayerImageryProvider.ts | 6 ++---- .../src/tile/map/MapLayerImageryProvider.ts | 5 ++++- .../frontend/src/tile/map/TerrainMeshProvider.ts | 5 ++++- 9 files changed, 25 insertions(+), 44 deletions(-) diff --git a/core/frontend/src/tile/RealityModelTileTree.ts b/core/frontend/src/tile/RealityModelTileTree.ts index bca0672e35b5..6279f756c246 100644 --- a/core/frontend/src/tile/RealityModelTileTree.ts +++ b/core/frontend/src/tile/RealityModelTileTree.ts @@ -949,10 +949,8 @@ export class RealityTreeReference extends RealityModelTileTree.Reference { } public override async addAttributions(cards: HTMLTableElement): Promise { - if (this._rdSourceKey.provider === RealityDataProvider.CesiumIonAsset && !cards.dataset.openStreetMapLogoCard) { - cards.dataset.openStreetMapLogoCard = "true"; - cards.appendChild(IModelApp.makeLogoCard({ heading: "OpenStreetMap", notice: `©OpenStreetMap ${IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap:OpenStreetMapContributors")}` })); - } + // eslint-disable-next-line @typescript-eslint/no-deprecated + return Promise.resolve(this.addLogoCards(cards)); } } diff --git a/core/frontend/src/tile/TileTreeReference.ts b/core/frontend/src/tile/TileTreeReference.ts index 8f019940b44e..641964ca7b47 100644 --- a/core/frontend/src/tile/TileTreeReference.ts +++ b/core/frontend/src/tile/TileTreeReference.ts @@ -267,7 +267,10 @@ export abstract class TileTreeReference /* implements RenderMemory.Consumer */ { /** Add attribution logo cards for the tile tree source logo cards to the viewport's logo div. * @beta */ - public async addAttributions(_cards: HTMLTableElement, _vp: ScreenViewport): Promise { } + public async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + // eslint-disable-next-line @typescript-eslint/no-deprecated + return Promise.resolve(this.addLogoCards(cards, vp)); + } /** Create a tile tree reference equivalent to this one that also supplies an implementation of [[GeometryTileTreeReference.collectTileGeometry]]. * Return `undefined` if geometry collection is not supported. diff --git a/core/frontend/src/tile/map/CesiumTerrainProvider.ts b/core/frontend/src/tile/map/CesiumTerrainProvider.ts index 36274ad212a0..a5136468df4d 100644 --- a/core/frontend/src/tile/map/CesiumTerrainProvider.ts +++ b/core/frontend/src/tile/map/CesiumTerrainProvider.ts @@ -215,16 +215,8 @@ class CesiumTerrainProvider extends TerrainMeshProvider { } public override async addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise { - if (cards.dataset.cesiumIonLogoCard) - return; - - cards.dataset.cesiumIonLogoCard = "true"; - let notice = IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.CesiumWorldTerrainAttribution"); - if (this._assetId === CesiumTerrainAssetId.Bathymetry) - notice = `${notice}\n${IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.CesiumBathymetryAttribution")}`; - - const card = IModelApp.makeLogoCard({ iconSrc: `${IModelApp.publicPath}images/cesium-ion.svg`, heading: "Cesium Ion", notice }); - cards.appendChild(card); + // eslint-disable-next-line @typescript-eslint/no-deprecated + return Promise.resolve(this.addLogoCards(cards)); } diff --git a/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts index 7c6d56285ffb..4536967259d1 100644 --- a/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts @@ -325,11 +325,9 @@ export class ArcGISMapLayerImageryProvider extends ArcGISImageryProvider { } } - public override async addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise { - if (!cards.dataset.arcGisLogoCard) { - cards.dataset.arcGisLogoCard = "true"; - cards.appendChild(IModelApp.makeLogoCard({ heading: "ArcGIS", notice: this._copyrightText })); - } + public override async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + // 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. diff --git a/core/frontend/src/tile/map/ImageryProviders/AzureMapsLayerImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/AzureMapsLayerImageryProvider.ts index 98570b5b3145..c5272e4622ab 100644 --- a/core/frontend/src/tile/map/ImageryProviders/AzureMapsLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/AzureMapsLayerImageryProvider.ts @@ -31,9 +31,7 @@ export class AzureMapsLayerImageryProvider extends MapLayerImageryProvider { } public override async addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise { - if (!cards.dataset.azureMapsLogoCard) { - cards.dataset.azureMapsLogoCard = "true"; - cards.appendChild(IModelApp.makeLogoCard({ heading: "Azure Maps", notice: IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.AzureMapsCopyright") })); - } + // eslint-disable-next-line @typescript-eslint/no-deprecated + return Promise.resolve(this.addLogoCards(cards)); } } diff --git a/core/frontend/src/tile/map/ImageryProviders/BingImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/BingImageryProvider.ts index e7aeb67f8bfb..ae869c92d389 100644 --- a/core/frontend/src/tile/map/ImageryProviders/BingImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/BingImageryProvider.ts @@ -167,20 +167,8 @@ export class BingMapsImageryLayerProvider extends MapLayerImageryProvider { } public override async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { - const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; - const matchingAttributions = this.getMatchingAttributions(tiles); - const copyrights: string[] = []; - for (const match of matchingAttributions) - copyrights.push(match.copyrightMessage); - - let copyrightMsg = ""; - for (let i = 0; i < copyrights.length; ++i) { - if (i > 0) - copyrightMsg += "
"; - copyrightMsg += copyrights[i]; - } - - cards.appendChild(IModelApp.makeLogoCard({ iconSrc: `${IModelApp.publicPath}images/bing.svg`, heading: "Microsoft Bing", notice: copyrightMsg })); + // eslint-disable-next-line @typescript-eslint/no-deprecated + return Promise.resolve(this.addLogoCards(cards, vp)); } // initializes the BingImageryProvider by reading the templateUrl, logo image, and attribution list. diff --git a/core/frontend/src/tile/map/ImageryProviders/MapBoxLayerImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/MapBoxLayerImageryProvider.ts index 0dadd6bbda43..660885aad3a2 100644 --- a/core/frontend/src/tile/map/ImageryProviders/MapBoxLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/MapBoxLayerImageryProvider.ts @@ -55,10 +55,8 @@ export class MapBoxLayerImageryProvider extends MapLayerImageryProvider { } public override async addAttributions (cards: HTMLTableElement, _vp: ScreenViewport): Promise { - if (!cards.dataset.mapboxLogoCard) { - cards.dataset.mapboxLogoCard = "true"; - cards.appendChild(IModelApp.makeLogoCard({ heading: "Mapbox", notice: IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.MapBoxCopyright") })); - } + // eslint-disable-next-line @typescript-eslint/no-deprecated + return Promise.resolve(this.addLogoCards(cards)); } // no initialization needed for MapBoxImageryProvider. diff --git a/core/frontend/src/tile/map/MapLayerImageryProvider.ts b/core/frontend/src/tile/map/MapLayerImageryProvider.ts index ed81cdfba65f..18889d5889d2 100644 --- a/core/frontend/src/tile/map/MapLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/MapLayerImageryProvider.ts @@ -141,7 +141,10 @@ export abstract class MapLayerImageryProvider { * @param _viewport Viewport to add logo cards to. * @beta */ - public async addAttributions(_cards: HTMLTableElement, _viewport: ScreenViewport): Promise { } + public async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + // eslint-disable-next-line @typescript-eslint/no-deprecated + return Promise.resolve(this.addLogoCards(cards, vp)); + } /** @internal */ protected _missingTileData?: Uint8Array; diff --git a/core/frontend/src/tile/map/TerrainMeshProvider.ts b/core/frontend/src/tile/map/TerrainMeshProvider.ts index 3e5464def47f..c5656572735e 100644 --- a/core/frontend/src/tile/map/TerrainMeshProvider.ts +++ b/core/frontend/src/tile/map/TerrainMeshProvider.ts @@ -96,7 +96,10 @@ export abstract class TerrainMeshProvider { * For example, a provider that produces meshes from [Bing Maps](https://docs.microsoft.com/en-us/bingmaps/rest-services/elevations/) would be required to * disclose any copyrighted data used in the production of those meshes. */ - public async addAttributions(_cards: HTMLTableElement, _vp: ScreenViewport): Promise {} + public async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + // eslint-disable-next-line @typescript-eslint/no-deprecated + return Promise.resolve(this.addLogoCards(cards, vp)); + } /** Return whether terrain data can be obtained for the [[MapTile]] specified by `quadId`. If it returns false, a terrain mesh will instead be produced for * that tile by up-sampling the terrain mesh provided by its parent tile. From 51f9e586d3b6258147f80514efab333daa372648 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Tue, 4 Feb 2025 15:49:59 -0500 Subject: [PATCH 10/44] lint error --- .../tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts b/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts index 4536967259d1..598a5a87cf82 100644 --- a/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/ImageryProviders/ArcGISMapLayerImageryProvider.ts @@ -325,7 +325,7 @@ export class ArcGISMapLayerImageryProvider extends ArcGISImageryProvider { } } - public override async addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise { + public override async addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise { // eslint-disable-next-line @typescript-eslint/no-deprecated return Promise.resolve(this.addLogoCards(cards)); } From ae31ddffa0b037e888f637d86ce7bcdfa4e78182 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Tue, 4 Feb 2025 15:50:26 -0500 Subject: [PATCH 11/44] Get decorations for map tile ref... --- core/frontend/src/Viewport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frontend/src/Viewport.ts b/core/frontend/src/Viewport.ts index 754a9bbdec9a..7d18eb36b729 100644 --- a/core/frontend/src/Viewport.ts +++ b/core/frontend/src/Viewport.ts @@ -3435,7 +3435,7 @@ export class ScreenViewport extends Viewport { this._decorationCache.prohibitRemoval = true; context.addFromDecorator(this.view); - for (const ref of this.tiledGraphicsProviderRefs()) { + for (const ref of this.getTileTreeRefs()) { context.addFromDecorator(ref); } From 5897c018c15b3ee170a3d19ff0c7e08f3af9a1c7 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 5 Feb 2025 11:05:13 -0500 Subject: [PATCH 12/44] Cleanup decorator code --- .../src/GoogleMaps/GoogleMapDecorator.ts | 91 ++++++++++++------- 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts index 6b1efda01f11..fef9952540d6 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts @@ -1,15 +1,45 @@ -import { CanvasDecoration, DecorateContext, Decorator, IconSprites, IModelApp, ScreenViewport, Sprite } from "@itwin/core-frontend"; +import { CanvasDecoration, DecorateContext, Decorator, IconSprites, IModelApp, Sprite } from "@itwin/core-frontend"; import { Point3d } from "@itwin/core-geometry"; import { MapTypesType } from "./GoogleMaps"; -// Similar to 'SprintLocation' but uses a viewport pixel position instead of world position -class ImagePixelLocationDecoration implements CanvasDecoration { - private _viewport?: ScreenViewport; + +/** A simple decorator that show logo at the a given screen position. + * @internal + */ +class LogoDecoration implements CanvasDecoration { private _sprite?: Sprite; - private _alpha?: number; + + /** The current position of the logo in view coordinates. */ public readonly position = new Point3d(); - // public get isActive(): boolean { return this._viewport !== undefined; } - private _isSpriteLoaded = false; + + private _offset: Point3d|undefined; + + public set offset(offset: Point3d|undefined) { + this._offset = offset; + } + + /** The logo offset in view coordinates.*/ + public get offset() { + return this._offset; + } + + /** Move the logo to the lower left corner of the screen. */ + public moveToLowerLeftCorner(context: DecorateContext) : boolean{ + if (!this._sprite || !this._sprite.isLoaded) + return false; + + this.position.x = this._offset?.x ?? 0; + this.position.y = context.viewport.parentDiv.clientHeight - this._sprite.size.y; + if (this._offset?.y) + this.position.y -= this._offset.y; + return true; + } + + /* TODO: Add other move methods as needed */ + + /** Indicate if the logo is loaded and ready to be drawn. */ + public get isLoaded() { return this._sprite?.isLoaded ?? false; } + public async activate(sprite: Sprite): Promise { this._sprite = sprite; return new Promise((resolve, _reject) => { @@ -19,50 +49,47 @@ class ImagePixelLocationDecoration implements CanvasDecoration { resolve (false); }); }); - - - } - - public deactivate() { - } /** Draw this sprite onto the supplied canvas. * @see [[CanvasDecoration.drawDecoration]] */ public drawDecoration(ctx: CanvasRenderingContext2D): void { - const sprite = this._sprite!; - if (undefined === sprite.image) - return; - - if (undefined !== this._alpha) - ctx.globalAlpha = this._alpha; - - ctx.drawImage(sprite.image, -sprite.offset.x, -sprite.offset.y); + if (this.isLoaded) { + // Draw image with an origin at the top left corner + ctx.drawImage(this._sprite!.image!, 0, 0); + } } - /** If this SpriteLocation is active and the supplied DecorateContext is for its Viewport, add the Sprite to decorations. */ public decorate(context: DecorateContext) { - this._viewport = context.viewport; - const vpHeight = context.viewport.parentDiv.clientHeight; - this.position.setFrom({x: 120, y: vpHeight - 25, z: 0}); context.addCanvasDecoration(this); } } +/** A decorator that adds the Google Maps logo to the lower left corner of the screen. + * @internal +*/ export class GoogleMapsDecorator implements Decorator { - public readonly logo = new ImagePixelLocationDecoration(); - private _sprite: Sprite|undefined; - public constructor() { - } + public readonly logo = new LogoDecoration(); + /** Activate the logo with the given map type. */ public async activate(mapType: MapTypesType): Promise { - const imageName = mapType === "roadmap" ? "google_on_white_hdpi" : "google_on_non_white_hdpi"; - this._sprite = IconSprites.getSpriteFromUrl(`${IModelApp.publicPath}images/${imageName}.png`); - return this.logo.activate(this._sprite); + // Pick the logo that is the most visible on the background map + const imageName = mapType === "roadmap" ? + "google_on_white_hdpi" : + "google_on_non_white_hdpi"; + + // We need to move the logo right after the 'i.js' button + this.logo.offset = new Point3d(45, 5); + + return this.logo.activate(IconSprites.getSpriteFromUrl(`${IModelApp.publicPath}images/${imageName}.png`)); }; + /** Decorate implementation */ public decorate = (context: DecorateContext) => { + if (!this.logo.isLoaded) + return; + this.logo.moveToLowerLeftCorner(context); this.logo.decorate(context); }; } From f87cedd7b4378ad9c3826eb018465cbda235a817 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Thu, 6 Feb 2025 07:55:59 -0500 Subject: [PATCH 13/44] Got rid of some `GoogleMaps` prefix --- .../src/GoogleMaps/GoogleMapDecorator.ts | 2 +- .../src/GoogleMaps/GoogleMaps.ts | 29 +++++++++---------- .../GoogleMaps/GoogleMapsImageryProvider.ts | 7 ++--- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts index fef9952540d6..901bd01294c1 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts @@ -72,7 +72,7 @@ class LogoDecoration implements CanvasDecoration { export class GoogleMapsDecorator implements Decorator { public readonly logo = new LogoDecoration(); - /** Activate the logo with the given map type. */ + /** Activate the logo based on the given map type. */ public async activate(mapType: MapTypesType): Promise { // Pick the logo that is the most visible on the background map const imageName = mapType === "roadmap" ? diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index 8cdf867f78b8..f181bb33778c 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -13,7 +13,7 @@ export type ImageFormatsType = "jpeg" | "png"; /** * Represents the options to create a Google Maps session. */ -export interface CreateGoogleMapsSessionOptions { +export interface CreateSessionOptions { /** * The type of base map. * @@ -76,7 +76,7 @@ export interface GoogleMapsSession { * Represents the maximum zoom level available within a bounding rectangle. * @beta */ -export interface GoogleMapsMaxZoomRect { +export interface MaxZoomRectangle { maxZoom: number; north: number; south: number; @@ -88,12 +88,12 @@ export interface GoogleMapsMaxZoomRect { * Indicate which areas of given viewport have imagery, and at which zoom levels. * @beta */ -export interface GoogleMapsViewportInfo { +export interface ViewportInfo { /** Attribution string that you must display on your map when you display roadmap and satellite tiles. */ copyright: string; /** Array of bounding rectangles that overlap with the current viewport. Also contains the maximum zoom level available within each rectangle.. */ - maxZoomRects: GoogleMapsMaxZoomRect[]; + maxZoomRects: MaxZoomRectangle[]; } /** @@ -123,12 +123,12 @@ export const GoogleMaps = { * @param opts Options to create the session * @beta */ - createSession: async (apiKey: string, opts: CreateGoogleMapsSessionOptions): Promise => { + createSession: async (apiKey: string, opts: CreateSessionOptions): Promise => { const url = `https://tile.googleapis.com/v1/createSession?key=${apiKey}`; const request = new Request(url, {method: "POST", body: JSON.stringify(opts)}); const response = await fetch (request); if (!response.ok) { - throw new Error(`CreateSession request failed: ${response.status} - ${response.statusText}`); + throw new Error(`CreateSession request failed: ${response.status} - ${response.statusText}`); } Logger.logInfo(loggerCategory, `Session created successfully`); return response.json(); @@ -139,18 +139,17 @@ export const GoogleMaps = { * @param opts Options to create the session * @beta */ - createPropertiesFromSessionOptions: (opts: CreateGoogleMapsSessionOptions): MapLayerProviderProperties => { + createPropertiesFromSessionOptions: (opts: CreateSessionOptions): MapLayerProviderProperties => { const properties: MapLayerProviderProperties = { mapType: opts.mapType, - language: opts.mapType, + language: opts.language, + region: opts.region, } if (opts.layerTypes !== undefined) { properties.layerTypes = opts.layerTypes; } - if (opts.region !== undefined) { - properties.region = opts.region; - } + return properties }, @@ -160,7 +159,7 @@ export const GoogleMaps = { * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) * @beta */ - createMapLayerSettings: (name?: string, opts?: CreateGoogleMapsSessionOptions): ImageMapLayerSettings => { + createMapLayerSettings: (name?: string, opts?: CreateSessionOptions): ImageMapLayerSettings => { return ImageMapLayerSettings.fromJSON(GoogleMaps.createMapLayerProps(name, opts)); }, @@ -170,7 +169,7 @@ export const GoogleMaps = { * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) * @beta */ - createMapLayerProps: (name: string = "GoogleMaps", opts?: CreateGoogleMapsSessionOptions): ImageMapLayerProps => { + createMapLayerProps: (name: string = "GoogleMaps", opts?: CreateSessionOptions): ImageMapLayerProps => { if (!IModelApp.mapLayerFormatRegistry.isRegistered(GoogleMapsMapLayerFormat.formatId)) { IModelApp.mapLayerFormatRegistry.register(GoogleMapsMapLayerFormat); } @@ -188,7 +187,7 @@ export const GoogleMaps = { * @returns The maximum zoom level available within the bounding rectangle. * @beta */ - getViewportInfo: async (params: ViewportInfoRequestParams): Promise=> { + getViewportInfo: async (params: ViewportInfoRequestParams): Promise=> { const {rectangle, session, key, zoom} = params; const north = Angle.radiansToDegrees(rectangle.north); const south = Angle.radiansToDegrees(rectangle.south); @@ -201,6 +200,6 @@ export const GoogleMaps = { return undefined; } const json = await response.json(); - return json as GoogleMapsViewportInfo;; + return json as ViewportInfo;; }, }; diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 5d33bb883936..a7f05236d3bc 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -1,6 +1,6 @@ import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile, TileUrlImageryProvider } from "@itwin/core-frontend"; -import { CreateGoogleMapsSessionOptions, GoogleMaps, GoogleMapsSession, LayerTypesType, MapTypesType } from "./GoogleMaps"; +import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, LayerTypesType, MapTypesType } from "./GoogleMaps"; import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; import { GoogleMapsDecorator } from "./GoogleMapDecorator"; const loggerCategory = "MapLayersFormats.GoogleMaps"; @@ -69,7 +69,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { } } - private createCreateSessionOptions(): CreateGoogleMapsSessionOptions { + private createCreateSessionOptions(): CreateSessionOptions { const layerPropertyKeys = this._settings.properties ? Object.keys(this._settings.properties) : undefined; if (layerPropertyKeys === undefined || !layerPropertyKeys.includes("mapType") || @@ -80,8 +80,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { throw new BentleyError(BentleyStatus.ERROR, msg); } - const createSessionOptions: CreateGoogleMapsSessionOptions = { - + const createSessionOptions: CreateSessionOptions = { mapType: this._settings.properties!.mapType as MapTypesType, region: this._settings.properties!.region as string, language: this._settings.properties!.language as string, From ae93c2e2e786f069356d4b34cda2e1a3443c26ff Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Mon, 10 Feb 2025 10:33:18 -0500 Subject: [PATCH 14/44] Added tests. --- .../src/GoogleMaps/GoogleMapDecorator.ts | 2 +- .../GoogleMaps/GoogleMapsImageryProvider.ts | 10 +- .../src/test/GoogleMaps/GoogleMaps.test.ts | 220 ++++++++++++++++++ .../map-layers-formats/src/test/TestUtils.ts | 45 ++++ 4 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts create mode 100644 extensions/map-layers-formats/src/test/TestUtils.ts diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts index 901bd01294c1..c0c9c7e22d52 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts @@ -6,7 +6,7 @@ import { MapTypesType } from "./GoogleMaps"; /** A simple decorator that show logo at the a given screen position. * @internal */ -class LogoDecoration implements CanvasDecoration { +export class LogoDecoration implements CanvasDecoration { private _sprite?: Sprite; /** The current position of the logo in view coordinates. */ diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index a7f05236d3bc..5cddd0d496f2 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -109,10 +109,10 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { const zooms = new Set(); const matchingAttributions: string[] = []; - // Viewport info requests must be made for a specific zoom level + // Viewport info requests must be made for a specific zoom level tiles.forEach((tile) => zooms.add(tile.depth)); - for (const zoom of zooms) { + for (const zoom of zooms) { let cartoRect: MapCartoRectangle|undefined; for (const tile of tiles) { if (tile.depth === zoom && tile instanceof MapTile) { @@ -197,9 +197,13 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { this._decorator.decorate(context); } + private getSelectedTiles(vp: ScreenViewport) { + return IModelApp.tileAdmin.getTilesForUser(vp)?.selected + } + public override async addAttributions (cards: HTMLTableElement, vp: ScreenViewport): Promise { let copyrightMsg = ""; - const tiles = IModelApp.tileAdmin.getTilesForUser(vp)?.selected; + const tiles = this.getSelectedTiles(vp); if (tiles) { try { const attrList = await this.fetchAttributions(tiles); diff --git a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts new file mode 100644 index 000000000000..dcddc9784d68 --- /dev/null +++ b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts @@ -0,0 +1,220 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ + +import * as sinon from "sinon"; +import { Frustum, ImageMapLayerSettings, MapLayerProviderProperties } from "@itwin/core-common"; +import { expect } from "chai"; +import { GoogleMapsImageryProvider } from "../../GoogleMaps/GoogleMapsImageryProvider"; +import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; +import { fakeJsonFetch } from "../TestUtils"; +import { LogoDecoration } from "../../GoogleMaps/GoogleMapDecorator"; +import { DecorateContext, Decorations, IconSprites, IModelApp, MapCartoRectangle, MapTile, MapTileTree, QuadId, ScreenViewport, Sprite, TilePatch } from "@itwin/core-frontend"; +import { Range3d } from "@itwin/core-geometry"; + +class FakeMapTile extends MapTile { + public override depth: number; + constructor(contentId: string) { + super({contentId, range:Range3d.createNull(), maximumSize: 256}, + {} as MapTileTree, + QuadId.createFromContentId(contentId), + {} as TilePatch, + MapCartoRectangle.createXY(0, 0), + undefined, + []); + this.depth = this.quadId.level; + } +} + +const getTestSettings = (properties?: MapLayerProviderProperties) => { + return ImageMapLayerSettings.fromJSON({ + name: "test", + url: "", + formatId: "GoogleMaps", + properties + }); +}; + +const createProvider = (settings: ImageMapLayerSettings) => { + settings.accessKey = {key: "key", value: "dummyKey"}; + return new GoogleMapsImageryProvider(settings); +} + +const stubCreateSession = (sandbox:sinon.SinonSandbox, session: GoogleMapsSession) => sandbox.stub(GoogleMaps, "createSession").callsFake(async function _(_apiKey: string, _opts: CreateSessionOptions) { + return session; +}); + +const minCreateSessionOptions: CreateSessionOptions = {mapType: "satellite", language: "en-US", region: "US"} +const createSessionOptions2: CreateSessionOptions = {...minCreateSessionOptions, layerTypes: ["layerRoadmap"]}; + +const defaultPngSession = {tileWidth: 256, tileHeight: 256, imageFormat: "image/png", expiry: 0, session: "dummySession"}; +describe.only("GoogleMapsProvider", () => { + const sandbox = sinon.createSandbox(); + + beforeEach(async () => { + sandbox.stub(LogoDecoration.prototype, "activate").callsFake(async function _(_sprite: Sprite) { + return Promise.resolve(true); + }); + }); + + afterEach(async () => { + sandbox.restore(); + }); + + + it ("Provider properties round-trips through JSON", async () => { + const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(createSessionOptions2)); + const json = settings.toJSON(); + const deserializedSettings = ImageMapLayerSettings.fromJSON(json); + expect(deserializedSettings.properties).to.deep.eq(settings.properties); + }); + + it("should not initialize with no properties provided", async () => { + const settings = getTestSettings(); + const createSessionSub = stubCreateSession(sandbox, defaultPngSession); + const provider = createProvider(settings); + await expect(provider.initialize()).to.be.rejectedWith("Missing session options"); + expect(createSessionSub.called).to.be.false; + }); + + it("should initialize with required properties", async () => { + + fakeJsonFetch(sandbox, defaultPngSession); + const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(minCreateSessionOptions)); + + const createSessionSub = stubCreateSession(sandbox, defaultPngSession); + const provider = createProvider(settings); + + await expect(provider.initialize()).to.be.fulfilled; + expect(createSessionSub.called).to.be.true; + expect(createSessionSub.firstCall.args[1]).to.deep.eq(minCreateSessionOptions); + }); + + + it("should initialize with properties", async () => { + + fakeJsonFetch(sandbox, defaultPngSession); + const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(createSessionOptions2)); + + const createSessionSub = stubCreateSession(sandbox, defaultPngSession); + const provider = createProvider(settings); + + await expect(provider.initialize()).to.be.fulfilled; + expect(createSessionSub.called).to.be.true; + expect(createSessionSub.firstCall.args[1]).to.deep.eq(createSessionOptions2); + expect(provider.tileSize).to.eq(256); + expect(settings.unsavedQueryParams).to.deep.eq({session: "dummySession"}); + }); + + it("should create proper tile url", async () => { + + fakeJsonFetch(sandbox, defaultPngSession); + const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(createSessionOptions2)); + + const makeTileRequestStub = sandbox.stub(GoogleMapsImageryProvider.prototype, "makeTileRequest").callsFake(async function _(_url: string, _timeoutMs?: number ) { + const obj = { + headers: { "content-type": "image/jpeg" }, + arrayBuffer: async () => { + return Promise.resolve(new Uint8Array(100)); + }, + status: 200, + } as unknown; // By using unknown type, I can define parts of Response I really need + return (obj as Response); + }); + + stubCreateSession(sandbox, defaultPngSession); + const provider = createProvider(settings); + + await provider.initialize(); + await provider.loadTile(49592, 37981, 17); + expect(makeTileRequestStub.called).to.be.true; + expect(makeTileRequestStub.firstCall.args[0]).to.eq("https://tile.googleapis.com/v1/2dtiles/17/37981/49592?key=dummyKey&session=dummySession"); + }); + + + it("should add attributions", async () => { + + fakeJsonFetch(sandbox, defaultPngSession); + const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(createSessionOptions2)); + + sandbox.stub(GoogleMapsImageryProvider.prototype as any, "getSelectedTiles").callsFake(function _(_vp: unknown) { + const set = new Set(); + set.add(new FakeMapTile("17_37981_49592")); + return set; + }); + + const getViewportInfoStub = sandbox.stub(GoogleMaps, "getViewportInfo").callsFake(async function _(_params: ViewportInfoRequestParams) { + return {copyright: "fake copyright", maxZoomRects: []}; + }); + + sinon.stub(IModelApp, 'publicPath').get(() => 'public/'); + + const provider = createProvider(settings); + + await provider.initialize(); + const table = document.createElement('table'); + await provider.addAttributions(table, {} as ScreenViewport); + + expect(getViewportInfoStub.called).to.be.true; + // Important : When satellite base layer is used, the logo should be white + expect(table.innerHTML).to.includes(``); + expect(table.innerHTML).to.includes(`

fake copyright

`); + + // Now re-do the test with roadmap base layer + const settings2 = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions({mapType: "roadmap", language: "en-US", region: "US"})); + const provider2 = createProvider(settings2); + await provider2.initialize(); + const table2 = document.createElement('table'); + await provider2.addAttributions(table2, {} as ScreenViewport); + expect(table2.innerHTML).to.includes(``); + expect(table2.innerHTML).to.includes(`

fake copyright

`); + }); + + + it("logo should be activated with the 'on non-white' logo", async () => { + + fakeJsonFetch(sandbox, defaultPngSession); + const getSpriteStub = sandbox.stub(IconSprites, "getSpriteFromUrl").callsFake(function _(_url: string) { + return {} as Sprite; + }); + const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions({mapType: "satellite", language: "en-US", region: "US"})); + const provider = createProvider(settings); + await provider.initialize(); + + expect(getSpriteStub.firstCall.args[0]).to.eq("public/images/google_on_non_white_hdpi.png"); + }); + + it("logo should be activated with the 'on white' logo", async () => { + + fakeJsonFetch(sandbox, defaultPngSession); + const getSpriteStub = sandbox.stub(IconSprites, "getSpriteFromUrl").callsFake(function _(_url: string) { + return {} as Sprite; + }); + const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions({mapType: "roadmap", language: "en-US", region: "US"})); + const provider = createProvider(settings); + await provider.initialize(); + + expect(getSpriteStub.firstCall.args[0]).to.eq("public/images/google_on_white_hdpi.png"); + }); + + + it("should decorate", async () => { + + fakeJsonFetch(sandbox, defaultPngSession); + const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(minCreateSessionOptions)); + + const provider = createProvider(settings); + + await provider.initialize(); + + const addCanvasDecorationStub = sinon.stub(DecorateContext.prototype, "addCanvasDecoration"); + sinon.stub(LogoDecoration.prototype, "isLoaded").get(() => true); + const context = DecorateContext.create({ viewport: {getFrustum: ()=>new Frustum()} as ScreenViewport, output: new Decorations() }); + + provider.decorate(context); + + expect(addCanvasDecorationStub.called).to.be.true; + }); + +}); diff --git a/extensions/map-layers-formats/src/test/TestUtils.ts b/extensions/map-layers-formats/src/test/TestUtils.ts new file mode 100644 index 000000000000..4636f53d5d8d --- /dev/null +++ b/extensions/map-layers-formats/src/test/TestUtils.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +import { ByteStream } from "@itwin/core-bentley"; + +export const createFakeTileResponse = (contentType: string, data?: Uint8Array) => { + const test = { + headers: new Headers( { "content-type" : contentType}), + arrayBuffer: async () => { + return Promise.resolve(data ? ByteStream.fromUint8Array(data).arrayBuffer : undefined); + }, + status: 200, + } as unknown; // By using unknown type, I can define parts of Response I really need + return (test as Response ); +}; + +export const fakeTextFetch = (sandbox: sinon.SinonSandbox, text: string) => { + return sandbox.stub(globalThis, "fetch").callsFake(async function (_input: RequestInfo | URL, _init?: RequestInit) { + return Promise.resolve((({ + text: async () => text, + ok: true, + status: 200, + } as unknown) as Response)); + }); +}; + +export const fakeJsonFetch = (sandbox: sinon.SinonSandbox, data: any) => { + return sandbox.stub(globalThis, "fetch").callsFake(async function (_input: RequestInfo | URL, _init?: RequestInit) { + return Promise.resolve((({ + json: async () => data, + ok: true, + status: 200, + } as unknown) as Response)); + }); +}; + + +export const indexedArrayFromUrlParams = (urlParams: URLSearchParams): {[key: string]: string} => { + const array: {[key: string]: string} = {}; + urlParams.forEach((value: string, key: string) => { + array[key] = value; + }); + return array; +}; From c6490dda2980218ac02d0ae6d54f5092b17dc25f Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Mon, 10 Feb 2025 14:50:41 -0500 Subject: [PATCH 15/44] Make sure we deep copy provider properties. --- extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index f181bb33778c..240bf66f64a5 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -147,7 +147,7 @@ export const GoogleMaps = { } if (opts.layerTypes !== undefined) { - properties.layerTypes = opts.layerTypes; + properties.layerTypes = [...opts.layerTypes]; } return properties From d9af31c96fe9936b9f894e7538bc8677bdd8ed66 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Tue, 11 Feb 2025 10:35:43 -0500 Subject: [PATCH 16/44] Added Google maps ui to display-tes-app --- .../src/common/DtaConfiguration.ts | 4 + .../src/frontend/DisplayTestApp.ts | 3 +- .../src/frontend/GoogleMaps.ts | 308 ++++++++++++++++++ .../display-test-app/src/frontend/Surface.ts | 9 +- .../display-test-app/src/frontend/Viewer.ts | 20 ++ 5 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 test-apps/display-test-app/src/frontend/GoogleMaps.ts diff --git a/test-apps/display-test-app/src/common/DtaConfiguration.ts b/test-apps/display-test-app/src/common/DtaConfiguration.ts index 39a2a3ac4ad9..cee7b49ef130 100644 --- a/test-apps/display-test-app/src/common/DtaConfiguration.ts +++ b/test-apps/display-test-app/src/common/DtaConfiguration.ts @@ -56,6 +56,7 @@ export interface DtaStringConfiguration { oidcScope?: string; // default is undefined, used for auth setup oidcRedirectUri?: string; // default is undefined, used for auth setup frontendTilesUrlTemplate?: string; // if set, specifies url for @itwin/frontend-tiles to obtain tile trees for spatial views. See README.md + googleMapsUi?: boolean; // if set, a Google Maps toolbar icon will be displayed in the UI } export interface DtaNumberConfiguration { @@ -157,6 +158,9 @@ export const getConfig = (): DtaConfiguration => { if (undefined !== process.env.IMJS_WINDOW_SIZE) configuration.windowSize = process.env.IMJS_WINDOW_SIZE; + if (undefined !== process.env.IMJS_GOOGLEMAPS_UI) + configuration.googleMapsUi = !!process.env.IMJS_GOOGLEMAPS_UI; + configuration.devTools = undefined === process.env.IMJS_NO_DEV_TOOLS; configuration.cacheTileMetadata = undefined !== process.env.IMJS_CACHE_TILE_METADATA; configuration.useProjectExtents = undefined === process.env.IMJS_NO_USE_PROJECT_EXTENTS; diff --git a/test-apps/display-test-app/src/frontend/DisplayTestApp.ts b/test-apps/display-test-app/src/frontend/DisplayTestApp.ts index 7a5fc8b7552f..1fc1dec33a37 100644 --- a/test-apps/display-test-app/src/frontend/DisplayTestApp.ts +++ b/test-apps/display-test-app/src/frontend/DisplayTestApp.ts @@ -323,7 +323,7 @@ async function initView(iModel: IModelConnection | undefined) { input: document.getElementById("browserFileSelector") as HTMLInputElement, } : undefined; - DisplayTestApp.surface = new Surface(document.getElementById("app-surface")!, document.getElementById("toolBar")!, fileSelector, configuration.openReadWrite ?? false); + DisplayTestApp.surface = new Surface(configuration, document.getElementById("app-surface")!, document.getElementById("toolBar")!, fileSelector, configuration.openReadWrite ?? false); // We need layout to complete so that the div we want to stick our viewport into has non-zero dimensions. // Consistently reproducible for some folks, not others... @@ -334,6 +334,7 @@ async function initView(iModel: IModelConnection | undefined) { iModel, defaultViewName: configuration.viewName, disableEdges: true === configuration.disableEdges, + configuration }); viewer.dock(Dock.Full); diff --git a/test-apps/display-test-app/src/frontend/GoogleMaps.ts b/test-apps/display-test-app/src/frontend/GoogleMaps.ts new file mode 100644 index 000000000000..bd78baf479b1 --- /dev/null +++ b/test-apps/display-test-app/src/frontend/GoogleMaps.ts @@ -0,0 +1,308 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ + +import { assert, dispose } from "@itwin/core-bentley"; +import { + ComboBox, ComboBoxEntry, createButton, createCheckBox, createComboBox, + createTextBox + + , +} from "@itwin/frontend-devtools"; +import { BackgroundMapType, BaseMapLayerSettings, ContourGroupProps, ImageMapLayerSettings } from "@itwin/core-common"; +import { Viewport } from "@itwin/core-frontend"; +import { ToolBarDropDown } from "./ToolBar"; +import { CreateSessionOptions, GoogleMaps, LayerTypes, MapTypes, ScaleFactors } from "@itwin/map-layers-formats"; + +// size of widget or panel +const winSize = { top: 0, left: 0, width: 318, height: 300 }; + +export class GoogleMapsSettings implements Disposable { + private readonly _vp: Viewport; + private readonly _parent: HTMLElement; + private readonly _element: HTMLElement; + private _currentTerrainProps: ContourGroupProps = {}; + private _enabled: boolean = false; + private _overlay: boolean = false; + private _mapTypesCombobox: ComboBox; + private _mapType: MapTypes; + private _scaleFactor: ScaleFactors = "scaleFactor1x"; + private _layerTypes: LayerTypes[] = []; + private _lang = "en-US"; + + + public constructor(vp: Viewport, parent: HTMLElement) { + this._currentTerrainProps.contourDef = {}; + this._currentTerrainProps.subCategories = ""; + + this._vp = vp; + this._parent = parent; + this._mapType = "satellite"; + this._enabled = false; + + this._element = document.createElement("div"); + this._element.className = "toolMenu"; + this._element.style.display = "block"; + this._element.style.overflowX = "none"; + this._element.style.overflowY = "none"; + const width = winSize.width * 0.98; + this._element.style.width = `${width}px`; + const firstLayer = this._vp.displayStyle.getMapLayers(false).length === 1 && this._vp.displayStyle.getMapLayers(false)[0] ? this._vp.displayStyle.getMapLayers(false)[0] : undefined; + const googleLayer = firstLayer instanceof ImageMapLayerSettings && firstLayer.formatId === "GoogleMaps" ? firstLayer : undefined; + + const isGoogleBase = vp.displayStyle.backgroundMapBase instanceof BaseMapLayerSettings && vp.displayStyle.backgroundMapBase.formatId === "GoogleMaps"; + this._enabled = isGoogleBase || googleLayer !== undefined; + this._overlay = googleLayer !== undefined; + if (isGoogleBase) { + const baseSettings = vp.displayStyle.backgroundMapBase as BaseMapLayerSettings; + const properties = baseSettings.properties; + this._mapType = (properties?.mapType??"") as MapTypes; + + this._layerTypes = (properties?.layerTypes ?? []) as LayerTypes[]; + this._lang = (properties?.language??"") as string; + + if (properties?.scale) + this._scaleFactor = properties.scale as ScaleFactors; + } + + + createCheckBox({ + parent: this._element, + name: "Enable google maps", + id: "cbx_toggle_google_maps", + isChecked: this._enabled, + handler: (checkbox) => { + assert(this._vp.view.is3d()); + this._enabled = checkbox.checked + this.sync(); + }, + }); + this._element.appendChild(document.createElement("br")); + + //////////////// + // Map types + const mapTypes: ComboBoxEntry[] = [ + { name: "roadmap", value: "roadmap" }, + { name: "satellite", value: "satellite" }, + { name: "terrain", value: "terrain" }, + ]; + this._mapTypesCombobox = createComboBox({ + parent: this._element, + name: "map type: ", + entries: mapTypes, + id: "google_map_type_cbx", + value: this._mapType, + handler: (cbx) => { + this._mapType = cbx.value as MapTypes; + }, + }); + this._element.appendChild(document.createElement("br")); + + + //////////////// + // Layer types + const layerTypesDiv = document.createElement("div"); + const layerTypesLabel = document.createElement("label"); + layerTypesLabel.innerText = "Layer types:"; + layerTypesLabel.style.fontWeight = "bold"; + layerTypesLabel.style.display = "inline"; + layerTypesDiv.appendChild(layerTypesLabel); + + createCheckBox({ + parent: layerTypesDiv, + name: "Roadmap", + id: "google_layertype_roadmap", + isChecked: this._layerTypes.includes("layerRoadmap"), + handler: (cb) => { + if (cb.checked) { + this._layerTypes.push("layerRoadmap"); + } else { + this._layerTypes = this._layerTypes.filter((layerType) => layerType !== "layerRoadmap"); + } + }, + }); + + createCheckBox({ + parent: layerTypesDiv, + name: "Traffic", + id: " google_layertype_traffic", + isChecked: this._layerTypes.includes("layerTraffic"), + handler: (cb) => { + if (cb.checked) { + this._layerTypes.push("layerTraffic"); + } else { + this._layerTypes = this._layerTypes.filter((layerType) => layerType !== "layerTraffic"); + } + }, + }); + + createCheckBox({ + parent: layerTypesDiv, + name: "Streetview", + id: "google_layertype_streetview", + isChecked: this._layerTypes.includes("layerStreetview"), + handler: (cb) => { + if (cb.checked) { + this._layerTypes.push("layerStreetview"); + } else { + this._layerTypes = this._layerTypes.filter((layerType) => layerType !== "layerStreetview"); + } + }, + }); + this._element.appendChild(layerTypesDiv); + this._element.appendChild(document.createElement("br")); + + ///////////////// + // Language + const langDiv = document.createElement("div"); + const langLabel = document.createElement("label"); + langLabel.innerText = "Language:"; + langLabel.style.fontWeight = "bold"; + langLabel.style.display = "inline"; + langDiv.appendChild(langLabel); + const langTbox = createTextBox({ + id: "txt_lang", + parent: langDiv, + tooltip: "lang", + handler: (tb) => {this._lang = tb.value.trim()}, + }); + langTbox.textbox.style.display = "inline"; + langTbox.textbox.value = this._lang; + this._element.appendChild(langDiv); + this._element.appendChild(document.createElement("br")); + + + + //////////////// + // Scale factors + const scaleFactors: ComboBoxEntry[] = [ + { name: "1x", value: "scaleFactor1x" }, + { name: "2x", value: "scaleFactor2x" }, + { name: "4x", value: "scaleFactor4x" }, + ]; + createComboBox({ + parent: this._element, + name: "scale factor: ", + entries: scaleFactors, + id: "google_scale_factors_cbx", + value: this._scaleFactor, + handler: (cbx) => { + this._scaleFactor = cbx.value as ScaleFactors; + }, + }); + this._element.appendChild(document.createElement("br")); + + createCheckBox({ + parent: this._element, + name: "Overlay mode", + id: "cbx_toggle_overlay", + isChecked: this._overlay, + handler: (checkbox) => { + this._overlay = checkbox.checked; + this.sync(); + }, + }); + this._element.appendChild(document.createElement("br")); + + + /////////// + // Buttons + this._mapTypesCombobox.label!.style.fontWeight = "bold"; + const buttonDiv = document.createElement("div"); + buttonDiv.style.textAlign = "center"; + createButton({ + value: "Apply", + handler: () => { this.apply(); }, + parent: buttonDiv, + inline: true, + tooltip: "Apply contour settings for this definition", + }); + + this._element.appendChild(buttonDiv); + this._element.appendChild(document.createElement("br")); + parent.appendChild(this._element); + + } + + public [Symbol.dispose](): void { + this._parent.removeChild(this._element); + } + + private sync(): void { + + this._vp.synchWithView(); + } + + private apply() { + const removeExistingMapLayer = (isOverlay: boolean) => { + const mapLayers = this._vp.displayStyle.getMapLayers(isOverlay); + if (mapLayers.length > 0) + this._vp.displayStyle.detachMapLayerByIndex({index: 0, isOverlay}); + } + if (!this._enabled) { + removeExistingMapLayer(false); + this._vp.view.displayStyle.changeBackgroundMapProvider({ name: "BingProvider", type: BackgroundMapType.Hybrid }); + return; + } + + + + if (this._overlay) { + this._vp.displayStyle.backgroundMapBase = BaseMapLayerSettings.fromJSON({ + formatId: "ArcGIS", + url: "https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer", + name: "ESRI World Imagery" + }); + const opts: CreateSessionOptions = { + mapType: "satellite", + language: "en-US", + region: "US", + overlay: true, + layerTypes: this._layerTypes, + }; + removeExistingMapLayer(false); + this._vp.displayStyle.attachMapLayer({mapLayerIndex: {index: 0, isOverlay: false}, settings: GoogleMaps.createMapLayerSettings("GoogleMaps", opts)}); + } else { + removeExistingMapLayer(false); + const opts: CreateSessionOptions = { + mapType: this._mapType, + layerTypes: this._layerTypes, + language: this._lang, + region: "US", + scale: this._scaleFactor, + }; + try { + this._vp.displayStyle.backgroundMapBase = BaseMapLayerSettings.fromJSON(GoogleMaps.createMapLayerProps("GoogleMaps", opts)); + } catch (e: any) { + // eslint-disable-next-line no-console + console.log(e.message); + } + + } + + this.sync(); + } + +} + +export class GoogleMapsPanel extends ToolBarDropDown { + private readonly _vp: Viewport; + private readonly _parent: HTMLElement; + private _settings?: GoogleMapsSettings; + + public constructor(vp: Viewport, parent: HTMLElement) { + super(); + this._vp = vp; + this._parent = parent; + this.open(); + } + + public override get onViewChanged(): Promise { + return Promise.resolve(); + } + + protected _open(): void { this._settings = new GoogleMapsSettings(this._vp, this._parent); } + protected _close(): void { this._settings = dispose(this._settings); } + public get isOpen(): boolean { return undefined !== this._settings; } +} diff --git a/test-apps/display-test-app/src/frontend/Surface.ts b/test-apps/display-test-app/src/frontend/Surface.ts index 1731111201d2..7a4a362cff0d 100644 --- a/test-apps/display-test-app/src/frontend/Surface.ts +++ b/test-apps/display-test-app/src/frontend/Surface.ts @@ -19,6 +19,7 @@ import { openIModel, OpenIModelProps } from "./openIModel"; import { setTitle } from "./Title"; import { openAnalysisStyleExample } from "./AnalysisStyleExample"; import { openDecorationGeometryExample } from "./DecorationGeometryExample"; +import { DtaConfiguration } from "../common/DtaConfiguration"; // cspell:ignore textbox topdiv @@ -32,13 +33,15 @@ export class Surface { private readonly _toolbar: ToolBar; public readonly browserFileSelector?: BrowserFileSelector; public readonly openReadWrite: boolean; + public readonly configuration: DtaConfiguration; public static get instance() { return DisplayTestApp.surface; } - public constructor(surfaceDiv: HTMLElement, toolbarDiv: HTMLElement, browserFileSelector: BrowserFileSelector | undefined, openReadWrite: boolean) { + public constructor(configuration: DtaConfiguration, surfaceDiv: HTMLElement, toolbarDiv: HTMLElement, browserFileSelector: BrowserFileSelector | undefined, openReadWrite: boolean) { // Ensure iModel gets closed on page close/reload window.onbeforeunload = () => this.closeAllViewers(); + this.configuration = configuration; this.element = surfaceDiv; this.openReadWrite = openReadWrite; this.browserFileSelector = browserFileSelector; @@ -171,7 +174,7 @@ export class Surface { name: props?.name ?? "blank connection test", }); - const viewer = await this.createViewer({ iModel }); + const viewer = await this.createViewer({ iModel, configuration: this.configuration }); viewer.dock(Dock.Full); return viewer; } @@ -190,7 +193,7 @@ export class Surface { try { const iModel = await openIModel(props); setTitle(iModel); - const viewer = await this.createViewer({ iModel }); + const viewer = await this.createViewer({ iModel, configuration: this.configuration }); viewer.dock(Dock.Full); } catch (err: any) { alert(`Error opening iModel: ${err.toString()}`); diff --git a/test-apps/display-test-app/src/frontend/Viewer.ts b/test-apps/display-test-app/src/frontend/Viewer.ts index 58b473388e72..b4c305ccc51b 100644 --- a/test-apps/display-test-app/src/frontend/Viewer.ts +++ b/test-apps/display-test-app/src/frontend/Viewer.ts @@ -1,3 +1,4 @@ + /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. @@ -30,6 +31,9 @@ import { openIModel, OpenIModelProps } from "./openIModel"; import { HubPicker } from "./HubPicker"; import { RealityModelSettingsPanel } from "./RealityModelDisplaySettingsWidget"; import { ContoursPanel } from "./Contours"; +import { GoogleMapsPanel } from "./GoogleMaps"; +import { DtaConfiguration } from "../common/DtaConfiguration"; + // cspell:ignore savedata topdiv savedview viewtop @@ -159,6 +163,7 @@ export interface ViewerProps { iModel: IModelConnection; defaultViewName?: string; disableEdges?: boolean; + configuration: DtaConfiguration; } export class Viewer extends Window { @@ -171,6 +176,7 @@ export class Viewer extends Window { private readonly _3dOnly: HTMLElement[] = []; private _isSavedView = false; private _debugWindow?: DebugWindow; + private _configuration: DtaConfiguration; public static async create(surface: Surface, props: ViewerProps): Promise { const views = await ViewList.create(props.iModel, props.defaultViewName); @@ -184,6 +190,7 @@ export class Viewer extends Window { const viewer = new Viewer(Surface.instance, view, this.views, { iModel: view.iModel, disableEdges: this.disableEdges, + configuration: this._configuration }); if (!this.isDocked) { @@ -210,6 +217,8 @@ export class Viewer extends Window { private constructor(surface: Surface, view: ViewState, views: ViewList, props: ViewerProps) { super(surface, { scrollbars: true }); + this._configuration = props.configuration; + // Allow HTMLElements beneath viewport to be visible if background color has transparency. this.contentDiv.style.backgroundColor = "transparent"; this.container.style.backgroundColor = "transparent"; @@ -416,6 +425,17 @@ export class Viewer extends Window { tooltip: "Contour display", }); + if(this._configuration.googleMapsUi) { + this.toolBar.addDropDown({ + iconUnicode: "\ue9e8", + createDropDown: async (container: HTMLElement) => { + const panel = new GoogleMapsPanel(this.viewport, container); + return panel; + }, + tooltip: "Google Maps", + }); + } + this.updateTitle(); this.updateActiveSettings(); } From 84135d4003f8d0066559baba95c7334389c338cc Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Tue, 11 Feb 2025 10:39:30 -0500 Subject: [PATCH 17/44] wip --- .../src/GoogleMaps/GoogleMapDecorator.ts | 4 +- .../src/GoogleMaps/GoogleMaps.ts | 53 +++++++++++++++---- .../GoogleMaps/GoogleMapsImageryProvider.ts | 17 ++++-- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts index c0c9c7e22d52..5667178fafab 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts @@ -1,6 +1,6 @@ import { CanvasDecoration, DecorateContext, Decorator, IconSprites, IModelApp, Sprite } from "@itwin/core-frontend"; import { Point3d } from "@itwin/core-geometry"; -import { MapTypesType } from "./GoogleMaps"; +import { MapTypes } from "./GoogleMaps"; /** A simple decorator that show logo at the a given screen position. @@ -73,7 +73,7 @@ export class GoogleMapsDecorator implements Decorator { public readonly logo = new LogoDecoration(); /** Activate the logo based on the given map type. */ - public async activate(mapType: MapTypesType): Promise { + public async activate(mapType: MapTypes): Promise { // Pick the logo that is the most visible on the background map const imageName = mapType === "roadmap" ? "google_on_white_hdpi" : diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index 240bf66f64a5..07ab3cd516ff 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -1,4 +1,4 @@ -import { ImageMapLayerProps, ImageMapLayerSettings, MapLayerProviderProperties } from "@itwin/core-common"; +import { BaseMapLayerSettings, ImageMapLayerProps, ImageMapLayerSettings, MapLayerProviderProperties } from "@itwin/core-common"; import { IModelApp, MapCartoRectangle } from "@itwin/core-frontend"; import { GoogleMapsMapLayerFormat } from "./GoogleMapsImageryFormat"; import { Angle } from "@itwin/core-geometry"; @@ -6,12 +6,16 @@ import { Logger } from "@itwin/core-bentley"; const loggerCategory = "MapLayersFormats.GoogleMaps"; -export type LayerTypesType = "layerRoadmap" | "layerStreetview" | "layerTraffic"; -export type MapTypesType = "roadmap"|"satellite"|"terrain"; -export type ImageFormatsType = "jpeg" | "png"; +/** @beta*/ +export type LayerTypes = "layerRoadmap" | "layerStreetview" | "layerTraffic"; +/** @beta*/ +export type MapTypes = "roadmap"|"satellite"|"terrain"; +/** @beta*/ +export type ScaleFactors = "scaleFactor1x" | "scaleFactor2x" | "scaleFactor4x"; /** * Represents the options to create a Google Maps session. +* @beta */ export interface CreateSessionOptions { /** @@ -24,7 +28,7 @@ export interface CreateSessionOptions { * `terrain`: Terrain imagery. When selecting `terrain` as the map type, you must also include the `layerRoadmap` layer type. * @beta * */ - mapType: MapTypesType, + mapType: MapTypes, /** * An {@link https://en.wikipedia.org/wiki/IETF_language_tag | IETF language tag} that specifies the language used to display information on the tiles. For example, `en-US` specifies the English language as spoken in the United States. */ @@ -44,11 +48,28 @@ export interface CreateSessionOptions { * `layerTraffic`: Displays current traffic conditions. * @beta * */ - layerTypes?: LayerTypesType[]; + layerTypes?: LayerTypes[]; + /** - * Specifies the file format to return. Valid values are either jpeg or png. JPEG files don't support transparency, therefore they aren't recommended for overlay tiles. If you don't specify an imageFormat, then the best format for the tile is chosen automatically. - */ - imageFormat?: ImageFormatsType; + * Scales-up the size of map elements (such as road labels), while retaining the tile size and coverage area of the default tile. + * Increasing the scale also reduces the number of labels on the map, which reduces clutter. + * + * `scaleFactor1x`: The default. + * + * `scaleFactor2x`: Doubles label size and removes minor feature labels. + * + * `scaleFactor4x`: Quadruples label size and removes minor feature labels. + * @beta + * */ + scale?: ScaleFactors + + /** + * A boolean value that specifies whether layerTypes should be rendered as a separate overlay, or combined with the base imagery. + * When true, the base map isn't displayed. If you haven't defined any layerTypes, then this value is ignored. + * Default is false. + * @beta + * */ + overlay? : boolean; }; /** @@ -150,7 +171,14 @@ export const GoogleMaps = { properties.layerTypes = [...opts.layerTypes]; } - return properties + if (opts.scale !== undefined) { + properties.scale = opts.scale; + } + + if (opts.overlay !== undefined) { + properties.overlay = opts.overlay; + } + return properties; }, /** @@ -163,6 +191,11 @@ export const GoogleMaps = { return ImageMapLayerSettings.fromJSON(GoogleMaps.createMapLayerProps(name, opts)); }, + createBaseLayerSettings: (name?: string, opts?: CreateSessionOptions): ImageMapLayerSettings => { + return BaseMapLayerSettings.fromJSON(GoogleMaps.createMapLayerProps(name, opts)); + }, + + /** * Creates a Google Maps layer props. * @param name Name of the layer (Defaults to "GoogleMaps") diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 5cddd0d496f2..072f4934b562 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -1,6 +1,6 @@ import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile, TileUrlImageryProvider } from "@itwin/core-frontend"; -import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, LayerTypesType, MapTypesType } from "./GoogleMaps"; +import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, LayerTypes, MapTypes, ScaleFactors } from "./GoogleMaps"; import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; import { GoogleMapsDecorator } from "./GoogleMapDecorator"; const loggerCategory = "MapLayersFormats.GoogleMaps"; @@ -61,7 +61,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { } this._tileSize = session.tileWidth; // assuming here tiles are square - const isActivated = await this._decorator.activate(this._settings.properties!.mapType as MapTypesType); + const isActivated = await this._decorator.activate(this._settings.properties!.mapType as MapTypes); if (!isActivated) { const msg = `Failed to activate decorator`; Logger.logError(loggerCategory, msg); @@ -81,17 +81,24 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { } const createSessionOptions: CreateSessionOptions = { - mapType: this._settings.properties!.mapType as MapTypesType, + mapType: this._settings.properties!.mapType as MapTypes, region: this._settings.properties!.region as string, language: this._settings.properties!.language as string, } if (this._settings.properties?.layerTypes !== undefined) { - createSessionOptions.layerTypes = this._settings.properties!.layerTypes as LayerTypesType[]; + createSessionOptions.layerTypes = this._settings.properties.layerTypes as LayerTypes[]; + } + + if (this._settings.properties?.scale !== undefined) { + createSessionOptions.scale = this._settings.properties.scale as ScaleFactors; } - return createSessionOptions; + if (this._settings.properties?.overlay !== undefined) { + createSessionOptions.overlay = this._settings.properties.overlay as boolean; } + return createSessionOptions; + } // construct the Url from the desired Tile public async constructUrl(row: number, column: number, level: number): Promise { From 0ede483df67b18217b3005301fe12e7fc6542294 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Tue, 11 Feb 2025 15:34:41 -0500 Subject: [PATCH 18/44] wip --- .../src/GoogleMaps/GoogleMaps.ts | 184 ++++++++++-------- .../GoogleMaps/GoogleMapsImageryProvider.ts | 9 +- .../src/test/GoogleMaps/GoogleMaps.test.ts | 55 +++--- .../src/frontend/GoogleMaps.ts | 31 ++- 4 files changed, 167 insertions(+), 112 deletions(-) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index 07ab3cd516ff..91da6020baee 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -136,103 +136,133 @@ export interface ViewportInfoRequestParams { } -// eslint-disable-next-line @typescript-eslint/naming-convention -export const GoogleMaps = { /** * Creates a Google Maps session. * @param apiKey Google Cloud API key * @param opts Options to create the session - * @beta + * @internal */ - createSession: async (apiKey: string, opts: CreateSessionOptions): Promise => { - const url = `https://tile.googleapis.com/v1/createSession?key=${apiKey}`; - const request = new Request(url, {method: "POST", body: JSON.stringify(opts)}); - const response = await fetch (request); - if (!response.ok) { - throw new Error(`CreateSession request failed: ${response.status} - ${response.statusText}`); - } - Logger.logInfo(loggerCategory, `Session created successfully`); - return response.json(); - }, +const createSession = async (apiKey: string, opts: CreateSessionOptions): Promise => { + const url = `https://tile.googleapis.com/v1/createSession?key=${apiKey}`; + const request = new Request(url, {method: "POST", body: JSON.stringify(opts)}); + const response = await fetch (request); + if (!response.ok) { + throw new Error(`CreateSession request failed: ${response.status} - ${response.statusText}`); + } + Logger.logInfo(loggerCategory, `Session created successfully`); + return response.json(); +}; + +/** + * Register the google maps format if it is not already registered. + * @internal +*/ +const registerFormatIfNeeded = () => { + if (!IModelApp.mapLayerFormatRegistry.isRegistered(GoogleMapsMapLayerFormat.formatId)) { + IModelApp.mapLayerFormatRegistry.register(GoogleMapsMapLayerFormat); + } +} + + /** + * Creates a Google Maps layer props. + * @param name Name of the layer (Defaults to "GoogleMaps") + * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) + * @internal +*/ +const createMapLayerProps = (name: string = "GoogleMaps", opts?: CreateSessionOptions): ImageMapLayerProps => { + _internal.registerFormatIfNeeded(); + + return { + formatId: GoogleMapsMapLayerFormat.formatId, + url: "", + name, + properties: createPropertiesFromSessionOptions(opts ?? {mapType: "satellite", language: "en-US", region: "US:", layerTypes: ["layerRoadmap"]}), + }; +}; + +/** +* Retrieves the maximum zoom level available within a bounding rectangle. +* @param rectangle The bounding rectangle +* @returns The maximum zoom level available within the bounding rectangle. +* @internal +*/ +const getViewportInfo = async (params: ViewportInfoRequestParams): Promise=> { + const {rectangle, session, key, zoom} = params; + const north = Angle.radiansToDegrees(rectangle.north); + const south = Angle.radiansToDegrees(rectangle.south); + const east = Angle.radiansToDegrees(rectangle.east); + const west = Angle.radiansToDegrees(rectangle.west); + const url = `https://tile.googleapis.com/tile/v1/viewport?session=${session}&key=${key}&zoom=${zoom}&north=${north}&south=${south}&east=${east}&west=${west}`; + const request = new Request(url, {method: "GET"}); + const response = await fetch (request); + if (!response.ok) { + return undefined; + } + const json = await response.json(); + return json as ViewportInfo;; +}; /** * Converts the session options to provider properties * @param opts Options to create the session - * @beta + * @internal */ - createPropertiesFromSessionOptions: (opts: CreateSessionOptions): MapLayerProviderProperties => { - const properties: MapLayerProviderProperties = { - mapType: opts.mapType, - language: opts.language, - region: opts.region, - } - - if (opts.layerTypes !== undefined) { - properties.layerTypes = [...opts.layerTypes]; - } - - if (opts.scale !== undefined) { - properties.scale = opts.scale; - } - - if (opts.overlay !== undefined) { - properties.overlay = opts.overlay; - } - return properties; - }, +const createPropertiesFromSessionOptions = (opts: CreateSessionOptions): MapLayerProviderProperties => { + const properties: MapLayerProviderProperties = { + mapType: opts.mapType, + language: opts.language, + region: opts.region, + } + + if (opts.layerTypes !== undefined) { + properties.layerTypes = [...opts.layerTypes]; + } + + if (opts.scale !== undefined) { + properties.scale = opts.scale; + } + + if (opts.overlay !== undefined) { + properties.overlay = opts.overlay; + } + return properties; +}; /** - * Creates a Google Maps layer settings. + * Google Maps API + * @beta +*/ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const GoogleMaps = { +/** + * Creates Google Maps map-layer settings. * @param name Name of the layer (Defaults to "GoogleMaps") * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) * @beta */ - createMapLayerSettings: (name?: string, opts?: CreateSessionOptions): ImageMapLayerSettings => { - return ImageMapLayerSettings.fromJSON(GoogleMaps.createMapLayerProps(name, opts)); - }, - - createBaseLayerSettings: (name?: string, opts?: CreateSessionOptions): ImageMapLayerSettings => { - return BaseMapLayerSettings.fromJSON(GoogleMaps.createMapLayerProps(name, opts)); + createMapLayerSettings: (name?: string, opts?: CreateSessionOptions) => { + return ImageMapLayerSettings.fromJSON(_internal.createMapLayerProps(name, opts)); }, - - /** - * Creates a Google Maps layer props. +/** + * Creates Google Maps base layer settings. * @param name Name of the layer (Defaults to "GoogleMaps") * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) * @beta */ - createMapLayerProps: (name: string = "GoogleMaps", opts?: CreateSessionOptions): ImageMapLayerProps => { - if (!IModelApp.mapLayerFormatRegistry.isRegistered(GoogleMapsMapLayerFormat.formatId)) { - IModelApp.mapLayerFormatRegistry.register(GoogleMapsMapLayerFormat); - } - return { - formatId: GoogleMapsMapLayerFormat.formatId, - url: "", - name, - properties: GoogleMaps.createPropertiesFromSessionOptions(opts ?? {mapType: "satellite", language: "en-US", region: "US:", layerTypes: ["layerRoadmap"]}), - }; - }, - - /** - * Retrieves the maximum zoom level available within a bounding rectangle. - * @param rectangle The bounding rectangle - * @returns The maximum zoom level available within the bounding rectangle. - * @beta - */ - getViewportInfo: async (params: ViewportInfoRequestParams): Promise=> { - const {rectangle, session, key, zoom} = params; - const north = Angle.radiansToDegrees(rectangle.north); - const south = Angle.radiansToDegrees(rectangle.south); - const east = Angle.radiansToDegrees(rectangle.east); - const west = Angle.radiansToDegrees(rectangle.west); - const url = `https://tile.googleapis.com/tile/v1/viewport?session=${session}&key=${key}&zoom=${zoom}&north=${north}&south=${south}&east=${east}&west=${west}`; - const request = new Request(url, {method: "GET"}); - const response = await fetch (request); - if (!response.ok) { - return undefined; - } - const json = await response.json(); - return json as ViewportInfo;; - }, + createBaseLayerSettings: (opts?: CreateSessionOptions) => { + return BaseMapLayerSettings.fromJSON(_internal.createMapLayerProps("GoogleMaps", opts)); + } }; + +/** + * Internal function export for testing purposes, do not use. + * @internal + * */ +export const _internal = { + createMapLayerProps, + createSession, + createPropertiesFromSessionOptions, + getViewportInfo, + registerFormatIfNeeded, +}; \ No newline at end of file diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 072f4934b562..21c112c9f469 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -1,6 +1,6 @@ import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile, TileUrlImageryProvider } from "@itwin/core-frontend"; -import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, LayerTypes, MapTypes, ScaleFactors } from "./GoogleMaps"; +import { _internal, CreateSessionOptions, GoogleMapsSession, LayerTypes, MapTypes, ScaleFactors } from "./GoogleMaps"; import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; import { GoogleMapsDecorator } from "./GoogleMapDecorator"; const loggerCategory = "MapLayersFormats.GoogleMaps"; @@ -10,6 +10,9 @@ const columnToken = "{column}"; const urlTemplate = `https://tile.googleapis.com/v1/2dtiles/${levelToken}/${columnToken}/${rowToken}`; +// eslint-disable-next-line @typescript-eslint/naming-convention +const GoogleMapsUtils = _internal; + /* * Google Maps imagery provider * @internal @@ -31,7 +34,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { const sessionOptions = this.createCreateSessionOptions(); if (this._settings.accessKey ) { // Create session and store in query parameters - const sessionObj = await GoogleMaps.createSession(this._settings.accessKey.value, sessionOptions); + const sessionObj = await GoogleMapsUtils.createSession(this._settings.accessKey.value, sessionOptions); this._settings.unsavedQueryParams = {session: sessionObj.session}; return sessionObj; } else { @@ -133,7 +136,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { } if (cartoRect) { try { - const viewportInfo = await GoogleMaps.getViewportInfo({ + const viewportInfo = await GoogleMapsUtils.getViewportInfo({ rectangle: cartoRect, session: this._settings.collectQueryParams().session, key: this._settings.accessKey!.value, diff --git a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts index dcddc9784d68..0064435fe312 100644 --- a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts +++ b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts @@ -7,12 +7,15 @@ import * as sinon from "sinon"; import { Frustum, ImageMapLayerSettings, MapLayerProviderProperties } from "@itwin/core-common"; import { expect } from "chai"; import { GoogleMapsImageryProvider } from "../../GoogleMaps/GoogleMapsImageryProvider"; -import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; +import { _internal , CreateSessionOptions, GoogleMaps, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; import { fakeJsonFetch } from "../TestUtils"; import { LogoDecoration } from "../../GoogleMaps/GoogleMapDecorator"; import { DecorateContext, Decorations, IconSprites, IModelApp, MapCartoRectangle, MapTile, MapTileTree, QuadId, ScreenViewport, Sprite, TilePatch } from "@itwin/core-frontend"; import { Range3d } from "@itwin/core-geometry"; +// eslint-disable-next-line @typescript-eslint/naming-convention +const GoogleMapsUtils = _internal; + class FakeMapTile extends MapTile { public override depth: number; constructor(contentId: string) { @@ -27,21 +30,21 @@ class FakeMapTile extends MapTile { } } -const getTestSettings = (properties?: MapLayerProviderProperties) => { - return ImageMapLayerSettings.fromJSON({ - name: "test", - url: "", - formatId: "GoogleMaps", - properties - }); -}; +// const getTestSettings = (properties?: MapLayerProviderProperties) => { +// return GoogleMaps.createBaseLayerSettings({ +// name: "test", +// url: "", +// formatId: "GoogleMaps", +// properties +// }); +// }; const createProvider = (settings: ImageMapLayerSettings) => { settings.accessKey = {key: "key", value: "dummyKey"}; return new GoogleMapsImageryProvider(settings); } -const stubCreateSession = (sandbox:sinon.SinonSandbox, session: GoogleMapsSession) => sandbox.stub(GoogleMaps, "createSession").callsFake(async function _(_apiKey: string, _opts: CreateSessionOptions) { +const stubCreateSession = (sandbox:sinon.SinonSandbox, session: GoogleMapsSession) => sandbox.stub(GoogleMapsUtils, "createSession").callsFake(async function _(_apiKey: string, _opts: CreateSessionOptions) { return session; }); @@ -56,22 +59,22 @@ describe.only("GoogleMapsProvider", () => { sandbox.stub(LogoDecoration.prototype, "activate").callsFake(async function _(_sprite: Sprite) { return Promise.resolve(true); }); + sandbox.stub(GoogleMapsUtils, "registerFormatIfNeeded"); }); afterEach(async () => { sandbox.restore(); }); - - it ("Provider properties round-trips through JSON", async () => { - const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(createSessionOptions2)); + it("Provider properties round-trips through JSON", async () => { + const settings = GoogleMaps.createMapLayerSettings("", createSessionOptions2); const json = settings.toJSON(); const deserializedSettings = ImageMapLayerSettings.fromJSON(json); expect(deserializedSettings.properties).to.deep.eq(settings.properties); }); it("should not initialize with no properties provided", async () => { - const settings = getTestSettings(); + const settings = ImageMapLayerSettings.fromJSON({name: "test", formatId: "GoogleMaps", url: ""}); const createSessionSub = stubCreateSession(sandbox, defaultPngSession); const provider = createProvider(settings); await expect(provider.initialize()).to.be.rejectedWith("Missing session options"); @@ -81,7 +84,7 @@ describe.only("GoogleMapsProvider", () => { it("should initialize with required properties", async () => { fakeJsonFetch(sandbox, defaultPngSession); - const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(minCreateSessionOptions)); + const settings = GoogleMaps.createBaseLayerSettings(minCreateSessionOptions); const createSessionSub = stubCreateSession(sandbox, defaultPngSession); const provider = createProvider(settings); @@ -91,12 +94,10 @@ describe.only("GoogleMapsProvider", () => { expect(createSessionSub.firstCall.args[1]).to.deep.eq(minCreateSessionOptions); }); - it("should initialize with properties", async () => { fakeJsonFetch(sandbox, defaultPngSession); - const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(createSessionOptions2)); - + const settings = GoogleMaps.createBaseLayerSettings(createSessionOptions2); const createSessionSub = stubCreateSession(sandbox, defaultPngSession); const provider = createProvider(settings); @@ -110,7 +111,7 @@ describe.only("GoogleMapsProvider", () => { it("should create proper tile url", async () => { fakeJsonFetch(sandbox, defaultPngSession); - const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(createSessionOptions2)); + const settings = GoogleMaps.createBaseLayerSettings(createSessionOptions2); const makeTileRequestStub = sandbox.stub(GoogleMapsImageryProvider.prototype, "makeTileRequest").callsFake(async function _(_url: string, _timeoutMs?: number ) { const obj = { @@ -132,11 +133,10 @@ describe.only("GoogleMapsProvider", () => { expect(makeTileRequestStub.firstCall.args[0]).to.eq("https://tile.googleapis.com/v1/2dtiles/17/37981/49592?key=dummyKey&session=dummySession"); }); - it("should add attributions", async () => { fakeJsonFetch(sandbox, defaultPngSession); - const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(createSessionOptions2)); + const settings = GoogleMaps.createBaseLayerSettings(createSessionOptions2); sandbox.stub(GoogleMapsImageryProvider.prototype as any, "getSelectedTiles").callsFake(function _(_vp: unknown) { const set = new Set(); @@ -144,7 +144,7 @@ describe.only("GoogleMapsProvider", () => { return set; }); - const getViewportInfoStub = sandbox.stub(GoogleMaps, "getViewportInfo").callsFake(async function _(_params: ViewportInfoRequestParams) { + const getViewportInfoStub = sandbox.stub(GoogleMapsUtils, "getViewportInfo").callsFake(async function _(_params: ViewportInfoRequestParams) { return {copyright: "fake copyright", maxZoomRects: []}; }); @@ -162,7 +162,7 @@ describe.only("GoogleMapsProvider", () => { expect(table.innerHTML).to.includes(`

fake copyright

`); // Now re-do the test with roadmap base layer - const settings2 = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions({mapType: "roadmap", language: "en-US", region: "US"})); + const settings2 = GoogleMaps.createBaseLayerSettings({mapType: "roadmap", language: "en-US", region: "US"}); const provider2 = createProvider(settings2); await provider2.initialize(); const table2 = document.createElement('table'); @@ -171,14 +171,13 @@ describe.only("GoogleMapsProvider", () => { expect(table2.innerHTML).to.includes(`

fake copyright

`); }); - it("logo should be activated with the 'on non-white' logo", async () => { fakeJsonFetch(sandbox, defaultPngSession); const getSpriteStub = sandbox.stub(IconSprites, "getSpriteFromUrl").callsFake(function _(_url: string) { return {} as Sprite; }); - const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions({mapType: "satellite", language: "en-US", region: "US"})); + const settings = GoogleMaps.createBaseLayerSettings({mapType: "satellite", language: "en-US", region: "US"}); const provider = createProvider(settings); await provider.initialize(); @@ -191,18 +190,16 @@ describe.only("GoogleMapsProvider", () => { const getSpriteStub = sandbox.stub(IconSprites, "getSpriteFromUrl").callsFake(function _(_url: string) { return {} as Sprite; }); - const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions({mapType: "roadmap", language: "en-US", region: "US"})); + const settings = GoogleMaps.createBaseLayerSettings({mapType: "roadmap", language: "en-US", region: "US"}); const provider = createProvider(settings); await provider.initialize(); expect(getSpriteStub.firstCall.args[0]).to.eq("public/images/google_on_white_hdpi.png"); }); - it("should decorate", async () => { - fakeJsonFetch(sandbox, defaultPngSession); - const settings = getTestSettings(GoogleMaps.createPropertiesFromSessionOptions(minCreateSessionOptions)); + const settings = GoogleMaps.createBaseLayerSettings(minCreateSessionOptions); const provider = createProvider(settings); diff --git a/test-apps/display-test-app/src/frontend/GoogleMaps.ts b/test-apps/display-test-app/src/frontend/GoogleMaps.ts index bd78baf479b1..1c18610f8523 100644 --- a/test-apps/display-test-app/src/frontend/GoogleMaps.ts +++ b/test-apps/display-test-app/src/frontend/GoogleMaps.ts @@ -5,6 +5,7 @@ import { assert, dispose } from "@itwin/core-bentley"; import { + CheckBox, ComboBox, ComboBoxEntry, createButton, createCheckBox, createComboBox, createTextBox @@ -31,6 +32,7 @@ export class GoogleMapsSettings implements Disposable { private _layerTypes: LayerTypes[] = []; private _lang = "en-US"; + private _roadmapLayerCheckox: CheckBox|undefined; public constructor(vp: Viewport, parent: HTMLElement) { this._currentTerrainProps.contourDef = {}; @@ -54,6 +56,15 @@ export class GoogleMapsSettings implements Disposable { const isGoogleBase = vp.displayStyle.backgroundMapBase instanceof BaseMapLayerSettings && vp.displayStyle.backgroundMapBase.formatId === "GoogleMaps"; this._enabled = isGoogleBase || googleLayer !== undefined; this._overlay = googleLayer !== undefined; + if (googleLayer) { + const opts = googleLayer.properties; + if (opts) { + this._mapType = opts.mapType as MapTypes; + this._layerTypes = opts.layerTypes as LayerTypes[] ?? []; + this._lang = opts.language as string ?? "en-US"; + this._scaleFactor = opts.scale as ScaleFactors ?? "scaleFactor1x"; + } + } if (isGoogleBase) { const baseSettings = vp.displayStyle.backgroundMapBase as BaseMapLayerSettings; const properties = baseSettings.properties; @@ -95,6 +106,20 @@ export class GoogleMapsSettings implements Disposable { value: this._mapType, handler: (cbx) => { this._mapType = cbx.value as MapTypes; + if (this._mapType === "terrain" && !this._layerTypes.includes("layerRoadmap")) { + this._layerTypes.push("layerRoadmap"); + } + + // Force roadmap layer to be enabled if terrain is selected + if (this._roadmapLayerCheckox) { + if (this._mapType === "terrain") { + this._roadmapLayerCheckox.checkbox.checked = true; + this._roadmapLayerCheckox.checkbox.disabled = true; + } else { + this._roadmapLayerCheckox.checkbox.disabled = false; + } + } + }, }); this._element.appendChild(document.createElement("br")); @@ -109,7 +134,7 @@ export class GoogleMapsSettings implements Disposable { layerTypesLabel.style.display = "inline"; layerTypesDiv.appendChild(layerTypesLabel); - createCheckBox({ + this._roadmapLayerCheckox = createCheckBox({ parent: layerTypesDiv, name: "Roadmap", id: "google_layertype_roadmap", @@ -122,6 +147,7 @@ export class GoogleMapsSettings implements Disposable { } }, }); + this._roadmapLayerCheckox.checkbox.disabled = this._mapType === "terrain"; createCheckBox({ parent: layerTypesDiv, @@ -247,7 +273,6 @@ export class GoogleMapsSettings implements Disposable { } - if (this._overlay) { this._vp.displayStyle.backgroundMapBase = BaseMapLayerSettings.fromJSON({ formatId: "ArcGIS", @@ -273,7 +298,7 @@ export class GoogleMapsSettings implements Disposable { scale: this._scaleFactor, }; try { - this._vp.displayStyle.backgroundMapBase = BaseMapLayerSettings.fromJSON(GoogleMaps.createMapLayerProps("GoogleMaps", opts)); + this._vp.displayStyle.backgroundMapBase = GoogleMaps.createBaseLayerSettings(opts); } catch (e: any) { // eslint-disable-next-line no-console console.log(e.message); From bc8aad223359104d3808ddd6f3d1cb911a7c5712 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Tue, 11 Feb 2025 15:56:38 -0500 Subject: [PATCH 19/44] lint --- core/frontend/src/tile/map/ImageryTileTree.ts | 2 +- core/frontend/src/tile/map/MapLayerImageryProvider.ts | 3 +-- .../src/GoogleMaps/GoogleMapsImageryProvider.ts | 2 +- .../map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/frontend/src/tile/map/ImageryTileTree.ts b/core/frontend/src/tile/map/ImageryTileTree.ts index 99bd69aacbba..416c1cadac49 100644 --- a/core/frontend/src/tile/map/ImageryTileTree.ts +++ b/core/frontend/src/tile/map/ImageryTileTree.ts @@ -360,7 +360,7 @@ class ImageryMapLayerTreeSupplier implements TileTreeSupplier { if (0 !== cmp) break; } else { - cmp = compareSimpleTypes(lhsProp as number | string | boolean, rhsProp as number | string | boolean); + cmp = compareSimpleTypes(lhsProp, rhsProp as number | string | boolean); if (0 !== cmp) break; } diff --git a/core/frontend/src/tile/map/MapLayerImageryProvider.ts b/core/frontend/src/tile/map/MapLayerImageryProvider.ts index 18889d5889d2..cf15a547029a 100644 --- a/core/frontend/src/tile/map/MapLayerImageryProvider.ts +++ b/core/frontend/src/tile/map/MapLayerImageryProvider.ts @@ -204,8 +204,7 @@ export abstract class MapLayerImageryProvider { /** @internal */ public decorate(_context: DecorateContext): void { - console.log ("MapLayerImageryProvider.Decorate called"); - } + } /** @internal */ protected async getImageFromTileResponse(tileResponse: Response, zoomLevel: number) { diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 21c112c9f469..a92017048bce 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -1,5 +1,5 @@ import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; -import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile, TileUrlImageryProvider } from "@itwin/core-frontend"; +import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile } from "@itwin/core-frontend"; import { _internal, CreateSessionOptions, GoogleMapsSession, LayerTypes, MapTypes, ScaleFactors } from "./GoogleMaps"; import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; import { GoogleMapsDecorator } from "./GoogleMapDecorator"; diff --git a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts index 0064435fe312..08ab1527c6f6 100644 --- a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts +++ b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as sinon from "sinon"; -import { Frustum, ImageMapLayerSettings, MapLayerProviderProperties } from "@itwin/core-common"; +import { Frustum, ImageMapLayerSettings } from "@itwin/core-common"; import { expect } from "chai"; import { GoogleMapsImageryProvider } from "../../GoogleMaps/GoogleMapsImageryProvider"; import { _internal , CreateSessionOptions, GoogleMaps, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; From 457b2146f2444eab354342ec58c320b00964e4c5 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 12 Feb 2025 09:56:26 -0500 Subject: [PATCH 20/44] Fixed issues in tile tree comparison --- core/frontend/src/tile/map/ImageryTileTree.ts | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/core/frontend/src/tile/map/ImageryTileTree.ts b/core/frontend/src/tile/map/ImageryTileTree.ts index 416c1cadac49..bc9a628346b8 100644 --- a/core/frontend/src/tile/map/ImageryTileTree.ts +++ b/core/frontend/src/tile/map/ImageryTileTree.ts @@ -348,23 +348,31 @@ class ImageryMapLayerTreeSupplier implements TileTreeSupplier { cmp = compareBooleans(lhs.settings.transparentBackground, rhs.settings.transparentBackground); if (0 === cmp) { if (lhs.settings.properties && rhs.settings.properties) { - for (const key of Object.keys(lhs.settings.properties)) { - const lhsProp = lhs.settings.properties[key]; - const rhsProp = rhs.settings.properties[key]; - if (typeof lhsProp !== typeof rhsProp) { - cmp = 1; - break; - } - if (Array.isArray(lhsProp)) { - cmp = compareSimpleArrays(lhsProp, rhsProp as (number | string | boolean)[]); - if (0 !== cmp) - break; - } else { - cmp = compareSimpleTypes(lhsProp, rhsProp as number | string | boolean); - if (0 !== cmp) + const lhsKeysLength = Object.keys(lhs.settings.properties).length; + const rhsKeysLength = Object.keys(rhs.settings.properties).length; + + if (lhsKeysLength !== rhsKeysLength) { + cmp = lhsKeysLength - rhsKeysLength; + } else { + for (const key of Object.keys(lhs.settings.properties)) { + const lhsProp = lhs.settings.properties[key]; + const rhsProp = rhs.settings.properties[key]; + if (typeof lhsProp !== typeof rhsProp) { + cmp = 1; break; + } + if (Array.isArray(lhsProp) || Array.isArray(rhsProp)) { + cmp = compareSimpleArrays(lhsProp as (number | string | boolean)[], rhsProp as (number | string | boolean)[]); + if (0 !== cmp) + break; + } else { + cmp = compareSimpleTypes(lhsProp, rhsProp); + if (0 !== cmp) + break; + } } } + if (0 === cmp) { cmp = compareNumbers(lhs.settings.subLayers.length, rhs.settings.subLayers.length); if (0 === cmp) { From 15b0635d2af13c2bb0bfd980ef58a0822fb91dc7 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 12 Feb 2025 09:57:47 -0500 Subject: [PATCH 21/44] wip --- .../src/GoogleMaps/GoogleMaps.ts | 22 ++++++++++-- .../GoogleMaps/GoogleMapsImageryProvider.ts | 4 +++ .../src/frontend/GoogleMaps.ts | 34 +++++++++++-------- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index 91da6020baee..4b88eb22578b 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -7,7 +7,7 @@ import { Logger } from "@itwin/core-bentley"; const loggerCategory = "MapLayersFormats.GoogleMaps"; /** @beta*/ -export type LayerTypes = "layerRoadmap" | "layerStreetview" | "layerTraffic"; +export type LayerTypes = "layerRoadmap" | "layerStreetview"; /** @beta*/ export type MapTypes = "roadmap"|"satellite"|"terrain"; /** @beta*/ @@ -45,7 +45,6 @@ export interface CreateSessionOptions { * * `layerStreetview`: Shows Street View-enabled streets and locations using blue outlines on the map. * - * `layerTraffic`: Displays current traffic conditions. * @beta * */ layerTypes?: LayerTypes[]; @@ -70,6 +69,12 @@ export interface CreateSessionOptions { * @beta * */ overlay? : boolean; + + /** + * An array of values specifying additional options to apply. + * @beta + * */ + apiOptions?: string[]; }; /** @@ -225,6 +230,11 @@ const createPropertiesFromSessionOptions = (opts: CreateSessionOptions): MapLaye if (opts.overlay !== undefined) { properties.overlay = opts.overlay; } + + if (opts.apiOptions !== undefined) { + properties.apiOptions = [...opts.apiOptions]; + } + return properties; }; @@ -238,6 +248,11 @@ export const GoogleMaps = { * Creates Google Maps map-layer settings. * @param name Name of the layer (Defaults to "GoogleMaps") * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) + * @example + * const ds = IModelApp.viewManager.selectedView.displayStyle; + * ds.attachMapLayer({ + * mapLayerIndex: {index: 0, isOverlay: false}, + * settings: GoogleMaps.createMapLayerSettings("GoogleMaps")}); * @beta */ createMapLayerSettings: (name?: string, opts?: CreateSessionOptions) => { @@ -248,6 +263,9 @@ export const GoogleMaps = { * Creates Google Maps base layer settings. * @param name Name of the layer (Defaults to "GoogleMaps") * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) + * @example + * const ds = IModelApp.viewManager.selectedView.displayStyle; + * ds.backgroundMapBase = GoogleMaps.createBaseLayerSettings(); * @beta */ createBaseLayerSettings: (opts?: CreateSessionOptions) => { diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index a92017048bce..57b1777d3df9 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -100,6 +100,10 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { if (this._settings.properties?.overlay !== undefined) { createSessionOptions.overlay = this._settings.properties.overlay as boolean; } + + if (this._settings.properties?.apiOptions !== undefined) { + createSessionOptions.apiOptions = this._settings.properties.apiOptions as string[]; + } return createSessionOptions; } diff --git a/test-apps/display-test-app/src/frontend/GoogleMaps.ts b/test-apps/display-test-app/src/frontend/GoogleMaps.ts index 1c18610f8523..317a7063e01f 100644 --- a/test-apps/display-test-app/src/frontend/GoogleMaps.ts +++ b/test-apps/display-test-app/src/frontend/GoogleMaps.ts @@ -26,6 +26,7 @@ export class GoogleMapsSettings implements Disposable { private _currentTerrainProps: ContourGroupProps = {}; private _enabled: boolean = false; private _overlay: boolean = false; + private _newStyle: boolean = false; private _mapTypesCombobox: ComboBox; private _mapType: MapTypes; private _scaleFactor: ScaleFactors = "scaleFactor1x"; @@ -75,6 +76,12 @@ export class GoogleMapsSettings implements Disposable { if (properties?.scale) this._scaleFactor = properties.scale as ScaleFactors; + + if (properties?.apiOptions){ + const apiOptions = properties.apiOptions as string[]; + + this._newStyle = apiOptions.includes("MCYJ5E517XR2JC"); + } } @@ -149,20 +156,6 @@ export class GoogleMapsSettings implements Disposable { }); this._roadmapLayerCheckox.checkbox.disabled = this._mapType === "terrain"; - createCheckBox({ - parent: layerTypesDiv, - name: "Traffic", - id: " google_layertype_traffic", - isChecked: this._layerTypes.includes("layerTraffic"), - handler: (cb) => { - if (cb.checked) { - this._layerTypes.push("layerTraffic"); - } else { - this._layerTypes = this._layerTypes.filter((layerType) => layerType !== "layerTraffic"); - } - }, - }); - createCheckBox({ parent: layerTypesDiv, name: "Streetview", @@ -231,6 +224,18 @@ export class GoogleMapsSettings implements Disposable { }); this._element.appendChild(document.createElement("br")); + createCheckBox({ + parent: this._element, + name: "New 2025 style", + id: "cbx_toggle_newStyle", + isChecked: this._newStyle, + handler: (checkbox) => { + this._newStyle = checkbox.checked; + this.sync(); + }, + }); + this._element.appendChild(document.createElement("br")); + /////////// // Buttons @@ -296,6 +301,7 @@ export class GoogleMapsSettings implements Disposable { language: this._lang, region: "US", scale: this._scaleFactor, + apiOptions: this._newStyle ? ["MCYJ5E517XR2JC"] : undefined, }; try { this._vp.displayStyle.backgroundMapBase = GoogleMaps.createBaseLayerSettings(opts); From eacb75d8d8be371825af1166f52bf64257fbeb7c Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 12 Feb 2025 11:17:42 -0500 Subject: [PATCH 22/44] wip --- core/common/src/MapLayerSettings.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/common/src/MapLayerSettings.ts b/core/common/src/MapLayerSettings.ts index 03356e00faf3..5cd11e7a1b06 100644 --- a/core/common/src/MapLayerSettings.ts +++ b/core/common/src/MapLayerSettings.ts @@ -19,7 +19,7 @@ export type ImageryMapLayerFormatId = "ArcGIS" | "BingMaps" | "MapboxImagery" | /** @public */ export type SubLayerId = string | number; -/** @public */ +/** @beta */ export type MapLayerProviderArrayProperty = Array; export interface MapLayerProviderProperties { [key: string]: number | string | boolean | MapLayerProviderArrayProperty }; @@ -136,10 +136,12 @@ export interface CommonMapLayerProps { /** A user-friendly name for the layer. */ name: string; + /** A transparency value from 0.0 (fully opaque) to 1.0 (fully transparent) to apply to map graphics when drawing, * or false to indicate the transparency should not be overridden. * Default value: 0. */ + transparency?: number; /** True to indicate background is transparent. * Default: true. @@ -173,7 +175,7 @@ export interface ImageMapLayerProps extends CommonMapLayerProps { */ queryParams?: { [key: string]: string }; - /** Data specific to each map layer provider. + /** Properties specific to the map layer provider. * @beta */ properties?: MapLayerProviderProperties; @@ -307,7 +309,7 @@ export class ImageMapLayerSettings extends MapLayerSettings { */ public unsavedQueryParams?: { [key: string]: string }; - /** TODO + /** Properties specific to the map layer provider. * @beta */ public readonly properties?: MapLayerProviderProperties; From 9f3aa2abda2bd92d262b37a1258c60b2e72ec9f9 Mon Sep 17 00:00:00 2001 From: Michel D'Astous Date: Wed, 12 Feb 2025 11:34:15 -0500 Subject: [PATCH 23/44] added deprecated apis to NextVersion.md --- docs/changehistory/NextVersion.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index e40c40b3f9a8..17a1fcbd0fb3 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -116,6 +116,10 @@ If a walker operation would advance outside the mesh (e.g., `edgeMate` of a boun - Deprecated [HiliteSet.setHilite]($core-frontend) - use `add`, `remove`, `replace` methods instead. +- Deprecated [addLogoCards]($core-frontend)-related APIs: + - `TileTreeReference.addLogoCard` : use `addAttributions` method instead + - `MapLayerImageryProvider.addLogoCard` : use `addAttributions` method instead + - [IModelConnection.fontMap]($frontend) caches potentially-stale mappings of [FontId]($common)s to font names. If you need access to font Ids on the front-end for some reason, implement an [Ipc method](../learning/IpcInterface.md) that uses [IModelDb.fonts]($backend). ### @itwin/presentation-common From b57feb234adbfefb80e239491fc8bd0afe0b1a1a Mon Sep 17 00:00:00 2001 From: Michel D'Astous Date: Wed, 12 Feb 2025 11:49:05 -0500 Subject: [PATCH 24/44] added map-layers section to NextVersion.md --- docs/changehistory/NextVersion.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 17a1fcbd0fb3..d3a1f42e34f2 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -11,6 +11,8 @@ Table of contents: - [Font APIs](#font-apis) - [Geometry](#geometry) - [Polyface Traversal](#polyface-traversal) + - [Map layers](#map-layers) + - [Google Maps 2D tiles API](#google-maps-2d-tiles-api) - [API deprecations](#api-deprecations) - [@itwin/core-bentley](#itwincore-bentley) - [@itwin/core-common](#itwincore-common) @@ -71,6 +73,25 @@ The new class [IndexedPolyfaceWalker]($core-geometry) has methods to complete th If a walker operation would advance outside the mesh (e.g., `edgeMate` of a boundary edge), it returns an invalid walker. +## Map layers + +### Google Maps 2D tiles API + +The `itwin\map-layers-formats` package now include an API for consuming Google Maps 2 tiles. +To enable it as a base map, it's simple as: + + ```typescript +const ds = IModelApp.viewManager.selectedView.displayStyle; +ds.backgroundMapBase = GoogleMaps.createBaseLayerSettings(); +``` + +Can also be attached as a map-layer: +```typescript +ds.attachMapLayer({ + mapLayerIndex: {index: 0, isOverlay: false}, + settings: GoogleMaps.createMapLayerSettings("GoogleMaps")}); +``` + ## API deprecations ### @itwin/core-bentley From 7609e35ca95c07f1021840843b3cfbf08543e19f Mon Sep 17 00:00:00 2001 From: Michel D'Astous Date: Wed, 12 Feb 2025 11:51:24 -0500 Subject: [PATCH 25/44] updated NextVersion.md --- docs/changehistory/NextVersion.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index d3a1f42e34f2..95f2ece066ac 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -77,7 +77,8 @@ If a walker operation would advance outside the mesh (e.g., `edgeMate` of a boun ### Google Maps 2D tiles API -The `itwin\map-layers-formats` package now include an API for consuming Google Maps 2 tiles. +The `itwin\map-layers-formats` package now include an API for consuming Google Maps 2D tiles. + To enable it as a base map, it's simple as: ```typescript From 91beceb5f01fcf56f2b2359f68c8c161aa747dc3 Mon Sep 17 00:00:00 2001 From: Michel D'Astous Date: Wed, 12 Feb 2025 14:22:44 -0500 Subject: [PATCH 26/44] Update NextVersion.md --- docs/changehistory/NextVersion.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 95f2ece066ac..d67edcc7d9b4 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -92,7 +92,12 @@ ds.attachMapLayer({ mapLayerIndex: {index: 0, isOverlay: false}, settings: GoogleMaps.createMapLayerSettings("GoogleMaps")}); ``` - + > ***IMPORTANT***: Make sure to configure your Google Cloud's API key in the `MapLayerOptions` when starting your IModelApp application: +``` + mapLayerOptions: { + GoogleMaps: { key: "key", value: "YOUR_KEY_HERE" } + } +``` ## API deprecations ### @itwin/core-bentley From b4b13111fc6931ba477b4c816961371a995cb3c7 Mon Sep 17 00:00:00 2001 From: Michel D'Astous Date: Wed, 12 Feb 2025 14:23:32 -0500 Subject: [PATCH 27/44] Update NextVersion.md --- docs/changehistory/NextVersion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index d67edcc7d9b4..a01b72179d89 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -77,7 +77,7 @@ If a walker operation would advance outside the mesh (e.g., `edgeMate` of a boun ### Google Maps 2D tiles API -The `itwin\map-layers-formats` package now include an API for consuming Google Maps 2D tiles. +The `itwin\map-layers-formats` package now includes an API for consuming Google Maps 2D tiles. To enable it as a base map, it's simple as: From da13e1b46dce624411e4223a13ec0efe7b6178c6 Mon Sep 17 00:00:00 2001 From: Michel D'Astous Date: Wed, 12 Feb 2025 14:27:55 -0500 Subject: [PATCH 28/44] Update NextVersion.md --- docs/changehistory/NextVersion.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index a01b72179d89..388a27d9c6ba 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -82,6 +82,7 @@ The `itwin\map-layers-formats` package now includes an API for consuming Google To enable it as a base map, it's simple as: ```typescript +import { GoogleMaps } from "@itwin/map-layers-formats"; const ds = IModelApp.viewManager.selectedView.displayStyle; ds.backgroundMapBase = GoogleMaps.createBaseLayerSettings(); ``` From c039a7ff8f7d648de07e07b53b9aab35bc1934f4 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 12 Feb 2025 14:39:46 -0500 Subject: [PATCH 29/44] changelog --- .../MichelD-GoogleMaps_2025-02-12-19-37.json | 10 ++++++++++ .../MichelD-GoogleMaps_2025-02-12-19-37.json | 10 ++++++++++ .../MichelD-GoogleMaps_2025-02-12-19-37.json | 10 ++++++++++ .../MichelD-GoogleMaps_2025-02-12-19-37.json | 10 ++++++++++ 4 files changed, 40 insertions(+) create mode 100644 common/changes/@itwin/core-bentley/MichelD-GoogleMaps_2025-02-12-19-37.json create mode 100644 common/changes/@itwin/core-common/MichelD-GoogleMaps_2025-02-12-19-37.json create mode 100644 common/changes/@itwin/core-frontend/MichelD-GoogleMaps_2025-02-12-19-37.json create mode 100644 common/changes/@itwin/map-layers-formats/MichelD-GoogleMaps_2025-02-12-19-37.json diff --git a/common/changes/@itwin/core-bentley/MichelD-GoogleMaps_2025-02-12-19-37.json b/common/changes/@itwin/core-bentley/MichelD-GoogleMaps_2025-02-12-19-37.json new file mode 100644 index 000000000000..7ffffe38456f --- /dev/null +++ b/common/changes/@itwin/core-bentley/MichelD-GoogleMaps_2025-02-12-19-37.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-bentley", + "comment": "Add `compareSimpleTypes` and `compareSimpleArrays` to compare simple data.", + "type": "none" + } + ], + "packageName": "@itwin/core-bentley" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-common/MichelD-GoogleMaps_2025-02-12-19-37.json b/common/changes/@itwin/core-common/MichelD-GoogleMaps_2025-02-12-19-37.json new file mode 100644 index 000000000000..681cdde6a21e --- /dev/null +++ b/common/changes/@itwin/core-common/MichelD-GoogleMaps_2025-02-12-19-37.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-common", + "comment": "Add new `properties` property to `ImageMapLayerSettings` class.", + "type": "none" + } + ], + "packageName": "@itwin/core-common" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-frontend/MichelD-GoogleMaps_2025-02-12-19-37.json b/common/changes/@itwin/core-frontend/MichelD-GoogleMaps_2025-02-12-19-37.json new file mode 100644 index 000000000000..97a1820815c9 --- /dev/null +++ b/common/changes/@itwin/core-frontend/MichelD-GoogleMaps_2025-02-12-19-37.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-frontend", + "comment": "Add new async `addAttributions` method on `TileTreeReference`. Also make sure ImageryTileTree / providers are included when decorating the view.", + "type": "none" + } + ], + "packageName": "@itwin/core-frontend" +} \ No newline at end of file diff --git a/common/changes/@itwin/map-layers-formats/MichelD-GoogleMaps_2025-02-12-19-37.json b/common/changes/@itwin/map-layers-formats/MichelD-GoogleMaps_2025-02-12-19-37.json new file mode 100644 index 000000000000..1ed639c09dab --- /dev/null +++ b/common/changes/@itwin/map-layers-formats/MichelD-GoogleMaps_2025-02-12-19-37.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/map-layers-formats", + "comment": "Added Google Maps 2D tiles support.", + "type": "none" + } + ], + "packageName": "@itwin/map-layers-formats" +} \ No newline at end of file From f5ac2cc880e75c9551051dd3c387fba7d257f446 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 12 Feb 2025 14:54:18 -0500 Subject: [PATCH 30/44] wip --- core/bentley/src/Compare.ts | 2 ++ core/common/src/MapLayerSettings.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/core/bentley/src/Compare.ts b/core/bentley/src/Compare.ts index a8e84acc2ad6..a231470fc94c 100644 --- a/core/bentley/src/Compare.ts +++ b/core/bentley/src/Compare.ts @@ -93,6 +93,7 @@ export function areEqualPossiblyUndefined(t: T | undefined, u: U | undefin return areEqual(t, u); } +/** @beta */ export function compareSimpleTypes(lhs: number | string | boolean, rhs: number | string | boolean): number { // Make sure the types are the same if (typeof lhs !== typeof rhs) { @@ -115,6 +116,7 @@ export function compareSimpleTypes(lhs: number | string | boolean, rhs: number | return cmp; } +/** @beta */ export function compareSimpleArrays (lhs?: Array, rhs?: Array ) { if (undefined === lhs) return undefined === rhs ? 0 : -1; diff --git a/core/common/src/MapLayerSettings.ts b/core/common/src/MapLayerSettings.ts index 5cd11e7a1b06..27d42dcec690 100644 --- a/core/common/src/MapLayerSettings.ts +++ b/core/common/src/MapLayerSettings.ts @@ -21,6 +21,7 @@ export type SubLayerId = string | number; /** @beta */ export type MapLayerProviderArrayProperty = Array; +/** @beta */ export interface MapLayerProviderProperties { [key: string]: number | string | boolean | MapLayerProviderArrayProperty }; /** JSON representation of the settings associated with a map sublayer included within a [[MapLayerProps]]. From a8f495cb523c3c28cb5d40fb900aa893e8cc092e Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 12 Feb 2025 15:04:50 -0500 Subject: [PATCH 31/44] api extract --- common/api/core-bentley.api.md | 6 ++ common/api/core-common.api.md | 13 ++++ common/api/core-frontend.api.md | 27 +++++++ common/api/map-layers-formats.api.md | 76 +++++++++++++++++++ common/api/summary/core-bentley.exports.csv | 2 + common/api/summary/core-common.exports.csv | 2 + .../summary/map-layers-formats.exports.csv | 12 ++- 7 files changed, 137 insertions(+), 1 deletion(-) diff --git a/common/api/core-bentley.api.md b/common/api/core-bentley.api.md index d44efa7fc986..c88ede68b3d4 100644 --- a/common/api/core-bentley.api.md +++ b/common/api/core-bentley.api.md @@ -224,6 +224,12 @@ export function compareNumbersOrUndefined(lhs?: number, rhs?: number): number; // @public (undocumented) export function comparePossiblyUndefined(compareDefined: (lhs: T, rhs: T) => number, lhs?: T, rhs?: T): number; +// @beta (undocumented) +export function compareSimpleArrays(lhs?: Array, rhs?: Array): number; + +// @beta (undocumented) +export function compareSimpleTypes(lhs: number | string | boolean, rhs: number | string | boolean): number; + // @public (undocumented) export function compareStrings(a: string, b: string): number; diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md index 9eecc43b2541..253a26a438f1 100644 --- a/common/api/core-common.api.md +++ b/common/api/core-common.api.md @@ -4789,6 +4789,8 @@ export interface ImageMapLayerProps extends CommonMapLayerProps { // @internal (undocumented) modelId?: never; // @beta + properties?: MapLayerProviderProperties; + // @beta queryParams?: { [key: string]: string; }; @@ -4823,6 +4825,8 @@ export class ImageMapLayerSettings extends MapLayerSettings { // (undocumented) password?: string; // @beta + readonly properties?: MapLayerProviderProperties; + // @beta savedQueryParams?: { [key: string]: string; }; @@ -5732,6 +5736,15 @@ export interface MapLayerKey { // @public export type MapLayerProps = ImageMapLayerProps | ModelMapLayerProps; +// @beta (undocumented) +export type MapLayerProviderArrayProperty = Array; + +// @beta (undocumented) +export interface MapLayerProviderProperties { + // (undocumented) + [key: string]: number | string | boolean | MapLayerProviderArrayProperty; +} + // @public export abstract class MapLayerSettings { // @internal diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index c35c94d6cc5d..19419ee50f17 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -1379,6 +1379,8 @@ export abstract class ArcGISImageryProvider extends MapLayerImageryProvider { export class ArcGISMapLayerImageryProvider extends ArcGISImageryProvider { constructor(settings: ImageMapLayerSettings); // (undocumented) + addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(cards: HTMLTableElement): void; // (undocumented) constructUrl(row: number, column: number, zoomLevel: number): Promise; @@ -1566,6 +1568,8 @@ export abstract class AuxCoordSystemState extends ElementState implements AuxCoo export class AzureMapsLayerImageryProvider extends MapLayerImageryProvider { constructor(settings: ImageMapLayerSettings); // (undocumented) + addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(cards: HTMLTableElement): void; // (undocumented) constructUrl(y: number, x: number, zoom: number): Promise; @@ -1859,6 +1863,8 @@ export class BingLocationProvider { export class BingMapsImageryLayerProvider extends MapLayerImageryProvider { constructor(settings: ImageMapLayerSettings); // (undocumented) + addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(cards: HTMLTableElement, vp: ScreenViewport): void; // (undocumented) constructUrl(row: number, column: number, zoomLevel: number): Promise; @@ -5262,6 +5268,8 @@ export class ImageryMapTile extends RealityTile { export class ImageryMapTileTree extends RealityTileTree { constructor(params: RealityTileTreeParams, _imageryLoader: ImageryTileLoader); // (undocumented) + addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(cards: HTMLTableElement, vp: ScreenViewport): void; // (undocumented) cartoRectangleFromQuadId(quadId: QuadId): MapCartoRectangle; @@ -6349,6 +6357,8 @@ export enum ManipulatorToolEvent { export class MapBoxLayerImageryProvider extends MapLayerImageryProvider { constructor(settings: ImageMapLayerSettings); // (undocumented) + addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(cards: HTMLTableElement): void; // (undocumented) constructUrl(row: number, column: number, zoomLevel: number): Promise; @@ -6523,6 +6533,8 @@ export type MapLayerFormatType = typeof MapLayerFormat; // @beta export abstract class MapLayerImageryProvider { constructor(_settings: ImageMapLayerSettings, _usesCachedTiles: boolean); + addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(_cards: HTMLTableElement, _viewport: ScreenViewport): void; // @internal protected appendCustomParams(url: string): string; @@ -6532,6 +6544,8 @@ export abstract class MapLayerImageryProvider { cartoRange?: MapCartoRectangle; // (undocumented) abstract constructUrl(row: number, column: number, zoomLevel: number): Promise; + // @internal (undocumented) + decorate(_context: DecorateContext): void; // @internal protected readonly defaultMaximumZoomLevel = 22; // @internal @@ -6771,6 +6785,8 @@ export abstract class MapLayerTileTreeReference extends TileTreeReference { // (undocumented) canSupplyToolTip(hit: HitDetail): boolean; // (undocumented) + decorate(_context: DecorateContext): void; + // (undocumented) getToolTip(hit: HitDetail): Promise; get imageryProvider(): MapLayerImageryProvider | undefined; // (undocumented) @@ -7099,6 +7115,8 @@ export class MapTileTree extends RealityTileTree { // @internal export class MapTileTreeReference extends TileTreeReference { constructor(settings: BackgroundMapSettings, _baseLayerSettings: BaseLayerSettings | undefined, _layerSettings: MapLayerSettings[], iModel: IModelConnection, tileUserId: number, isOverlay: boolean, _isDrape: boolean, _overrideTerrainDisplay?: CheckTerrainDisplayOverride | undefined); + addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(cards: HTMLTableElement, vp: ScreenViewport): void; addToScene(context: SceneContext): void; // (undocumented) @@ -7114,6 +7132,8 @@ export class MapTileTreeReference extends TileTreeReference { // (undocumented) protected _createGeometryTreeReference(): GeometryTileTreeReference | undefined; // (undocumented) + decorate(context: DecorateContext): void; + // (undocumented) discloseTileTrees(trees: DisclosedTileTreeSet): void; // (undocumented) forEachLayerTileTreeRef(func: (ref: TileTreeReference) => void): void; @@ -9644,6 +9664,8 @@ export interface RealityTileTreeParams extends TileTreeParams { export class RealityTreeReference extends RealityModelTileTree.Reference { constructor(props: RealityModelTileTree.ReferenceProps); // (undocumented) + addAttributions(cards: HTMLTableElement): Promise; + // @deprecated (undocumented) addLogoCards(cards: HTMLTableElement): void; // (undocumented) addToScene(context: SceneContext): void; @@ -11845,6 +11867,8 @@ export class TerrainDisplayOverrides { // @public export abstract class TerrainMeshProvider { + addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(_cards: HTMLTableElement, _vp: ScreenViewport): void; forceTileLoad(_tile: MapTile): boolean; getChildHeightRange(quadId: QuadId, rectangle: MapCartoRectangle, parent: MapTile): Range1d | undefined; @@ -12816,6 +12840,9 @@ export interface TileTreeParams { export abstract class TileTreeReference { // (undocumented) accumulateTransformedRange(range: Range3d, matrix: Matrix4d, frustumPlanes?: FrustumPlanes): void; + // @beta + addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise; + // @deprecated (undocumented) addLogoCards(_cards: HTMLTableElement, _vp: ScreenViewport): void; addToScene(context: SceneContext): void; canSupplyToolTip(_hit: HitDetail): boolean; diff --git a/common/api/map-layers-formats.api.md b/common/api/map-layers-formats.api.md index 6423ec7fa6cb..00684052826b 100644 --- a/common/api/map-layers-formats.api.md +++ b/common/api/map-layers-formats.api.md @@ -5,12 +5,14 @@ ```ts import { ArcGISImageryProvider } from '@itwin/core-frontend'; +import { BaseMapLayerSettings } from '@itwin/core-common'; import { BeButtonEvent } from '@itwin/core-frontend'; import { BeEvent } from '@itwin/core-bentley'; import { Cartographic } from '@itwin/core-common'; import { ColorDef } from '@itwin/core-common'; import { EventHandled } from '@itwin/core-frontend'; import { HitDetail } from '@itwin/core-frontend'; +import { ImageMapLayerProps } from '@itwin/core-common'; import { ImageMapLayerSettings } from '@itwin/core-common'; import { ImageryMapTileTree } from '@itwin/core-frontend'; import { ImageSource } from '@itwin/core-common'; @@ -18,9 +20,11 @@ import { Listener } from '@itwin/core-bentley'; import { Localization } from '@itwin/core-common'; import { LocateFilterStatus } from '@itwin/core-frontend'; import { LocateResponse } from '@itwin/core-frontend'; +import { MapCartoRectangle } from '@itwin/core-frontend'; import { MapFeatureInfo } from '@itwin/core-frontend'; import { MapFeatureInfoOptions } from '@itwin/core-frontend'; import { MapLayerFeatureInfo } from '@itwin/core-frontend'; +import { MapLayerProviderProperties } from '@itwin/core-common'; import { PrimitiveTool } from '@itwin/core-frontend'; import { QuadId } from '@itwin/core-frontend'; import { Transform } from '@itwin/core-geometry'; @@ -56,6 +60,17 @@ export class ArcGisFeatureProvider extends ArcGISImageryProvider { get tileSize(): number; } +// @beta +export interface CreateSessionOptions { + apiOptions?: string[]; + language: string; + layerTypes?: LayerTypes[]; + mapType: MapTypes; + overlay?: boolean; + region: string; + scale?: ScaleFactors; +} + // @internal (undocumented) export class DefaultArcGiSymbology implements FeatureDefaultSymbology { // (undocumented) @@ -72,6 +87,33 @@ export class DefaultArcGiSymbology implements FeatureDefaultSymbology { initialize(): Promise; } +// @beta +export const GoogleMaps: { + createMapLayerSettings: (name?: string, opts?: CreateSessionOptions) => ImageMapLayerSettings; + createBaseLayerSettings: (opts?: CreateSessionOptions) => BaseMapLayerSettings; +}; + +// @beta +export interface GoogleMapsSession { + expiry: number; + imageFormat: string; + session: string; + tileHeight: number; + tileWidth: number; +} + +// @internal +export const _internal: { + createMapLayerProps: (name?: string, opts?: CreateSessionOptions) => ImageMapLayerProps; + createSession: (apiKey: string, opts: CreateSessionOptions) => Promise; + createPropertiesFromSessionOptions: (opts: CreateSessionOptions) => MapLayerProviderProperties; + getViewportInfo: (params: ViewportInfoRequestParams) => Promise; + registerFormatIfNeeded: () => void; +}; + +// @beta (undocumented) +export type LayerTypes = "layerRoadmap" | "layerStreetview"; + // @beta export class MapFeatureInfoTool extends PrimitiveTool { // (undocumented) @@ -126,6 +168,40 @@ export interface MapLayersFormatsConfig { localization?: Localization; } +// @beta (undocumented) +export type MapTypes = "roadmap" | "satellite" | "terrain"; + +// @beta +export interface MaxZoomRectangle { + // (undocumented) + east: number; + // (undocumented) + maxZoom: number; + // (undocumented) + north: number; + // (undocumented) + south: number; + // (undocumented) + west: number; +} + +// @beta (undocumented) +export type ScaleFactors = "scaleFactor1x" | "scaleFactor2x" | "scaleFactor4x"; + +// @beta +export interface ViewportInfo { + copyright: string; + maxZoomRects: MaxZoomRectangle[]; +} + +// @beta +export interface ViewportInfoRequestParams { + key: string; + rectangle: MapCartoRectangle; + session: string; + zoom: number; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/common/api/summary/core-bentley.exports.csv b/common/api/summary/core-bentley.exports.csv index 37cc23cb9cd6..5bfcdd72352f 100644 --- a/common/api/summary/core-bentley.exports.csv +++ b/common/api/summary/core-bentley.exports.csv @@ -25,6 +25,8 @@ public;function;compareBooleansOrUndefined public;function;compareNumbers public;function;compareNumbersOrUndefined public;function;comparePossiblyUndefined +beta;function;compareSimpleArrays +beta;function;compareSimpleTypes public;function;compareStrings public;function;compareStringsOrUndefined public;function;compareWithTolerance diff --git a/common/api/summary/core-common.exports.csv b/common/api/summary/core-common.exports.csv index 4fa92b7dbb7d..ab49a6890df3 100644 --- a/common/api/summary/core-common.exports.csv +++ b/common/api/summary/core-common.exports.csv @@ -491,6 +491,8 @@ public;interface;MapImageryProps public;class;MapImagerySettings public;interface;MapLayerKey public;type;MapLayerProps +beta;type;MapLayerProviderArrayProperty +beta;interface;MapLayerProviderProperties public;class;MapLayerSettings public;interface;MapSubLayerProps public;class;MapSubLayerSettings diff --git a/common/api/summary/map-layers-formats.exports.csv b/common/api/summary/map-layers-formats.exports.csv index d25ca026d468..96808077ca61 100644 --- a/common/api/summary/map-layers-formats.exports.csv +++ b/common/api/summary/map-layers-formats.exports.csv @@ -1,8 +1,18 @@ sep=; Release Tag;API Item Type;API Item Name internal;class;ArcGisFeatureProvider +beta;interface;CreateSessionOptions internal;class;DefaultArcGiSymbology +beta;const;GoogleMaps +beta;interface;GoogleMapsSession +internal;const;_internal +beta;type;LayerTypes beta;class;MapFeatureInfoTool beta;interface;MapFeatureInfoToolData beta;class;MapLayersFormats -beta;interface;MapLayersFormatsConfig \ No newline at end of file +beta;interface;MapLayersFormatsConfig +beta;type;MapTypes +beta;interface;MaxZoomRectangle +beta;type;ScaleFactors +beta;interface;ViewportInfo +beta;interface;ViewportInfoRequestParams \ No newline at end of file From 1c2f24145f6ba11afa988c0bd56af142c4ed7354 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 12 Feb 2025 16:01:34 -0500 Subject: [PATCH 32/44] Attempt to better expose @internal api --- .../src/GoogleMaps/GoogleMaps.ts | 122 +----------------- .../GoogleMaps/GoogleMapsImageryProvider.ts | 6 +- .../src/internal/GoogleMapsUtils.ts | 114 ++++++++++++++++ .../src/internal/cross-package.ts | 5 + .../src/map-layers-formats.ts | 1 + .../src/test/GoogleMaps/GoogleMaps.test.ts | 17 +-- 6 files changed, 131 insertions(+), 134 deletions(-) create mode 100644 extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts create mode 100644 extensions/map-layers-formats/src/internal/cross-package.ts diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index 4b88eb22578b..347e3a8ebdcb 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -1,10 +1,7 @@ -import { BaseMapLayerSettings, ImageMapLayerProps, ImageMapLayerSettings, MapLayerProviderProperties } from "@itwin/core-common"; -import { IModelApp, MapCartoRectangle } from "@itwin/core-frontend"; -import { GoogleMapsMapLayerFormat } from "./GoogleMapsImageryFormat"; -import { Angle } from "@itwin/core-geometry"; -import { Logger } from "@itwin/core-bentley"; +import { BaseMapLayerSettings, ImageMapLayerSettings } from "@itwin/core-common"; +import { MapCartoRectangle } from "@itwin/core-frontend"; +import { GoogleMapsUtils } from "../internal/GoogleMapsUtils"; -const loggerCategory = "MapLayersFormats.GoogleMaps"; /** @beta*/ export type LayerTypes = "layerRoadmap" | "layerStreetview"; @@ -141,103 +138,6 @@ export interface ViewportInfoRequestParams { } -/** - * Creates a Google Maps session. - * @param apiKey Google Cloud API key - * @param opts Options to create the session - * @internal -*/ -const createSession = async (apiKey: string, opts: CreateSessionOptions): Promise => { - const url = `https://tile.googleapis.com/v1/createSession?key=${apiKey}`; - const request = new Request(url, {method: "POST", body: JSON.stringify(opts)}); - const response = await fetch (request); - if (!response.ok) { - throw new Error(`CreateSession request failed: ${response.status} - ${response.statusText}`); - } - Logger.logInfo(loggerCategory, `Session created successfully`); - return response.json(); -}; - -/** - * Register the google maps format if it is not already registered. - * @internal -*/ -const registerFormatIfNeeded = () => { - if (!IModelApp.mapLayerFormatRegistry.isRegistered(GoogleMapsMapLayerFormat.formatId)) { - IModelApp.mapLayerFormatRegistry.register(GoogleMapsMapLayerFormat); - } -} - - /** - * Creates a Google Maps layer props. - * @param name Name of the layer (Defaults to "GoogleMaps") - * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) - * @internal -*/ -const createMapLayerProps = (name: string = "GoogleMaps", opts?: CreateSessionOptions): ImageMapLayerProps => { - _internal.registerFormatIfNeeded(); - - return { - formatId: GoogleMapsMapLayerFormat.formatId, - url: "", - name, - properties: createPropertiesFromSessionOptions(opts ?? {mapType: "satellite", language: "en-US", region: "US:", layerTypes: ["layerRoadmap"]}), - }; -}; - -/** -* Retrieves the maximum zoom level available within a bounding rectangle. -* @param rectangle The bounding rectangle -* @returns The maximum zoom level available within the bounding rectangle. -* @internal -*/ -const getViewportInfo = async (params: ViewportInfoRequestParams): Promise=> { - const {rectangle, session, key, zoom} = params; - const north = Angle.radiansToDegrees(rectangle.north); - const south = Angle.radiansToDegrees(rectangle.south); - const east = Angle.radiansToDegrees(rectangle.east); - const west = Angle.radiansToDegrees(rectangle.west); - const url = `https://tile.googleapis.com/tile/v1/viewport?session=${session}&key=${key}&zoom=${zoom}&north=${north}&south=${south}&east=${east}&west=${west}`; - const request = new Request(url, {method: "GET"}); - const response = await fetch (request); - if (!response.ok) { - return undefined; - } - const json = await response.json(); - return json as ViewportInfo;; -}; - - /** - * Converts the session options to provider properties - * @param opts Options to create the session - * @internal -*/ -const createPropertiesFromSessionOptions = (opts: CreateSessionOptions): MapLayerProviderProperties => { - const properties: MapLayerProviderProperties = { - mapType: opts.mapType, - language: opts.language, - region: opts.region, - } - - if (opts.layerTypes !== undefined) { - properties.layerTypes = [...opts.layerTypes]; - } - - if (opts.scale !== undefined) { - properties.scale = opts.scale; - } - - if (opts.overlay !== undefined) { - properties.overlay = opts.overlay; - } - - if (opts.apiOptions !== undefined) { - properties.apiOptions = [...opts.apiOptions]; - } - - return properties; -}; - /** * Google Maps API * @beta @@ -256,7 +156,7 @@ export const GoogleMaps = { * @beta */ createMapLayerSettings: (name?: string, opts?: CreateSessionOptions) => { - return ImageMapLayerSettings.fromJSON(_internal.createMapLayerProps(name, opts)); + return ImageMapLayerSettings.fromJSON(GoogleMapsUtils.createMapLayerProps(name, opts)); }, /** @@ -269,18 +169,6 @@ export const GoogleMaps = { * @beta */ createBaseLayerSettings: (opts?: CreateSessionOptions) => { - return BaseMapLayerSettings.fromJSON(_internal.createMapLayerProps("GoogleMaps", opts)); + return BaseMapLayerSettings.fromJSON(GoogleMapsUtils.createMapLayerProps("GoogleMaps", opts)); } -}; - -/** - * Internal function export for testing purposes, do not use. - * @internal - * */ -export const _internal = { - createMapLayerProps, - createSession, - createPropertiesFromSessionOptions, - getViewportInfo, - registerFormatIfNeeded, }; \ No newline at end of file diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 57b1777d3df9..6b9376b685fa 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -1,8 +1,9 @@ import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile } from "@itwin/core-frontend"; -import { _internal, CreateSessionOptions, GoogleMapsSession, LayerTypes, MapTypes, ScaleFactors } from "./GoogleMaps"; +import { CreateSessionOptions, GoogleMapsSession, LayerTypes, MapTypes, ScaleFactors } from "./GoogleMaps"; import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; import { GoogleMapsDecorator } from "./GoogleMapDecorator"; +import { GoogleMapsUtils } from "../internal/GoogleMapsUtils"; const loggerCategory = "MapLayersFormats.GoogleMaps"; const levelToken = "{level}"; const rowToken = "{row}"; @@ -10,9 +11,6 @@ const columnToken = "{column}"; const urlTemplate = `https://tile.googleapis.com/v1/2dtiles/${levelToken}/${columnToken}/${rowToken}`; -// eslint-disable-next-line @typescript-eslint/naming-convention -const GoogleMapsUtils = _internal; - /* * Google Maps imagery provider * @internal diff --git a/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts b/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts new file mode 100644 index 000000000000..83a019eb7865 --- /dev/null +++ b/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ + +import { IModelApp } from "@itwin/core-frontend"; +import { GoogleMapsMapLayerFormat } from "../GoogleMaps/GoogleMapsImageryFormat"; +import { Logger } from "@itwin/core-bentley"; +import { ImageMapLayerProps, MapLayerProviderProperties } from "@itwin/core-common"; +import { Angle } from "@itwin/core-geometry"; +import { CreateSessionOptions, GoogleMapsSession, ViewportInfo, ViewportInfoRequestParams } from "../GoogleMaps/GoogleMaps"; + +const loggerCategory = "MapLayersFormats.GoogleMaps"; + +/** @internal */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const GoogleMapsUtils = { + /** + * Creates a Google Maps session. + * @param apiKey Google Cloud API key + * @param opts Options to create the session + * @internal + */ + createSession: async (apiKey: string, opts: CreateSessionOptions): Promise => { + const url = `https://tile.googleapis.com/v1/createSession?key=${apiKey}`; + const request = new Request(url, {method: "POST", body: JSON.stringify(opts)}); + const response = await fetch (request); + if (!response.ok) { + throw new Error(`CreateSession request failed: ${response.status} - ${response.statusText}`); + } + Logger.logInfo(loggerCategory, `Session created successfully`); + return response.json(); + }, + + /** + * Register the google maps format if it is not already registered. + * @internal + */ + registerFormatIfNeeded: () => { + if (!IModelApp.mapLayerFormatRegistry.isRegistered(GoogleMapsMapLayerFormat.formatId)) { + IModelApp.mapLayerFormatRegistry.register(GoogleMapsMapLayerFormat); + } + }, + + /** + * Creates a Google Maps layer props. + * @param name Name of the layer (Defaults to "GoogleMaps") + * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) + * @internal +*/ + createMapLayerProps: (name: string = "GoogleMaps", opts?: CreateSessionOptions): ImageMapLayerProps => { + GoogleMapsUtils.registerFormatIfNeeded(); + + return { + formatId: GoogleMapsMapLayerFormat.formatId, + url: "", + name, + properties: GoogleMapsUtils.createPropertiesFromSessionOptions(opts ?? {mapType: "satellite", language: "en-US", region: "US:", layerTypes: ["layerRoadmap"]}), + }; + }, + + /** + * Retrieves the maximum zoom level available within a bounding rectangle. + * @param rectangle The bounding rectangle + * @returns The maximum zoom level available within the bounding rectangle. + * @internal + */ + getViewportInfo: async (params: ViewportInfoRequestParams): Promise=> { + const {rectangle, session, key, zoom} = params; + const north = Angle.radiansToDegrees(rectangle.north); + const south = Angle.radiansToDegrees(rectangle.south); + const east = Angle.radiansToDegrees(rectangle.east); + const west = Angle.radiansToDegrees(rectangle.west); + const url = `https://tile.googleapis.com/tile/v1/viewport?session=${session}&key=${key}&zoom=${zoom}&north=${north}&south=${south}&east=${east}&west=${west}`; + const request = new Request(url, {method: "GET"}); + const response = await fetch (request); + if (!response.ok) { + return undefined; + } + const json = await response.json(); + return json as ViewportInfo;; + }, + + /** + * Converts the session options to provider properties + * @param opts Options to create the session + * @internal + */ + createPropertiesFromSessionOptions: (opts: CreateSessionOptions): MapLayerProviderProperties => { + const properties: MapLayerProviderProperties = { + mapType: opts.mapType, + language: opts.language, + region: opts.region, + } + + if (opts.layerTypes !== undefined) { + properties.layerTypes = [...opts.layerTypes]; + } + + if (opts.scale !== undefined) { + properties.scale = opts.scale; + } + + if (opts.overlay !== undefined) { + properties.overlay = opts.overlay; + } + + if (opts.apiOptions !== undefined) { + properties.apiOptions = [...opts.apiOptions]; + } + + return properties; + }, +} \ No newline at end of file diff --git a/extensions/map-layers-formats/src/internal/cross-package.ts b/extensions/map-layers-formats/src/internal/cross-package.ts new file mode 100644 index 000000000000..cd8333b507f9 --- /dev/null +++ b/extensions/map-layers-formats/src/internal/cross-package.ts @@ -0,0 +1,5 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +export { GoogleMapsUtils } from "./GoogleMapsUtils"; \ No newline at end of file diff --git a/extensions/map-layers-formats/src/map-layers-formats.ts b/extensions/map-layers-formats/src/map-layers-formats.ts index cbb94179613b..543d53653d02 100644 --- a/extensions/map-layers-formats/src/map-layers-formats.ts +++ b/extensions/map-layers-formats/src/map-layers-formats.ts @@ -7,6 +7,7 @@ export * from "./mapLayersFormats"; export * from "./ArcGisFeature/ArcGisFeatureProvider"; export * from "./Tools/MapFeatureInfoTool"; export * from "./GoogleMaps/GoogleMaps"; +export * from "./internal/cross-package"; /** @docs-package-description diff --git a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts index 08ab1527c6f6..501c19b0071c 100644 --- a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts +++ b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts @@ -7,14 +7,14 @@ import * as sinon from "sinon"; import { Frustum, ImageMapLayerSettings } from "@itwin/core-common"; import { expect } from "chai"; import { GoogleMapsImageryProvider } from "../../GoogleMaps/GoogleMapsImageryProvider"; -import { _internal , CreateSessionOptions, GoogleMaps, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; +import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; +import { GoogleMapsUtils } from "../../internal/cross-package"; + import { fakeJsonFetch } from "../TestUtils"; import { LogoDecoration } from "../../GoogleMaps/GoogleMapDecorator"; import { DecorateContext, Decorations, IconSprites, IModelApp, MapCartoRectangle, MapTile, MapTileTree, QuadId, ScreenViewport, Sprite, TilePatch } from "@itwin/core-frontend"; import { Range3d } from "@itwin/core-geometry"; -// eslint-disable-next-line @typescript-eslint/naming-convention -const GoogleMapsUtils = _internal; class FakeMapTile extends MapTile { public override depth: number; @@ -30,15 +30,6 @@ class FakeMapTile extends MapTile { } } -// const getTestSettings = (properties?: MapLayerProviderProperties) => { -// return GoogleMaps.createBaseLayerSettings({ -// name: "test", -// url: "", -// formatId: "GoogleMaps", -// properties -// }); -// }; - const createProvider = (settings: ImageMapLayerSettings) => { settings.accessKey = {key: "key", value: "dummyKey"}; return new GoogleMapsImageryProvider(settings); @@ -48,7 +39,7 @@ const stubCreateSession = (sandbox:sinon.SinonSandbox, session: GoogleMapsSessi return session; }); -const minCreateSessionOptions: CreateSessionOptions = {mapType: "satellite", language: "en-US", region: "US"} +const minCreateSessionOptions: GoogleMapsUtils.CreateSessionOptions = {mapType: "satellite", language: "en-US", region: "US"} const createSessionOptions2: CreateSessionOptions = {...minCreateSessionOptions, layerTypes: ["layerRoadmap"]}; const defaultPngSession = {tileWidth: 256, tileHeight: 256, imageFormat: "image/png", expiry: 0, session: "dummySession"}; From 3fa218d0723550455e08b97398a7d3031ececd25 Mon Sep 17 00:00:00 2001 From: pmconne <22944042+pmconne@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:23:26 -0500 Subject: [PATCH 33/44] adjust internal APIs --- .../map-layers-formats/src/internal/GoogleMapsUtils.ts | 2 +- extensions/map-layers-formats/src/internal/cross-package.ts | 5 ----- extensions/map-layers-formats/src/map-layers-formats.ts | 2 -- .../src/test/GoogleMaps/GoogleMaps.test.ts | 4 ++-- 4 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 extensions/map-layers-formats/src/internal/cross-package.ts diff --git a/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts b/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts index 83a019eb7865..c875da7c06c3 100644 --- a/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts +++ b/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts @@ -111,4 +111,4 @@ export const GoogleMapsUtils = { return properties; }, -} \ No newline at end of file +} diff --git a/extensions/map-layers-formats/src/internal/cross-package.ts b/extensions/map-layers-formats/src/internal/cross-package.ts deleted file mode 100644 index cd8333b507f9..000000000000 --- a/extensions/map-layers-formats/src/internal/cross-package.ts +++ /dev/null @@ -1,5 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Bentley Systems, Incorporated. All rights reserved. -* See LICENSE.md in the project root for license terms and full copyright notice. -*--------------------------------------------------------------------------------------------*/ -export { GoogleMapsUtils } from "./GoogleMapsUtils"; \ No newline at end of file diff --git a/extensions/map-layers-formats/src/map-layers-formats.ts b/extensions/map-layers-formats/src/map-layers-formats.ts index 543d53653d02..79b89448206e 100644 --- a/extensions/map-layers-formats/src/map-layers-formats.ts +++ b/extensions/map-layers-formats/src/map-layers-formats.ts @@ -7,8 +7,6 @@ export * from "./mapLayersFormats"; export * from "./ArcGisFeature/ArcGisFeatureProvider"; export * from "./Tools/MapFeatureInfoTool"; export * from "./GoogleMaps/GoogleMaps"; -export * from "./internal/cross-package"; - /** @docs-package-description * This package provides support for additional map layer formats that are not included in the @itwin/core-frontend package. diff --git a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts index 501c19b0071c..2b49a829df39 100644 --- a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts +++ b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts @@ -8,7 +8,7 @@ import { Frustum, ImageMapLayerSettings } from "@itwin/core-common"; import { expect } from "chai"; import { GoogleMapsImageryProvider } from "../../GoogleMaps/GoogleMapsImageryProvider"; import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; -import { GoogleMapsUtils } from "../../internal/cross-package"; +import { GoogleMapsUtils } from "../../internal/GoogleMapsUtils"; import { fakeJsonFetch } from "../TestUtils"; import { LogoDecoration } from "../../GoogleMaps/GoogleMapDecorator"; @@ -39,7 +39,7 @@ const stubCreateSession = (sandbox:sinon.SinonSandbox, session: GoogleMapsSessi return session; }); -const minCreateSessionOptions: GoogleMapsUtils.CreateSessionOptions = {mapType: "satellite", language: "en-US", region: "US"} +const minCreateSessionOptions: CreateSessionOptions = {mapType: "satellite", language: "en-US", region: "US"} const createSessionOptions2: CreateSessionOptions = {...minCreateSessionOptions, layerTypes: ["layerRoadmap"]}; const defaultPngSession = {tileWidth: 256, tileHeight: 256, imageFormat: "image/png", expiry: 0, session: "dummySession"}; From c550bccbf42c1fd1481d5cfcaf07047744be5bea Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 12 Feb 2025 16:57:00 -0500 Subject: [PATCH 34/44] extract-api --- common/api/map-layers-formats.api.md | 11 ----------- common/api/summary/map-layers-formats.exports.csv | 1 - 2 files changed, 12 deletions(-) diff --git a/common/api/map-layers-formats.api.md b/common/api/map-layers-formats.api.md index 00684052826b..61f5c1894519 100644 --- a/common/api/map-layers-formats.api.md +++ b/common/api/map-layers-formats.api.md @@ -12,7 +12,6 @@ import { Cartographic } from '@itwin/core-common'; import { ColorDef } from '@itwin/core-common'; import { EventHandled } from '@itwin/core-frontend'; import { HitDetail } from '@itwin/core-frontend'; -import { ImageMapLayerProps } from '@itwin/core-common'; import { ImageMapLayerSettings } from '@itwin/core-common'; import { ImageryMapTileTree } from '@itwin/core-frontend'; import { ImageSource } from '@itwin/core-common'; @@ -24,7 +23,6 @@ import { MapCartoRectangle } from '@itwin/core-frontend'; import { MapFeatureInfo } from '@itwin/core-frontend'; import { MapFeatureInfoOptions } from '@itwin/core-frontend'; import { MapLayerFeatureInfo } from '@itwin/core-frontend'; -import { MapLayerProviderProperties } from '@itwin/core-common'; import { PrimitiveTool } from '@itwin/core-frontend'; import { QuadId } from '@itwin/core-frontend'; import { Transform } from '@itwin/core-geometry'; @@ -102,15 +100,6 @@ export interface GoogleMapsSession { tileWidth: number; } -// @internal -export const _internal: { - createMapLayerProps: (name?: string, opts?: CreateSessionOptions) => ImageMapLayerProps; - createSession: (apiKey: string, opts: CreateSessionOptions) => Promise; - createPropertiesFromSessionOptions: (opts: CreateSessionOptions) => MapLayerProviderProperties; - getViewportInfo: (params: ViewportInfoRequestParams) => Promise; - registerFormatIfNeeded: () => void; -}; - // @beta (undocumented) export type LayerTypes = "layerRoadmap" | "layerStreetview"; diff --git a/common/api/summary/map-layers-formats.exports.csv b/common/api/summary/map-layers-formats.exports.csv index 96808077ca61..13da2cd3ac83 100644 --- a/common/api/summary/map-layers-formats.exports.csv +++ b/common/api/summary/map-layers-formats.exports.csv @@ -5,7 +5,6 @@ beta;interface;CreateSessionOptions internal;class;DefaultArcGiSymbology beta;const;GoogleMaps beta;interface;GoogleMapsSession -internal;const;_internal beta;type;LayerTypes beta;class;MapFeatureInfoTool beta;interface;MapFeatureInfoToolData From c9c0f0d19e45aa9b375f2cc604a866ddfb603bba Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Thu, 13 Feb 2025 11:09:46 -0500 Subject: [PATCH 35/44] Fixed tests failures and improved existing tests --- .../src/test/tile/map/ImageryTileTree.test.ts | 13 +++- core/frontend/src/tile/map/ImageryTileTree.ts | 63 ++++++++++--------- core/frontend/src/tile/map/MapTileTree.ts | 5 +- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/core/frontend/src/test/tile/map/ImageryTileTree.test.ts b/core/frontend/src/test/tile/map/ImageryTileTree.test.ts index ae09fb6ffbad..8673db63ffab 100644 --- a/core/frontend/src/test/tile/map/ImageryTileTree.test.ts +++ b/core/frontend/src/test/tile/map/ImageryTileTree.test.ts @@ -58,7 +58,7 @@ describe("ImageryTileTree", () => { vi.restoreAllMocks(); }); - it("tree supplier", async () => { + it.only("tree supplier", async () => { const baseProps: ImageMapLayerProps = { formatId: "Custom1", url: "https://dummy.com", name: "CustomLayer", subLayers: [{name: "sub0", visible: true}]}; const dataset: DatasetEntry[] = [ {lhs: {...baseProps}, rhs: {...baseProps}, expectSameTileTree:true}, @@ -67,8 +67,19 @@ describe("ImageryTileTree", () => { {lhs: {...baseProps, formatId:"Custom2"}, rhs: {...baseProps}, expectSameTileTree:false}, {lhs: {...baseProps, subLayers: [{name: "sub0", visible: false}]}, rhs: {...baseProps}, expectSameTileTree:false}, {lhs: {...baseProps, subLayers: [{name: "sub1", visible: true}]}, rhs: {...baseProps}, expectSameTileTree:false}, + {lhs: {...baseProps, properties: {key: "value"}}, rhs: {...baseProps}, expectSameTileTree:false}, + {lhs: {...baseProps}, rhs: {...baseProps, properties: {key: "value"}}, expectSameTileTree:false}, + {lhs: {...baseProps, properties: {key: "value"}}, rhs: {...baseProps, properties: {key: "value"}}, expectSameTileTree:true}, + {lhs: {...baseProps, properties: {key: [1,2,3]}}, rhs: {...baseProps}, expectSameTileTree:false}, + {lhs: {...baseProps}, rhs: {...baseProps, properties: {key: [1,2,3]}}, expectSameTileTree:false}, + {lhs: {...baseProps, properties: {key: "value"}}, rhs: {...baseProps, properties: {key: [1,2,3]}}, expectSameTileTree:false}, + {lhs: {...baseProps, properties: {key: [1,2,3,4]}}, rhs: {...baseProps, properties: {key: [1,2,3]}}, expectSameTileTree:false}, + {lhs: {...baseProps, properties: {key: [1,2,3]}}, rhs: {...baseProps, properties: {key: [1,2,3,4]}}, expectSameTileTree:false}, + {lhs: {...baseProps, properties: {key: [1,2,3]}}, rhs: {...baseProps, properties: {key: [1,2,3]}}, expectSameTileTree:true}, ]; + let i = 1; for (const entry of dataset) { + console.log(`Test ${i++}`); const settingsLhs = ImageMapLayerSettings.fromJSON(entry.lhs); const treeRefLhs = new ImageryMapLayerTreeReference({ layerSettings: settingsLhs, layerIndex: 0, iModel: imodel }); const treeOwnerLhs = treeRefLhs.treeOwner; diff --git a/core/frontend/src/tile/map/ImageryTileTree.ts b/core/frontend/src/tile/map/ImageryTileTree.ts index bc9a628346b8..438b94720d33 100644 --- a/core/frontend/src/tile/map/ImageryTileTree.ts +++ b/core/frontend/src/tile/map/ImageryTileTree.ts @@ -347,40 +347,45 @@ class ImageryMapLayerTreeSupplier implements TileTreeSupplier { if (0 === cmp) { cmp = compareBooleans(lhs.settings.transparentBackground, rhs.settings.transparentBackground); if (0 === cmp) { - if (lhs.settings.properties && rhs.settings.properties) { - const lhsKeysLength = Object.keys(lhs.settings.properties).length; - const rhsKeysLength = Object.keys(rhs.settings.properties).length; - - if (lhsKeysLength !== rhsKeysLength) { - cmp = lhsKeysLength - rhsKeysLength; - } else { - for (const key of Object.keys(lhs.settings.properties)) { - const lhsProp = lhs.settings.properties[key]; - const rhsProp = rhs.settings.properties[key]; - if (typeof lhsProp !== typeof rhsProp) { - cmp = 1; - break; - } - if (Array.isArray(lhsProp) || Array.isArray(rhsProp)) { - cmp = compareSimpleArrays(lhsProp as (number | string | boolean)[], rhsProp as (number | string | boolean)[]); - if (0 !== cmp) - break; - } else { - cmp = compareSimpleTypes(lhsProp, rhsProp); - if (0 !== cmp) + if (lhs.settings.properties || rhs.settings.properties) { + if (lhs.settings.properties && rhs.settings.properties) { + const lhsKeysLength = Object.keys(lhs.settings.properties).length; + const rhsKeysLength = Object.keys(rhs.settings.properties).length; + + if (lhsKeysLength !== rhsKeysLength) { + cmp = lhsKeysLength - rhsKeysLength; + } else { + for (const key of Object.keys(lhs.settings.properties)) { + const lhsProp = lhs.settings.properties[key]; + const rhsProp = rhs.settings.properties[key]; + if (typeof lhsProp !== typeof rhsProp) { + cmp = 1; break; + } + if (Array.isArray(lhsProp) || Array.isArray(rhsProp)) { + cmp = compareSimpleArrays(lhsProp as (number | string | boolean)[], rhsProp as (number | string | boolean)[]); + if (0 !== cmp) + break; + } else { + cmp = compareSimpleTypes(lhsProp, rhsProp); + if (0 !== cmp) + break; + } } } + } else if (!lhs.settings.properties) { + cmp = 1; + } else { + cmp = -1; } - + } + if (0 === cmp) { + cmp = compareNumbers(lhs.settings.subLayers.length, rhs.settings.subLayers.length); if (0 === cmp) { - cmp = compareNumbers(lhs.settings.subLayers.length, rhs.settings.subLayers.length); - if (0 === cmp) { - for (let i = 0; i < lhs.settings.subLayers.length && 0 === cmp; i++) { - cmp = compareStrings(lhs.settings.subLayers[i].name, rhs.settings.subLayers[i].name); - if (0 === cmp) { - cmp = compareBooleans(lhs.settings.subLayers[i].visible, rhs.settings.subLayers[i].visible); - } + for (let i = 0; i < lhs.settings.subLayers.length && 0 === cmp; i++) { + cmp = compareStrings(lhs.settings.subLayers[i].name, rhs.settings.subLayers[i].name); + if (0 === cmp) { + cmp = compareBooleans(lhs.settings.subLayers[i].visible, rhs.settings.subLayers[i].visible); } } } diff --git a/core/frontend/src/tile/map/MapTileTree.ts b/core/frontend/src/tile/map/MapTileTree.ts index b0db08834200..4b18a46e2572 100644 --- a/core/frontend/src/tile/map/MapTileTree.ts +++ b/core/frontend/src/tile/map/MapTileTree.ts @@ -1263,7 +1263,10 @@ export class MapTileTreeReference extends TileTreeReference { } public override decorate(context: DecorateContext): void { - this.forEachLayerTileTreeRef((ref) => ref.decorate(context)); + for (const layerTree of this._layerTrees) { + if (layerTree) + layerTree.decorate(context); + } } } From af4e8f278bc1af43eda6b2825cc8ad68914dcb5e Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Thu, 13 Feb 2025 11:14:03 -0500 Subject: [PATCH 36/44] clean up --- core/frontend/src/test/tile/map/ImageryTileTree.test.ts | 4 +--- .../map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/frontend/src/test/tile/map/ImageryTileTree.test.ts b/core/frontend/src/test/tile/map/ImageryTileTree.test.ts index 8673db63ffab..e93b0c604001 100644 --- a/core/frontend/src/test/tile/map/ImageryTileTree.test.ts +++ b/core/frontend/src/test/tile/map/ImageryTileTree.test.ts @@ -58,7 +58,7 @@ describe("ImageryTileTree", () => { vi.restoreAllMocks(); }); - it.only("tree supplier", async () => { + it("tree supplier", async () => { const baseProps: ImageMapLayerProps = { formatId: "Custom1", url: "https://dummy.com", name: "CustomLayer", subLayers: [{name: "sub0", visible: true}]}; const dataset: DatasetEntry[] = [ {lhs: {...baseProps}, rhs: {...baseProps}, expectSameTileTree:true}, @@ -77,9 +77,7 @@ describe("ImageryTileTree", () => { {lhs: {...baseProps, properties: {key: [1,2,3]}}, rhs: {...baseProps, properties: {key: [1,2,3,4]}}, expectSameTileTree:false}, {lhs: {...baseProps, properties: {key: [1,2,3]}}, rhs: {...baseProps, properties: {key: [1,2,3]}}, expectSameTileTree:true}, ]; - let i = 1; for (const entry of dataset) { - console.log(`Test ${i++}`); const settingsLhs = ImageMapLayerSettings.fromJSON(entry.lhs); const treeRefLhs = new ImageryMapLayerTreeReference({ layerSettings: settingsLhs, layerIndex: 0, iModel: imodel }); const treeOwnerLhs = treeRefLhs.treeOwner; diff --git a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts index 2bbc49f3c5d2..8b59ba8e4af8 100644 --- a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts +++ b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts @@ -44,7 +44,7 @@ const minCreateSessionOptions: CreateSessionOptions = {mapType: "satellite", lan const createSessionOptions2: CreateSessionOptions = {...minCreateSessionOptions, layerTypes: ["layerRoadmap"]}; const defaultPngSession = {tileWidth: 256, tileHeight: 256, imageFormat: "image/png", expiry: 0, session: "dummySession"}; -describe.only("GoogleMapsProvider", () => { +describe("GoogleMapsProvider", () => { const sandbox = sinon.createSandbox(); beforeEach(async () => { From 307a33f8dc57b1908b95703e31bd4d5b5e5c3a91 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Thu, 13 Feb 2025 11:25:20 -0500 Subject: [PATCH 37/44] api extract --- common/api/core-frontend.api.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index 714b5ce6cbd4..aa56fdbb2f18 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -1273,8 +1273,6 @@ export abstract class ArcGISImageryProvider extends MapLayerImageryProvider { get supportsMapFeatureInfo(): boolean; } - addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise; - // @deprecated (undocumented) // @internal export interface ArcGISServiceMetadata { accessTokenRequired: boolean; @@ -1404,8 +1402,6 @@ export abstract class AuxCoordSystemState extends ElementState implements AuxCoo type: number; } - addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise; - // @deprecated (undocumented) // @internal export class BackgroundMapGeometry { constructor(_bimElevationBias: number, globeMode: GlobeMode, _iModel: IModelConnection); @@ -1667,8 +1663,6 @@ export class BingLocationProvider { getLocation(query: string): Promise; } - addAttributions(cards: HTMLTableElement, vp: ScreenViewport): Promise; - // @deprecated (undocumented) // @public export class BlankConnection extends IModelConnection { close(): Promise; @@ -5515,8 +5509,6 @@ export enum ManipulatorToolEvent { Suspend = 3, // (undocumented) Unsuspend = 4 - addAttributions(cards: HTMLTableElement, _vp: ScreenViewport): Promise; - // @deprecated (undocumented) } // @public @@ -8185,8 +8177,6 @@ export class RealityTileTree extends TileTree { readonly yAxisUp: boolean; } - addAttributions(cards: HTMLTableElement): Promise; - // @deprecated (undocumented) // @beta export function registerWorker(impl: WorkerImplementation): void; From e45000291f8443e1bd4814a8de69d544d633259d Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Mon, 17 Feb 2025 09:04:53 -0500 Subject: [PATCH 38/44] fix dta configuration --- test-apps/display-test-app/src/common/DtaConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-apps/display-test-app/src/common/DtaConfiguration.ts b/test-apps/display-test-app/src/common/DtaConfiguration.ts index cee7b49ef130..0542aa85ec9f 100644 --- a/test-apps/display-test-app/src/common/DtaConfiguration.ts +++ b/test-apps/display-test-app/src/common/DtaConfiguration.ts @@ -34,6 +34,7 @@ export interface DtaBooleanConfiguration { ignoreCache?: boolean; // default is undefined, set to true to delete a cached version of a remote imodel before opening it. noElectronAuth?: boolean; // if true, don't initialize auth client. It currently has a bug that produces an exception on every attempt to obtain access token, i.e., every RPC call. noImdlWorker?: boolean; // if true, parse iMdl content on main thread instead of web worker (easier to debug). + googleMapsUi?: boolean; // if set, a Google Maps toolbar icon will be displayed in the UI } export interface DtaStringConfiguration { @@ -56,7 +57,6 @@ export interface DtaStringConfiguration { oidcScope?: string; // default is undefined, used for auth setup oidcRedirectUri?: string; // default is undefined, used for auth setup frontendTilesUrlTemplate?: string; // if set, specifies url for @itwin/frontend-tiles to obtain tile trees for spatial views. See README.md - googleMapsUi?: boolean; // if set, a Google Maps toolbar icon will be displayed in the UI } export interface DtaNumberConfiguration { From ed4524f4a2bd1fa6983284baae4b0db104b8858b Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Mon, 17 Feb 2025 14:20:22 -0500 Subject: [PATCH 39/44] added module tags --- .../map-layers-formats/src/GoogleMaps/GoogleMaps.ts | 8 ++++++++ .../src/GoogleMaps/GoogleMapsImageryFormat.ts | 3 +++ .../src/GoogleMaps/GoogleMapsImageryProvider.ts | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index 347e3a8ebdcb..76e7f1d5119b 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -1,3 +1,11 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module MapLayersFormats + */ + import { BaseMapLayerSettings, ImageMapLayerSettings } from "@itwin/core-common"; import { MapCartoRectangle } from "@itwin/core-frontend"; import { GoogleMapsUtils } from "../internal/GoogleMapsUtils"; diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryFormat.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryFormat.ts index 6ee4a8e92a02..f99374fe68fe 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryFormat.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryFormat.ts @@ -2,6 +2,9 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module MapLayersFormats + */ import { ImageMapLayerSettings } from "@itwin/core-common"; import { ImageryMapLayerFormat, MapLayerImageryProvider } from "@itwin/core-frontend"; diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 6b9376b685fa..1036fea4dcf8 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -1,3 +1,11 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module MapLayersFormats + */ + import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile } from "@itwin/core-frontend"; import { CreateSessionOptions, GoogleMapsSession, LayerTypes, MapTypes, ScaleFactors } from "./GoogleMaps"; From 11951d2b09c47140c5e5cef6524da6847ee68c9f Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:40:39 -0500 Subject: [PATCH 40/44] fix slash direction --- docs/changehistory/NextVersion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 0b7757d5a52d..72b1340410e5 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -100,7 +100,7 @@ The unified selection system has been part of `@itwin/presentation-frontend` for ### Google Maps 2D tiles API -The `itwin\map-layers-formats` package now includes an API for consuming Google Maps 2D tiles. +The `itwin/map-layers-formats` package now includes an API for consuming Google Maps 2D tiles. To enable it as a base map, it's simple as: From 383519ee057aab16612abdb066323b2e451d571e Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:41:01 -0500 Subject: [PATCH 41/44] clarify deprecation statement --- docs/changehistory/NextVersion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 72b1340410e5..ccdd85d37dc4 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -168,7 +168,7 @@ ds.attachMapLayer({ - Deprecated [HiliteSet.setHilite]($core-frontend) - use `add`, `remove`, `replace` methods instead. -- Deprecated [addLogoCards]($core-frontend)-related APIs: +- Deprecated synchronous [addLogoCards]($core-frontend)-related APIs in favor of new asynchronous ones: - `TileTreeReference.addLogoCard` : use `addAttributions` method instead - `MapLayerImageryProvider.addLogoCard` : use `addAttributions` method instead From 31c3752c3f93d6ced68fdb6050e9f3951c63924c Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Tue, 18 Feb 2025 15:51:33 -0500 Subject: [PATCH 42/44] added example code --- common/config/rush/pnpm-lock.yaml | 3 + docs/changehistory/NextVersion.md | 20 ++--- example-code/snippets/package.json | 1 + .../snippets/src/frontend/GoogleMaps.ts | 78 +++++++++++++++++++ 4 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 example-code/snippets/src/frontend/GoogleMaps.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 0a0ef4240b57..2ed21a014fb0 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1794,6 +1794,9 @@ importers: '@itwin/itwins-client': specifier: ^1.2.0 version: 1.2.0 + '@itwin/map-layers-formats': + specifier: workspace:* + version: link:../../extensions/map-layers-formats '@itwin/service-authorization': specifier: ^1.0.0 version: 1.0.0(@itwin/core-bentley@..+core+bentley)(@itwin/core-geometry@..+core+geometry) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 0b7757d5a52d..0678fb25b6e4 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -111,17 +111,17 @@ ds.backgroundMapBase = GoogleMaps.createBaseLayerSettings(); ``` Can also be attached as a map-layer: -```typescript -ds.attachMapLayer({ - mapLayerIndex: {index: 0, isOverlay: false}, - settings: GoogleMaps.createMapLayerSettings("GoogleMaps")}); + +```ts +[[include:GoogleMaps_AttachMapLayerSimple]] ``` + > ***IMPORTANT***: Make sure to configure your Google Cloud's API key in the `MapLayerOptions` when starting your IModelApp application: + +```ts +[[include:GoogleMaps_SetGoogleMapsApiKey]] ``` - mapLayerOptions: { - GoogleMaps: { key: "key", value: "YOUR_KEY_HERE" } - } -``` + ## API deprecations ### @itwin/core-bentley @@ -148,7 +148,7 @@ ds.attachMapLayer({ } ``` - > Note that while public types with deterministic cleanup logic in iTwin.js will continue to implement _both_ `IDisposable` and `Disposable` until the former is fully removed in iTwin.js 7.0 (in accordance with our [API support policy](../learning/api-support-policies)), disposable objects should still only be disposed once - _either_ with [IDisposable.dispose]($core-bentley) _or_ `Symbol.dispose()` but not both! Where possible, prefer `using` declarations or the [dispose]($core-bentley) helper function over directly calling either method. + > Note that while public types with deterministic cleanup logic in iTwin.js will continue to implement *both* `IDisposable` and `Disposable` until the former is fully removed in iTwin.js 7.0 (in accordance with our [API support policy](../learning/api-support-policies)), disposable objects should still only be disposed once - *either* with [IDisposable.dispose]($core-bentley) *or* `Symbol.dispose()` but not both! Where possible, prefer `using` declarations or the [dispose]($core-bentley) helper function over directly calling either method. ### @itwin/core-common @@ -554,7 +554,7 @@ Starting from version 5.x, iTwin.js has transitioned from using the merge method The merging process in this method follows these steps: -1. Initially, each incoming change is attempted to be applied using the _fast-forward_ method. If successful, the process is complete. +1. Initially, each incoming change is attempted to be applied using the *fast-forward* method. If successful, the process is complete. 2. If the fast-forward method fails for any incoming change, that changeset is abandoned and the rebase method is used instead. 3. The rebase process is executed as follows: - All local transactions are reversed. diff --git a/example-code/snippets/package.json b/example-code/snippets/package.json index 3ca4d5be1ae8..4e6c9e70809e 100644 --- a/example-code/snippets/package.json +++ b/example-code/snippets/package.json @@ -32,6 +32,7 @@ "@itwin/ecschema-metadata": "workspace:*", "@itwin/imodel-transformer": "^0.4.2", "@itwin/itwins-client": "^1.2.0", + "@itwin/map-layers-formats": "workspace:*", "@itwin/service-authorization": "^1.0.0", "@xmldom/xmldom": "~0.8.5", "azurite": "^3.33.0", diff --git a/example-code/snippets/src/frontend/GoogleMaps.ts b/example-code/snippets/src/frontend/GoogleMaps.ts new file mode 100644 index 000000000000..c906f21bc1f6 --- /dev/null +++ b/example-code/snippets/src/frontend/GoogleMaps.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ + +import { IModelApp } from "@itwin/core-frontend"; +import {GoogleMaps} from "@itwin/map-layers-formats"; + +// __PUBLISH_EXTRACT_START__ GoogleMaps_BaseMapSimple +function setGoogleMapsBaseMap() { + const vp = IModelApp.viewManager.selectedView; + if (vp) { + const ds = vp.displayStyle; + ds.backgroundMapBase = GoogleMaps.createBaseLayerSettings(); + } + } +// __PUBLISH_EXTRACT_END__ +// __PUBLISH_EXTRACT_START__ GoogleMaps_BaseMapOpts +function setGoogleMapsBaseMapOpts() { + const vp = IModelApp.viewManager.selectedView; + if (vp) { + const ds = vp.displayStyle; + ds.backgroundMapBase = GoogleMaps.createBaseLayerSettings({ + mapType: "satellite", + layerTypes: ["layerRoadmap"], + language: "en-US", + region: "US" + }); + } +} +// __PUBLISH_EXTRACT_END__ +// __PUBLISH_EXTRACT_START__ GoogleMaps_AttachMapLayerSimple +function attachGoogleMapsMapLayerSimple() { + const vp = IModelApp.viewManager.selectedView; + if (vp) { + const ds = vp.displayStyle; + ds.attachMapLayer({ + mapLayerIndex: {index: 0, isOverlay: false}, + settings: GoogleMaps.createMapLayerSettings("GoogleMaps")}); + } +} +// __PUBLISH_EXTRACT_END__ +// __PUBLISH_EXTRACT_START__ GoogleMaps_AttachMapLayerSimple +function attachGoogleMapsMapLayerOpts() { + const vp = IModelApp.viewManager.selectedView; + if (vp) { + const ds = vp.displayStyle; + ds.attachMapLayer({ + mapLayerIndex: {index: 0, isOverlay: false}, + settings: GoogleMaps.createMapLayerSettings("GoogleMaps", { + mapType: "roadmap", + layerTypes: ["layerRoadmap"], + overlay: true, + language: "en-US", + region: "US" + } + )}); + } +} +// __PUBLISH_EXTRACT_END__ +// __PUBLISH_EXTRACT_START__ GoogleMaps_SetGoogleMapsApiKey +async function setGoogleMapsApiKey() { + await IModelApp.startup({ + applicationId: "myAppId", + mapLayerOptions: { + // eslint-disable-next-line @typescript-eslint/naming-convention + GoogleMaps: { + key: "key", value: "abc123" + } + } + }); +} +// __PUBLISH_EXTRACT_END__ +setGoogleMapsBaseMap(); +setGoogleMapsBaseMapOpts(); +attachGoogleMapsMapLayerSimple(); +attachGoogleMapsMapLayerOpts(); +setGoogleMapsApiKey().catch(() => {}); \ No newline at end of file From 093c74017d3a1e6bfd31a63174fda2734d8f4cd9 Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 19 Feb 2025 08:18:48 -0500 Subject: [PATCH 43/44] Prefixed GoogleMaps's types with `GoogleMaps` --- .../src/GoogleMaps/GoogleMapDecorator.ts | 4 +-- .../src/GoogleMaps/GoogleMaps.ts | 18 ++++++------ .../GoogleMaps/GoogleMapsImageryProvider.ts | 14 +++++----- .../src/internal/GoogleMapsUtils.ts | 8 +++--- .../src/test/GoogleMaps/GoogleMaps.test.ts | 8 +++--- .../src/frontend/GoogleMaps.ts | 28 +++++++++---------- 6 files changed, 40 insertions(+), 40 deletions(-) diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts index 5667178fafab..89670440bcb4 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapDecorator.ts @@ -1,6 +1,6 @@ import { CanvasDecoration, DecorateContext, Decorator, IconSprites, IModelApp, Sprite } from "@itwin/core-frontend"; import { Point3d } from "@itwin/core-geometry"; -import { MapTypes } from "./GoogleMaps"; +import { GoogleMapsMapTypes } from "./GoogleMaps"; /** A simple decorator that show logo at the a given screen position. @@ -73,7 +73,7 @@ export class GoogleMapsDecorator implements Decorator { public readonly logo = new LogoDecoration(); /** Activate the logo based on the given map type. */ - public async activate(mapType: MapTypes): Promise { + public async activate(mapType: GoogleMapsMapTypes): Promise { // Pick the logo that is the most visible on the background map const imageName = mapType === "roadmap" ? "google_on_white_hdpi" : diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts index 76e7f1d5119b..287823f3d826 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMaps.ts @@ -12,17 +12,17 @@ import { GoogleMapsUtils } from "../internal/GoogleMapsUtils"; /** @beta*/ -export type LayerTypes = "layerRoadmap" | "layerStreetview"; +export type GoogleMapsLayerTypes = "layerRoadmap" | "layerStreetview"; /** @beta*/ -export type MapTypes = "roadmap"|"satellite"|"terrain"; +export type GoogleMapsMapTypes = "roadmap"|"satellite"|"terrain"; /** @beta*/ -export type ScaleFactors = "scaleFactor1x" | "scaleFactor2x" | "scaleFactor4x"; +export type GoogleMapsScaleFactors = "scaleFactor1x" | "scaleFactor2x" | "scaleFactor4x"; /** * Represents the options to create a Google Maps session. * @beta */ -export interface CreateSessionOptions { +export interface GoogleMapsCreateSessionOptions { /** * The type of base map. * @@ -33,7 +33,7 @@ export interface CreateSessionOptions { * `terrain`: Terrain imagery. When selecting `terrain` as the map type, you must also include the `layerRoadmap` layer type. * @beta * */ - mapType: MapTypes, + mapType: GoogleMapsMapTypes, /** * An {@link https://en.wikipedia.org/wiki/IETF_language_tag | IETF language tag} that specifies the language used to display information on the tiles. For example, `en-US` specifies the English language as spoken in the United States. */ @@ -52,7 +52,7 @@ export interface CreateSessionOptions { * * @beta * */ - layerTypes?: LayerTypes[]; + layerTypes?: GoogleMapsLayerTypes[]; /** * Scales-up the size of map elements (such as road labels), while retaining the tile size and coverage area of the default tile. @@ -65,7 +65,7 @@ export interface CreateSessionOptions { * `scaleFactor4x`: Quadruples label size and removes minor feature labels. * @beta * */ - scale?: ScaleFactors + scale?: GoogleMapsScaleFactors /** * A boolean value that specifies whether layerTypes should be rendered as a separate overlay, or combined with the base imagery. @@ -163,7 +163,7 @@ export const GoogleMaps = { * settings: GoogleMaps.createMapLayerSettings("GoogleMaps")}); * @beta */ - createMapLayerSettings: (name?: string, opts?: CreateSessionOptions) => { + createMapLayerSettings: (name?: string, opts?: GoogleMapsCreateSessionOptions) => { return ImageMapLayerSettings.fromJSON(GoogleMapsUtils.createMapLayerProps(name, opts)); }, @@ -176,7 +176,7 @@ export const GoogleMaps = { * ds.backgroundMapBase = GoogleMaps.createBaseLayerSettings(); * @beta */ - createBaseLayerSettings: (opts?: CreateSessionOptions) => { + createBaseLayerSettings: (opts?: GoogleMapsCreateSessionOptions) => { return BaseMapLayerSettings.fromJSON(GoogleMapsUtils.createMapLayerProps("GoogleMaps", opts)); } }; \ No newline at end of file diff --git a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts index 1036fea4dcf8..2c82053a285b 100644 --- a/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts +++ b/extensions/map-layers-formats/src/GoogleMaps/GoogleMapsImageryProvider.ts @@ -8,7 +8,7 @@ import { ImageMapLayerSettings, ImageSource } from "@itwin/core-common"; import { DecorateContext, IModelApp, MapCartoRectangle, MapLayerImageryProvider, MapLayerSourceStatus, MapLayerSourceValidation, MapTile, ScreenViewport, Tile } from "@itwin/core-frontend"; -import { CreateSessionOptions, GoogleMapsSession, LayerTypes, MapTypes, ScaleFactors } from "./GoogleMaps"; +import { GoogleMapsCreateSessionOptions, GoogleMapsLayerTypes, GoogleMapsMapTypes, GoogleMapsScaleFactors, GoogleMapsSession } from "./GoogleMaps"; import { BentleyError, BentleyStatus, Logger } from "@itwin/core-bentley"; import { GoogleMapsDecorator } from "./GoogleMapDecorator"; import { GoogleMapsUtils } from "../internal/GoogleMapsUtils"; @@ -70,7 +70,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { } this._tileSize = session.tileWidth; // assuming here tiles are square - const isActivated = await this._decorator.activate(this._settings.properties!.mapType as MapTypes); + const isActivated = await this._decorator.activate(this._settings.properties!.mapType as GoogleMapsMapTypes); if (!isActivated) { const msg = `Failed to activate decorator`; Logger.logError(loggerCategory, msg); @@ -78,7 +78,7 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { } } - private createCreateSessionOptions(): CreateSessionOptions { + private createCreateSessionOptions(): GoogleMapsCreateSessionOptions { const layerPropertyKeys = this._settings.properties ? Object.keys(this._settings.properties) : undefined; if (layerPropertyKeys === undefined || !layerPropertyKeys.includes("mapType") || @@ -89,18 +89,18 @@ export class GoogleMapsImageryProvider extends MapLayerImageryProvider { throw new BentleyError(BentleyStatus.ERROR, msg); } - const createSessionOptions: CreateSessionOptions = { - mapType: this._settings.properties!.mapType as MapTypes, + const createSessionOptions: GoogleMapsCreateSessionOptions = { + mapType: this._settings.properties!.mapType as GoogleMapsMapTypes, region: this._settings.properties!.region as string, language: this._settings.properties!.language as string, } if (this._settings.properties?.layerTypes !== undefined) { - createSessionOptions.layerTypes = this._settings.properties.layerTypes as LayerTypes[]; + createSessionOptions.layerTypes = this._settings.properties.layerTypes as GoogleMapsLayerTypes[]; } if (this._settings.properties?.scale !== undefined) { - createSessionOptions.scale = this._settings.properties.scale as ScaleFactors; + createSessionOptions.scale = this._settings.properties.scale as GoogleMapsScaleFactors; } if (this._settings.properties?.overlay !== undefined) { diff --git a/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts b/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts index c875da7c06c3..705fed7b402a 100644 --- a/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts +++ b/extensions/map-layers-formats/src/internal/GoogleMapsUtils.ts @@ -8,7 +8,7 @@ import { GoogleMapsMapLayerFormat } from "../GoogleMaps/GoogleMapsImageryFormat" import { Logger } from "@itwin/core-bentley"; import { ImageMapLayerProps, MapLayerProviderProperties } from "@itwin/core-common"; import { Angle } from "@itwin/core-geometry"; -import { CreateSessionOptions, GoogleMapsSession, ViewportInfo, ViewportInfoRequestParams } from "../GoogleMaps/GoogleMaps"; +import { GoogleMapsCreateSessionOptions, GoogleMapsSession, ViewportInfo, ViewportInfoRequestParams } from "../GoogleMaps/GoogleMaps"; const loggerCategory = "MapLayersFormats.GoogleMaps"; @@ -21,7 +21,7 @@ export const GoogleMapsUtils = { * @param opts Options to create the session * @internal */ - createSession: async (apiKey: string, opts: CreateSessionOptions): Promise => { + createSession: async (apiKey: string, opts: GoogleMapsCreateSessionOptions): Promise => { const url = `https://tile.googleapis.com/v1/createSession?key=${apiKey}`; const request = new Request(url, {method: "POST", body: JSON.stringify(opts)}); const response = await fetch (request); @@ -48,7 +48,7 @@ export const GoogleMapsUtils = { * @param opts Options to create the session (Defaults to satellite map type, English language, US region, and roadmap layer type) * @internal */ - createMapLayerProps: (name: string = "GoogleMaps", opts?: CreateSessionOptions): ImageMapLayerProps => { + createMapLayerProps: (name: string = "GoogleMaps", opts?: GoogleMapsCreateSessionOptions): ImageMapLayerProps => { GoogleMapsUtils.registerFormatIfNeeded(); return { @@ -86,7 +86,7 @@ export const GoogleMapsUtils = { * @param opts Options to create the session * @internal */ - createPropertiesFromSessionOptions: (opts: CreateSessionOptions): MapLayerProviderProperties => { + createPropertiesFromSessionOptions: (opts: GoogleMapsCreateSessionOptions): MapLayerProviderProperties => { const properties: MapLayerProviderProperties = { mapType: opts.mapType, language: opts.language, diff --git a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts index 8b59ba8e4af8..345862462339 100644 --- a/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts +++ b/extensions/map-layers-formats/src/test/GoogleMaps/GoogleMaps.test.ts @@ -7,7 +7,7 @@ import * as sinon from "sinon"; import { Frustum, ImageMapLayerSettings } from "@itwin/core-common"; import { expect } from "chai"; import { GoogleMapsImageryProvider } from "../../GoogleMaps/GoogleMapsImageryProvider"; -import { CreateSessionOptions, GoogleMaps, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; +import { GoogleMaps, GoogleMapsCreateSessionOptions, GoogleMapsSession, ViewportInfoRequestParams } from "../../map-layers-formats"; import { GoogleMapsUtils } from "../../internal/GoogleMapsUtils"; import { fakeJsonFetch } from "../TestUtils"; @@ -36,12 +36,12 @@ const createProvider = (settings: ImageMapLayerSettings) => { return new GoogleMapsImageryProvider(settings); } -const stubCreateSession = (sandbox:sinon.SinonSandbox, session: GoogleMapsSession) => sandbox.stub(GoogleMapsUtils, "createSession").callsFake(async function _(_apiKey: string, _opts: CreateSessionOptions) { +const stubCreateSession = (sandbox:sinon.SinonSandbox, session: GoogleMapsSession) => sandbox.stub(GoogleMapsUtils, "createSession").callsFake(async function _(_apiKey: string, _opts: GoogleMapsCreateSessionOptions) { return session; }); -const minCreateSessionOptions: CreateSessionOptions = {mapType: "satellite", language: "en-US", region: "US"} -const createSessionOptions2: CreateSessionOptions = {...minCreateSessionOptions, layerTypes: ["layerRoadmap"]}; +const minCreateSessionOptions: GoogleMapsCreateSessionOptions = {mapType: "satellite", language: "en-US", region: "US"} +const createSessionOptions2: GoogleMapsCreateSessionOptions = {...minCreateSessionOptions, layerTypes: ["layerRoadmap"]}; const defaultPngSession = {tileWidth: 256, tileHeight: 256, imageFormat: "image/png", expiry: 0, session: "dummySession"}; describe("GoogleMapsProvider", () => { diff --git a/test-apps/display-test-app/src/frontend/GoogleMaps.ts b/test-apps/display-test-app/src/frontend/GoogleMaps.ts index 317a7063e01f..b571d05beb0c 100644 --- a/test-apps/display-test-app/src/frontend/GoogleMaps.ts +++ b/test-apps/display-test-app/src/frontend/GoogleMaps.ts @@ -14,7 +14,7 @@ import { import { BackgroundMapType, BaseMapLayerSettings, ContourGroupProps, ImageMapLayerSettings } from "@itwin/core-common"; import { Viewport } from "@itwin/core-frontend"; import { ToolBarDropDown } from "./ToolBar"; -import { CreateSessionOptions, GoogleMaps, LayerTypes, MapTypes, ScaleFactors } from "@itwin/map-layers-formats"; +import { GoogleMaps, GoogleMapsCreateSessionOptions, GoogleMapsLayerTypes, GoogleMapsMapTypes, GoogleMapsScaleFactors } from "@itwin/map-layers-formats"; // size of widget or panel const winSize = { top: 0, left: 0, width: 318, height: 300 }; @@ -28,9 +28,9 @@ export class GoogleMapsSettings implements Disposable { private _overlay: boolean = false; private _newStyle: boolean = false; private _mapTypesCombobox: ComboBox; - private _mapType: MapTypes; - private _scaleFactor: ScaleFactors = "scaleFactor1x"; - private _layerTypes: LayerTypes[] = []; + private _mapType: GoogleMapsMapTypes; + private _scaleFactor: GoogleMapsScaleFactors = "scaleFactor1x"; + private _layerTypes: GoogleMapsLayerTypes[] = []; private _lang = "en-US"; private _roadmapLayerCheckox: CheckBox|undefined; @@ -60,22 +60,22 @@ export class GoogleMapsSettings implements Disposable { if (googleLayer) { const opts = googleLayer.properties; if (opts) { - this._mapType = opts.mapType as MapTypes; - this._layerTypes = opts.layerTypes as LayerTypes[] ?? []; + this._mapType = opts.mapType as GoogleMapsMapTypes; + this._layerTypes = opts.layerTypes as GoogleMapsLayerTypes[] ?? []; this._lang = opts.language as string ?? "en-US"; - this._scaleFactor = opts.scale as ScaleFactors ?? "scaleFactor1x"; + this._scaleFactor = opts.scale as GoogleMapsScaleFactors ?? "scaleFactor1x"; } } if (isGoogleBase) { const baseSettings = vp.displayStyle.backgroundMapBase as BaseMapLayerSettings; const properties = baseSettings.properties; - this._mapType = (properties?.mapType??"") as MapTypes; + this._mapType = (properties?.mapType??"") as GoogleMapsMapTypes; - this._layerTypes = (properties?.layerTypes ?? []) as LayerTypes[]; + this._layerTypes = (properties?.layerTypes ?? []) as GoogleMapsLayerTypes[]; this._lang = (properties?.language??"") as string; if (properties?.scale) - this._scaleFactor = properties.scale as ScaleFactors; + this._scaleFactor = properties.scale as GoogleMapsScaleFactors; if (properties?.apiOptions){ const apiOptions = properties.apiOptions as string[]; @@ -112,7 +112,7 @@ export class GoogleMapsSettings implements Disposable { id: "google_map_type_cbx", value: this._mapType, handler: (cbx) => { - this._mapType = cbx.value as MapTypes; + this._mapType = cbx.value as GoogleMapsMapTypes; if (this._mapType === "terrain" && !this._layerTypes.includes("layerRoadmap")) { this._layerTypes.push("layerRoadmap"); } @@ -207,7 +207,7 @@ export class GoogleMapsSettings implements Disposable { id: "google_scale_factors_cbx", value: this._scaleFactor, handler: (cbx) => { - this._scaleFactor = cbx.value as ScaleFactors; + this._scaleFactor = cbx.value as GoogleMapsScaleFactors; }, }); this._element.appendChild(document.createElement("br")); @@ -284,7 +284,7 @@ export class GoogleMapsSettings implements Disposable { url: "https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer", name: "ESRI World Imagery" }); - const opts: CreateSessionOptions = { + const opts: GoogleMapsCreateSessionOptions = { mapType: "satellite", language: "en-US", region: "US", @@ -295,7 +295,7 @@ export class GoogleMapsSettings implements Disposable { this._vp.displayStyle.attachMapLayer({mapLayerIndex: {index: 0, isOverlay: false}, settings: GoogleMaps.createMapLayerSettings("GoogleMaps", opts)}); } else { removeExistingMapLayer(false); - const opts: CreateSessionOptions = { + const opts: GoogleMapsCreateSessionOptions = { mapType: this._mapType, layerTypes: this._layerTypes, language: this._lang, From 2059859fe4f68fdfa042f0ab11f80c1932ef9d8f Mon Sep 17 00:00:00 2001 From: mdastous-bentley Date: Wed, 19 Feb 2025 11:26:43 -0500 Subject: [PATCH 44/44] PR comments --- core/bentley/src/Compare.ts | 26 ++++++++++++++----- core/common/src/MapLayerSettings.ts | 13 +++++++--- core/frontend/src/tile/map/ImageryTileTree.ts | 9 +++---- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/core/bentley/src/Compare.ts b/core/bentley/src/Compare.ts index a231470fc94c..d1c79c4df201 100644 --- a/core/bentley/src/Compare.ts +++ b/core/bentley/src/Compare.ts @@ -93,14 +93,19 @@ export function areEqualPossiblyUndefined(t: T | undefined, u: U | undefin return areEqual(t, u); } -/** @beta */ +/** + * Compare two simples types (number, string, boolean) + * This essentially wraps the existing type-specific comparison functions + * @beta */ export function compareSimpleTypes(lhs: number | string | boolean, rhs: number | string | boolean): number { + let cmp = 0; + // Make sure the types are the same - if (typeof lhs !== typeof rhs) { - return 1; + cmp = compareStrings(typeof lhs, typeof rhs); + if (cmp !== 0) { + return cmp; } - let cmp = 0; // Compare actual values switch (typeof lhs) { case "number": @@ -115,9 +120,18 @@ export function compareSimpleTypes(lhs: number | string | boolean, rhs: number | } return cmp; } +/** + * An array of simple types (number, string, boolean) + * @beta + */ +export type SimpleTypesArray = number[] | string[] | boolean[]; + +/** + * Compare two arrays of simple types (number, string, boolean) + * @beta + */ -/** @beta */ -export function compareSimpleArrays (lhs?: Array, rhs?: Array ) { +export function compareSimpleArrays (lhs?: SimpleTypesArray, rhs?: SimpleTypesArray ) { if (undefined === lhs) return undefined === rhs ? 0 : -1; else if (undefined === rhs) diff --git a/core/common/src/MapLayerSettings.ts b/core/common/src/MapLayerSettings.ts index 27d42dcec690..f981cf1bd002 100644 --- a/core/common/src/MapLayerSettings.ts +++ b/core/common/src/MapLayerSettings.ts @@ -19,9 +19,16 @@ export type ImageryMapLayerFormatId = "ArcGIS" | "BingMaps" | "MapboxImagery" | /** @public */ export type SubLayerId = string | number; -/** @beta */ -export type MapLayerProviderArrayProperty = Array; -/** @beta */ +/** + * Type for map layer provider array property. + * @beta + */ +export type MapLayerProviderArrayProperty = number[] | string[] | boolean[]; + +/** + * Type for map layer provider properties. + * @beta + */ export interface MapLayerProviderProperties { [key: string]: number | string | boolean | MapLayerProviderArrayProperty }; /** JSON representation of the settings associated with a map sublayer included within a [[MapLayerProps]]. diff --git a/core/frontend/src/tile/map/ImageryTileTree.ts b/core/frontend/src/tile/map/ImageryTileTree.ts index 438b94720d33..a67b04c42ff3 100644 --- a/core/frontend/src/tile/map/ImageryTileTree.ts +++ b/core/frontend/src/tile/map/ImageryTileTree.ts @@ -8,7 +8,7 @@ import { assert, compareBooleans, compareNumbers, compareSimpleArrays, compareSimpleTypes, compareStrings, compareStringsOrUndefined, dispose, Logger,} from "@itwin/core-bentley"; import { Angle, Range3d, Transform } from "@itwin/core-geometry"; -import { Cartographic, ImageMapLayerSettings, ImageSource, MapLayerSettings, RenderTexture, ViewFlagOverrides } from "@itwin/core-common"; +import { Cartographic, ImageMapLayerSettings, ImageSource, MapLayerProviderArrayProperty, MapLayerSettings, RenderTexture, ViewFlagOverrides } from "@itwin/core-common"; import { IModelApp } from "../../IModelApp"; import { IModelConnection } from "../../IModelConnection"; import { RenderMemory } from "../../render/RenderMemory"; @@ -358,12 +358,11 @@ class ImageryMapLayerTreeSupplier implements TileTreeSupplier { for (const key of Object.keys(lhs.settings.properties)) { const lhsProp = lhs.settings.properties[key]; const rhsProp = rhs.settings.properties[key]; - if (typeof lhsProp !== typeof rhsProp) { - cmp = 1; + cmp = compareStrings(typeof lhsProp, typeof rhsProp); + if (0 !== cmp) break; - } if (Array.isArray(lhsProp) || Array.isArray(rhsProp)) { - cmp = compareSimpleArrays(lhsProp as (number | string | boolean)[], rhsProp as (number | string | boolean)[]); + cmp = compareSimpleArrays(lhsProp as MapLayerProviderArrayProperty, rhsProp as MapLayerProviderArrayProperty); if (0 !== cmp) break; } else {