terriajs
Version:
Geospatial data visualization platform.
619 lines (529 loc) • 18.3 kB
text/typescript
import i18next from "i18next";
import uniqWith from "lodash-es/uniqWith";
import { computed, makeObservable, override, runInAction } from "mobx";
import GeographicTilingScheme from "terriajs-cesium/Source/Core/GeographicTilingScheme";
import JulianDate from "terriajs-cesium/Source/Core/JulianDate";
import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme";
import URI from "urijs";
import AsyncLoader from "../../../Core/AsyncLoader";
import { JsonObject, isJsonObject } from "../../../Core/Json";
import TerriaError from "../../../Core/TerriaError";
import createDiscreteTimesFromIsoSegments from "../../../Core/createDiscreteTimes";
import createTransformerAllowUndefined from "../../../Core/createTransformerAllowUndefined";
import filterOutUndefined from "../../../Core/filterOutUndefined";
import isDefined from "../../../Core/isDefined";
import loadJson from "../../../Core/loadJson";
import replaceUnderscores from "../../../Core/replaceUnderscores";
import { scaleDenominatorToLevel } from "../../../Core/scaleToDenominator";
import ArcGisImageServerImageryProvider from "../../../Map/ImageryProvider/ArcGisImageServerImageryProvider";
import Reproject from "../../../Map/Vector/Reproject";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import DiscretelyTimeVaryingMixin from "../../../ModelMixins/DiscretelyTimeVaryingMixin";
import MappableMixin, {
ImageryParts
} from "../../../ModelMixins/MappableMixin";
import MinMaxLevelMixin from "../../../ModelMixins/MinMaxLevelMixin";
import UrlMixin from "../../../ModelMixins/UrlMixin";
import ArcGisImageServerCatalogItemTraits, {
ArcGisImageServerAvailableRasterFunctionTraits,
ArcGisImageServerRenderingRule
} from "../../../Traits/TraitsClasses/ArcGisImageServerCatalogItemTraits";
import DiscreteTimeTraits from "../../../Traits/TraitsClasses/DiscreteTimeTraits";
import LegendTraits, {
LegendItemTraits
} from "../../../Traits/TraitsClasses/LegendTraits";
import CreateModel from "../../Definition/CreateModel";
import LoadableStratum from "../../Definition/LoadableStratum";
import { BaseModel, ModelConstructorParameters } from "../../Definition/Model";
import StratumFromTraits from "../../Definition/StratumFromTraits";
import StratumOrder from "../../Definition/StratumOrder";
import createStratumInstance from "../../Definition/createStratumInstance";
import { RectangleCoordinates } from "../../FunctionParameters/RectangleParameter";
import { SelectableDimensionEnum } from "../../SelectableDimensions/SelectableDimensions";
import getToken from "../../getToken";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
import { ImageServer, Legends } from "./ArcGisInterfaces";
import { getRectangleFromLayer } from "./ArcGisMapServerCatalogItem";
class ImageServerStratum extends LoadableStratum(
ArcGisImageServerCatalogItemTraits
) {
static stratumName = "arcgisImageserver";
constructor(
private readonly _item: ArcGisImageServerCatalogItem,
readonly imageServer: ImageServer,
private readonly _token: string | undefined
) {
super();
makeObservable(this);
}
duplicateLoadableStratum(newModel: BaseModel): this {
return new ImageServerStratum(
newModel as ArcGisImageServerCatalogItem,
this.imageServer,
this._token
) as this;
}
static async load(item: ArcGisImageServerCatalogItem) {
if (!isDefined(item.uri)) {
throw new TerriaError({
title: i18next.t("models.arcGisImageServerCatalogItem.invalidUrlTitle"),
message: i18next.t(
"models.arcGisImageServerCatalogItem.invalidUrlMessage"
)
});
}
let token: string | undefined;
if (isDefined(item.tokenUrl) && isDefined(item.url)) {
token = await getToken(item.terria, item.tokenUrl, item.url);
} else if (isDefined(item.token)) {
token = item.token;
}
let serviceUri = getBaseURI(item);
if (isDefined(token)) {
serviceUri = serviceUri.addQuery("token", token);
}
const serviceMetadata: ImageServer | undefined = await getJson(
item,
serviceUri
);
if (!isDefined(serviceMetadata)) {
throw new TerriaError({
title: i18next.t("models.arcGisService.invalidServerTitle"),
message: i18next.t("models.arcGisService.invalidServerMessage")
});
}
if (!serviceMetadata.capabilities?.includes("Image"))
throw new TerriaError({
title: i18next.t(
"models.arcGisImageServerCatalogItem.invalidServiceTitle"
),
message: i18next.t(
"models.arcGisImageServerCatalogItem.invalidServiceMessage"
)
});
// Add any Proj4 definitions if necessary
const epsgCode =
serviceMetadata.fullExtent.spatialReference?.latestWkid ??
serviceMetadata.fullExtent.spatialReference?.wkid;
if (epsgCode && item.terria.configParameters.proj4ServiceBaseUrl) {
await Reproject.checkProjection(
item.terria.configParameters.proj4ServiceBaseUrl,
`EPSG:${epsgCode}`
);
}
const stratum = new ImageServerStratum(item, serviceMetadata, token);
return stratum;
}
get name() {
return replaceUnderscores(this.imageServer.name);
}
get cacheDuration(): string {
return "1d";
}
get rectangle() {
const rectangle: RectangleCoordinates = {
west: Infinity,
south: Infinity,
east: -Infinity,
north: -Infinity
};
getRectangleFromLayer(this.imageServer.fullExtent, rectangle);
if (
rectangle.west === Infinity ||
rectangle.south === Infinity ||
rectangle.east === -Infinity ||
rectangle.north === -Infinity
)
return undefined;
return rectangle;
}
get description() {
return this.imageServer.description;
}
get attribution() {
return this.imageServer.copyrightText;
}
get token() {
return this._token;
}
/** Disable pre-cached tiles if we are making dynamic requests - using parameters, time or rendering rule. */
get usePreCachedTiles() {
if (
this._item.parameters ||
this._item.currentDiscreteJulianDate ||
this._item.renderingRule.rasterFunction
)
return false;
// We only support web mercator or wgs84
if (isDefined(this.imageServer.tileInfo)) {
const wkid =
this.imageServer.tileInfo.spatialReference?.latestWkid ??
this.imageServer.tileInfo.spatialReference?.wkid;
if (wkid === 102100 || wkid === 102113 || wkid === 3857 || wkid === 4326)
return true;
}
return false;
}
/** Override wkid to web mercator if using pre-cached tiles (and web mercator is supported) */
get wkid() {
if (this._item.usePreCachedTiles) {
const wkid = this.imageServer.tileInfo?.spatialReference.wkid;
if (wkid === 102100 || wkid === 102113 || wkid === 3857 || wkid === 4326)
return wkid;
}
}
get tileHeight() {
if (this._item.usePreCachedTiles) return this.imageServer.tileInfo?.rows;
}
get tileWidth() {
if (this._item.usePreCachedTiles) return this.imageServer.tileInfo?.cols;
}
get maximumScale() {
return this.imageServer.maxScale;
}
get maximumLevel() {
const maximumLevelFromScale = scaleDenominatorToLevel(
this._item.maximumScale,
true,
false
);
// Make sure the maximum level is not higher than the maximum level of the server tiles
if (this._item.usePreCachedTiles && this.imageServer.tileInfo?.lods)
return Math.min(
this.imageServer.tileInfo.lods[
this.imageServer.tileInfo.lods.length - 1
].level,
maximumLevelFromScale ?? Infinity
);
return maximumLevelFromScale;
}
get minimumLevel() {
if (this._item.usePreCachedTiles && this.imageServer.tileInfo?.lods)
return this.imageServer.tileInfo.lods[0].level;
}
get allowRasterFunction() {
return this.imageServer.allowRasterFunction;
}
get availableRasterFunctions() {
if (!this._item.allowRasterFunction) return [];
return this.imageServer?.rasterFunctionInfos
.filter((rasterFn) => rasterFn.name && rasterFn.name !== "None")
.map((rasterFn) => {
return createStratumInstance(
ArcGisImageServerAvailableRasterFunctionTraits,
{
name: rasterFn.name,
description:
rasterFn.description !== "A raster function template."
? rasterFn.description
: undefined,
help: rasterFn.help
}
);
});
}
get disableRasterFunctionSelectors() {
return !this._item.allowRasterFunction;
}
}
StratumOrder.addLoadStratum(ImageServerStratum.stratumName);
class ImageServerLegendStratum extends LoadableStratum(
ArcGisImageServerCatalogItemTraits
) {
static stratumName = "arcgisImageserverLegend";
constructor(
private readonly _item: ArcGisImageServerCatalogItem,
private readonly _legends: Legends | undefined
) {
super();
makeObservable(this);
}
duplicateLoadableStratum(newModel: BaseModel): this {
return new ImageServerLegendStratum(
newModel as ArcGisImageServerCatalogItem,
this._legends
) as this;
}
static async load(item: ArcGisImageServerCatalogItem) {
if (!isDefined(item.uri)) {
throw new TerriaError({
title: i18next.t("models.arcGisImageServerCatalogItem.invalidUrlTitle"),
message: i18next.t(
"models.arcGisImageServerCatalogItem.invalidUrlMessage"
)
});
}
let legendUri = getBaseURI(item).segment("legend");
legendUri.addQuery(item.flattenedParameters);
// Note we don't need to fetch token from tokenUrl here - as ImageServerStratum handles that, and will populate `token` trait with the response
if (isDefined(item.token)) {
legendUri = legendUri.addQuery("token", item.token);
}
const legendMetadata: Legends | undefined = await getJson(item, legendUri);
const stratum = new ImageServerLegendStratum(item, legendMetadata);
return stratum;
}
get legends() {
const noDataRegex = /^No[\s_-]?Data$/i;
const labelsRegex = /_Labels$/;
let items: StratumFromTraits<LegendItemTraits>[] = [];
(this._legends?.layers || []).forEach((l) => {
if (noDataRegex.test(l.layerName) || labelsRegex.test(l.layerName)) {
return;
}
l.legend?.forEach((leg) => {
const title = replaceUnderscores(
leg.label !== "" ? leg.label : l.layerName
);
const dataUrl = "data:" + leg.contentType + ";base64," + leg.imageData;
items.push(
createStratumInstance(LegendItemTraits, {
title,
imageUrl: dataUrl,
imageWidth: leg.width,
imageHeight: leg.height
})
);
});
});
items = uniqWith(items, (a, b) => a.imageUrl === b.imageUrl);
return [createStratumInstance(LegendTraits, { items })];
}
}
StratumOrder.addLoadStratum(ImageServerLegendStratum.stratumName);
export default class ArcGisImageServerCatalogItem extends UrlMixin(
DiscretelyTimeVaryingMixin(
MinMaxLevelMixin(
CatalogMemberMixin(
MappableMixin(CreateModel(ArcGisImageServerCatalogItemTraits))
)
)
)
) {
static readonly type = "esri-imageServer";
private _legendStratumLoader = new AsyncLoader(
this.forceLoadLegends.bind(this)
);
constructor(...args: ModelConstructorParameters) {
super(...args);
makeObservable(this);
}
get typeName() {
return i18next.t("models.arcGisImageServerCatalogItem.name");
}
get type() {
return ArcGisImageServerCatalogItem.type;
}
protected async forceLoadMetadata(): Promise<void> {
const stratum = await ImageServerStratum.load(this);
runInAction(() => {
this.strata.set(ImageServerStratum.stratumName, stratum);
});
await this._legendStratumLoader.load();
}
protected async forceLoadLegends(): Promise<void> {
const stratum = await ImageServerLegendStratum.load(this);
runInAction(() => {
this.strata.set(ImageServerLegendStratum.stratumName, stratum);
});
}
protected forceLoadMapItems(): Promise<void> {
return Promise.resolve();
}
get discreteTimes() {
const imageServerStratum: ImageServerStratum | undefined = this.strata.get(
ImageServerStratum.stratumName
) as ImageServerStratum | undefined;
if (imageServerStratum?.imageServer.timeInfo === undefined)
return undefined;
const result: (StratumFromTraits<DiscreteTimeTraits> & {
time: string;
})[] = [];
createDiscreteTimesFromIsoSegments(
result,
new Date(
imageServerStratum.imageServer.timeInfo.timeExtent[0]
).toISOString(),
new Date(
imageServerStratum.imageServer.timeInfo.timeExtent[1]
).toISOString(),
undefined,
this.maxRefreshIntervals
);
return result;
}
private get _currentImageryParts(): ImageryParts | undefined {
// Make sure legend stays up to date
this._legendStratumLoader.load();
const imageryProvider = this._createImageryProvider(
this.currentDiscreteJulianDate
);
if (imageryProvider) {
return {
imageryProvider,
alpha: this.opacity,
show: this.show,
clippingRectangle: this.clipToRectangle
? this.cesiumRectangle
: undefined
};
}
}
get _nextImageryParts(): ImageryParts | undefined {
if (
this.terria.timelineStack.contains(this) &&
!this.isPaused &&
this.nextDiscreteTimeTag
) {
const imageryProvider = this._createImageryProvider(
this.nextDiscreteJulianDate
);
if (imageryProvider) {
imageryProvider.enablePickFeatures = false;
return {
imageryProvider,
alpha: 0.0,
show: true,
clippingRectangle: this.clipToRectangle
? this.cesiumRectangle
: undefined
};
}
} else {
return undefined;
}
}
/** Flatten nested JSON parameters (stringify them). These are applied onto ArcGisImageServerImageryProvider requests.
* This will include renderingRule and bandIds if they are set.
*/
get flattenedParameters() {
const params = Object.entries(this.parameters ?? {}).reduce<JsonObject>(
(acc, [key, value]) => {
if (isJsonObject(value)) acc[key] = JSON.stringify(value);
else acc[key] = value;
return acc;
},
{}
);
if (this.renderingRule.rasterFunction)
params.renderingRule = JSON.stringify(
this.traits["renderingRule"].toJson(this.renderingRule)
);
if (this.bandIds) params.bandIds = this.bandIds.join(",");
return params;
}
private _createImageryProvider = createTransformerAllowUndefined(
(
time: JulianDate | undefined
): ArcGisImageServerImageryProvider | undefined => {
if (!isDefined(this.url)) {
return undefined;
}
const params = { ...this.flattenedParameters };
if (time) params.time = JulianDate.toDate(time).getTime();
let tilingScheme: WebMercatorTilingScheme | GeographicTilingScheme;
if (this.wkid === 102100 || this.wkid === 102113 || this.wkid === 3857) {
tilingScheme = new WebMercatorTilingScheme();
} else if (this.wkid === 4326) {
tilingScheme = new GeographicTilingScheme();
} else {
throw TerriaError.from(
`Tile spatial reference WKID ${this.wkid} is not supported.`
);
}
return new ArcGisImageServerImageryProvider({
url: cleanAndProxyUrl(this, this.url),
tilingScheme: tilingScheme,
maximumLevel: this.maximumLevel,
minimumLevel: this.minimumLevel,
tileHeight: this.tileHeight,
tileWidth: this.tileWidth,
parameters: params,
enablePickFeatures: this.allowFeaturePicking,
usePreCachedTiles: this.usePreCachedTiles,
token: this.token,
credit: this.attribution ?? ""
});
}
);
get mapItems() {
if (this.isLoadingMetadata) return [];
return [this._currentImageryParts, this._nextImageryParts].filter(
isDefined
);
}
get selectableDimensions() {
return filterOutUndefined([
...super.selectableDimensions,
this.rasterFunctionSelectableDimensions
]);
}
get rasterFunctionSelectableDimensions():
| SelectableDimensionEnum
| undefined {
if (this.disableRasterFunctionSelectors) return undefined;
return {
id: "raster-functions",
name: i18next.t("models.arcGisImageServerCatalogItem.rasterFunction"),
options: this.availableRasterFunctions.map((rasterFn) => ({
id: rasterFn.name,
name: rasterFn.name,
description: rasterFn.description
})),
selectedId: this.renderingRule?.rasterFunction,
allowUndefined: true,
undefinedLabel: "Default",
setDimensionValue: (strata, rasterFunction) => {
if (!this.renderingRule) {
this.setTrait(
strata,
"renderingRule",
createStratumInstance(ArcGisImageServerRenderingRule, {
rasterFunction
})
);
} else {
this.renderingRule.setTrait(strata, "rasterFunction", rasterFunction);
}
}
};
}
}
function getBaseURI(item: ArcGisImageServerCatalogItem) {
const uri = new URI(item.url);
const lastSegment = uri.segment(-1);
if (lastSegment && lastSegment.match(/\d+/)) {
uri.segment(-1, "");
}
return uri;
}
async function getJson(item: ArcGisImageServerCatalogItem, uri: URI) {
try {
const response = await loadJson(
proxyCatalogItemUrl(item, uri.addQuery("f", "json").toString())
);
return response;
} catch (err) {
console.log(err);
return undefined;
}
}
function cleanAndProxyUrl(
catalogItem: ArcGisImageServerCatalogItem,
url: string
) {
return proxyCatalogItemUrl(catalogItem, cleanUrl(url));
}
function cleanUrl(url: string) {
// Strip off the search portion of the URL
const uri = new URI(url);
uri.search("");
return uri.toString();
}