diff --git a/modules/3d-tiles/src/index.ts b/modules/3d-tiles/src/index.ts index 5224b838d2..026c0e8f44 100644 --- a/modules/3d-tiles/src/index.ts +++ b/modules/3d-tiles/src/index.ts @@ -28,3 +28,7 @@ export type { ImplicitTilingExensionData } from './types'; export type {Tiles3DLoaderOptions} from './tiles-3d-loader'; + +// EXPERIMENTAL SERVICES +export {Google3DTilesService as _Google3DTilesService} from './services/google-3d-tiles-service'; +export {CesiumIONService as _CesiumIONService} from './services/cesium-ion-service'; diff --git a/modules/3d-tiles/src/lib/ion/ion.ts b/modules/3d-tiles/src/lib/ion/ion.ts index 5c71bfa354..cbabfbd963 100644 --- a/modules/3d-tiles/src/lib/ion/ion.ts +++ b/modules/3d-tiles/src/lib/ion/ion.ts @@ -1,3 +1,4 @@ +// loaders.gl, MIT license // Minimal support to load tilsets from the Cesium ION services import {fetchFile} from '@loaders.gl/core'; diff --git a/modules/3d-tiles/src/services/cesium-ion-service.ts b/modules/3d-tiles/src/services/cesium-ion-service.ts new file mode 100644 index 0000000000..bfe0f87ec4 --- /dev/null +++ b/modules/3d-tiles/src/services/cesium-ion-service.ts @@ -0,0 +1,134 @@ +// loaders.gl, MIT license + +import {Tiles3DService} from '@loaders.gl/tiles'; +import {CesiumIonLoader} from '../cesium-ion-loader'; +import {fetchFile} from '@loaders.gl/core'; + +const CESIUM_ION_URL = 'https://api.cesium.com/v1/assets'; + +export type IONAsset = { + type: '3DTILES' | 'GLTF' | 'IMAGERY' | 'TERRAIN' | 'KML' | 'GEOJSON'; + id: string; + name: string; + description: string; + attribution: string; +}; + +export type IONAssetMetadata = { + type: '3DTILES' | 'GLTF' | 'IMAGERY' | 'TERRAIN' | 'KML' | 'GEOJSON'; + + // Asset info + id: string; + name: string; + description: string; + attribution: string; + + // Endpoint info + url: string; + /** Resource specific access token valid for ~1 hour. Re-request metadata to refresh */ + accessToken: string; + attributions: { + html: string; + collapsible?: boolean; + }[]; +}; + +/** + * Attribution for Cesium ion. + * @see https://cesium.com/legal/terms-of-service/ + */ +export class CesiumIONService extends Tiles3DService { + readonly id = 'cesium'; + readonly name = 'Cesium ion'; + readonly urlKey = 'ion.cesium'; + readonly attribution = { + title: 'Cesium.', + url: 'https://cesium.com/' + }; + + /** @todo remove CesiumIONLoader, integrate into service? */ + readonly loader = CesiumIonLoader; + + async getAssetCatalog(): Promise { + if (!this.accessToken) { + throw new Error(`No access token for ${this.name}`); + } + + const url = CESIUM_ION_URL; + const response = await fetchFile(url, {headers: {Authorization: `Bearer ${this.accessToken}`}}); + if (!response.ok) { + throw new Error(response.statusText); + } + const assetJson = await response.json(); + + const assets = assetJson.items; + // Remove any pending or errored assets + return assets.filter((asset) => asset.status === 'COMPLETE') as IONAsset[]; + } + + /** + * Retrieves metadata information about a specific ION asset. + * @param assetId + * @returns {url, headers, type, attributions} for an ion tileset + */ + async getAssetMetadata(assetId: string): Promise { + if (!this.accessToken) { + throw new Error(`No access token for ${this.name}`); + } + + // Retrieves metadata information about a specific asset. + // @see https://cesium.com/docs/rest-api/#operation/getAsset + const url = `${CESIUM_ION_URL}/${assetId}`; + let response = await fetchFile(`${url}`, { + headers: {Authorization: `Bearer ${this.accessToken}`} + }); + if (!response.ok) { + throw new Error(response.statusText); + } + let metadata = await response.json(); + if (metadata.status !== 'COMPLETE') { + throw new Error(`Incomplete ION asset ${assetId}`); + } + + // Retrieves information and credentials that allow you to access the tiled asset data for visualization and analysis. + // https://cesium.com/docs/rest-api/#operation/getAssetEndpoint + response = await fetchFile(`${url}/endpoint`, { + headers: {Authorization: `Bearer ${this.accessToken}`} + }); + if (!response.ok) { + throw new Error(response.statusText); + } + const tilesetInfo = await response.json(); + + // extract dataset description + metadata = { + ...metadata, + ...tilesetInfo, + headers: { + Authorization: `Bearer ${tilesetInfo.accessToken}` + } + }; + + return metadata as IONAssetMetadata; + } + + async getMetadataForUrl(url: string): Promise { + const parsedUrl = this.parseUrl(url); + if (!parsedUrl) { + throw new Error(`Invalid url ${url}`); + } + const assetMetadata = await this.getAssetMetadata(parsedUrl.assetId); + // return {name: this.name, ...assetMetadata}; + return assetMetadata; + } + + parseUrl(url: string): {assetId: string; resource: string} | null { + const matched = /\/([0-9]+)\/tileset.json/.exec(this.url); + const assetId = matched && matched[1]; + return assetId ? {assetId, resource: 'tileset.json'} : null; + } + + getUrl(tileUrl: string): string { + return `${tileUrl}?key=${this.accessToken}`; + } +} diff --git a/modules/3d-tiles/src/services/google-3d-tiles-service.ts b/modules/3d-tiles/src/services/google-3d-tiles-service.ts new file mode 100644 index 0000000000..ef94d21d00 --- /dev/null +++ b/modules/3d-tiles/src/services/google-3d-tiles-service.ts @@ -0,0 +1,60 @@ +// loaders.gl, MIT license + +import {Tiles3DService} from '@loaders.gl/tiles'; +import {Tiles3DLoader} from '../tiles-3d-loader'; + +export type GoogleAsset = { + id: string; + url: string; + name: string; +}; + +const ASSET_CATALOG: GoogleAsset[] = [ + { + id: '1', + url: 'https://tile.googleapis.com/v1/3dtiles/root.json', + name: 'Google 3D Tiles' + } +]; + +/** + * @see https://developers.google.com/maps/documentation/tile/policies + * attribution shouldn't be hidden (right now dataset attribution is only partly shown and expanded on hover). + * attribution should be visible (right now displayed with a grayish color, unnoticeable with Google 3d tiles) + * Google logo should be displayed on the bottom left side (where FSQ watermark is located). + */ +export class Google3DTilesService extends Tiles3DService { + readonly id = 'google'; + readonly name = 'Google 3D Tiles'; + readonly urlKey = 'google'; + // Also, attribution for Google 3D tiles is collected from visible tiles. + readonly attribution = { + title: 'Built with Google Maps.', + url: '', + logoUrl: + 'https://developers.google.com/static/maps/documentation/images/google_on_non_white.png' + }; + + readonly loader = Tiles3DLoader; + readonly minZoom = 8; + + getLoadOptions() { + return {fetch: {headers: {'X-GOOG-API-KEY': this.accessToken}}}; + } + + async getAssetCatalog(): Promise { + return ASSET_CATALOG; + } + + async getAssetMetadata(assetId: string): Promise { + if (!this.accessToken) { + throw new Error(`No access token for ${this.name}`); + } + const response = await fetch(`${this.url}?key=${this.accessToken}`); + if (!response.ok) { + throw new Error(`Failed to fetch ${this.name} ${response.status}`); + } + const json = await response.json(); + return {name: this.name, ...json}; + } +} diff --git a/modules/3d-tiles/src/static/google_on_non_white.png b/modules/3d-tiles/src/static/google_on_non_white.png new file mode 100644 index 0000000000..f650e98001 Binary files /dev/null and b/modules/3d-tiles/src/static/google_on_non_white.png differ diff --git a/modules/i3s/src/index.ts b/modules/i3s/src/index.ts index e12cc895c7..831f6916f3 100644 --- a/modules/i3s/src/index.ts +++ b/modules/i3s/src/index.ts @@ -47,3 +47,6 @@ export {I3SBuildingSceneLayerLoader} from './i3s-building-scene-layer-loader'; export {I3SNodePageLoader} from './i3s-node-page-loader'; export {ArcGISWebSceneLoader} from './arcgis-webscene-loader'; export {parseSLPK} from './lib/parsers/parse-slpk/parse-slpk'; + +// EXPERIMENTAL EXPORTS +export {ArcGISI3SService as _ArcGISI3SService} from './services/arcgis-i3s-service'; diff --git a/modules/i3s/src/services/arcgis-i3s-service.ts b/modules/i3s/src/services/arcgis-i3s-service.ts new file mode 100644 index 0000000000..d8788db4cc --- /dev/null +++ b/modules/i3s/src/services/arcgis-i3s-service.ts @@ -0,0 +1,34 @@ +// loaders.gl, MIT license + +import {Tiles3DService} from '@loaders.gl/tiles'; +import {I3SLoader} from '../i3s-loader'; + +/** + * Layout guidelines for ArcGIS attribution. + * @see https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/ + * Custom layout guidelines for ArcGIS attribution. + * @see https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/#layout-and-design-guidelines + */ +export class ArcGISI3SService extends Tiles3DService { + id = 'arcgis'; + name = 'ArcGIS'; + urlKey = 'arcgis'; + attribution = { + title: 'Powered by Esri.', + url: 'https://arcgis.com/' + }; + + loader = I3SLoader; + + async getAssetCatalog(): Promise { + return []; + } + + async getMetadata(): Promise { + const response = await fetch(this.url); + if (!response.ok) { + throw new Error(`Failed to fetch ${this.name} ${response.status}`); + } + return {name: this.name, ...(await response.json())}; + } +} diff --git a/modules/tiles/src/index.ts b/modules/tiles/src/index.ts index 4991575555..3faf184055 100644 --- a/modules/tiles/src/index.ts +++ b/modules/tiles/src/index.ts @@ -20,3 +20,6 @@ export { TILESET_TYPE, LOD_METRIC_TYPE } from './constants'; + +// EXPERIMENTAL EXPORTS +export {Tiles3DService} from './services/tiles-3d-service'; diff --git a/modules/tiles/src/services/tiles-3d-service.ts b/modules/tiles/src/services/tiles-3d-service.ts new file mode 100644 index 0000000000..c1d9aaa7cf --- /dev/null +++ b/modules/tiles/src/services/tiles-3d-service.ts @@ -0,0 +1,57 @@ +// loaders.gl, MIT license + +import type {Loader} from '@loaders.gl/loader-utils'; + +export type Tiles3DAttribution = { + title: string; + url: string; + logoUrl?: string; +}; + +/** + * Handles access token, metadata and attribution for a 3D tileset service. + */ +export abstract class Tiles3DService { + static getServiceFromUrl(url: string, services: Tiles3DService[]): Tiles3DService | null { + return services.find((service) => url.includes(service.urlKey)) || null; + } + + /** id string */ + abstract readonly id: string; + /** Human readable name */ + abstract readonly name: string; + /** Identifies this service by matching a user supplied URL against this key */ + abstract readonly urlKey: string; + /** Default attribution per supported tileset provider. */ + abstract readonly attribution: Tiles3DAttribution; + + /** Loader required by this particular service */ + abstract readonly loader: Loader; + + /** Base URL of this service */ + url: string; + /** Access token for this service */ + accessToken: string; + + /** + * + * @param url Base url to the service + * @param accessToken Access token for the service + */ + constructor(url: string, accessToken: string) { + this.url = url; + this.accessToken = accessToken; + } + + /** Queries metadata from a 3D tileset service. + * @param url Url of a 3D tileset. + * @param this.accessToken Optional access token. + * @returns Downloaded metadata. + */ + abstract getAssetCatalog(): Promise; + + /** Test against map state - @todo outside of loaders.gl scope... */ + isSupported(mapState: any): boolean { + return true; + } +}