terriajs
Version:
Geospatial data visualization platform.
310 lines (269 loc) • 9.52 kB
text/typescript
import { ApiClient, fromCatalog } from "@opendatasoft/api-client";
import { Dataset, Facet } from "@opendatasoft/api-client/dist/client/types";
import i18next from "i18next";
import { action, computed, runInAction, makeObservable } from "mobx";
import URI from "urijs";
import isDefined from "../../../Core/isDefined";
import runLater from "../../../Core/runLater";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import GroupMixin from "../../../ModelMixins/GroupMixin";
import UrlMixin from "../../../ModelMixins/UrlMixin";
import ModelReference from "../../../Traits/ModelReference";
import { MetadataUrlTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits";
import OpenDataSoftCatalogGroupTraits, {
RefineTraits
} from "../../../Traits/TraitsClasses/OpenDataSoftCatalogGroupTraits";
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 OpenDataSoftCatalogItem from "../CatalogItems/OpenDataSoftCatalogItem";
// "Valid" types which force some properties to be defined
export type ValidDataset = Dataset & { dataset_id: string };
export type ValidFacet = Facet & { name: string; facets?: ValidFacet[] };
export class OpenDataSoftCatalogStratum extends LoadableStratum(
OpenDataSoftCatalogGroupTraits
) {
static stratumName = "openDataSoftCatalog";
static async load(
catalogGroup: OpenDataSoftCatalogGroup
): Promise<OpenDataSoftCatalogStratum> {
if (!catalogGroup.url) throw "`url` must be set";
const client = new ApiClient({
domain: catalogGroup.url
});
const datasets: ValidDataset[] = [];
let facets: ValidFacet[] | undefined;
// If no facetFilters, try to get some facets
if (
catalogGroup.facetFilters &&
catalogGroup.facetFilters.length === 0 &&
!catalogGroup.flatten
) {
facets = (await client.get(fromCatalog().facets())).facets?.filter((f) =>
isValidFacet(f)
) as ValidFacet[];
}
// If no facets (or we have facetFiles) - get datasets
if (!facets || facets.length === 0) {
// We can only fetch datasets in batch of 100
let offset = 0;
let totalDatasets: number | undefined;
const limit = 100;
let q = fromCatalog()
.datasets()
.limit(limit)
.orderBy("title asc")
// Filter dataset with 'geo' or 'timeserie' features.
// Possible values: calendar, geo, image, apiproxy, timeserie, and aggregate
.where(`features = "geo" OR features = "timeserie"`);
// If facet filters, use them to filter datasets
if (catalogGroup.facetFilters && catalogGroup.facetFilters.length > 0) {
q = q.refine(
catalogGroup.facetFilters.map((f) => `${f.name}:${f.value}`).join(",")
);
}
while (!isDefined(totalDatasets) || offset < totalDatasets) {
q = q.offset(offset);
const catalog = await client.get(q);
totalDatasets = catalog.total_count ?? 0;
catalog.datasets?.forEach((response) => {
if (isValidDataset(response.dataset)) {
datasets.push(response.dataset);
}
});
offset += limit;
}
}
return new OpenDataSoftCatalogStratum(
catalogGroup,
undefined,
facets ?? [],
datasets ?? []
);
}
duplicateLoadableStratum(model: BaseModel): this {
return new OpenDataSoftCatalogStratum(
model as OpenDataSoftCatalogGroup,
this.facetName,
this.facets,
this.datasets
) as this;
}
constructor(
private readonly catalogGroup: OpenDataSoftCatalogGroup,
readonly facetName: string | undefined,
readonly facets: ValidFacet[],
readonly datasets: ValidDataset[]
) {
super();
makeObservable(this);
}
get members(): ModelReference[] {
return [
...this.facets.map((f) => this.getFacetId(f)),
...this.datasets.map((d) => this.getDatasetId(d))
];
}
createMembers() {
this.facets.forEach((facet) => this.createGroupFromFacet(facet));
this.datasets.forEach((dataset) => this.createMemberFromDataset(dataset));
}
/** Turn facet into OpenDataSoftCatalogGroup */
createGroupFromFacet(facet: ValidFacet) {
const layerId = this.getFacetId(facet);
if (!layerId) {
return;
}
const existingGroupModel = this.catalogGroup.terria.getModelById(
OpenDataSoftCatalogGroup,
layerId
);
let groupModel: OpenDataSoftCatalogGroup;
if (existingGroupModel === undefined) {
groupModel = new OpenDataSoftCatalogGroup(
layerId,
this.catalogGroup.terria,
undefined
);
this.catalogGroup.terria.addModel(groupModel);
} else {
groupModel = existingGroupModel;
}
// Replace the stratum inherited from the parent group.
groupModel.strata.delete(CommonStrata.definition);
groupModel.setTrait(
CommonStrata.definition,
"name",
`${facet.name}${facet.count ? ` (${facet.count ?? 0})` : ""}`
);
groupModel.setTrait(CommonStrata.definition, "url", this.catalogGroup.url);
// Set OpenDataSoftDatasetStratum so it doesn't have to be loaded gain
groupModel.strata.delete(OpenDataSoftCatalogStratum.stratumName);
// If no more facets, set facetFilter
if (
!facet.facets ||
!Array.isArray(facet.facets) ||
facet.facets.length === 0
) {
groupModel.setTrait(CommonStrata.definition, "facetFilters", [
createStratumInstance(RefineTraits, {
name: this.facetName,
value: facet.name
})
]);
} else {
groupModel.strata.set(
OpenDataSoftCatalogStratum.stratumName,
new OpenDataSoftCatalogStratum(groupModel, facet.name, facet.facets, [])
);
}
}
/** Turn dataset into OpenDataSoftCatalogItem */
createMemberFromDataset(dataset: ValidDataset) {
const layerId = this.getDatasetId(dataset);
if (!layerId) {
return;
}
const existingItemModel = this.catalogGroup.terria.getModelById(
OpenDataSoftCatalogItem,
layerId
);
let itemModel: OpenDataSoftCatalogItem;
if (existingItemModel === undefined) {
itemModel = new OpenDataSoftCatalogItem(
layerId,
this.catalogGroup.terria,
undefined
);
this.catalogGroup.terria.addModel(itemModel);
// Add older shareKey
this.catalogGroup.terria.addShareKey(
layerId,
`${this.catalogGroup.uniqueId}/${dataset.dataset_id}`
);
} else {
itemModel = existingItemModel;
}
// Replace the stratum inherited from the parent group.
itemModel.strata.delete(CommonStrata.definition);
itemModel.setTrait(
CommonStrata.definition,
"datasetId",
dataset.dataset_id
);
itemModel.setTrait(CommonStrata.definition, "url", this.catalogGroup.url);
itemModel.setTrait(
CommonStrata.definition,
"name",
dataset.metas?.default?.title ?? dataset.dataset_id
);
itemModel.setTrait(
CommonStrata.definition,
"description",
dataset.metas?.default?.description ?? undefined
);
itemModel.setTrait(CommonStrata.definition, "metadataUrls", [
createStratumInstance(MetadataUrlTraits, {
title: i18next.t("models.openDataSoft.viewDatasetPage"),
url: `${this.catalogGroup.url}/explore/dataset/${dataset.dataset_id}/information/`
})
]);
}
getDatasetId(dataset: ValidDataset) {
// Use OpenDataSoft server hostname for datasets, so we don't create multiple across facets
return `${
this.catalogGroup.url
? URI(this.catalogGroup.url).hostname()
: this.catalogGroup.uniqueId
}/${dataset.dataset_id}`;
}
getFacetId(facet: ValidFacet) {
return `${this.catalogGroup.uniqueId}/${facet.name}`;
}
}
StratumOrder.addLoadStratum(OpenDataSoftCatalogStratum.stratumName);
export default class OpenDataSoftCatalogGroup extends UrlMixin(
GroupMixin(CatalogMemberMixin(CreateModel(OpenDataSoftCatalogGroupTraits)))
) {
static readonly type = "opendatasoft-group";
get type() {
return OpenDataSoftCatalogGroup.type;
}
protected async forceLoadMetadata(): Promise<void> {
if (!this.strata.has(OpenDataSoftCatalogStratum.stratumName)) {
const stratum = await OpenDataSoftCatalogStratum.load(this);
runInAction(() => {
this.strata.set(OpenDataSoftCatalogStratum.stratumName, stratum);
});
}
}
protected async forceLoadMembers() {
const opendatasoftServerStratum = this.strata.get(
OpenDataSoftCatalogStratum.stratumName
) as OpenDataSoftCatalogStratum | undefined;
if (opendatasoftServerStratum) {
await runLater(() => opendatasoftServerStratum.createMembers());
}
}
}
export function isValidDataset(
dataset: Dataset | undefined
): dataset is ValidDataset {
return isDefined(dataset) && isDefined(dataset.dataset_id);
}
export function isValidFacet(facet: Facet | undefined): facet is ValidFacet {
return (
isDefined(facet) &&
isDefined(facet.name) &&
(facet.facets ?? []).reduce<boolean>(
(valid, current) => valid && isValidFacet(current),
true
)
);
}