UNPKG

terriajs

Version:

Geospatial data visualization platform.

605 lines (540 loc) 19.1 kB
import i18next from "i18next"; import { action, computed, runInAction } from "mobx"; import { createTransformer } from "mobx-utils"; import URI from "urijs"; import isDefined from "../../../Core/isDefined"; import { isJsonString } from "../../../Core/Json"; import loadJson from "../../../Core/loadJson"; import ReferenceMixin from "../../../ModelMixins/ReferenceMixin"; import UrlMixin from "../../../ModelMixins/UrlMixin"; import ModelTraits from "../../../Traits/ModelTraits"; import { InfoSectionTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits"; import CkanItemReferenceTraits from "../../../Traits/TraitsClasses/CkanItemReferenceTraits"; import CkanResourceFormatTraits from "../../../Traits/TraitsClasses/CkanResourceFormatTraits"; import CkanSharedTraits from "../../../Traits/TraitsClasses/CkanSharedTraits"; import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits"; 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 StratumFromTraits from "../../Definition/StratumFromTraits"; import StratumOrder from "../../Definition/StratumOrder"; import Terria from "../../Terria"; import CatalogMemberFactory from "../CatalogMemberFactory"; import WebMapServiceCatalogGroup from "../Ows/WebMapServiceCatalogGroup"; import WebMapServiceCatalogItem from "../Ows/WebMapServiceCatalogItem"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import CkanCatalogGroup, { createInheritedCkanSharedTraitsStratum } from "./CkanCatalogGroup"; import CkanDefaultFormatsStratum from "./CkanDefaultFormatsStratum"; import { CkanDataset, CkanDatasetServerResponse, CkanResource, CkanResourceServerResponse } from "./CkanDefinitions"; export class CkanDatasetStratum extends LoadableStratum( CkanItemReferenceTraits ) { static stratumName = "ckanDataset"; constructor( private readonly ckanItemReference: CkanItemReference, private readonly ckanCatalogGroup: CkanCatalogGroup | undefined ) { super(); } duplicateLoadableStratum(newModel: BaseModel): this { return new CkanDatasetStratum( this.ckanItemReference, this.ckanCatalogGroup ) as this; } static async load( ckanItemReference: CkanItemReference, ckanCatalogGroup: CkanCatalogGroup | undefined ) { if (ckanItemReference._ckanDataset === undefined) { // If we've got a dataset and no defined resource if ( ckanItemReference.datasetId !== undefined && ckanItemReference.resourceId !== undefined ) { ckanItemReference._ckanDataset = await loadCkanDataset( ckanItemReference ); ckanItemReference._ckanResource = findResourceInDataset( ckanItemReference._ckanDataset, ckanItemReference.resourceId ); ckanItemReference.setSupportedFormatFromResource( ckanItemReference._ckanResource ); } else if ( ckanItemReference.datasetId !== undefined && ckanItemReference.resourceId === undefined ) { ckanItemReference._ckanDataset = await loadCkanDataset( ckanItemReference ); const matched = getSupportedFormats( ckanItemReference._ckanDataset, ckanItemReference.preparedSupportedFormats ); if (matched[0] === undefined) return undefined; ckanItemReference._ckanResource = matched[0].resource; ckanItemReference._supportedFormat = matched[0].format; } else if ( ckanItemReference.datasetId === undefined && ckanItemReference.resourceId !== undefined ) { ckanItemReference._ckanResource = await loadCkanResource( ckanItemReference ); ckanItemReference._supportedFormat = isResourceInSupportedFormats( ckanItemReference._ckanResource, ckanItemReference.preparedSupportedFormats ); } } return new CkanDatasetStratum(ckanItemReference, ckanCatalogGroup); } @computed get ckanDataset(): CkanDataset | undefined { return this.ckanItemReference._ckanDataset; } @computed get ckanResource(): CkanResource | undefined { return this.ckanItemReference._ckanResource; } @computed get url() { return getCkanItemResourceUrl(this.ckanItemReference); } @computed get name() { return getCkanItemName(this.ckanItemReference); } @computed get rectangle() { if (this.ckanDataset === undefined) return undefined; if (this.ckanDataset.extras !== undefined) { const out: number[] = []; const bboxExtras = this.ckanDataset.extras.forEach((e) => { if (e.key === "bbox-west-long") out[0] = parseFloat(e.value); if (e.key === "bbox-south-lat") out[1] = parseFloat(e.value); if (e.key === "bbox-north-lat") out[2] = parseFloat(e.value); if (e.key === "bbox-east-long") out[3] = parseFloat(e.value); }); if (out.length === 4) { return createStratumInstance(RectangleTraits, { west: out[0], south: out[1], east: out[2], north: out[3] }); } } if (this.ckanDataset.geo_coverage !== undefined) { var bboxString = this.ckanDataset.geo_coverage; var parts = bboxString.split(","); if (parts.length === 4) { return createStratumInstance(RectangleTraits, { west: parseInt(parts[0]), south: parseInt(parts[1]), east: parseInt(parts[2]), north: parseInt(parts[3]) }); } } if ( isDefined(this.ckanDataset.spatial) && this.ckanDataset.spatial !== "" ) { var gj = JSON.parse(this.ckanDataset.spatial); if (gj.type === "Polygon" && gj.coordinates[0].length === 5) { return createStratumInstance(RectangleTraits, { west: gj.coordinates[0][0][0], south: gj.coordinates[0][0][1], east: gj.coordinates[0][2][0], north: gj.coordinates[0][2][1] }); } } return undefined; } @computed get info() { function prettifyDate(date: string) { if (date.match(/^\d\d\d\d-\d\d-\d\d.*/)) { return date.substr(0, 10); } else return date; } const outArray: StratumFromTraits<InfoSectionTraits>[] = []; if (this.ckanDataset === undefined) return outArray; if (this.ckanDataset.license_url !== undefined) { outArray.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.licence"), content: `[${ this.ckanDataset.license_title || this.ckanDataset.license_url }](${this.ckanDataset.license_url})` }) ); } else if (this.ckanDataset.license_title !== undefined) { outArray.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.licence"), content: this.ckanDataset.license_title }) ); } outArray.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.contact_point"), content: this.ckanDataset.contact_point }), createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.datasetDescription"), content: this.ckanDataset.notes }), createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.author"), content: this.ckanDataset.author }) ); if (this.ckanDataset.organization) { outArray.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.datasetCustodian"), content: this.ckanDataset.organization.description || this.ckanDataset.organization.title }) ); } outArray.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.metadata_created"), content: prettifyDate(this.ckanDataset.metadata_created) }), createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.metadata_modified"), content: prettifyDate(this.ckanDataset.metadata_modified) }), createStratumInstance(InfoSectionTraits, { name: i18next.t("models.ckan.update_freq"), content: this.ckanDataset.update_freq }) ); return outArray; } /** Set isGroup = true if this turns into WMS Group (See CkanItemReference.forceLoadReference for more info) */ @computed get isGroup() { if ( this.ckanItemReference._supportedFormat?.definition?.type === WebMapServiceCatalogItem.type && !this.ckanItemReference.wmsLayers ) return true; } } StratumOrder.addLoadStratum(CkanDatasetStratum.stratumName); export default class CkanItemReference extends UrlMixin( ReferenceMixin(CreateModel(CkanItemReferenceTraits)) ) { static readonly type = "ckan-item"; constructor( id: string | undefined, terria: Terria, sourceReference?: BaseModel, strata?: Map<string, StratumFromTraits<ModelTraits>> ) { super(id, terria, sourceReference, strata); this.strata.set( CkanDefaultFormatsStratum.stratumName, new CkanDefaultFormatsStratum() ); } get type() { return CkanItemReference.type; } get typeName() { return i18next.t("models.ckan.name"); } _ckanDataset: CkanDataset | undefined = undefined; _ckanResource: CkanResource | undefined = undefined; _ckanCatalogGroup: CkanCatalogGroup | undefined = undefined; _supportedFormat: PreparedSupportedFormat | undefined = undefined; @computed get preparedSupportedFormats(): PreparedSupportedFormat[] { return this.supportedResourceFormats ? this.supportedResourceFormats.map(prepareSupportedFormat) : []; } setDataset(ckanDataset: CkanDataset) { this._ckanDataset = ckanDataset; } setResource(ckanResource: CkanResource) { this._ckanResource = ckanResource; } setCkanCatalog(ckanCatalogGroup: CkanCatalogGroup) { this._ckanCatalogGroup = ckanCatalogGroup; } setSupportedFormat(format: PreparedSupportedFormat | undefined) { this._supportedFormat = format; } setSupportedFormatFromResource(resource: CkanResource | undefined) { this._supportedFormat = isResourceInSupportedFormats( resource, this.preparedSupportedFormats ); } @computed get cacheDuration(): string { if (isDefined(super.cacheDuration)) { return super.cacheDuration; } return "1d"; } // We will first attach this to the CkanItemReference // and then we'll attach it to the target model // I wonder if it needs to be on both? async setCkanStrata(model: BaseModel) { const stratum = await CkanDatasetStratum.load(this, this._ckanCatalogGroup); if (stratum === undefined) return; runInAction(() => { model.strata.set(CkanDatasetStratum.stratumName, stratum); }); } @action setSharedStratum( inheritedPropertiesStratum: Readonly<StratumFromTraits<CkanSharedTraits>> ) { // The values in this stratum should not be updated as the same object is used // in all CkanItemReferences this.strata.set( createInheritedCkanSharedTraitsStratum.stratumName, inheritedPropertiesStratum ); } async forceLoadReference( previousTarget: BaseModel | undefined ): Promise<BaseModel | undefined> { await this.setCkanStrata(this); if (this._supportedFormat === undefined) return undefined; const type = (this._supportedFormat.definition ?? {}).type; if (typeof type !== "string") return undefined; let model: BaseModel | undefined; // Special case for WMS // Check for `layers` before creating model // If WMS layers have been found - create WebMapServiceCatalogItem // If no WMS layers are found - create WebMapServiceCatalogGroup if (type === WebMapServiceCatalogItem.type) { // If WMS layers have been found if (this.wmsLayers) { model = new WebMapServiceCatalogItem(this.uniqueId, this.terria, this); model.setTrait( CommonStrata.definition, "layers", decodeURI(this.wmsLayers) ); } // if no WMS layers are found else { model = new WebMapServiceCatalogGroup(this.uniqueId, this.terria, this); } } else { model = CatalogMemberFactory.create( type, this.uniqueId, this.terria, this ); } if (model === undefined) return; previousTarget = model; await this.setCkanStrata(model); model.setTrait(CommonStrata.definition, "name", this.name); return model; } @computed get wmsLayers() { const params: Record<string, string | undefined> | undefined = new URI( getCkanItemResourceUrl(this) )?.search(true); const layersFromItemProperties = this.itemPropertiesByIds?.find( (itemProps) => this.uniqueId && itemProps.ids.includes(this.uniqueId) )?.itemProperties?.layers ?? this.itemPropertiesByType?.find( (itemProps) => itemProps.type === WebMapServiceCatalogItem.type )?.itemProperties?.layers ?? this.itemProperties?.layers; // Mixing ?? and || because for params we don't want to use empty string params if there are non-empty string parameters const rawLayers = (isJsonString(layersFromItemProperties) ? layersFromItemProperties : undefined) ?? this._ckanResource?.wms_layer ?? (params?.LAYERS || params?.layers || params?.typeName); // Improve the robustness. const cleanLayers = rawLayers ?.split(",") .map((layer) => layer.trim()) .join(","); return cleanLayers; } } export interface CkanResourceWithFormat { format: PreparedSupportedFormat; resource: CkanResource; } export interface PreparedSupportedFormat extends ModelPropertiesFromTraits<CkanResourceFormatTraits> { formatRegexParsed: RegExp | undefined; urlRegexParsed: RegExp | undefined; } async function loadCkanDataset(ckanItem: CkanItemReference) { var uri = new URI(ckanItem.url) .segment("api/3/action/package_show") .addQuery({ id: ckanItem.datasetId }); const response: CkanDatasetServerResponse = await loadJson( proxyCatalogItemUrl(ckanItem, uri.toString()) ); if (response.result) return response.result; return undefined; } async function loadCkanResource(ckanItem: CkanItemReference) { var uri = new URI(ckanItem.url) .segment("api/3/action/resource_show") .addQuery({ id: ckanItem.resourceId }); const response: CkanResourceServerResponse = await loadJson( proxyCatalogItemUrl(ckanItem, uri.toString()) ); if (response.result) return response.result; return undefined; } function findResourceInDataset( ckanDataset: CkanDataset | undefined, resourceId: string ) { if (ckanDataset === undefined) return undefined; for (let i = 0; i < ckanDataset.resources.length; ++i) { if (ckanDataset.resources[i].id === resourceId) { return ckanDataset.resources[i]; } } return undefined; } export const prepareSupportedFormat = createTransformer( (format: ModelPropertiesFromTraits<CkanResourceFormatTraits>) => { return { id: format.id, definition: format.definition, maxFileSize: format.maxFileSize, removeDuplicates: format.removeDuplicates, onlyUseIfSoleResource: format.onlyUseIfSoleResource, formatRegex: format.formatRegex, urlRegex: format.urlRegex, formatRegexParsed: format.formatRegex ? new RegExp(format.formatRegex, "i") : undefined, urlRegexParsed: format.urlRegex ? new RegExp(format.urlRegex, "i") : undefined }; } ); export function getSupportedFormats( dataset: CkanDataset | undefined, formats: PreparedSupportedFormat[] ) { if (!dataset) return []; const supported: CkanResourceWithFormat[] = []; for (let i = 0; i < formats.length; ++i) { const format = formats[i]; for (let j = 0; j < dataset.resources.length; ++j) { const resource = dataset.resources[j]; if (resourceIsSupported(resource, format)) { supported.push({ resource: resource, format: format }); } } } return supported; } export function isResourceInSupportedFormats( resource: CkanResource | undefined, formats: PreparedSupportedFormat[] ): PreparedSupportedFormat | undefined { if (resource === undefined) return undefined; let matches: PreparedSupportedFormat[] = []; for (let i = 0; i < formats.length; ++i) { const format = formats[i]; if (resourceIsSupported(resource, format)) return format; } return undefined; } export function resourceIsSupported( resource: CkanResource, format: PreparedSupportedFormat ) { let match = false; // Does format match (formatRegex is required) if (!isDefined(format.formatRegexParsed)) return false; if (format.formatRegexParsed.test(resource.format)) { match = true; } // Does URL match (urlRegex is optional) if ( match && isDefined(format.urlRegexParsed) && !format.urlRegexParsed.test(resource.url) ) { match = false; } // Is resource.size (in bytes) greater than maxFileSize? (maxFileSize is optional) if ( match && isDefined(format.maxFileSize) && format.maxFileSize !== null && isDefined(resource.size) && resource.size !== null && resource.size / (1024 * 1024) > format.maxFileSize ) { match = false; } return match; } export function getCkanItemName(item: CkanItemReference) { if (!item._ckanResource) return; if (item.useResourceName) return item._ckanResource.name; // via @steve9164 /** Switched the order [check `useCombinationNameWhereMultipleResources` * first ] that these are checked so the default is checked last. Otherwise * setting useCombinationNameWhereMultipleResources without setting * useDatasetNameAndFormatWhereMultipleResources to false doesn't do * anything */ if (item._ckanDataset) { if ( item.useCombinationNameWhereMultipleResources && item._ckanDataset.resources.length > 1 ) { return item._ckanDataset.title + " - " + item._ckanResource.name; } if ( item.useDatasetNameAndFormatWhereMultipleResources && item._ckanDataset.resources.length > 1 ) { return item._ckanDataset.title + " - " + item._ckanResource.format; } return item._ckanDataset.title; } return item._ckanResource.name; } function getCkanItemResourceUrl(item: CkanItemReference) { if (item._ckanResource === undefined) return undefined; if (item._supportedFormat !== undefined) { if ( (item._supportedFormat.definition ?? {}).type === "wms" && item._ckanResource.wms_api_url ) { return item._ckanResource.wms_api_url; } } return item._ckanResource.url; }