UNPKG

@seasketch/geoprocessing

Version:

Geoprocessing and reporting framework for SeaSketch 2.0

336 lines • 16.3 kB
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