UNPKG

terriajs

Version:

Geospatial data visualization platform.

426 lines (384 loc) 13.9 kB
import DOMPurify from "dompurify"; import i18next from "i18next"; import { computed, runInAction, makeObservable, override } from "mobx"; import { createTransformer } from "mobx-utils"; import URI from "urijs"; import isDefined from "../../../Core/isDefined"; import { JsonObject } from "../../../Core/Json"; import loadJson from "../../../Core/loadJson"; import AccessControlMixin from "../../../ModelMixins/AccessControlMixin"; import ReferenceMixin from "../../../ModelMixins/ReferenceMixin"; import UrlMixin from "../../../ModelMixins/UrlMixin"; import ArcGisPortalItemFormatTraits from "../../../Traits/TraitsClasses/ArcGisPortalItemFormatTraits"; import ArcGisPortalItemTraits from "../../../Traits/TraitsClasses/ArcGisPortalItemTraits"; import { InfoSectionTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits"; import ModelTraits from "../../../Traits/ModelTraits"; import ArcGisPortalCatalogGroup from "./ArcGisPortalCatalogGroup"; import { ArcGisItem } from "./ArcGisPortalDefinitions"; import CatalogMemberFactory from "../CatalogMemberFactory"; import CommonStrata from "../../Definition/CommonStrata"; import CreateModel from "../../Definition/CreateModel"; import createStratumInstance from "../../Definition/createStratumInstance"; import LoadableStratum from "../../Definition/LoadableStratum"; import { BaseModel } from "../../Definition/Model"; import ModelPropertiesFromTraits from "../../Definition/ModelPropertiesFromTraits"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import StratumFromTraits from "../../Definition/StratumFromTraits"; import StratumOrder from "../../Definition/StratumOrder"; import Terria from "../../Terria"; export class ArcGisPortalItemStratum extends LoadableStratum( ArcGisPortalItemTraits ) { static stratumName = "arcGisPortalDataset"; constructor( private readonly arcgisPortalItemReference: ArcGisPortalItemReference, private readonly arcgisPortalCatalogGroup: | ArcGisPortalCatalogGroup | undefined ) { super(); makeObservable(this); } duplicateLoadableStratum(_newModel: BaseModel): this { return new ArcGisPortalItemStratum( this.arcgisPortalItemReference, this.arcgisPortalCatalogGroup ) as this; } static async load( arcgisPortalItemReference: ArcGisPortalItemReference, arcgisPortalCatalogGroup: ArcGisPortalCatalogGroup | undefined ) { if (arcgisPortalItemReference._arcgisItem === undefined) { if (arcgisPortalItemReference.uniqueId !== undefined) { arcgisPortalItemReference._portalRootUrl = arcgisPortalItemReference.url; arcgisPortalItemReference._arcgisItem = await loadPortalItem( arcgisPortalItemReference ); arcgisPortalItemReference.setSupportedFormatFromItem( arcgisPortalItemReference._arcgisItem ); } } else if (arcgisPortalCatalogGroup !== undefined) { arcgisPortalItemReference._portalRootUrl = arcgisPortalCatalogGroup.url; } return new ArcGisPortalItemStratum( arcgisPortalItemReference, arcgisPortalCatalogGroup ); } @computed get arcgisPortalItem(): ArcGisItem | undefined { if (this.arcgisPortalItemReference._arcgisItem === undefined) return undefined; return this.arcgisPortalItemReference._arcgisItem; } @computed get portalItemUrl(): string | undefined { if ( this.arcgisPortalItem === undefined || this.arcgisPortalItemReference._portalRootUrl === undefined ) return undefined; const uri = new URI(this.arcgisPortalItemReference._portalRootUrl) .segment(`/home/item.html`) .addQuery("id", this.arcgisPortalItem.id); return uri.toString(); } @computed get url() { if (this.arcgisPortalItem === undefined) return undefined; if (this.arcgisPortalItem.type === "Scene Service") return `/i3s-to-3dtiles/${this.arcgisPortalItem.url}`; return this.arcgisPortalItem.url; } @computed get name() { if (this.arcgisPortalItem === undefined) return undefined; return this.arcgisPortalItem.title; } @computed get description() { if (this.arcgisPortalItem === undefined) return undefined; // return this.arcgisPortalItem.description return DOMPurify.sanitize(this.arcgisPortalItem.description, { FORBID_ATTR: ["style"], FORBID_TAGS: ["font"] }); } @computed get info() { const outArray: StratumFromTraits<InfoSectionTraits>[] = []; if (this.arcgisPortalItem === undefined) return outArray; if (this.arcgisPortalItem.licenseInfo !== undefined) { outArray.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.arcgisPortal.licence"), content: DOMPurify.sanitize(this.arcgisPortalItem.licenseInfo, { FORBID_ATTR: ["style"], FORBID_TAGS: ["font"] }) }) ); } outArray.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.arcgisPortal.openInPortal"), content: `<a href="${this.portalItemUrl}"><button>${i18next.t( "models.arcgisPortal.openInPortal" )}</button></a>` }) ); return outArray; } } StratumOrder.addLoadStratum(ArcGisPortalItemStratum.stratumName); export default class ArcGisPortalItemReference extends AccessControlMixin( UrlMixin(ReferenceMixin(CreateModel(ArcGisPortalItemTraits))) ) { static readonly defaultSupportedFormats: StratumFromTraits<ArcGisPortalItemFormatTraits>[] = [ createStratumInstance(ArcGisPortalItemFormatTraits, { id: "I3S", formatRegex: "Scene Service", urlRegex: "SceneServer$|SceneServer/$", definition: { type: "3d-tiles" } }), // createStratumInstance(ArcGisPortalItemFormatTraits, { // id: "WFS", // formatRegex: "WFS", // urlRegex: "WFSServer", // definition: { // type: "wfs" // } // }), createStratumInstance(ArcGisPortalItemFormatTraits, { id: "WMS", formatRegex: "WMS", urlRegex: "WMSServer", definition: { type: "wms" } }), createStratumInstance(ArcGisPortalItemFormatTraits, { id: "ArcGIS MapServer Group", formatRegex: "Map Service", urlRegex: "MapServer$|MapServer/$", definition: { type: "esri-mapServer-group" } }), createStratumInstance(ArcGisPortalItemFormatTraits, { id: "ArcGIS MapServer", formatRegex: "Map Service", urlRegex: /MapServer\/\d/, definition: { type: "esri-mapServer" } }), createStratumInstance(ArcGisPortalItemFormatTraits, { id: "ArcGIS FeatureServer Group", formatRegex: "Feature Service", urlRegex: "FeatureServer$|FeatureServer/$", definition: { type: "esri-featureServer-group" } }), createStratumInstance(ArcGisPortalItemFormatTraits, { id: "ArcGIS FeatureServer", formatRegex: "Feature Service", urlRegex: /FeatureServer\/\d/, definition: { type: "esri-featureServer" } }), createStratumInstance(ArcGisPortalItemFormatTraits, { id: "Kml", formatRegex: "KML", definition: { type: "kml" } }) ]; static readonly type = "arcgis-portal-item"; get type() { return ArcGisPortalItemReference.type; } get typeName() { return i18next.t("models.arcgisPortal.name"); } _arcgisItem: ArcGisItem | undefined = undefined; _arcgisPortalCatalogGroup: ArcGisPortalCatalogGroup | undefined = undefined; _supportedFormat: PreparedSupportedFormat | undefined = undefined; _portalRootUrl: string | undefined = undefined; constructor( id: string | undefined, terria: Terria, sourceReference?: BaseModel, strata?: Map<string, StratumFromTraits<ModelTraits>> ) { super(id, terria, sourceReference, strata); makeObservable(this); this.setTrait( CommonStrata.defaults, "supportedFormats", ArcGisPortalItemReference.defaultSupportedFormats ); } @override get cacheDuration(): string { if (isDefined(super.cacheDuration)) { return super.cacheDuration; } else if (isDefined(this._arcgisPortalCatalogGroup)) { return this._arcgisPortalCatalogGroup.cacheDuration; } return "0d"; } @computed get preparedSupportedFormats(): PreparedSupportedFormat[] { return ( this.supportedFormats && this.supportedFormats.map(prepareSupportedFormat) ); } isItemInSupportedFormats( item: ArcGisItem | undefined ): PreparedSupportedFormat | undefined { if (item === undefined) return undefined; for (let i = 0; i < this.preparedSupportedFormats.length; ++i) { const format = this.preparedSupportedFormats[i]; // This is a bit clunky but format handling for non-Esri stuff in portal is a bit unpredictable if (format.formatRegex === undefined || format.urlRegex === undefined) { // eg KML we can match using format regex but not the URL if (format.formatRegex) { if (format.formatRegex.test(item.type)) return format; } else if (format.urlRegex) { if (format.urlRegex.test(item.url)) return format; } } else { if ( format.formatRegex.test(item.type) && format.urlRegex.test(item.url) ) { return format; } } } return undefined; } setDataset(item: ArcGisItem) { this._arcgisItem = item; } setArcgisPortalCatalog(arcgisCatalogGroup: ArcGisPortalCatalogGroup) { this._arcgisPortalCatalogGroup = arcgisCatalogGroup; } setSupportedFormatFromItem(item: ArcGisItem | undefined) { this._supportedFormat = this.isItemInSupportedFormats(item); } async setArcgisStrata(model: BaseModel) { const stratum = await ArcGisPortalItemStratum.load( this, this._arcgisPortalCatalogGroup ); if (stratum === undefined) return; runInAction(() => { model.strata.set(ArcGisPortalItemStratum.stratumName, stratum); }); } async forceLoadReference( _previousTarget: BaseModel | undefined ): Promise<BaseModel | undefined> { // So when we first crawl we'll get this far await this.setArcgisStrata(this); // this.setSupportedFormatFromItem(this._arcgisItem); if (this._supportedFormat === undefined) return undefined; // See comments below re this sequence const itemDataInfo: ArcGisItemInfo | undefined = await loadAdditionalPortalInfo(this); if (itemDataInfo !== undefined && this._arcgisItem !== undefined) { if (!itemDataInfo.error && itemDataInfo.layers) { if (itemDataInfo.layers.length === 1) { this._arcgisItem.url = `${this._arcgisItem.url}/${itemDataInfo.layers[0].id}`; } } this.setSupportedFormatFromItem(this._arcgisItem); } // One final catch to handing types // Tiled MapServices dont use a single layer if ( this._arcgisItem !== undefined && this._arcgisItem.type === "Map Service" && this._arcgisItem.typeKeywords.indexOf("Tiled") > -1 ) { const mapServerFormat = this.preparedSupportedFormats.filter( (f) => f.definition.type === "esri-mapServer" ); if (mapServerFormat.length === 1) this._supportedFormat = mapServerFormat[0]; } const model = CatalogMemberFactory.create( this._supportedFormat.definition.type as string, this.uniqueId, this.terria, this ); if (model === undefined) return; await this.setArcgisStrata(model); const defintionStratum = this.strata.get(CommonStrata.definition); if (defintionStratum) { model.strata.set(CommonStrata.definition, defintionStratum); model.setTrait(CommonStrata.definition, "url", undefined); } return model; } } interface ArcGisItemLayerInfo { id: string; } interface ArcGisItemInfo { layers?: ArcGisItemLayerInfo[]; error?: JsonObject; } interface PreparedSupportedFormat { formatRegex: RegExp | undefined; urlRegex: RegExp | undefined; definition: JsonObject; } async function loadPortalItem(portalItem: ArcGisPortalItemReference) { const uri = new URI(portalItem._portalRootUrl) .segment(`/sharing/rest/content/items/${portalItem.itemId}`) .addQuery({ f: "json" }); const response: ArcGisItem = await loadJson( proxyCatalogItemUrl(portalItem, uri.toString(), portalItem.cacheDuration) ); return response; } // Portal has this weird habit of storing useful additional info about an item // such as which layers to use from a group of layers at the /data endpoint // But this only relevant on some layers async function loadAdditionalPortalInfo(portalItem: ArcGisPortalItemReference) { if (portalItem._arcgisItem === undefined) return undefined; const baseUrl = portalItem._portalRootUrl; const uri = new URI(baseUrl) .segment(`/sharing/rest/content/items/${portalItem._arcgisItem.id}/data`) .addQuery({ f: "json" }); // The request also returns 200 even if the page is blank causing json errors // Sometimes it actually returns json containing an error, but not always try { const response: ArcGisItemInfo = await loadJson( proxyCatalogItemUrl(portalItem, uri.toString(), portalItem.cacheDuration) ); return response; } catch (_err) { return undefined; } } const prepareSupportedFormat = createTransformer( (format: ModelPropertiesFromTraits<ArcGisPortalItemFormatTraits>) => { return { urlRegex: format.urlRegex ? new RegExp(format.urlRegex as any, "i") : undefined, formatRegex: format.formatRegex ? new RegExp(format.formatRegex, "i") : undefined, definition: format.definition || {} }; } );