terriajs
Version:
Geospatial data visualization platform.
508 lines (448 loc) • 14.9 kB
text/typescript
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);
}
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 */
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)
*/
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());
}
}
}