Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

#140-geodata route for fetching values from geodata tables in db #149

Merged
merged 2 commits into from
Aug 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions api/src/database/geodata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import config from '../config/config';
import APIError from '../helpers/APIError';

import mongoDb from './mongoDb/models/geoDataModel';
import postgis from './postgis/models/geoDataModel';

export const getGeoData = async (tableName: string, bottomLeft: string, topRight: string, proj: string) => {
if (config.postgresUser && config.postgresPassword && config.postgresDb) {
return postgis.getGeoData(tableName, bottomLeft, topRight, proj);
}
if (config.mongoUri) {
return mongoDb.getGeoData(tableName, bottomLeft, topRight);
}
throw new APIError('No credentials for database', 500, false, undefined);
};

export const getProperty = async (tableName, propertyName) => {
if (config.postgresUser && config.postgresPassword && config.postgresDb) {
return postgis.getProperty(tableName, propertyName);
}
if (config.mongoUri) {
return mongoDb.getProperty(tableName, propertyName);
}
};

export const getUniqueValuesForProperty = async (tableName, propertyName) => {
if (config.postgresUser && config.postgresPassword && config.postgresDb) {
return postgis.getUniqueValuesForProperty(tableName, propertyName);
}
if (config.mongoUri) {
return mongoDb.getUniqueValuesForProperty(tableName, propertyName);
}
throw new APIError('No credentials for database', 500, false, undefined);
};

export const getPropertySum = async (tableName, propertyName) => {
if (config.postgresUser && config.postgresPassword && config.postgresDb) {
return postgis.getPropertySum(tableName, propertyName);
}
if (config.mongoUri) {
return mongoDb.getPropertySum(tableName, propertyName);
}
};

export default { getGeoData, getProperty, getUniqueValuesForProperty, getPropertySum };
64 changes: 64 additions & 0 deletions api/src/database/mongoDb/models/geoDataModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import mongoose from 'mongoose';
import { filterCoordinates } from '../filters';

const getGeoData = async (tableName: string, bottomLeft: string, topRight: string) => {
const { connection } = mongoose;
const { db } = connection;

let filter = {};
if (bottomLeft && topRight) {
filter = { bbox: filterCoordinates(filter, bottomLeft, topRight) };
}

const features = await db
.collection(tableName)
.aggregate([
{ $match: filter },
{
$project: {
_id: 0,
id: '$_id',
type: 1,
properties: 1,
geometry: 1,
},
},
])
.toArray();
return features;
};

const getProperty = async (tableName: string, propertyName: string) => {
const { connection } = mongoose;
const { db } = connection;

const data = await db
.collection(tableName)
.aggregate([{ $project: { _id: 0, id: `$_id`, value: `$properties.${propertyName}` } }])
.toArray();
return data;
};

const getUniqueValuesForProperty = async (tableName: string, propertyName: string) => {
const { connection } = mongoose;
const { db } = connection;

const data = await db
.collection(tableName)
.aggregate([{ $group: { _id: `$properties.${propertyName}` } }])
.toArray();
return data.map((item) => item._id);
};

const getPropertySum = async (tableName: string, propertyName: string) => {
const { connection } = mongoose;
const { db } = connection;

const data = await db
.collection(tableName)
.aggregate([{ $group: { _id: null, value: { $sum: `$properties.${propertyName}` } } }])
.toArray();
return data;
};

export default { getGeoData, getProperty, getUniqueValuesForProperty, getPropertySum };
34 changes: 33 additions & 1 deletion api/src/database/postgis/filters.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getDb } from './index';
import APIError from '../../helpers/APIError';

/**
* Return bounding box in GeoJSON format used for filtering
* @param {string} bottomLeft - bottom left corner of boundingBox window, string 'lon,lat'
Expand All @@ -23,4 +26,33 @@ export const getBoundingBox = (bottomLeft: string, topRight: string) => {
return geometry;
};

export default { getBoundingBox };
/**
* Connect to database table to verify that projection of geometry data from API is compatible with table.
* @param {string} proj - projection of data from API
* @param {string} tableName - name of table with geographical data
* @param {} db=getDb() - database connection or transaction
*/
export const getProjectionFilter = async (proj, tableName, db = getDb()) => {
// projection in query must correspond to SRID in geometry column
if (proj) {
const SRIDarr = await db.distinct(db.raw(`ST_SRID(${tableName}.geometry)`)).from(tableName);

if (!SRIDarr.length) {
return;
}
const SRID = SRIDarr[0].st_srid;
const projNum = parseInt(proj.split(':')[1], 10);

if (SRID === projNum) {
return projNum;
}
throw new APIError(
`Projection SRID ${projNum} doesn't correspond to geometry column SRID ${SRID}`,
400,
true,
undefined,
);
}
};

export default { getBoundingBox, getProjectionFilter };
57 changes: 57 additions & 0 deletions api/src/database/postgis/models/geoDataModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { getDb } from '../index';
import { getBoundingBox, getProjectionFilter } from '../filters';
import APIError from '../../../helpers/APIError';

const getGeoData = async (tableName: string, bottomLeft?: string, topRight?: string, proj?: string, db = getDb()) => {
const filter: {
geometry?: {
type: string,
coordinates: Array<Array<Array<number>>>,
},
proj?: number,
} = {};
if (bottomLeft && topRight) {
filter.geometry = getBoundingBox(bottomLeft, topRight);
filter.proj = await getProjectionFilter(proj, tableName);
}

const data = await db
.from(tableName)
.select('id', db.raw('ST_AsGeoJSON(geometry) as geometry'), 'properties')
.where((qb) => {
if (filter.geometry && filter.proj) {
qb.andWhere(
db.raw(`ST_Intersects(geometry, ST_SetSRID(ST_GeomFromGeoJSON(?), ${filter.proj}))`, [
JSON.stringify(filter.geometry),
]),
);
}
});
return data.map((item) => ({ ...item, geometry: JSON.parse(item.geometry) }));
};

const getProperty = async (tableName: string, propertyName: string, db = getDb()) => {
const data = await db.from(tableName).select('id', db.raw(`properties->>'${propertyName}' as value`));
return data;
};

const getUniqueValuesForProperty = async (tableName: string, propertyName: string, db = getDb()) => {
const data = await db
.from(tableName)
.select(db.raw(`properties->>'${propertyName}' as property`))
.distinct(db.raw(`properties->>'${propertyName}'`));
return data.map((item) => item.property);
};

const getPropertySum = async (tableName: string, propertyName: string, db = getDb()) => {
try {
const data = await db.from(tableName).sum(db.raw(`properties->>'${propertyName}'`));
return data;
} catch (error) {
throw new APIError(`Attempting to sum non-numeric values for property ${propertyName}.`, 500, false, error);
}
};

export default { getGeoData, getProperty, getUniqueValuesForProperty, getPropertySum };
31 changes: 3 additions & 28 deletions api/src/database/postgis/models/pointAttributesModel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Knex } from 'knex';
import APIError from '../../../helpers/APIError';
import { POINT_ATTRIBUTES_TABLE } from '../constants';
import { getBoundingBox } from '../filters';
import { getBoundingBox, getProjectionFilter } from '../filters';
import { dateIsValidDatum } from '../../../helpers/utils';
import { getDb } from '../index';
import { PointAttributeFilter, PointAttribute } from '../../../types';
Expand Down Expand Up @@ -37,31 +37,6 @@ const getAttributesFilterConditions = (filter: PointAttributeFilter, qb: Knex.Qu
}
};

const getProjectionFilter = async (proj, db = getDb()) => {
// projection in query must correspond to SRID in geometry column
if (proj) {
const SRIDarr = await db
.distinct(db.raw(`ST_SRID(${POINT_ATTRIBUTES_TABLE}.geometry)`))
.from(POINT_ATTRIBUTES_TABLE);

if (!SRIDarr.length) {
return;
}
const SRID = SRIDarr[0].st_srid;
const projNum = parseInt(proj.split(':')[1], 10);

if (SRID === projNum) {
return projNum;
}
throw new APIError(
`Projection SRID ${projNum} doesn't correspond to geometry column SRID ${SRID}`,
400,
true,
undefined,
);
}
};

/**
* Compose filter from settings from query
* @param {string} attributeId - id of point attribute
Expand All @@ -88,7 +63,7 @@ const getFilteredPointAttributes = async (
}

if (filter.geometry) {
filter.proj = await getProjectionFilter(proj);
filter.proj = await getProjectionFilter(proj, POINT_ATTRIBUTES_TABLE);
}

// date filters
Expand Down Expand Up @@ -154,7 +129,7 @@ const getLastDatePointAttributes = async (
filter.attributeId = attributeId;
}
if (filter.geometry) {
filter.proj = await getProjectionFilter(proj);
filter.proj = await getProjectionFilter(proj, POINT_ATTRIBUTES_TABLE);
}

const lastDate = await db
Expand Down
78 changes: 78 additions & 0 deletions api/src/openapi/apiSchema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,84 @@ paths:
"200":
description: Successful response

/api/geodata/{tableName}:
get:
summary: Get geodata stored in database table
description: Get all geodata stored in database table
parameters:
- in: path
name: tableName
schema:
type: string
description: name of table
required: true
responses:
200:
description: Successful response

/api/geodata/{tableName}/properties/{propertyName}:
get:
summary: Get all values for one property
description: Get all values for one property
parameters:
- in: path
name: tableName
schema:
type: string
description: name of table
required: true
- in: path
name: propertyName
schema:
type: string
description: name of property
required: true
responses:
200:
description: Successful response

/api/geodata/{tableName}/uniqueProperties/{propertyName}:
get:
summary: Get all unique values for one property
description: Get all unique values for one property
parameters:
- in: path
name: tableName
schema:
type: string
description: name of table
required: true
- in: path
name: propertyName
schema:
type: string
description: name of property
required: true
responses:
200:
description: Successfull response

/api/geodata/{tableName}/properties/{propertyName}/sum:
get:
summary: Get sum of all values for property
description: Get sum of all values for property
parameters:
- in: path
name: tableName
schema:
type: string
description: name of table
required: true
- in: path
name: propertyName
schema:
type: string
description: name of property
required: true
responses:
200:
description: Successful resposne

/api/staticLayers:
get:
summary: Gets static layers data
Expand Down
Loading