UNPKG

terriajs

Version:

Geospatial data visualization platform.

508 lines (448 loc) 14.9 kB
import i18next from "i18next"; import { action, computed, runInAction, makeObservable } from "mobx"; import URI from "urijs"; import isDefined from "../../../Core/isDefined"; import loadJson from "../../../Core/loadJson"; import runLater from "../../../Core/runLater"; import TerriaError, { networkRequestError } from "../../../Core/TerriaError"; import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin"; import GroupMixin from "../../../ModelMixins/GroupMixin"; import UrlMixin from "../../../ModelMixins/UrlMixin"; import ModelReference from "../../../Traits/ModelReference"; import { InfoSectionTraits, MetadataUrlTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits"; import SocrataCatalogGroupTraits, { FacetFilterTraits } from "../../../Traits/TraitsClasses/SocrataCatalogGroupTraits"; 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 StratumOrder from "../../Definition/StratumOrder"; import CatalogGroup from "../CatalogGroup"; import CsvCatalogItem from "../CatalogItems/CsvCatalogItem"; import GeoJsonCatalogItem from "../CatalogItems/GeoJsonCatalogItem"; import SocrataMapViewCatalogItem from "../CatalogItems/SocrataMapViewCatalogItem"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; export interface Facet { facet: string; count: number; values: { value: string; count: number }[]; } export interface ResultResponse { resultSetSize: number; timings: unknown; results: Result[]; } export interface Result { resource: { name: string; id: string; parent_fxf: unknown; description?: string; attribution?: string; attribution_link?: string; contact_email?: string; type: "dataset" | "map" | undefined; updatedAt: string; createdAt: string; metadata_updated_at: string; data_updated_at: string; // publication_date: string; not sure if this is official page_views: { page_views_last_week: number; page_views_last_month: number; page_views_total: number; page_views_last_week_log: number; page_views_last_month_log: number; page_views_total_log: number; }; columns_name: string[]; columns_field_name: string[]; columns_datatype: string[]; columns_description: string[]; columns_format: { precisionStyle?: string; noCommas?: string; align?: string; }[]; download_count: number; provenance: string; lens_view_type: | "tabular" | "blobby" | "href" | "geo" | "story" | "measure" | "gateway_plugin" | undefined; blob_mime_type: null | string; hide_from_data_json: boolean; }; classification: { categories: string[]; tags: string[]; domain_category: string; domain_tags: string[]; domain_metadata: { key: string; value: string }[]; }; metadata: { domain: string; license: string; }; permalink: string; link: string; owner?: { id: string; user_type?: string; display_name: string; }; creator?: { id: string; user_type?: string; display_name: string; }; } export interface SocrataError { code?: string; error: true | string; message?: string; } export class SocrataCatalogStratum extends LoadableStratum( SocrataCatalogGroupTraits ) { static stratumName = "socrataCatalog"; static async load( catalogGroup: SocrataCatalogGroup ): Promise<SocrataCatalogStratum> { if (!catalogGroup.url) throw "`url` must be set"; const filterQuery = Object.assign({}, catalogGroup.filterQuery, { only: "dataset,map" }); const domain = URI(catalogGroup.url).hostname(); let facets: Facet[] = []; let results: Result[] = []; // If not facet filters have been set - get facets if ( !isDefined(catalogGroup.facetFilters) || catalogGroup.facetFilters.length === 0 ) { const facetsToUse = catalogGroup.facetGroups; const facetUri = URI( `${catalogGroup.url}/api/catalog/v1/domains/${domain}/facets` ).addQuery(filterQuery); const facetResponse = await loadJson( proxyCatalogItemUrl(catalogGroup, facetUri.toString()) ); if (facetResponse.error) { throw facetResponse.message ?? facetResponse.error; } facets = facetResponse; if (!Array.isArray(facets)) throw `Could not fetch facets for domain ${domain}`; facets = facets.filter((f) => facetsToUse.includes(f.facet)); if (facets.length === 0) throw `Could not find any facets for domain ${domain}`; } // If facetFilter is set, use it to search for datasets (aka resources) else { const resultsUri = URI( `${catalogGroup.url}/api/catalog/v1?search_context=${domain}` ).addQuery(filterQuery); catalogGroup.facetFilters.forEach(({ name, value }) => name && isDefined(value) ? resultsUri.addQuery(name, value) : null ); const resultsResponse = await loadJson( proxyCatalogItemUrl(catalogGroup, resultsUri.toString()) ); if (resultsResponse.error) { throw resultsResponse.message ?? resultsResponse.error; } results = (resultsResponse as ResultResponse).results; if (!Array.isArray(results) || results.length === 0) throw `Could not find any results for domain ${domain} and facets: ${catalogGroup.facetFilters .map(({ name, value }) => `${name} = ${value}`) .join(", ")}`; } return new SocrataCatalogStratum(catalogGroup, facets, results); } duplicateLoadableStratum(model: BaseModel): this { return new SocrataCatalogStratum( model as SocrataCatalogGroup, this.facets, this.results ) as this; } constructor( private readonly catalogGroup: SocrataCatalogGroup, private readonly facets: Facet[], private readonly results: Result[] ) { super(); makeObservable(this); } @computed get members(): ModelReference[] { // If we only have one facet, return it's children instead of a single facet group if (this.facets.length === 1) return this.facets[0].values.map( (facetValue) => `${this.getFacetId(this.facets[0])}/${facetValue.value}` ); return [ ...this.facets.map((f) => this.getFacetId(f)), ...this.results.map((r) => this.getResultId(r)) ]; } createMembers() { this.facets.forEach((facet) => this.createGroupFromFacet(facet)); this.results.forEach((result) => this.createItemFromResult(result)); } /** Turn facet into SocrataCatalogGroup */ @action createGroupFromFacet(facet: Facet) { const facetGroupId = this.getFacetId(facet); // Create group for Facet let facetGroup = this.catalogGroup.terria.getModelById( CatalogGroup, facetGroupId ); if (facetGroup === undefined) { facetGroup = new CatalogGroup( facetGroupId, this.catalogGroup.terria, undefined ); this.catalogGroup.terria.addModel(facetGroup); } // Replace the stratum inherited from the parent group. facetGroup.strata.delete(CommonStrata.definition); facetGroup.setTrait(CommonStrata.definition, "name", facet.facet); // Create child groups for Facet values facet.values.forEach((facetValue) => { const facetValueId = `${facetGroupId}/${facetValue.value}`; let facetValueGroup = this.catalogGroup.terria.getModelById( SocrataCatalogGroup, facetValueId ); if (facetValueGroup === undefined) { facetValueGroup = new SocrataCatalogGroup( facetValueId, this.catalogGroup.terria, undefined ); this.catalogGroup.terria.addModel(facetValueGroup); } // Replace the stratum inherited from the parent group. facetValueGroup.strata.delete(CommonStrata.definition); facetValueGroup.setTrait( CommonStrata.definition, "name", `${facetValue.value}${ facetValue.count ? ` (${facetValue.count ?? 0})` : "" }` ); facetValueGroup.setTrait( CommonStrata.definition, "url", this.catalogGroup.url ); facetValueGroup.setTrait(CommonStrata.definition, "facetFilters", [ createStratumInstance(FacetFilterTraits, { name: facet.facet, value: facetValue.value }) ]); facetGroup!.add(CommonStrata.definition, facetValueGroup); }); } /** Turn Result into catalog item * If type is 'dataset': * - If has geometery -> create GeoJSONCatalogItem * - Otherwise -> create CsvCatalogItem * If type is 'map' -> SocrataMapViewCatalogItem * - Then the Socrata `views` API will be used to fetch data (this mimics how Socrata portal map visualisation works - it isn't an official API) */ @action createItemFromResult(result: Result) { const resultId = this.getResultId(result); // Add share key for old ID which included parents ID this.catalogGroup.terria.addShareKey( resultId, `${this.catalogGroup.uniqueId}/${result.resource.id}` ); let resultModel: | CsvCatalogItem | GeoJsonCatalogItem | SocrataMapViewCatalogItem | undefined; // If dataset resource // - If has geometry - create GeoJSONCatalogItem // - Otherwise - create CsvCatalogItem if (result.resource.type === "dataset") { if ( result.resource.columns_datatype.find((type) => [ "Point", "Line", "Polygon", "MultiLine", "MultiPoint", "MultiPolygon", "Location" ].includes(type) ) ) { resultModel = this.catalogGroup.terria.getModelById( GeoJsonCatalogItem, resultId ); if (resultModel === undefined) { resultModel = new GeoJsonCatalogItem( resultId, this.catalogGroup.terria, undefined ); this.catalogGroup.terria.addModel(resultModel); } // Replace the stratum inherited from the parent group. resultModel.strata.delete(CommonStrata.definition); resultModel.setTrait( CommonStrata.definition, "url", `${this.catalogGroup.url}/resource/${result.resource.id}.geojson?$limit=10000` ); } else { resultModel = this.catalogGroup.terria.getModelById( CsvCatalogItem, resultId ); if (resultModel === undefined) { resultModel = new CsvCatalogItem( resultId, this.catalogGroup.terria, undefined ); this.catalogGroup.terria.addModel(resultModel); } // Replace the stratum inherited from the parent group. resultModel.strata.delete(CommonStrata.definition); resultModel.setTrait( CommonStrata.definition, "url", `${this.catalogGroup.url}/resource/${result.resource.id}.csv?$limit=10000` ); } // If type is 'map' -> SocrataMapViewCatalogItem } else if (result.resource.type === "map") { resultModel = this.catalogGroup.terria.getModelById( SocrataMapViewCatalogItem, resultId ); if (resultModel === undefined) { resultModel = new SocrataMapViewCatalogItem( resultId, this.catalogGroup.terria, undefined ); this.catalogGroup.terria.addModel(resultModel); } // Replace the stratum inherited from the parent group. resultModel.strata.delete(CommonStrata.definition); resultModel.setTrait( CommonStrata.definition, "url", this.catalogGroup.url ); resultModel.setTrait( CommonStrata.definition, "resourceId", result.resource.id ); } if (resultModel) { resultModel.setTrait( CommonStrata.definition, "name", result.resource.name ); resultModel.setTrait( CommonStrata.definition, "description", result.resource.description ); resultModel.setTrait( CommonStrata.definition, "attribution", result.resource.attribution ); resultModel.setTrait(CommonStrata.definition, "info", [ createStratumInstance(InfoSectionTraits, { name: i18next.t("models.socrataServer.licence"), content: result.metadata.license }), createStratumInstance(InfoSectionTraits, { name: i18next.t("models.socrataServer.tags"), content: result.classification.tags.join(", ") }), createStratumInstance(InfoSectionTraits, { name: i18next.t("models.socrataServer.attributes"), content: result.resource.columns_name.join(", ") }) ]); resultModel.setTrait(CommonStrata.definition, "metadataUrls", [ createStratumInstance(MetadataUrlTraits, { title: i18next.t("models.openDataSoft.viewDatasetPage"), url: result.permalink }) ]); } } getFacetId(facet: Facet) { return `${this.catalogGroup.uniqueId}/${facet.facet}`; } getResultId(result: Result) { // Use Socrata server hostname for datasets, so we don't create multiple across facets return `${ this.catalogGroup.url ? URI(this.catalogGroup.url ?? "").hostname() : this.catalogGroup.uniqueId }/${result.resource.id}`; } } StratumOrder.addLoadStratum(SocrataCatalogStratum.stratumName); export default class SocrataCatalogGroup extends UrlMixin( GroupMixin(CatalogMemberMixin(CreateModel(SocrataCatalogGroupTraits))) ) { static readonly type = "socrata-group"; get type() { return SocrataCatalogGroup.type; } protected async forceLoadMetadata(): Promise<void> { try { if (!this.strata.has(SocrataCatalogStratum.stratumName)) { const stratum = await SocrataCatalogStratum.load(this); runInAction(() => { this.strata.set(SocrataCatalogStratum.stratumName, stratum); }); } } catch (e) { networkRequestError( TerriaError.from(e, { message: { key: "models.socrataServer.retrieveErrorMessage" } }) ); } } protected async forceLoadMembers() { const socrataServerStratum = this.strata.get( SocrataCatalogStratum.stratumName ) as SocrataCatalogStratum | undefined; if (socrataServerStratum) { await runLater(() => socrataServerStratum.createMembers()); } } }