@seasketch/geoprocessing
Version:
Geoprocessing and reporting framework for SeaSketch 2.0
336 lines • 16.3 kB
JavaScript
import { createMetrics } from "../metrics/helpers.js";
import { isInternalRasterDatasource, isImportVectorDatasourceConfig, isImportRasterDatasourceConfig, getInternalRasterDatasourceById, getInternalVectorDatasourceById, getExternalVectorDatasourceById, getRasterDatasourceById, getExternalRasterDatasourceById, isExternalVectorDatasource, isExternalRasterDatasource, isinternalDatasource, isExternalDatasource, getDatasourceById, getVectorDatasourceById, isInternalVectorDatasource, getFlatGeobufFilename, getCogFilename, } from "../datasources/helpers.js";
import { getObjectiveById, getMetricGroupObjectiveIds, } from "../helpers/index.js";
import { datasourcesSchema, geographiesSchema, metricsSchema, metricGroupsSchema, objectivesSchema, packageSchema, geoprocessingConfigSchema, projectSchema, } from "../types/index.js";
/**
* Client for reading project configuration/metadata.
*/
export class ProjectClientBase {
_project;
_datasources;
_metricGroups;
_geographies;
_objectives;
_package;
_geoprocessing;
_precalc;
constructor(config) {
this._project = projectSchema.parse(config.basic);
this._datasources = datasourcesSchema.parse(config.datasources);
this._metricGroups = metricGroupsSchema.parse(config.metricGroups);
this._geographies = geographiesSchema.parse(config.geographies);
this._objectives = objectivesSchema.parse(config.objectives);
this._package = packageSchema.parse(config.package);
this._geoprocessing = geoprocessingConfigSchema.parse(config.geoprocessing);
this._precalc = metricsSchema.parse(config.precalc);
}
// ASSETS //
/** Returns typed config from project.json */
get basic() {
return this._project;
}
/** Returns typed config from datasources.json */
get datasources() {
return this._datasources;
}
/** Returns internal datasources from datasources.json */
get internalDatasources() {
return this._datasources.filter((ds) => isinternalDatasource(ds));
}
/** Return external datasources from datasources.json */
get externalDatasources() {
return this._datasources.filter((ds) => isExternalDatasource(ds));
}
/** Returns typed config from geographies.json */
get geographies() {
return this._geographies;
}
/** Returns typed config from metrics.json */
get metricGroups() {
return this._metricGroups;
}
/** Returns precalculated metrics from precalc.json */
get precalc() {
return this._precalc;
}
/** Returns typed config from objectives.json */
get objectives() {
return this._objectives;
}
/** Returns typed config from package.json */
get package() {
return this._package;
}
/** Returns typed config from geoprocessing.json */
get geoprocessing() {
return this._geoprocessing;
}
/**
* Returns URL to dataset bucket for project. In test environment or if local parameter is true, will
* return local URL expected to serve up dist data folder
*/
dataBucketUrl(options = {}) {
const { local = false, port = 8080, subPath = "" } = options;
return process.env.NODE_ENV === "test" || local
? `http://127.0.0.1:${port}/${subPath ? subPath + "/" : ""}`
: `https://gp-${this._package.name}-datasets.s3.${this._geoprocessing.region}.amazonaws.com/`;
}
getFgbPath(ds) {
return `data/dist/${ds.datasourceId}.fgb`;
}
getDatasourceUrl(ds, options = {}) {
const { format, local, port, subPath } = options;
if (isInternalVectorDatasource(ds) || isImportVectorDatasourceConfig(ds)) {
// default to fgb format if not specified
if (!format) {
if (ds.formats.includes("fgb"))
return `${this.dataBucketUrl({
local,
port,
subPath,
})}${getFlatGeobufFilename(ds)}`;
}
else if (ds.formats.includes(format))
return `${this.dataBucketUrl({ local, port, subPath })}${ds.datasourceId}.${format}`;
else
throw new Error(`getDatasourceUrl: format not found for datasource ${ds.datasourceId}`);
}
else if (isInternalRasterDatasource(ds) ||
isImportRasterDatasourceConfig(ds)) {
// default to cog tif format if not specificed
if (!format) {
if (ds.formats.includes("tif"))
return `${this.dataBucketUrl({
local,
port,
subPath,
})}${getCogFilename(ds)}`;
}
else if (ds.formats.includes(format))
return `${this.dataBucketUrl({ local, port, subPath })}${ds.datasourceId}.${format}`;
else
throw new Error(`getDatasourceUrl: format not found for datasource ${ds.datasourceId}`);
}
else if (isExternalVectorDatasource(ds) ||
isExternalRasterDatasource(ds)) {
if (ds.url)
return ds.url;
else
throw new Error(`getDatasourceUrl: url undefined for external datasource ${ds.datasourceId}`);
}
throw new Error(`getDatasourceUrl: cannot generate url for datasource ${ds.datasourceId}`);
}
// DATASOURCES //
/** Returns Datasource given datasourceId */
getDatasourceById(datasourceId) {
return getDatasourceById(datasourceId, this._datasources);
}
/** Returns VectorDatasource given datasourceId, throws if not found */
getVectorDatasourceById(datasourceId) {
return getVectorDatasourceById(datasourceId, this._datasources);
}
/** Returns InternalVectorDatasource given datasourceId, throws if not found */
getInternalVectorDatasourceById(datasourceId) {
return getInternalVectorDatasourceById(datasourceId, this._datasources);
}
/** Returns ExternalVectorDatasource given datasourceId, throws if not found */
getExternalVectorDatasourceById(datasourceId) {
return getExternalVectorDatasourceById(datasourceId, this._datasources);
}
/** Returns RasterDatasource given datasourceId, throws if not found */
getRasterDatasourceById(datasourceId) {
return getRasterDatasourceById(datasourceId, this._datasources);
}
/** Returns InternalRasterDatasource given datasourceId, throws if not found */
getInternalRasterDatasourceById(datasourceId) {
return getInternalRasterDatasourceById(datasourceId, this._datasources);
}
/** Returns ExternalRasterDatasource given datasourceId, throws if not found */
getExternalRasterDatasourceById(datasourceId) {
return getExternalRasterDatasourceById(datasourceId, this._datasources);
}
// GEOGRAPHIES //
/**
* Returns project geography matching the provided ID, with optional fallback geography using fallbackGroup parameter
* @param geographyId The geography ID to search for
* @param options
* @param options.fallbackGroup The default group name to lookup if no geographyId is provided. expects there is only one geography with that group name
* @returns
* @throws if geography does not exist
*/
getGeographyById(geographyId, options = {}) {
const { fallbackGroup } = options;
if (geographyId && geographyId.length > 0) {
const curGeog = this._geographies.find((g) => g.geographyId === geographyId);
// verify matching geography exists
if (curGeog) {
return curGeog;
}
}
else if (fallbackGroup) {
// fallback to user-specified geography group
const planGeogs = this._geographies.filter((g) => g.groups?.includes(fallbackGroup));
if (planGeogs.length === 0) {
throw new Error(`Could not find geography with fallback group ${fallbackGroup}`);
}
else if (planGeogs.length > 1) {
throw new Error(`Found more than one geography with fallback group ${fallbackGroup}, there should be only one`);
}
else {
return planGeogs[0];
}
}
throw new Error(`getGeographyById - did not receive geographyID or fallbackGroup`);
}
/**
* @param group the name of the geography group
* @returns geographies with group name assigned
*/
getGeographyByGroup(group) {
return this._geographies.filter((g) => g.groups?.includes(group));
}
// OBJECTIVES //
/** Returns Objective given objectiveId */
getObjectiveById(objectiveId) {
return getObjectiveById(objectiveId, this._objectives);
}
// METRIC GROUPS //
/** Returns MetricGroup given metricId, optional translating display name, given i18n t function */
getMetricGroup(metricId, t) {
const mg = this._metricGroups.find((m) => m.metricId === metricId);
if (!mg)
throw new Error(`Missing MetricGroup ${metricId} in metrics.json`);
if (!t)
return mg;
return {
...mg,
classes: mg.classes.map((curClass) => ({
...curClass,
display: t(curClass.display) /* i18next-extract-disable-line */,
})),
};
}
/**
* Simple helper that given MetricGroup, returns a consistent ID string for a percent metric, defaults to metricId + 'Perc' added to the end
* @param mg - the MetricGroup
* @returns - ID string
*/
getMetricGroupPercId(mg) {
return `${mg.metricId}Perc`;
}
/**
* Returns Objectives for MetricGroup
* If at least one class has an objective assigned, then it returns those, missing classes with no objective get the top-level objective
* If no class-level objectives are found, then it returns the top-level objective
* If no objectives are found, returns an empty array
* Given i18n t function it will also translate the short description
* @param metricGroup
* @param t
* @returns
*/
getMetricGroupObjectives(metricGroup, t) {
const objectives = getMetricGroupObjectiveIds(metricGroup).map((objectiveId) => this.getObjectiveById(objectiveId));
if (!t)
return objectives;
return objectives.map((objective) => ({
...objective,
shortDesc: t(objective.shortDesc) /* i18next-extract-disable-line */,
}));
}
/**
* Returns datasource for given MetricGroup.
* If classId is provided, returns class-level datasource if assigned, otherwise falls back to top-level metricGroup datasource
* @param metricGroup - metricGroup to get datasource for
* @param options.classId - metricGroup class to get datasource for
* @returns the datasource object
* @throws if class does not exist in metric group with given classId
* @throws if datasourceId is missing for metricGroup and class
*/
getMetricGroupDatasource(metricGroup, options = {}) {
const { classId } = options;
if (classId) {
const dataClass = metricGroup.classes.find((c) => c.classId === classId);
if (dataClass && dataClass.datasourceId)
return this.getDatasourceById(dataClass.datasourceId);
}
if (!metricGroup.datasourceId)
throw new Error(`Could not find datasourceId for metric group ${metricGroup.metricId} or its class ${classId}, please add one`);
return this.getDatasourceById(metricGroup.datasourceId);
}
/**
* Returns classKey for given metric group, class-level if available, otherwise metricGroup level if not
* @param metricGroup - metricGroup to search for class and classKey
* @param options.classId - optional data class ID to specifically get classKey for
* @returns the classKey name or undefined
* @throws if class does not exist in metric group with given classId
*/
getMetricGroupClassKey(metricGroup, options = {}) {
const { classId } = options;
if (classId) {
const dataClass = metricGroup.classes.find((c) => c.classId === classId);
if (!dataClass)
throw new Error(`Class not found in metricGroup ${metricGroup.metricId} with classId ${classId}`);
if (dataClass.classKey)
return dataClass.classKey;
}
return metricGroup.classKey;
}
/**
* Returns precalc metrics from precalc.json. Optionally filters down to specific metricGroup and geographyId
* @param mg MetricGroup to get precalculated metrics for
* @param metricId string, "area", "count", or "sum"
* @param geographyId string, geographyId to get precalculated metrics for
* @returns Metric[] of precalculated metrics
*/
getPrecalcMetrics(mg, metricId, geographyId) {
if (!mg && !metricId && !geographyId) {
// default to return everything
return this._precalc;
}
else if (mg && metricId && geographyId) {
// or for specific metricGroup and geography
const metrics = mg.classes.map((curClass) => {
// use top-level datasourceId if available, otherwise fallback to class datasourceId
const datasourceId = mg.datasourceId || curClass.datasourceId;
if (!datasourceId)
throw new Error(`Missing datasourceId for ${mg.metricId}`);
// If class key (multiclass datasource), find that metric and return
const classKey = mg.classKey || curClass.classKey;
if (classKey) {
// Expect precalc metric classId to be in form `${datasourceId}-${classId}`
const metric = this._precalc.filter(function (pMetric) {
return (pMetric.metricId === metricId &&
pMetric.classId === datasourceId + "-" + curClass.classId &&
pMetric.geographyId === geographyId);
});
// Throw error if metric is unable to be found
if (!metric || metric.length === 0) {
throw new Error(`No matching total metric for ${datasourceId}-${curClass.classId}, ${metricId}, ${geographyId}`);
}
if (metric.length > 1) {
console.log(JSON.stringify(metric));
throw new Error(`Unexpectedly found more than one precalc metric for datasource-classId: ${datasourceId}-${curClass.classId}, metric: ${metricId}, geography: ${geographyId}`);
}
// Return metric, overwriting classId in its simple form
return { ...metric[0], classId: curClass.classId };
}
// Otherwise find metric for general, aka classId total, and add classId
const metric = this._precalc.filter(function (pMetric) {
return (pMetric.metricId === metricId &&
pMetric.classId === datasourceId + "-total" &&
pMetric.geographyId === geographyId);
});
if (!metric || !metric.length)
throw new Error(`Can't find precalc metric for datasource ${datasourceId}, geography ${geographyId}, metric ${metricId}. Do you need to run the precalc:data command?`);
if (metric.length > 1)
throw new Error(`Returned multiple precalc metrics for datasource ${datasourceId}, geography ${geographyId}, metric ${metricId}`);
// Returns metric, overwriting classId for easy match in report
return { ...metric[0], classId: curClass.classId };
});
return createMetrics(metrics);
}
throw new Error("getPrecalcMetrics must be called with no parameters, or all 3 of mg, metricId, and geographyId");
}
}
export default ProjectClientBase;
//# sourceMappingURL=ProjectClientBase.js.map