diff --git a/backend/src/controllers/regionController.js b/backend/src/controllers/regionController.js index cf371d1..8212ae2 100644 --- a/backend/src/controllers/regionController.js +++ b/backend/src/controllers/regionController.js @@ -1,4 +1,57 @@ const { Region } = require('../models'); +const { Model, DataTypes, QueryTypes } = require('sequelize'); +const sequelize = require("../config/db"); + + +exports.getGeometry = async (req, res) => { + const { regionId } = req.params; + const region = await Region.findOne({ + where: { id: regionId } + }); + + if (!region) { + return res.status(404).json({ message: 'Region not found' }); + } + + let geometry = region.geom; + + if (!geometry) { + const query = ` + WITH RECURSIVE Subregions AS ( + SELECT id, geom + FROM regions + WHERE id = :regionId + UNION ALL + SELECT r.id, r.geom + FROM regions r + INNER JOIN Subregions s ON r.parent_region_id = s.id + WHERE s.geom IS NULL + ) + SELECT ST_AsGeoJSON(ST_Multi(ST_Union(geom))) as geometry + FROM Subregions + WHERE geom IS NOT NULL; + `; + + const result = await sequelize.query(query, { + replacements: { regionId }, + type: QueryTypes.SELECT + }); + + geometry = result.length > 0 ? result[0].geometry : null; + + if (geometry) { + // Update the geometry in the database + await Region.update({ geom: geometry }, { + where: { id: regionId } + }); + } else { + return res.status(404).json({ message: 'Geometry not found' }); + } + } + + return res.status(200).json({ geometry }); +}; + exports.getAncestors = async (req, res) => { const { regionId } = req.params; @@ -51,9 +104,36 @@ exports.getRegionById = async (req, res) => { } }; + +async function getAllSubregions(regionId) { + const query = ` + WITH RECURSIVE Subregions AS ( + SELECT id, parent_region_id as parentRegionId, name + FROM regions + WHERE parent_region_id = :regionId + UNION ALL + SELECT r.id, r.parent_region_id as parentRegionId, r.name + FROM regions r + INNER JOIN Subregions s ON r.parent_region_id = s.id + ) + SELECT * FROM Subregions; +`; + + const result = await sequelize.query(query, { + replacements: { regionId }, + type: QueryTypes.SELECT, + mapToModel: true, // Required to map the result to Region instances + model: Region // The model to map to + }); + + return result; +} + + // Retrieve subregions for a specific region exports.getSubregions = async (req, res) => { const { regionId } = req.params; + const { getAll } = req.query; try { // Check if the region exists @@ -66,9 +146,15 @@ exports.getSubregions = async (req, res) => { } // Retrieve subregions - const subregions = await Region.findAll({ - where: { parentRegionId: regionId } - }); + let subregions; + // Check the getAll query parameter + if (getAll === 'true') { + subregions = await getAllSubregions(regionId); + } else { + subregions = await Region.findAll({ + where: { parentRegionId: regionId } + }); + } if (subregions.length === 0) { return res.status(202).json({ message: 'Region has no subregions' }); diff --git a/backend/src/models/Region.js b/backend/src/models/Region.js index 6b04dd1..2fa407a 100644 --- a/backend/src/models/Region.js +++ b/backend/src/models/Region.js @@ -62,6 +62,11 @@ Region.init({ type: DataTypes.INTEGER, allowNull: true, field: 'gadm_uid' + }, + geom: { + type: DataTypes.GEOMETRY('MULTIPOLYGON', 4326), + allowNull: true, + field: 'geom' } }, { sequelize, diff --git a/backend/src/routes/regionRoutes.js b/backend/src/routes/regionRoutes.js index 7bae00b..7ee453e 100644 --- a/backend/src/routes/regionRoutes.js +++ b/backend/src/routes/regionRoutes.js @@ -1,6 +1,6 @@ const express = require('express'); const regionController = require('../controllers/regionController'); -const { check, validationResult } = require('express-validator'); +const { check, query, validationResult } = require('express-validator'); const router = express.Router(); @@ -20,7 +20,8 @@ router.get('/:regionId', ); router.get('/:regionId/subregions', ...[ - check('regionId').isInt().withMessage('Region ID must be an integer') + check('regionId').isInt().withMessage('Region ID must be an integer'), + query('getAll').optional().isBoolean().withMessage('getAll must be a boolean') ], async (req, res) => { const errors = validationResult(req); @@ -28,7 +29,7 @@ router.get('/:regionId/subregions', const errorMessages = errors.array().map(error => error.msg); return res.status(400).json({errors: errorMessages}); } - await regionController.getSubregions(req, res); + await regionController.getSubregions(req, res, req.query.getAll); } ); @@ -46,4 +47,18 @@ router.get('/:regionId/ancestors', } ); +router.get('/:regionId/geometry', + ...[ + check('regionId').isInt().withMessage('Region ID must be an integer') + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + const errorMessages = errors.array().map(error => error.msg); + return res.status(400).json({errors: errorMessages}); + } + await regionController.getGeometry(req, res); + } +); + module.exports = router; diff --git a/deployment/init-db/init-regions-table.py b/deployment/init-db/init-regions-table.py index 109c15c..83b0054 100644 --- a/deployment/init-db/init-regions-table.py +++ b/deployment/init-db/init-regions-table.py @@ -172,6 +172,8 @@ def find_next_non_empty_level(idx, feature, geo_levels): print("Creating indexes...") # Create indexes on the Region table cur_pg.execute("CREATE INDEX IF NOT EXISTS parent_region_idx ON regions (parent_region_id)") +# Create a GiST index on the geometry column +cur_pg.execute("CREATE INDEX IF NOT EXISTS geom_idx ON regions USING GIST (geom)") print("Done") # Commit the changes and close the connections