terriajs
Version:
Geospatial data visualization platform.
605 lines (540 loc) • 19.1 kB
text/typescript
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);
}
get ckanDataset(): CkanDataset | undefined {
return this.ckanItemReference._ckanDataset;
}
get ckanResource(): CkanResource | undefined {
return this.ckanItemReference._ckanResource;
}
get url() {
return getCkanItemResourceUrl(this.ckanItemReference);
}
get name() {
return getCkanItemName(this.ckanItemReference);
}
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;
}
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) */
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;
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
);
}
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);
});
}
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;
}
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;
}