terriajs
Version:
Geospatial data visualization platform.
465 lines • 20.9 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import i18next from "i18next";
import { flatten } from "lodash-es";
import { action, computed, makeObservable, runInAction } from "mobx";
import URI from "urijs";
import TerriaError, { networkRequestError } from "../../../Core/TerriaError";
import filterOutUndefined from "../../../Core/filterOutUndefined";
import isDefined from "../../../Core/isDefined";
import loadWithXhr from "../../../Core/loadWithXhr";
import loadXML from "../../../Core/loadXML";
import runLater from "../../../Core/runLater";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import GroupMixin from "../../../ModelMixins/GroupMixin";
import UrlMixin from "../../../ModelMixins/UrlMixin";
import xml2json from "../../../ThirdParty/xml2json";
import CswCatalogGroupTraits from "../../../Traits/TraitsClasses/CswCatalogGroupTraits";
import CommonStrata from "../../Definition/CommonStrata";
import CreateModel from "../../Definition/CreateModel";
import LoadableStratum from "../../Definition/LoadableStratum";
import StratumOrder from "../../Definition/StratumOrder";
import CatalogGroup from "../CatalogGroup";
import CsvCatalogItem from "../CatalogItems/CsvCatalogItem";
import GeoJsonCatalogItem from "../CatalogItems/GeoJsonCatalogItem";
import KmlCatalogItem from "../CatalogItems/KmlCatalogItem";
import ArcGisMapServerCatalogItem from "../Esri/ArcGisMapServerCatalogItem";
import defaultGetRecordsTemplate from "../Ows/CswGetRecordsTemplate.xml";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
import WebMapServiceCatalogItem from "./WebMapServiceCatalogItem";
function toArray(val) {
if (!isDefined(val))
return undefined;
return Array.isArray(val) ? val : [val];
}
class CswStratum extends LoadableStratum(CswCatalogGroupTraits) {
catalogGroup;
metadataGroups;
records;
static stratumName = "CswStratum";
static async load(catalogGroup) {
if (catalogGroup.url === undefined) {
throw new TerriaError({
title: i18next.t("models.csw.missingUrlTitle"),
message: i18next.t("models.csw.missingUrlMessage")
});
}
const metadataGroups = [];
/**
* If domainSpecification properties are set (and we aren't flattening the catalog) - we will try to create MetadataGroups * from a GetDomain response (using the specified domainPropertyName).
*
* An example GetDomain response (see wwwroot\test\csw\Example1GetDomain.xml):
* ...
* <csw:Value>Multiple Use | Fisheries Effort</csw:Value>
* <csw:Value>Multiple Use | Pollution</csw:Value>
* <csw:Value>Multiple Use | Sea Surface Temperature</csw:Value>
* <csw:Value>Multiple Use | Seismic Surveys</csw:Value>
* <csw:Value>Multiple Use | Shipping</csw:Value>
* ...
*
* These strings are used to generate MetadataGroups with:
* - Hierarchy separator used to split strings into pieces (domainSpecification.hierarchySeparator = " | ")
* - Query property name used to match values to Records (domainSpecification.queryPropertyName = "subject"). That is to say, we will search through Record["subject"] to add Records to sepcific MetadataGroups
*/
if (!catalogGroup.flatten &&
catalogGroup.domainSpecification.domainPropertyName &&
catalogGroup.domainSpecification.hierarchySeparator &&
catalogGroup.domainSpecification.queryPropertyName) {
const getDomainUrl = new URI(proxyCatalogItemUrl(catalogGroup, catalogGroup.url)).query({
service: "CSW",
version: "2.0.2",
request: "GetDomain",
propertyname: catalogGroup.domainSpecification.domainPropertyName
});
let domainResponse;
try {
const xml = await loadXML(getDomainUrl.toString());
if (!xml ||
!xml.documentElement ||
xml.documentElement.localName !== "GetDomainResponse") {
throw `Invalid XML response`;
}
domainResponse = xml2json(xml);
}
catch (error) {
console.log(error);
throw networkRequestError({
sender: catalogGroup,
title: i18next.t("models.csw.notUseableTitle"),
message: i18next.t("models.csw.notUseableMessage")
});
}
if (!domainResponse) {
throw networkRequestError({
sender: catalogGroup,
title: i18next.t("models.csw.errorLoadingTitle"),
message: i18next.t("models.csw.checkCORSDomain")
});
}
// Get flat listOfValues
const listOfValues = flatten(toArray(domainResponse.DomainValues)?.map((d) => toArray(d?.ListOfValues?.Value))).filter((v) => typeof v === "string");
// Create metadataGroups from listOfValues
listOfValues.forEach((value) => {
const keys = value.split(catalogGroup.domainSpecification.hierarchySeparator);
// recursively find the group that the last key in keys should belong to and add that key
addMetadataGroups(keys, 0, metadataGroups, catalogGroup.domainSpecification.hierarchySeparator, catalogGroup.domainSpecification.queryPropertyName);
});
}
// Get Records
let paging = true;
let startPosition = 1;
const records = [];
// We have to paginate through Records
while (paging) {
// Replace {startPosition}
const postData = (catalogGroup.getRecordsTemplate ?? defaultGetRecordsTemplate).replace("{startPosition}", startPosition);
const xml = await loadWithXhr({
url: proxyCatalogItemUrl(catalogGroup, new URI(catalogGroup.url).query("").toString(), "1d"),
responseType: "document",
method: "POST",
overrideMimeType: "text/xml",
data: postData,
headers: {
"Content-Type": "application/xml"
}
});
if (!isDefined(xml)) {
throw networkRequestError({
sender: catalogGroup,
title: i18next.t("models.csw.errorLoadingRecordsTitle"),
message: i18next.t("models.csw.errorLoadingRecordsMessage")
});
}
const json = xml2json(xml);
if (json.Exception) {
let errorMessage = i18next.t("models.csw.unknownError");
if (json.Exception.ExceptionText) {
errorMessage = i18next.t("models.csw.exceptionMessage", {
exceptionText: json.Exception.ExceptionText
});
}
throw new TerriaError({
sender: catalogGroup,
title: i18next.t("models.csw.errorLoadingTitle"),
message: errorMessage
});
}
records.push(...(json?.SearchResults?.Record ?? []));
// Get next start position - or stop pageing
const nextRecord = typeof json?.SearchResults?.nextRecord === "string"
? parseInt(json?.SearchResults?.nextRecord ?? "0", 10)
: json?.SearchResults?.nextRecord;
const numberOfRecordsMatched = typeof json?.SearchResults?.numberOfRecordsMatched === "string"
? parseInt(json?.SearchResults?.numberOfRecordsMatched ?? "0", 10)
: json?.SearchResults?.numberOfRecordsMatched;
if (!isDefined(nextRecord) ||
nextRecord === 0 ||
nextRecord >= numberOfRecordsMatched) {
paging = false;
}
else {
startPosition = nextRecord;
}
}
// If we have metadataGroups, add records to them
if (metadataGroups.length > 0) {
records.forEach((record) => {
findGroup(metadataGroups, record)?.records.push(record);
});
}
return new CswStratum(catalogGroup, metadataGroups, records);
}
constructor(catalogGroup, metadataGroups, records) {
super();
this.catalogGroup = catalogGroup;
this.metadataGroups = metadataGroups;
this.records = records;
makeObservable(this);
}
duplicateLoadableStratum(model) {
return new CswStratum(model, this.metadataGroups, this.records);
}
get members() {
// If no metadataGroups - return flat list of record ids
if (this.metadataGroups.length === 0) {
return this.records.map((r) => `${this.catalogGroup.uniqueId}/${r.identifier}`);
}
return this.metadataGroups.map((g) => `${this.catalogGroup.uniqueId}/${g.groupName}`);
}
createMembersFromLayers() {
// If no metadata groups -> just create all records
if (this.metadataGroups.length === 0) {
this.records.forEach((record) => this.createRecord(this.catalogGroup.uniqueId, record));
// If metadata groups -> create them (records will then be created for each group)
}
else {
this.metadataGroups.forEach((metadataGroup) => this.createMetadataGroup(this.catalogGroup.uniqueId, metadataGroup));
}
}
createMetadataGroup(parentId, metadataGroup) {
const layerId = `${parentId}/${metadataGroup.groupName}`;
const existingModel = this.catalogGroup.terria.getModelById(CatalogGroup, layerId);
let model;
if (existingModel === undefined) {
model = new CatalogGroup(layerId, this.catalogGroup.terria);
this.catalogGroup.terria.addModel(model);
}
else {
model = existingModel;
}
// Replace the stratum inherited from the parent group.
model.strata.delete(CommonStrata.definition);
model.setTrait(CommonStrata.definition, "name", metadataGroup.groupName);
model.setTrait(CommonStrata.definition, "members", filterOutUndefined([
...metadataGroup.children.map((childMetadataGroup) => this.createMetadataGroup(layerId, childMetadataGroup).uniqueId),
...metadataGroup.records.map((record) => this.createRecord(layerId, record)?.uniqueId)
]));
return model;
}
createRecord(parentId, record) {
const uris = toArray(record.URI ?? record.references);
if (!isDefined(uris)) {
return;
}
/**
* Array of acceptable URLS for catalog item. Its indices map to resourceFormats' index.
* For example - if `acceptableUrls[1]` is defined, it maps to `resourceFormats[1]` - which is a ArcGisMapServerCatalogItem
*/
const acceptableUris = [];
/**
* There may be more than one url that results in a data layer here - so check for
* the acceptable ones, store the others as downloadUrls that can be
* displayed in the metadata summary for the layer
*/
const downloadUrls = [];
let legendUri = undefined;
const filteredResourceFormats = this.resourceFormats.filter((f) => f.enabled);
for (let m = 0; m < uris.length; m++) {
const uri = uris[m];
if (!uri)
return;
const resourceIndex = filteredResourceFormats.findIndex((f) => (uri.protocol ?? uri.scheme)?.match(f.regex));
// If matching resource is found, and an acceptable URL hasn't been set for it -> add it
if (resourceIndex !== -1 && !acceptableUris[resourceIndex]) {
acceptableUris[resourceIndex] = uri;
}
else {
if (uri?.description === "LegendUrl") {
legendUri = uri;
}
downloadUrls.push({
url: uri.toString(),
description: uri.description ? uri.description : uri.name
});
}
}
const layerId = `${parentId}/${record.identifier}`;
const urlIndex = acceptableUris.findIndex((url) => isDefined(url));
if (urlIndex !== -1) {
const modelConstructor = this.resourceFormats[urlIndex].contructor;
const existingModel = this.catalogGroup.terria.getModelById(modelConstructor, layerId);
let model;
if (existingModel === undefined) {
model = new modelConstructor(layerId, this.catalogGroup.terria);
this.catalogGroup.terria.addModel(model);
}
else {
model = existingModel;
}
// Replace the stratum inherited from the parent group.
model.strata.delete(CommonStrata.definition);
model.setTrait(CommonStrata.definition, "name", record.title ?? record.identifier);
const uri = acceptableUris[urlIndex];
model.setTrait(CommonStrata.definition, "url", uri.toString());
if (record.abstract) {
model.setTrait(CommonStrata.definition, "description", toArray(record.abstract)?.join("\n\n"));
}
else if (record.description) {
model.setTrait(CommonStrata.definition, "description", toArray(record.description)?.join("\n\n"));
}
const infoSections = [];
if (record.contributor && toArray(record.contributor).length > 0) {
infoSections.push({
name: i18next.t("models.csw.dataResponsibility"),
content: toArray(record.contributor)?.join("\n\n")
});
}
infoSections.push({
name: i18next.t("models.csw.links"),
content: downloadUrls
.map((d) => `[${d.description}](${d.url})`)
.join("\n\n")
});
model.setTrait(CommonStrata.definition, "info", infoSections);
model.setTrait(CommonStrata.definition, "metadataUrls", [
{
title: i18next.t("models.csw.metadataURL"),
url: new URI(proxyCatalogItemUrl(this.catalogGroup, this.catalogGroup.url))
.query({
service: "CSW",
version: "2.0.2",
request: "GetRecordById",
outputSchema: "http://www.opengis.net/cat/csw/2.0.2",
ElementSetName: "full",
id: record.identifier
})
.toString()
}
]);
if (legendUri) {
model.setTrait(CommonStrata.definition, "legends", [
{ url: legendUri.toString() }
]);
}
// If this is a WMS item, we MUST set `layers` trait to `uri.name`
if (model instanceof WebMapServiceCatalogItem) {
if (!uri.name) {
return;
}
model.setTrait(CommonStrata.definition, "layers", uri.name);
}
// Same with ArcGis MapServer
if (model instanceof ArcGisMapServerCatalogItem) {
if (!uri.name) {
return;
}
model.setTrait(CommonStrata.definition, "layers", uri.name);
}
return model;
}
}
/**
* These are used to match URLs to model constructors
*/
get resourceFormats() {
return [
{
enabled: this.catalogGroup.includeWms,
regex: new RegExp(this.catalogGroup.wmsResourceFormat, "i"),
contructor: WebMapServiceCatalogItem
},
{
enabled: this.catalogGroup.includeEsriMapServer,
regex: new RegExp(this.catalogGroup.esriMapServerResourceFormat, "i"),
contructor: ArcGisMapServerCatalogItem
},
{
enabled: this.catalogGroup.includeKml,
regex: new RegExp(this.catalogGroup.kmlResourceFormat, "i"),
contructor: KmlCatalogItem
},
{
enabled: this.catalogGroup.includeGeoJson,
regex: new RegExp(this.catalogGroup.geoJsonResourceFormat, "i"),
contructor: GeoJsonCatalogItem
},
{
enabled: this.catalogGroup.includeCsv,
regex: new RegExp(this.catalogGroup.csvResourceFormat, "i"),
contructor: CsvCatalogItem
}
];
}
}
__decorate([
computed
], CswStratum.prototype, "members", null);
__decorate([
action
], CswStratum.prototype, "createMembersFromLayers", null);
__decorate([
action
], CswStratum.prototype, "createMetadataGroup", null);
__decorate([
action
], CswStratum.prototype, "createRecord", null);
__decorate([
computed
], CswStratum.prototype, "resourceFormats", null);
/**
* Recursively add MetadataGroups from keys (which are split DomainValue)
*/
function addMetadataGroups(keys, index, group, separator, queryField) {
if (index > keys.length - 1)
return;
let groupIndex = group.findIndex((g) => g.groupName === keys[index]);
if (groupIndex === -1) {
// not found so add it
let value;
let regex = true;
// if we aren't at the last key, use a regex and tack on another separator to avoid mismatches
if (index + 1 !== keys.length) {
const sepRegex = separator.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
value = "^" + keys.slice(0, index + 1).join(sepRegex) + sepRegex;
}
else {
value = keys.slice(0, index + 1).join(separator);
regex = false;
}
group.push({
field: queryField,
value: value,
regex: regex,
groupName: keys[index],
children: [],
records: []
});
groupIndex = group.length - 1;
}
addMetadataGroups(keys, index + 1, group[groupIndex].children, separator, queryField);
}
// find groups that the record belongs to
function findGroup(metadataGroups, record) {
for (let i = 0; i < metadataGroups.length; i++) {
const group = metadataGroups[i];
if (group.field) {
const fields = filterOutUndefined(toArray(record[group.field]) ?? []);
if (fields.find((f) => matchValue(group.value, f, group.regex))) {
if (group.children) {
// recurse to see if it fits into any of the children
const childGroup = findGroup(group.children, record);
if (isDefined(childGroup)) {
return childGroup;
}
}
return group;
}
}
}
}
function matchValue(value, recordValue, regex) {
if (isDefined(regex) && regex) {
// regular expression so parse it and check string against it
const regExp = new RegExp(value);
return regExp.test(recordValue);
}
else {
return value === recordValue;
}
}
StratumOrder.addLoadStratum(CswStratum.stratumName);
export default class CswCatalogGroup extends UrlMixin(GroupMixin(CatalogMemberMixin(CreateModel(CswCatalogGroupTraits)))) {
static type = "csw-group";
get type() {
return CswCatalogGroup.type;
}
async forceLoadMetadata() {
if (this.strata.get(CswStratum.stratumName) !== undefined)
return;
const stratum = await CswStratum.load(this);
runInAction(() => {
this.strata.set(CswStratum.stratumName, stratum);
});
}
async forceLoadMembers() {
const cswStratum = this.strata.get(CswStratum.stratumName);
if (cswStratum) {
await runLater(() => cswStratum.createMembersFromLayers());
}
}
}
//# sourceMappingURL=CswCatalogGroup.js.map