UNPKG

terriajs

Version:

Geospatial data visualization platform.

672 lines (588 loc) 21.1 kB
import i18next from "i18next"; import { computed, makeObservable } from "mobx"; import proj4 from "proj4"; import URI from "urijs"; import isDefined from "../../../Core/isDefined"; import loadJson from "../../../Core/loadJson"; import replaceUnderscores from "../../../Core/replaceUnderscores"; import Proj4Definitions from "../../../Map/Vector/Proj4Definitions"; import { hasUnsupportedStylesForProtomaps } from "../../../Map/Vector/Protomaps/tableStyleToProtomaps"; import ArcGisFeatureServerCatalogItemTraits from "../../../Traits/TraitsClasses/ArcGisFeatureServerCatalogItemTraits"; import { InfoSectionTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits"; import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits"; import TableColorStyleTraits, { EnumColorTraits } from "../../../Traits/TraitsClasses/Table/ColorStyleTraits"; import TableColumnTraits from "../../../Traits/TraitsClasses/Table/ColumnTraits"; import TableOutlineStyleTraits, { BinOutlineSymbolTraits, EnumOutlineSymbolTraits } from "../../../Traits/TraitsClasses/Table/OutlineStyleTraits"; import TablePointSizeStyleTraits from "../../../Traits/TraitsClasses/Table/PointSizeStyleTraits"; import TablePointStyleTraits, { BinPointSymbolTraits, EnumPointSymbolTraits } from "../../../Traits/TraitsClasses/Table/PointStyleTraits"; import TableStyleTraits from "../../../Traits/TraitsClasses/Table/StyleTraits"; 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 ArcGisFeatureServerCatalogItem from "./ArcGisFeatureServerCatalogItem"; import { esriStyleToTableStyle } from "./esriStyleToTableStyle"; type EsriStyleTypes = | "esriPMS" // simple picture style | "esriSMS" // simple marker style | "esriSLS" // simple line style | "esriSFS"; // simple fill style /** as defined https://developers.arcgis.com/web-map-specification/objects/esriSFS_symbol/ */ export type SupportedFillStyle = | "esriSFSSolid" // fill line with color | "esriSFSNull"; // no fill /** as defined https://developers.arcgis.com/web-map-specification/objects/esriSMS_symbol/ */ export type SupportedEsriSimpleMarkerStyle = | "esriSMSCircle" | "esriSMSCross" | "esriSMSDiamond" | "esriSMSSquare" | "esriSMSTriangle" | "esriSMSX"; /** as defined https://developers.arcgis.com/web-map-specification/objects/esriSLS_symbol/ */ export type SupportedLineStyle = | "esriSLSSolid" // solid line | "esriSLSDash" // dashes (-----) | "esriSLSDashDot" // line (-.-.-) | "esriSLSDashDotDot" // line (-..-..-) | "esriSLSDot" // dotted line (.....) | "esriSLSLongDash" | "esriSLSLongDashDot" | "esriSLSShortDash" | "esriSLSShortDashDot" | "esriSLSShortDashDotDot" | "esriSLSShortDot" | "esriSLSNull"; /** as defined https://developers.arcgis.com/web-map-specification/objects/symbol/ */ export interface EsriStyle { contentType: string; color?: number[]; outline?: Outline; imageData?: any; xoffset?: number; yoffset?: number; width?: number; height?: number; angle?: number; size?: number; type: EsriStyleTypes; url?: string; style?: | SupportedEsriSimpleMarkerStyle | SupportedLineStyle | SupportedFillStyle; } interface Outline { type: EsriStyleTypes; color: number[]; width: number; style?: SupportedLineStyle; } interface Renderer { type: "simple" | "uniqueValue" | "classBreaks"; } interface ClassBreakInfo extends SimpleRenderer { classMaxValue: number; classMinValue?: number; } interface ClassBreaksRenderer extends Renderer { field: string; classBreakInfos: ClassBreakInfo[]; defaultSymbol: EsriStyle | null; /** Note, value expressions are not supported */ valueExpression?: string; valueExpressionTitle?: string; /** Note, visual variables are not supported. See https://developers.arcgis.com/web-map-specification/objects/visualVariable/ */ visualVariables?: unknown[]; } interface UniqueValueInfo extends SimpleRenderer { value: string; } /** * See https://developers.arcgis.com/web-map-specification/objects/uniqueValueRenderer/ */ interface UniqueValueRenderer extends Renderer { /** Terria only supports `field1`, not multiple fields (`field2` or `field3`). */ field1: string; field2?: string; field3?: string; fieldDelimiter?: string; uniqueValueInfos: UniqueValueInfo[]; defaultSymbol: EsriStyle | null; /** Note, value expressions are not supported */ valueExpression?: string; valueExpressionTitle?: string; /** Note, visual variables are not supported. See https://developers.arcgis.com/web-map-specification/objects/visualVariable/ */ visualVariables?: unknown[]; } // https://developers.arcgis.com/web-map-specification/objects/simpleRenderer/ interface SimpleRenderer extends Renderer { label?: string; symbol: EsriStyle | null; /** Note, visual variables are not supported. See https://developers.arcgis.com/web-map-specification/objects/visualVariable/ */ visualVariables?: unknown[]; } /** For JSON Schema for FeatureServer Layer response see https://developers.arcgis.com/rest/services-reference/enterprise/layer-feature-service/#json-response-syntax */ interface FeatureServer { documentInfo?: { Author?: string; }; name?: string; description?: string; copyrightText?: string; drawingInfo?: { renderer: Renderer; }; extent?: Extent; minScale?: number; maxScale?: number; advancedQueryCapabilities?: { supportsPagination: boolean; }; supportedQueryFormats?: string; maxRecordCount?: number; tileMaxRecordCount?: number; maxRecordCountFactor?: number; supportsCoordinatesQuantization?: boolean; supportsTilesAndBasicQueriesMode?: boolean; objectIdField?: string; fields?: Field[]; } type FieldType = | "esriFieldTypeSmallInteger" | "esriFieldTypeInteger" | "esriFieldTypeSingle" | "esriFieldTypeDouble" | "esriFieldTypeString" | "esriFieldTypeDate" | "esriFieldTypeOID" | "esriFieldTypeGeometry" | "esriFieldTypeBlob" | "esriFieldTypeRaster" | "esriFieldTypeGUID" | "esriFieldTypeGlobalID" | "esriFieldTypeXML" | "esriFieldTypeBigInteger"; const fieldTypeToTableColumn: Record<FieldType, string> = { esriFieldTypeSmallInteger: "scalar", esriFieldTypeInteger: "scalar", esriFieldTypeSingle: "scalar", esriFieldTypeDouble: "scalar", esriFieldTypeString: "text", esriFieldTypeDate: "time", esriFieldTypeOID: "scalar", esriFieldTypeGeometry: "hidden", esriFieldTypeBlob: "hidden", esriFieldTypeRaster: "hidden", esriFieldTypeGUID: "hidden", esriFieldTypeGlobalID: "hidden", esriFieldTypeXML: "hidden", esriFieldTypeBigInteger: "scalar" }; interface Field { name: string; type: FieldType; alias?: string; domain?: unknown; editable?: boolean; nullable?: boolean; length?: number; defaultValue?: unknown; modelName?: string; } interface SpatialReference { wkid?: string; latestWkid?: string; } interface Extent { xmin: number; ymin: number; xmax: number; ymax: number; spatialReference?: SpatialReference; } export class ArcGisFeatureServerStratum extends LoadableStratum( ArcGisFeatureServerCatalogItemTraits ) { static stratumName = "featureServer"; constructor( private readonly _item: ArcGisFeatureServerCatalogItem, private readonly _featureServer: FeatureServer | undefined, private readonly _token: string | undefined ) { super(); makeObservable(this); } duplicateLoadableStratum(newModel: BaseModel): this { return new ArcGisFeatureServerStratum( newModel as ArcGisFeatureServerCatalogItem, this._featureServer, this._token ) as this; } static async load( item: ArcGisFeatureServerCatalogItem ): Promise<ArcGisFeatureServerStratum> { if (item.url === undefined) { return new ArcGisFeatureServerStratum(item, undefined, undefined); } 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; } const metaUrl = buildMetadataUrl(item, token); const featureServer = await loadJson(metaUrl); return new ArcGisFeatureServerStratum(item, featureServer, token); } get token() { return this._token; } @computed get shortReport(): string | undefined { // Show notice if reached if ( this._item.readyData?.features !== undefined && this._item.readyData!.features.length >= this._item.maxFeatures ) { return i18next.t( "models.arcGisFeatureServerCatalogItem.reachedMaxFeatureLimit", this._item ); } return undefined; } @computed get maxScaleDenominator(): number | undefined { return this._featureServer?.minScale; } @computed get minScaleDenominator(): number | undefined { return this._featureServer?.maxScale; } @computed get name(): string | undefined { if ( this._featureServer?.name !== undefined && this._featureServer.name.length > 0 ) { return replaceUnderscores(this._featureServer.name); } } @computed get dataCustodian(): string | undefined { if ( this._featureServer?.documentInfo && this._featureServer?.documentInfo.Author && this._featureServer?.documentInfo.Author.length > 0 ) { return this._featureServer.documentInfo.Author; } } @computed get rectangle(): StratumFromTraits<RectangleTraits> | undefined { const extent = this._featureServer?.extent; const wkidCode = extent?.spatialReference?.latestWkid ?? extent?.spatialReference?.wkid; if (isDefined(extent) && isDefined(wkidCode)) { const wkid = "EPSG:" + wkidCode; if (!isDefined(Proj4Definitions[wkid])) { return undefined; } const source = Proj4Definitions[wkid]; const dest = "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]; const rectangle = { west: west, south: south, east: east, north: north }; return createStratumInstance(RectangleTraits, rectangle); } return undefined; } @computed get info() { return [ createStratumInstance(InfoSectionTraits, { name: i18next.t("models.arcGisMapServerCatalogItem.dataDescription"), content: this._featureServer?.description }), createStratumInstance(InfoSectionTraits, { name: i18next.t("models.arcGisMapServerCatalogItem.copyrightText"), content: this._featureServer?.copyrightText }) ]; } @computed get supportsPagination(): boolean { if ( this._featureServer === undefined || this._featureServer.advancedQueryCapabilities === undefined ) { return false; } return !!this._featureServer.advancedQueryCapabilities.supportsPagination; } @computed get activeStyle() { return "ESRI"; } @computed get styles() { const renderer = this._featureServer?.drawingInfo?.renderer; if (!renderer) return []; const rendererType = renderer.type; if (rendererType === "simple") { const simpleRenderer = renderer as SimpleRenderer; const symbol = simpleRenderer.symbol; if (simpleRenderer.visualVariables?.length) { console.warn( `WARNING: Terria does not support visual variables in ArcGisFeatureService SimpleRenderers` ); } if (!symbol) return []; const symbolStyle = esriStyleToTableStyle(symbol, simpleRenderer.label); return [ createStratumInstance(TableStyleTraits, { id: "ESRI", hidden: false, color: createStratumInstance(TableColorStyleTraits, { nullColor: symbolStyle.color ?? "#ffffff" }), pointSize: symbolStyle.pointSize, point: createStratumInstance(TablePointStyleTraits, { null: symbolStyle.point }), outline: createStratumInstance(TableOutlineStyleTraits, { null: symbolStyle.outline }) }) ]; } else if (rendererType === "uniqueValue") { const uniqueValueRenderer = renderer as UniqueValueRenderer; const symbolStyles = uniqueValueRenderer.uniqueValueInfos.map((v) => { return esriStyleToTableStyle(v.symbol, v.label); }); const defaultSymbolStyle = esriStyleToTableStyle( uniqueValueRenderer.defaultSymbol, undefined ); // Only include color if there are any styles which aren't esriPMS const includeColor = !!uniqueValueRenderer.uniqueValueInfos.find( (u) => u.symbol?.type !== "esriPMS" ); if (uniqueValueRenderer.field2 || uniqueValueRenderer.field3) { console.warn( `WARNING: Terria only supports ArcGisFeatureService UniqueValueRenderers with a single field (\`field1\`), not multiple fields (\`field2\` or \`field3\`)` ); } if (uniqueValueRenderer.visualVariables?.length) { console.warn( `WARNING: Terria does not support visual variables in ArcGisFeatureService UniqueValueRenderers` ); } if (uniqueValueRenderer.valueExpression) { console.warn( `WARNING: Terria does not support value expressions in ArcGisFeatureService UniqueValueRenderers` ); } if (!uniqueValueRenderer.field1) { console.warn( `WARNING: Terria does not support empty field1 in UniqueValueRenderers, using default style` ); return []; } return [ createStratumInstance(TableStyleTraits, { id: "ESRI", hidden: false, color: includeColor ? createStratumInstance(TableColorStyleTraits, { colorColumn: uniqueValueRenderer.field1, mapType: "enum", enumColors: uniqueValueRenderer.uniqueValueInfos.map((v, i) => createStratumInstance(EnumColorTraits, { value: v.value, color: symbolStyles[i].color ?? "#ffffff" }) ), nullColor: defaultSymbolStyle.color }) : createStratumInstance(TableColorStyleTraits, { nullColor: "#FFFFFF" }), pointSize: createStratumInstance(TablePointSizeStyleTraits, {}), point: createStratumInstance(TablePointStyleTraits, { mapType: "enum", column: uniqueValueRenderer.field1, enum: uniqueValueRenderer.uniqueValueInfos.map((v, i) => createStratumInstance(EnumPointSymbolTraits, { value: v.value, ...symbolStyles[i].point }) ), null: defaultSymbolStyle.point }), outline: createStratumInstance(TableOutlineStyleTraits, { mapType: "enum", column: uniqueValueRenderer.field1, enum: uniqueValueRenderer.uniqueValueInfos.map((v, i) => createStratumInstance(EnumOutlineSymbolTraits, { value: v.value, ...symbolStyles[i].outline }) ), null: defaultSymbolStyle.outline }) }) ]; } else if (rendererType === "classBreaks") { const classBreaksRenderer = renderer as ClassBreaksRenderer; if (classBreaksRenderer.visualVariables?.length) { console.warn( `WARNING: Terria does not support visual variables in ArcGisFeatureService ClassBreakRenderers` ); } if (classBreaksRenderer.valueExpression) { console.warn( `WARNING: Terria does not support value expressions in ArcGisFeatureService ClassBreakRenderers` ); } const symbolStyles = classBreaksRenderer.classBreakInfos.map((c) => esriStyleToTableStyle(c.symbol, c.label) ); const defaultSymbolStyle = esriStyleToTableStyle( classBreaksRenderer.defaultSymbol, undefined ); // Only include color if there are any styles which aren't esriPMS const includeColor = !!classBreaksRenderer.classBreakInfos.find( (u) => u.symbol?.type !== "esriPMS" ); return [ createStratumInstance(TableStyleTraits, { id: "ESRI", hidden: false, color: includeColor ? createStratumInstance(TableColorStyleTraits, { mapType: "bin", colorColumn: classBreaksRenderer.field, binColors: symbolStyles.map((s) => s.color ?? ""), binMaximums: classBreaksRenderer.classBreakInfos.map( (c) => c.classMaxValue ), nullColor: defaultSymbolStyle.color }) : createStratumInstance(TableColorStyleTraits, { nullColor: "#FFFFFF" }), pointSize: createStratumInstance(TablePointSizeStyleTraits, {}), point: createStratumInstance(TablePointStyleTraits, { mapType: "bin", column: classBreaksRenderer.field, bin: classBreaksRenderer.classBreakInfos.map((c, i) => createStratumInstance(BinPointSymbolTraits, { maxValue: c.classMaxValue, ...symbolStyles[i].point }) ), null: defaultSymbolStyle.point }), outline: createStratumInstance(TableOutlineStyleTraits, { mapType: "bin", column: classBreaksRenderer.field, bin: classBreaksRenderer.classBreakInfos.map((c, i) => createStratumInstance(BinOutlineSymbolTraits, { maxValue: c.classMaxValue, ...symbolStyles[i].outline }) ), null: defaultSymbolStyle.outline }) }) ]; } else { console.warn( `WARNING: Terria does not support ArcGisFeatureService renderers of type ${rendererType}` ); } } // Map ESRI fields to Terria columns. This just sets the name, title and type of the column @computed get columns() { return ( this._featureServer?.fields ?.filter((field) => { if (!fieldTypeToTableColumn[field.type]) { console.warn( `WARNING: Terria does not support ESRI field type ${field.type}` ); return false; } return true; }) .map((field) => createStratumInstance(TableColumnTraits, { name: field.name, title: field.alias, type: fieldTypeToTableColumn[field.type]?.toString() }) ) ?? [] ); } get featuresPerRequest() { return this._featureServer?.maxRecordCount; } get featuresPerTileRequest() { return this._featureServer?.tileMaxRecordCount; } get maxRecordCountFactor() { return this._featureServer?.maxRecordCountFactor; } get supportsQuantization() { return !!this._featureServer?.supportsCoordinatesQuantization; } /** Enable tileRequests by default if supported and no unsupported point/label styles are used */ @computed get tileRequests() { if (this._item.forceCesiumPrimitives) return false; const supportsPbfTiles = this._featureServer?.supportsTilesAndBasicQueriesMode && typeof this._featureServer?.supportedQueryFormats === "string" && (this._featureServer.supportedQueryFormats as string) .toLowerCase() .includes("pbf"); if (supportsPbfTiles) { return !hasUnsupportedStylesForProtomaps(this._item); } return undefined; } get objectIdField() { return this._featureServer?.objectIdField; } // Add properties/columns to outFields if they are needed for styling. Otherwise, these properties won't be in tile features @computed get outFields() { return Array.from( new Set([ this._item.objectIdField, this._item.activeTableStyle.tableColorMap.colorTraits.colorColumn, this._item.activeTableStyle.outlineStyleMap.traits?.column, this._item.activeTableStyle.pointStyleMap.traits?.column ]) ).filter((t): t is string => !!t); } } function buildMetadataUrl( catalogItem: ArcGisFeatureServerCatalogItem, token: string | undefined ) { const url = new URI(catalogItem.url).addQuery("f", "json"); if (token) { url.addQuery("token", token); } return proxyCatalogItemUrl(catalogItem, url.toString()); } StratumOrder.addLoadStratum(ArcGisFeatureServerStratum.stratumName);