UNPKG

terriajs

Version:

Geospatial data visualization platform.

225 lines 10.6 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { featureCollection } from "@turf/helpers"; import i18next from "i18next"; import { computed, makeObservable, override, runInAction } from "mobx"; import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme"; import URI from "urijs"; import isDefined from "../../../Core/isDefined"; import loadJson from "../../../Core/loadJson"; import Result from "../../../Core/Result"; import { networkRequestError } from "../../../Core/TerriaError"; import ProtomapsImageryProvider from "../../../Map/ImageryProvider/ProtomapsImageryProvider"; import featureDataToGeoJson from "../../../Map/PickedFeatures/featureDataToGeoJson"; import { ProtomapsArcGisPbfSource } from "../../../Map/Vector/Protomaps/ProtomapsArcGisPbfSource"; import { tableStyleToProtomaps } from "../../../Map/Vector/Protomaps/tableStyleToProtomaps"; import GeoJsonMixin from "../../../ModelMixins/GeojsonMixin"; import MinMaxLevelMixin from "../../../ModelMixins/MinMaxLevelMixin"; import ArcGisFeatureServerCatalogItemTraits from "../../../Traits/TraitsClasses/ArcGisFeatureServerCatalogItemTraits"; import CreateModel from "../../Definition/CreateModel"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import { ArcGisFeatureServerStratum } from "./ArcGisFeatureServerStratum"; export default class ArcGisFeatureServerCatalogItem extends MinMaxLevelMixin(GeoJsonMixin(CreateModel(ArcGisFeatureServerCatalogItemTraits))) { static type = "esri-featureServer"; constructor(...args) { super(...args); makeObservable(this); } get type() { return ArcGisFeatureServerCatalogItem.type; } get typeName() { return i18next.t("models.arcGisFeatureServerCatalogItem.name"); } async forceLoadMetadata() { if (this.strata.get(ArcGisFeatureServerStratum.stratumName) === undefined) { const stratum = await ArcGisFeatureServerStratum.load(this); runInAction(() => { this.strata.set(ArcGisFeatureServerStratum.stratumName, stratum); }); } } async forceLoadGeojsonData() { // If we are tiling requests, then we use the ProtomapsImageryProvider - see mapItems if (this.tileRequests) return featureCollection([]); const getEsriLayerJson = async (resultOffset) => { const url = proxyCatalogItemUrl(this, this.buildEsriJsonUrl(resultOffset).throwIfUndefined().toString()); return await loadJson(url); }; if (!this.supportsPagination) { // Make a single request without pagination return (featureDataToGeoJson(await getEsriLayerJson()) ?? { type: "FeatureCollection", features: [] }); } // Esri Feature Servers have a maximum limit to how many features they'll return at once, so for a service with many // features, we have to make multiple requests. We can't figure out how many features we need to request ahead of // time (there's an API for it but it times out for services with thousands of features), so we just keep trying // until we run out of features or hit the limit const featuresPerRequest = this.featuresPerRequest; const maxFeatures = this.maxFeatures; const combinedEsriLayerJson = await getEsriLayerJson(0); const mapObjectIds = (features) => features.map((feature) => feature.attributes.OBJECTID ?? feature.attributes.objectid); const seenIDs = new Set(mapObjectIds(combinedEsriLayerJson.features)); let currentOffset = 0; let exceededTransferLimit = combinedEsriLayerJson.exceededTransferLimit; while (combinedEsriLayerJson.features.length <= maxFeatures && exceededTransferLimit === true) { currentOffset += featuresPerRequest; const newEsriLayerJson = await getEsriLayerJson(currentOffset); if (newEsriLayerJson.features === undefined || newEsriLayerJson.features.length === 0) { break; } const newIds = mapObjectIds(newEsriLayerJson.features); if (newIds.every((id) => seenIDs.has(id))) { // We're getting data that we've received already, assume have everything we need and stop fetching break; } newIds.forEach((id) => seenIDs.add(id)); combinedEsriLayerJson.features = combinedEsriLayerJson.features.concat(newEsriLayerJson.features); exceededTransferLimit = newEsriLayerJson.exceededTransferLimit; if (exceededTransferLimit) { console.log("warning: exceeded transfer limit"); } } return (featureDataToGeoJson(combinedEsriLayerJson) ?? { type: "FeatureCollection", features: [] }); } get imageryProvider() { // Don't return an imagery provider if we haven't loaded metadata yet if (!this.strata.has(ArcGisFeatureServerStratum.stratumName)) { return undefined; } const { paintRules, labelRules } = tableStyleToProtomaps(this, false, true); const uri = this.buildEsriJsonUrl().logError("Failed to create valid FeatureServer URL"); if (!uri) return; const url = proxyCatalogItemUrl(this, uri.toString()); let provider = new ProtomapsImageryProvider({ maximumZoom: this.getMaximumLevel(false), minimumZoom: this.getMinimumLevel(false), terria: this.terria, data: new ProtomapsArcGisPbfSource({ url: url, outFields: [...this.outFields], featuresPerTileRequest: this.featuresPerTileRequest, maxRecordCountFactor: this.maxRecordCountFactor, maxTiledFeatures: this.maxTiledFeatures, tilingScheme: new WebMercatorTilingScheme(), enablePickFeatures: this.allowFeaturePicking, objectIdField: this.objectIdField, supportsQuantization: this.supportsQuantization }), id: this.uniqueId, paintRules, labelRules }); provider = this.wrapImageryPickFeatures(provider); provider = this.updateRequestImage(provider); return provider; } get mapItems() { // If we aren't tiling requests, then we use GeoJsonMixin forceLoadGeojsonData if (!this.tileRequests) return super.mapItems; if (!this.imageryProvider) return []; return [ { imageryProvider: this.imageryProvider, show: this.show, alpha: this.opacity, clippingRectangle: this.clipToRectangle ? this.cesiumRectangle : undefined } ]; } get dataColumnMajor() { if (super.dataColumnMajor.length > 0) { return super.dataColumnMajor; } // If we are tiling requests, then we don't have geojson/tabular data // We have to populate columns with empty strings, otherwise TableMixin.tableColumns will be empty. return this.columns.map((column) => [column.name ?? ""]); } /** * Constructs the url for a request to a feature server * @param resultOffset Allows for pagination of results. * See https://developers.arcgis.com/rest/services-reference/enterprise/query-feature-service-layer-.htm */ buildEsriJsonUrl(resultOffset) { const url = cleanUrl(this.url || "0d"); const urlComponents = splitLayerIdFromPath(url); const layerId = urlComponents.layerId; if (!isDefined(layerId)) { return Result.error(networkRequestError({ title: { key: "models.arcGisFeatureServerCatalogItem.invalidServiceTitle" }, message: { key: "models.arcGisFeatureServerCatalogItem.invalidServiceMessage" } })); } // We used to make a call to a different ArcGIS API endpoint // (https://developers.arcgis.com/rest/services-reference/enterprise/query-feature-service-.htm) which took a // `layerdef` parameter, which is more or less equivalent to `where`. To avoid breaking old catalog items, we need // to use `layerDef` if `where` hasn't been set const where = this.where === "1=1" ? this.layerDef : this.where; const uri = new URI(url) .segment("query") .addQuery("f", "json") .addQuery("where", where) .addQuery("outFields", "*") .addQuery("outSR", "4326"); if (this.token) { uri.addQuery("token", this.token); } if (resultOffset !== undefined) { // Pagination specific parameters uri .addQuery("resultRecordCount", this.featuresPerRequest) .addQuery("resultOffset", resultOffset); } return new Result(uri); } } __decorate([ computed ], ArcGisFeatureServerCatalogItem.prototype, "imageryProvider", null); __decorate([ override ], ArcGisFeatureServerCatalogItem.prototype, "mapItems", null); __decorate([ override ], ArcGisFeatureServerCatalogItem.prototype, "dataColumnMajor", null); function splitLayerIdFromPath(url) { const regex = /^(.*FeatureServer)\/(\d+)/; const matches = url.match(regex); if (isDefined(matches) && matches !== null && matches.length > 2) { return { layerId: matches[2], urlWithoutLayerId: matches[1] }; } return { urlWithoutLayerId: url }; } function cleanUrl(url) { // Strip off the search portion of the URL const uri = new URI(url); uri.search(""); return uri.toString(); } //# sourceMappingURL=ArcGisFeatureServerCatalogItem.js.map