UNPKG

terriajs

Version:

Geospatial data visualization platform.

548 lines (476 loc) 17 kB
import i18next from "i18next"; import { action, computed, makeObservable } from "mobx"; import RequestErrorEvent from "terriajs-cesium/Source/Core/RequestErrorEvent"; import Resource from "terriajs-cesium/Source/Core/Resource"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import flatten from "../../../Core/flatten"; import isDefined from "../../../Core/isDefined"; import { regexMatches } from "../../../Core/regexMatches"; import TerriaError from "../../../Core/TerriaError"; import ModelReference from "../../../Traits/ModelReference"; import SdmxCatalogGroupTraits from "../../../Traits/TraitsClasses/SdmxCatalogGroupTraits"; import CatalogGroup from "../CatalogGroup"; import CommonStrata from "../../Definition/CommonStrata"; import LoadableStratum from "../../Definition/LoadableStratum"; import { BaseModel } from "../../Definition/Model"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import StratumOrder from "../../Definition/StratumOrder"; import SdmxCatalogGroup from "./SdmxJsonCatalogGroup"; import SdmxJsonCatalogItem from "./SdmxJsonCatalogItem"; import { Agency, AgencyScheme, AgencySchemes, Categories, Categorisations, Category, CategoryScheme, CategorySchemes, Dataflow, Dataflows, SdmxJsonStructureMessage } from "./SdmxJsonStructureMessage"; export interface SdmxServer { agencySchemes?: AgencySchemes; categorySchemes?: CategorySchemes; categorisations?: Categorisations; dataflows: Dataflows; } export class SdmxServerStratum extends LoadableStratum(SdmxCatalogGroupTraits) { static stratumName = "sdmxServer"; static async load( catalogGroup: SdmxCatalogGroup ): Promise<SdmxServerStratum> { // Load agency schemes (may be undefined) const agencySchemes = ( await loadSdmxJsonStructure( proxyCatalogItemUrl(catalogGroup, `${catalogGroup.url}/agencyscheme/`), true ) )?.data?.agencySchemes; // Load category schemes (may be undefined) const categorySchemeResponse = await loadSdmxJsonStructure( proxyCatalogItemUrl( catalogGroup, `${catalogGroup.url}/categoryscheme?references=parentsandsiblings` ), true ); let dataflows = categorySchemeResponse?.data?.dataflows; // If no dataflows from category schemes -> try getting all of them through `dataflow` endpoint if (!isDefined(dataflows)) { dataflows = ( await loadSdmxJsonStructure( proxyCatalogItemUrl(catalogGroup, `${catalogGroup.url}/dataflow/`), true ) )?.data?.dataflows; if (!isDefined(dataflows)) { throw new TerriaError({ title: i18next.t("models.sdmxServerStratum.loadDataErrorTitle"), message: i18next.t("models.sdmxServerStratum.loadDataErrorMessage") }); } } return new SdmxServerStratum(catalogGroup, { agencySchemes, categorySchemes: categorySchemeResponse?.data?.categorySchemes, categorisations: categorySchemeResponse?.data?.categorisations, dataflows }); } duplicateLoadableStratum(model: BaseModel): this { return new SdmxServerStratum( model as SdmxCatalogGroup, this.sdmxServer ) as this; } private readonly dataflowTree: DataflowTree = {}; constructor( private readonly catalogGroup: SdmxCatalogGroup, private readonly sdmxServer: SdmxServer ) { super(); makeObservable(this); // If categorisations exist => organise Dataflows into a tree! if (isDefined(this.sdmxServer.categorisations)) { this.sdmxServer.categorisations.forEach((categorisiation) => { const categorySchemeUrn = parseSdmxUrn(categorisiation.target); const agencyId = categorySchemeUrn?.agencyId; const categorySchemeId = categorySchemeUrn?.resourceId; const categoryIds = categorySchemeUrn?.descendantIds; // Only support 1 level of categorisiation for now const dataflowId = parseSdmxUrn(categorisiation.source)?.resourceId; if ( !isDefined(agencyId) || !isDefined(categorySchemeId) || !isDefined(categoryIds) || !isDefined(dataflowId) ) return; let agencyNode = this.dataflowTree[agencyId]; // Create agency node if it doesn't exist if (!isDefined(agencyNode)) { const agency = this.getAgency(agencyId); if (!isDefined(agency)) return; this.dataflowTree[agencyId] = { type: "agencyScheme", item: agency, members: {} }; agencyNode = this.dataflowTree[agencyId]; } let categorySchemeNode = agencyNode.members![categorySchemeId]; // Create categoryScheme node if it doesn't exist if (!isDefined(categorySchemeNode)) { const categoryScheme = this.getCategoryScheme(categorySchemeId); if (!isDefined(categoryScheme)) return; agencyNode.members![categorySchemeId] = { type: "categoryScheme", item: categoryScheme, members: {} }; categorySchemeNode = agencyNode.members![categorySchemeId]; } let categoryParentNode = categorySchemeNode; // Make category nodes (may be nested) categoryIds.forEach((categoryId) => { // Create category node if it doesn't exist if (!isDefined(categoryParentNode.members![categoryId])) { let category: Category | undefined; // Find current categoryId in parent categoryScheme or parent category if (categoryParentNode.type === "categoryScheme") { category = this.getCategoryFromCatagoryScheme( categorySchemeId, categoryId ); } else if (categoryParentNode.type === "category") { category = this.getCategoryFromCategories( categoryParentNode.item?.categories, categoryId ); } if (!isDefined(category)) return; categoryParentNode.members![categoryId] = { type: "category", item: category, members: {} }; } // Swap parent node to newly created category node categoryParentNode = categoryParentNode.members![categoryId]; }); // Create dataflow! const dataflow = this.getDataflow(dataflowId); if (!isDefined(dataflow)) return; categoryParentNode.members![dataflowId] = { type: "dataflow", item: dataflow }; }); // No categorisations exist => add flat list of dataflows } else { this.dataflowTree = this.sdmxServer.dataflows.reduce<DataflowTree>( (tree, dataflow) => { if (isDefined(dataflow.id)) { tree[dataflow.id] = { type: "dataflow", item: dataflow }; } return tree; }, {} ); } } @computed get members(): ModelReference[] { // Find first node in tree which has more than 1 child const findRootGroup = (node: DataflowTreeNode): DataflowTreeNode => { const children: DataflowTreeNode[] | undefined = isDefined(node?.members) ? Object.values(node!.members) : undefined; // If only 1 child -> keep searching return isDefined(children) && children.length === 1 ? findRootGroup(children[0]) : node; }; let rootTreeNodes = Object.values(this.dataflowTree); // If only a single group -> try to find next nested group with more than 1 child if (rootTreeNodes.length === 1 && isDefined(rootTreeNodes[0])) { rootTreeNodes = Object.values( findRootGroup(rootTreeNodes[0]).members ?? this.dataflowTree ); } return rootTreeNodes.map((node) => this.getMemberId(node)); } createMembers() { Object.values(this.dataflowTree).forEach((node) => this.createMemberFromLayer(node) ); } @action createMemberFromLayer(node: DataflowTreeNode) { const layerId = this.getMemberId(node); if (!layerId) { return; } // Replace the stratum inherited from the parent group. // If has nested layers -> create model for CatalogGroup if (node.members && Object.keys(node.members).length > 0) { // Create nested layers Object.values(node.members).forEach((member) => this.createMemberFromLayer(member) ); // Create group const existingGroupModel = this.catalogGroup.terria.getModelById( CatalogGroup, layerId ); let groupModel: CatalogGroup; if (existingGroupModel === undefined) { groupModel = new CatalogGroup(layerId, this.catalogGroup.terria); this.catalogGroup.terria.addModel(groupModel); } else { groupModel = existingGroupModel; } groupModel.setTrait( CommonStrata.definition, "name", node.item.name || node.item.id ); groupModel.setTrait( CommonStrata.definition, "members", filterOutUndefined( Object.values(node.members).map((member) => this.getMemberId(member)) ) ); // Set group description if (node.item.description) { groupModel.setTrait( CommonStrata.definition, "description", node.item.description ); } return; } // No nested layers (and type is dataflow) -> create model for SdmxJsonCatalogItem if ( node.type !== "dataflow" || !isDefined(node.item.id) || !isDefined(node.item.agencyID) ) return; const existingItemModel = this.catalogGroup.terria.getModelById( SdmxJsonCatalogItem, layerId ); let itemModel: SdmxJsonCatalogItem; if (existingItemModel === undefined) { itemModel = new SdmxJsonCatalogItem( layerId, this.catalogGroup.terria, undefined ); this.catalogGroup.terria.addModel(itemModel); } else { itemModel = existingItemModel; } itemModel.strata.delete(CommonStrata.definition); itemModel.setTrait( CommonStrata.definition, "name", node.item.name || node.item.id ); itemModel.setTrait(CommonStrata.definition, "url", this.catalogGroup.url); // Set group description if (node.item.description) { itemModel.setTrait( CommonStrata.definition, "description", node.item.description ); } itemModel.setTrait( CommonStrata.definition, "agencyId", node.item.agencyID as string ); itemModel.setTrait(CommonStrata.definition, "dataflowId", node.item.id); itemModel.setTrait( CommonStrata.definition, "modelOverrides", this.catalogGroup.traits.modelOverrides.toJson( this.catalogGroup.modelOverrides ) ); } getMemberId(node: DataflowTreeNode) { return `${this.catalogGroup.uniqueId}/${node.type}-${node.item.id}`; } getDataflow(id?: string) { if (!isDefined(id)) return; return this.sdmxServer.dataflows.find((d) => d.id === id); } getCategoryScheme(id?: string) { if (!isDefined(id)) return; return this.sdmxServer.categorySchemes?.find((d) => d.id === id); } getCategoryFromCatagoryScheme( categoryScheme: CategoryScheme | string | undefined, id?: string ) { if (!isDefined(id)) return; const resolvedCategoryScheme = typeof categoryScheme === "string" ? this.getCategoryScheme(categoryScheme) : categoryScheme; return this.getCategoryFromCategories( resolvedCategoryScheme?.categories, id ); } getCategoryFromCategories(categories: Categories | undefined, id?: string) { return isDefined(id) ? categories?.find((c) => c.id === id) : undefined; } getAgency(id?: string) { if (!isDefined(id)) return; const agencies = this.sdmxServer.agencySchemes?.map( (agencyScheme) => agencyScheme.agencies ); if (!isDefined(agencies)) return; return flatten(filterOutUndefined(agencies)).find( (d) => d.id === id ) as Agency; } } StratumOrder.addLoadStratum(SdmxServerStratum.stratumName); export function parseSdmxUrn(urn?: string) { if (!isDefined(urn)) return; // Format urn:sdmx:org.sdmx.infomodel.xxx.xxx=AGENCY:RESOURCEID(VERSION).SUBRESOURCEID.SUBSUBRESOURCEID... // Example urn:sdmx:org.sdmx.infomodel.categoryscheme.Category=SPC:CAS_COM_TOPIC(1.0).ECO // Sub resource ID and (and sub sub...) are optional const matches = regexMatches(/.+=(.+):(.+)\((.+)\)\.*(.*)/g, urn); if ( matches.length >= 1 && matches[0].length >= 3 && !isDefined([0, 1, 2].find((idx) => matches[0][idx] === null)) ) { return { agencyId: matches[0][0], resourceId: matches[0][1], version: matches[0][2], descendantIds: matches[0][3] !== null ? matches[0][3].split(".") : undefined }; } } export async function loadSdmxJsonStructure( url: string, allowNotImplemeted: false ): Promise<SdmxJsonStructureMessage>; export async function loadSdmxJsonStructure( url: string, allowNotImplemeted: true ): Promise<SdmxJsonStructureMessage | undefined>; export async function loadSdmxJsonStructure( url: string, allowNotImplemeted: boolean ) { try { return JSON.parse( await new Resource({ url, headers: { Accept: "application/vnd.sdmx.structure+json; charset=utf-8; version=1.0" } }).fetch() ) as SdmxJsonStructureMessage; } catch (error) { // If SDMX server has returned an error message if (error instanceof RequestErrorEvent && isDefined(error.response)) { if (!allowNotImplemeted) { throw new TerriaError({ title: i18next.t( "models.sdmxServerStratum.sdmxStructureLoadErrorTitle" ), message: sdmxErrorString.has(error.statusCode) ? `${sdmxErrorString.get(error.statusCode)}: ${error.response}` : `${error.response}` }); } // Not sure what happened (maybe CORS) } else if (!allowNotImplemeted) { throw new TerriaError({ title: i18next.t( "models.sdmxServerStratum.sdmxStructureLoadErrorTitle" ), message: `Unkown error occurred${ isDefined(error) ? typeof error === "string" ? `: ${error}` : `: ${JSON.stringify(error)}` : "" }` }); } } } type DataflowTreeNodeBase<T, I> = { type: T; item: I; members?: DataflowTree; }; type DataflowTreeNodeAgencyScheme = DataflowTreeNodeBase< "agencyScheme", AgencyScheme >; type DataflowTreeNodeCategoryScheme = DataflowTreeNodeBase< "categoryScheme", CategoryScheme >; type DataflowTreeNodeCategory = DataflowTreeNodeBase<"category", Category>; type DataflowTreeNodeDataflow = DataflowTreeNodeBase<"dataflow", Dataflow>; type DataflowTreeNode = | DataflowTreeNodeAgencyScheme | DataflowTreeNodeCategoryScheme | DataflowTreeNodeCategory | DataflowTreeNodeDataflow; type DataflowTree = { [key: string]: DataflowTreeNode }; export enum SdmxHttpErrorCodes { // SDMX to HTTP Error Mapping - taken from https://github.com/sdmx-twg/sdmx-rest/blob/7366f56ac08fe4eed758204e32d2e1ca62c78acf/v2_1/ws/rest/docs/4_7_errors.md#sdmx-to-http-error-mapping NoChanges = 304, // 100 No results found = 404 Not found NoResults = 404, // 110 Unauthorized = 401 Unauthorized Unauthorized = 401, // 130 Response too large due to client request = 413 Request entity too large // 510 Response size exceeds service limit = 413 Request entity too large ReponseTooLarge = 413, // 140 Syntax error = 400 Bad syntax SyntaxError = 400, // 150 Semantic error = 403 Forbidden SemanticError = 403, UriTooLong = 414, // 500 Internal Server error = 500 Internal server error // 1000+ = 500 Internal server error ServerError = 500, // 501 Not implemented = 501 Not implemented NotImplemented = 501, // 503 Service unavailable = 503 Service unavailable ServiceUnavailable = 503 } export const sdmxErrorString = new Map<SdmxHttpErrorCodes, string>(); sdmxErrorString.set(SdmxHttpErrorCodes.NoChanges, "No changes"); sdmxErrorString.set(SdmxHttpErrorCodes.NoResults, "No results"); sdmxErrorString.set(SdmxHttpErrorCodes.Unauthorized, "Unauthorised"); sdmxErrorString.set(SdmxHttpErrorCodes.ReponseTooLarge, "Response too large"); sdmxErrorString.set(SdmxHttpErrorCodes.SyntaxError, "Syntax error"); sdmxErrorString.set(SdmxHttpErrorCodes.SemanticError, "Semantic error"); sdmxErrorString.set(SdmxHttpErrorCodes.UriTooLong, "URI too long"); sdmxErrorString.set(SdmxHttpErrorCodes.ServerError, "Server error"); sdmxErrorString.set(SdmxHttpErrorCodes.NotImplemented, "Not implemented"); sdmxErrorString.set( SdmxHttpErrorCodes.ServiceUnavailable, "Service unavailable" );