terriajs
Version:
Geospatial data visualization platform.
614 lines (531 loc) • 18.6 kB
text/typescript
import i18next from "i18next";
import uniqWith from "lodash-es/uniqWith";
import { computed, runInAction } from "mobx";
import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme";
import ArcGisMapServerImageryProvider from "terriajs-cesium/Source/Scene/ArcGisMapServerImageryProvider";
import URI from "urijs";
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 TerriaError, { networkRequestError } from "../../../Core/TerriaError";
import proj4definitions from "../../../Map/Vector/Proj4Definitions";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import DiscretelyTimeVaryingMixin from "../../../ModelMixins/DiscretelyTimeVaryingMixin";
import { ImageryParts } from "../../../ModelMixins/MappableMixin";
import UrlMixin from "../../../ModelMixins/UrlMixin";
import ArcGisMapServerCatalogItemTraits from "../../../Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits";
import { InfoSectionTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits";
import DiscreteTimeTraits from "../../../Traits/TraitsClasses/DiscreteTimeTraits";
import LegendTraits, {
LegendItemTraits
} from "../../../Traits/TraitsClasses/LegendTraits";
import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits";
import CreateModel from "../../Definition/CreateModel";
import createStratumInstance from "../../Definition/createStratumInstance";
import LoadableStratum from "../../Definition/LoadableStratum";
import { BaseModel } from "../../Definition/Model";
import StratumFromTraits from "../../Definition/StratumFromTraits";
import StratumOrder from "../../Definition/StratumOrder";
import getToken from "../../getToken";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
import MinMaxLevelMixin from "./../../../ModelMixins/MinMaxLevelMixin";
import { Extent, Layer, MapServer } from "./ArcGisInterfaces";
const proj4 = require("proj4").default;
interface RectangleExtent {
east: number;
south: number;
west: number;
north: number;
}
interface Legend {
label?: string;
contentType: string;
imageData: string;
width: number;
height: number;
}
interface Legends {
layers?: { layerId: number; layerName: string; legend?: Legend[] }[];
}
class MapServerStratum extends LoadableStratum(
ArcGisMapServerCatalogItemTraits
) {
static stratumName = "mapServer";
constructor(
private readonly _item: ArcGisMapServerCatalogItem,
readonly mapServer: MapServer,
readonly allLayers: Layer[],
private readonly _legends: Legends | undefined,
readonly token: string | undefined
) {
super();
}
duplicateLoadableStratum(newModel: BaseModel): this {
return new MapServerStratum(
newModel as ArcGisMapServerCatalogItem,
this.mapServer,
this.allLayers,
this._legends,
this.token
) as this;
}
static async load(item: ArcGisMapServerCatalogItem) {
if (!isDefined(item.uri)) {
throw new TerriaError({
title: i18next.t("models.arcGisMapServerCatalogItem.invalidUrlTitle"),
message: i18next.t(
"models.arcGisMapServerCatalogItem.invalidUrlMessage"
)
});
}
let token: string | undefined;
if (isDefined(item.tokenUrl)) {
token = await getToken(item.terria, item.tokenUrl, item.url);
}
let layerId;
const lastSegment = item.uri.segment(-1);
if (lastSegment && lastSegment.match(/\d+/)) {
// URL is a single REST layer, like .../arcgis/rest/services/Society/Society_SCRC/MapServer/16
layerId = lastSegment;
}
let serviceUri = getBaseURI(item);
let layersUri = getBaseURI(item).segment(layerId || "layers"); // either 'layers' or a number
let legendUri = getBaseURI(item).segment("legend");
if (isDefined(token)) {
serviceUri = serviceUri.addQuery("token", token);
layersUri = layersUri.addQuery("token", token);
legendUri = legendUri.addQuery("token", token);
}
// TODO: if tokenUrl, fetch and pass token as parameter
const serviceMetadata: MapServer | undefined = await getJson(
item,
serviceUri
);
if (!isDefined(serviceMetadata)) {
throw networkRequestError({
title: i18next.t("models.arcGisService.invalidServerTitle"),
message: i18next.t("models.arcGisService.invalidServerMessage")
});
}
const legendMetadata: Legends | undefined = await getJson(item, legendUri);
let layers: Layer[] = [];
// If this MapServer is a single fused map cache - we can't request individual layers
// If it is not - we request layer metadata
if (
!(
serviceMetadata.singleFusedMapCache &&
serviceMetadata.capabilities?.includes("TilesOnly")
)
) {
const layersMetadataResponse = await getJson(item, layersUri);
// Use the slightly more basic layer metadata
if (isDefined(serviceMetadata.layers)) {
layers = serviceMetadata.layers;
}
if (isDefined(layersMetadataResponse?.layers)) {
layers = layersMetadataResponse.layers;
// If layersMetadata is only a single layer -> shove into an array
} else if (isDefined(layersMetadataResponse?.id)) {
layers = [layersMetadataResponse];
}
if (!isDefined(layers) || layers.length === 0) {
throw networkRequestError({
title: i18next.t(
"models.arcGisMapServerCatalogItem.noLayersFoundTitle"
),
message: i18next.t(
"models.arcGisMapServerCatalogItem.noLayersFoundMessage",
item
)
});
}
}
const stratum = new MapServerStratum(
item,
serviceMetadata,
layers,
legendMetadata,
token
);
return stratum;
}
get maximumScale() {
if (this._item.layersArray.length === 0) {
return this.mapServer.maxScale;
}
return Math.min(
...filterOutUndefined(
this._item.layersArray.map(({ maxScale }) => maxScale)
)
);
}
get layers() {
/** Try to pull out MapServer layer from URL
* eg https://exmaple.com/arcgis/rest/services/MapServer/{layer}
*/
if (isDefined(this._item.uri)) {
const lastSegment = this._item.uri.segment(-1);
if (isDefined(lastSegment) && lastSegment.match(/\d+/)) {
return lastSegment;
}
}
}
get name() {
// single layer
if (
this._item.layersArray.length === 1 &&
this._item.layersArray[0].name &&
this._item.layersArray[0].name.length > 0
) {
return replaceUnderscores(this._item.layersArray[0].name);
}
// group of layers (or single fused map cache)
else if (
this.mapServer.documentInfo &&
this.mapServer.documentInfo.Title &&
this.mapServer.documentInfo.Title.length > 0
) {
return replaceUnderscores(this.mapServer.documentInfo.Title);
} else if (this.mapServer.mapName && this.mapServer.mapName.length > 0) {
return replaceUnderscores(this.mapServer.mapName);
}
}
get dataCustodian() {
if (
this.mapServer.documentInfo &&
this.mapServer.documentInfo.Author &&
this.mapServer.documentInfo.Author.length > 0
) {
return this.mapServer.documentInfo.Author;
}
}
get rectangle() {
const rectangle: RectangleExtent = {
west: Infinity,
south: Infinity,
east: -Infinity,
north: -Infinity
};
// If we only have the summary layer info
if (
this._item.layersArray.length === 0 ||
!("extent" in this._item.layersArray[0])
) {
getRectangleFromLayer(this.mapServer.fullExtent, rectangle);
} else {
getRectangleFromLayers(rectangle, this._item.layersArray);
}
if (rectangle.west === Infinity) return;
return createStratumInstance(RectangleTraits, rectangle);
}
get info() {
// If we are requesting a single layer, use it to populate InfoSections
// If we are requesting multiple layers - we only show MapServer metadata (not metadata per layer)
const singleLayer =
this._item.layersArray.length === 1
? this._item.layersArray[0]
: undefined;
return filterOutUndefined([
singleLayer
? createStratumInstance(InfoSectionTraits, {
name: i18next.t(
"models.arcGisMapServerCatalogItem.dataDescription"
),
content: singleLayer.description
})
: undefined,
createStratumInstance(InfoSectionTraits, {
name: i18next.t("models.arcGisMapServerCatalogItem.serviceDescription"),
content: this.mapServer.description
}),
createStratumInstance(InfoSectionTraits, {
name: i18next.t("models.arcGisMapServerCatalogItem.copyrightText"),
content: singleLayer?.copyrightText ?? this.mapServer.copyrightText
})
]);
}
get legends() {
const layers = this._item.layersArray;
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;
}
if (
layers.length > 0 &&
!layers.find((layer) => layer.id === l.layerId) &&
!layers.find((layer) => layer.name === l.layerName)
) {
// layer not selected
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(MapServerStratum.stratumName);
export default class ArcGisMapServerCatalogItem extends UrlMixin(
DiscretelyTimeVaryingMixin(
MinMaxLevelMixin(
CatalogMemberMixin(CreateModel(ArcGisMapServerCatalogItemTraits))
)
)
) {
static readonly type = "esri-mapServer";
get typeName() {
return i18next.t("models.arcGisMapServerCatalogItem.name");
}
get type() {
return ArcGisMapServerCatalogItem.type;
}
protected async forceLoadMetadata(): Promise<void> {
const stratum = await MapServerStratum.load(this);
runInAction(() => {
this.strata.set(MapServerStratum.stratumName, stratum);
});
}
protected forceLoadMapItems(): Promise<void> {
return Promise.resolve();
}
get cacheDuration(): string {
if (isDefined(super.cacheDuration)) {
return super.cacheDuration;
}
return "1d";
}
get discreteTimes() {
const mapServerStratum: MapServerStratum | undefined = this.strata.get(
MapServerStratum.stratumName
) as MapServerStratum | undefined;
if (mapServerStratum?.mapServer.timeInfo === undefined) return undefined;
// Add union type - as `time` is always defined
const result: (StratumFromTraits<DiscreteTimeTraits> & {
time: string;
})[] = [];
createDiscreteTimesFromIsoSegments(
result,
new Date(mapServerStratum.mapServer.timeInfo.timeExtent[0]).toISOString(),
new Date(mapServerStratum.mapServer.timeInfo.timeExtent[1]).toISOString(),
undefined,
this.maxRefreshIntervals
);
return result;
}
private get _currentImageryParts(): ImageryParts | undefined {
const dateAsUnix: string | undefined =
this.currentDiscreteTimeTag === undefined
? undefined
: new Date(this.currentDiscreteTimeTag).getTime().toString();
const imageryProvider = this._createImageryProvider(dateAsUnix);
if (imageryProvider === undefined) {
return undefined;
}
return {
imageryProvider,
alpha: this.opacity,
show: this.show,
clippingRectangle: this.clipToRectangle ? this.cesiumRectangle : undefined
};
}
private get _nextImageryParts(): ImageryParts | undefined {
if (
this.terria.timelineStack.contains(this) &&
!this.isPaused &&
this.nextDiscreteTimeTag
) {
const dateAsUnix: number = new Date(this.nextDiscreteTimeTag).getTime();
const imageryProvider = this._createImageryProvider(
dateAsUnix.toString()
);
if (imageryProvider === undefined) {
return undefined;
}
imageryProvider.enablePickFeatures = false;
return {
imageryProvider,
alpha: 0.0,
show: true,
clippingRectangle: this.clipToRectangle
? this.cesiumRectangle
: undefined
};
} else {
return undefined;
}
}
private _createImageryProvider = createTransformerAllowUndefined(
(time: string | undefined): ArcGisMapServerImageryProvider | undefined => {
const stratum = <MapServerStratum>(
this.strata.get(MapServerStratum.stratumName)
);
if (!isDefined(this.url) || !isDefined(stratum)) {
return;
}
const params: any = Object.assign({}, this.parameters);
if (time !== undefined) {
params.time = time;
}
const maximumLevel = scaleDenominatorToLevel(
this.maximumScale,
true,
false
);
const imageryProvider = new ArcGisMapServerImageryProvider({
url: cleanAndProxyUrl(this, getBaseURI(this).toString()),
layers: this.layersArray.map((l) => l.id).join(","),
tilingScheme: new WebMercatorTilingScheme(),
maximumLevel: maximumLevel,
tileHeight: this.tileHeight,
tileWidth: this.tileWidth,
parameters: params,
enablePickFeatures: this.allowFeaturePicking,
/** Only used "pre-cached" tiles if we aren't requesting any specific layers
* If the `layersArray` property is specified, we request individual dynamic layers and ignore the fused map cache.
*/
usePreCachedTilesIfAvailable: this.layersArray.length == 0,
mapServerData: stratum.mapServer,
token: stratum.token,
credit: this.attribution
});
return this.updateRequestImage(imageryProvider, false);
}
);
get mapItems() {
const result = [];
const current = this._currentImageryParts;
if (current) {
result.push(current);
}
const next = this._nextImageryParts;
if (next) {
result.push(next);
}
return result;
}
/** Return array of MapServer layers from `layers` trait (which is CSV of layer IDs) - this will only return **valid** MapServer layers.*/
get layersArray() {
const stratum = <MapServerStratum | undefined>(
this.strata.get(MapServerStratum.stratumName)
);
if (!stratum) return [];
return filterOutUndefined(findLayers(stratum.allLayers, this.layers));
}
}
function getBaseURI(item: ArcGisMapServerCatalogItem) {
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: ArcGisMapServerCatalogItem, uri: any) {
try {
const response = await loadJson(
proxyCatalogItemUrl(item, uri.addQuery("f", "json").toString())
);
return response;
} catch (err) {
console.log(err);
return undefined;
}
}
/* Given a comma-separated string of layer names, returns the layer objects corresponding to them. */
function findLayers(layers: Layer[], names: string | undefined) {
function findLayer(layers: Layer[], id: string) {
var idLowerCase = id.toLowerCase();
var foundByName;
for (var i = 0; i < layers.length; ++i) {
var layer = layers[i];
if (layer.id.toString() === id) {
return layer;
} else if (
isDefined(layer.name) &&
layer.name.toLowerCase() === idLowerCase
) {
foundByName = layer;
}
}
return foundByName;
}
if (!isDefined(names)) {
// If a list of layers is not specified, we're using all layers.
return layers;
}
return names.split(",").map(function (id) {
return findLayer(layers, id);
});
}
function updateBbox(extent: Extent, rectangle: RectangleExtent) {
if (extent.xmin < rectangle.west) rectangle.west = extent.xmin;
if (extent.ymin < rectangle.south) rectangle.south = extent.ymin;
if (extent.xmax > rectangle.east) rectangle.east = extent.xmax;
if (extent.ymax > rectangle.north) rectangle.north = extent.ymax;
}
function getRectangleFromLayer(extent: Extent, rectangle: RectangleExtent) {
const wkidCode =
extent?.spatialReference?.latestWkid ?? extent?.spatialReference?.wkid;
if (isDefined(extent) && isDefined(wkidCode)) {
if (wkidCode === 4326) {
return updateBbox(extent, rectangle);
}
const wkid = "EPSG:" + wkidCode;
if (!isDefined((proj4definitions as any)[wkid])) {
return;
}
const source = new proj4.Proj((proj4definitions as any)[wkid]);
const dest = new proj4.Proj("EPSG:4326");
let p = proj4(source, dest, [extent.xmin, extent.ymin]);
const west = p[0];
const south = p[1];
p = proj4(source, dest, [extent.xmax, extent.ymax]);
const east = p[0];
const north = p[1];
return updateBbox(
{ xmin: west, ymin: south, xmax: east, ymax: north },
rectangle
);
}
}
function getRectangleFromLayers(rectangle: RectangleExtent, layers: Layer[]) {
layers.forEach(function (item) {
item.extent && getRectangleFromLayer(item.extent, rectangle);
});
}
function cleanAndProxyUrl(
catalogItem: ArcGisMapServerCatalogItem,
url: string
) {
return proxyCatalogItemUrl(catalogItem, cleanUrl(url));
}
function cleanUrl(url: string) {
// Strip off the search portion of the URL
var uri = new URI(url);
uri.search("");
return uri.toString();
}