terriajs
Version:
Geospatial data visualization platform.
426 lines (384 loc) • 13.9 kB
text/typescript
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
);
}
get arcgisPortalItem(): ArcGisItem | undefined {
if (this.arcgisPortalItemReference._arcgisItem === undefined)
return undefined;
return this.arcgisPortalItemReference._arcgisItem;
}
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();
}
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;
}
get name() {
if (this.arcgisPortalItem === undefined) return undefined;
return this.arcgisPortalItem.title;
}
get description() {
if (this.arcgisPortalItem === undefined) return undefined;
// return this.arcgisPortalItem.description
return DOMPurify.sanitize(this.arcgisPortalItem.description, {
FORBID_ATTR: ["style"],
FORBID_TAGS: ["font"]
});
}
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
);
}
get cacheDuration(): string {
if (isDefined(super.cacheDuration)) {
return super.cacheDuration;
} else if (isDefined(this._arcgisPortalCatalogGroup)) {
return this._arcgisPortalCatalogGroup.cacheDuration;
}
return "0d";
}
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 || {}
};
}
);