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

Commit

Permalink
Merge pull request #149 from openkfw/140-geodata-route
Browse files Browse the repository at this point in the history
#140-geodata route for fetching values from geodata tables in db
  • Loading branch information
MartinJurcoGlina authored Aug 12, 2022
2 parents d6ac4e2 + 2a125a2 commit 5cd7436
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 31 deletions.
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

0 comments on commit 5cd7436

Please sign in to comment.