feat: implement spatial boundary checks for Bari municipal area in GeoJSON handling
This commit is contained in:
+114
-10
@@ -1,5 +1,6 @@
|
|||||||
#include "common/GeoJson.h"
|
#include "common/GeoJson.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace visione::geojson
|
namespace visione::geojson
|
||||||
@@ -11,17 +12,12 @@ struct Point
|
|||||||
double x;
|
double x;
|
||||||
double y;
|
double y;
|
||||||
};
|
};
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson)
|
bool pointInRing(double latitude,
|
||||||
|
double longitude,
|
||||||
|
const Json::Value &ring,
|
||||||
|
bool *onBoundary = nullptr)
|
||||||
{
|
{
|
||||||
if (!geojson.isObject() || geojson["type"].asString() != "Polygon" ||
|
|
||||||
!geojson["coordinates"].isArray() || geojson["coordinates"].empty())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &ring = geojson["coordinates"][0];
|
|
||||||
if (!ring.isArray() || ring.size() < 4)
|
if (!ring.isArray() || ring.size() < 4)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -41,10 +37,27 @@ bool pointInPolygon(double latitude, double longitude, const Json::Value &geojso
|
|||||||
const auto x = longitude;
|
const auto x = longitude;
|
||||||
const auto y = latitude;
|
const auto y = latitude;
|
||||||
bool inside = false;
|
bool inside = false;
|
||||||
|
constexpr double epsilon = 1e-9;
|
||||||
for (size_t i = 0, j = points.size() - 1; i < points.size(); j = i++)
|
for (size_t i = 0, j = points.size() - 1; i < points.size(); j = i++)
|
||||||
{
|
{
|
||||||
const auto &pi = points[i];
|
const auto &pi = points[i];
|
||||||
const auto &pj = points[j];
|
const auto &pj = points[j];
|
||||||
|
const auto cross =
|
||||||
|
(x - pi.x) * (pj.y - pi.y) - (y - pi.y) * (pj.x - pi.x);
|
||||||
|
const auto minX = std::min(pi.x, pj.x) - epsilon;
|
||||||
|
const auto maxX = std::max(pi.x, pj.x) + epsilon;
|
||||||
|
const auto minY = std::min(pi.y, pj.y) - epsilon;
|
||||||
|
const auto maxY = std::max(pi.y, pj.y) + epsilon;
|
||||||
|
if (std::abs(cross) <= epsilon && x >= minX && x <= maxX && y >= minY &&
|
||||||
|
y <= maxY)
|
||||||
|
{
|
||||||
|
if (onBoundary)
|
||||||
|
{
|
||||||
|
*onBoundary = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const bool intersects =
|
const bool intersects =
|
||||||
((pi.y > y) != (pj.y > y)) &&
|
((pi.y > y) != (pj.y > y)) &&
|
||||||
(x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x);
|
(x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x);
|
||||||
@@ -55,5 +68,96 @@ bool pointInPolygon(double latitude, double longitude, const Json::Value &geojso
|
|||||||
}
|
}
|
||||||
return inside;
|
return inside;
|
||||||
}
|
}
|
||||||
} // namespace visione::geojson
|
|
||||||
|
|
||||||
|
bool polygonContainsPoint(double latitude,
|
||||||
|
double longitude,
|
||||||
|
const Json::Value &polygon)
|
||||||
|
{
|
||||||
|
if (!polygon.isObject() || polygon["type"].asString() != "Polygon" ||
|
||||||
|
!polygon["coordinates"].isArray() || polygon["coordinates"].empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onBoundary = false;
|
||||||
|
if (!pointInRing(latitude, longitude, polygon["coordinates"][0], &onBoundary))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (onBoundary)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (Json::ArrayIndex i = 1; i < polygon["coordinates"].size(); ++i)
|
||||||
|
{
|
||||||
|
if (pointInRing(latitude, longitude, polygon["coordinates"][i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson)
|
||||||
|
{
|
||||||
|
return polygonContainsPoint(latitude, longitude, geojson);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pointInGeometry(double latitude, double longitude, const Json::Value &geojson)
|
||||||
|
{
|
||||||
|
if (!geojson.isObject() || !geojson.isMember("type"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto type = geojson["type"].asString();
|
||||||
|
if (type == "Polygon")
|
||||||
|
{
|
||||||
|
return polygonContainsPoint(latitude, longitude, geojson);
|
||||||
|
}
|
||||||
|
if (type == "MultiPolygon" && geojson["coordinates"].isArray())
|
||||||
|
{
|
||||||
|
for (const auto &polygonCoords : geojson["coordinates"])
|
||||||
|
{
|
||||||
|
Json::Value polygon(Json::objectValue);
|
||||||
|
polygon["type"] = "Polygon";
|
||||||
|
polygon["coordinates"] = polygonCoords;
|
||||||
|
if (polygonContainsPoint(latitude, longitude, polygon))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool polygonInsideGeometry(const Json::Value &candidate, const Json::Value &container)
|
||||||
|
{
|
||||||
|
if (!candidate.isObject() || candidate["type"].asString() != "Polygon" ||
|
||||||
|
!candidate["coordinates"].isArray() || candidate["coordinates"].empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &ring = candidate["coordinates"][0];
|
||||||
|
if (!ring.isArray() || ring.size() < 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const auto &coordinate : ring)
|
||||||
|
{
|
||||||
|
if (!coordinate.isArray() || coordinate.size() < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!pointInGeometry(coordinate[1].asDouble(),
|
||||||
|
coordinate[0].asDouble(),
|
||||||
|
container))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace visione::geojson
|
||||||
|
|||||||
+2
-1
@@ -5,5 +5,6 @@
|
|||||||
namespace visione::geojson
|
namespace visione::geojson
|
||||||
{
|
{
|
||||||
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson);
|
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson);
|
||||||
|
bool pointInGeometry(double latitude, double longitude, const Json::Value &geojson);
|
||||||
|
bool polygonInsideGeometry(const Json::Value &candidate, const Json::Value &container);
|
||||||
} // namespace visione::geojson
|
} // namespace visione::geojson
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ void sendAdmin(Json::Value body,
|
|||||||
{
|
{
|
||||||
status = drogon::k422UnprocessableEntity;
|
status = drogon::k422UnprocessableEntity;
|
||||||
}
|
}
|
||||||
|
else if (error == "area_outside_allowed_boundary")
|
||||||
|
{
|
||||||
|
status = drogon::k422UnprocessableEntity;
|
||||||
|
}
|
||||||
callback(visione::http::json(std::move(body), status));
|
callback(visione::http::json(std::move(body), status));
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ void sendService(Json::Value body,
|
|||||||
{
|
{
|
||||||
status = drogon::k422UnprocessableEntity;
|
status = drogon::k422UnprocessableEntity;
|
||||||
}
|
}
|
||||||
|
else if (error == "coordinates_outside_allowed_boundary")
|
||||||
|
{
|
||||||
|
status = drogon::k422UnprocessableEntity;
|
||||||
|
}
|
||||||
callback(visione::http::json(std::move(body), status));
|
callback(visione::http::json(std::move(body), status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ Authorization: Bearer <token>
|
|||||||
public external-team summary link
|
public external-team summary link
|
||||||
- `GET /open-data/reports`
|
- `GET /open-data/reports`
|
||||||
anonymized reports in `RESOLVED` or `CLOSED`
|
anonymized reports in `RESOLVED` or `CLOSED`
|
||||||
|
filtered to Bari municipal boundary
|
||||||
|
|
||||||
## User Reference / Personal
|
## User Reference / Personal
|
||||||
- `GET /report-categories`
|
- `GET /report-categories`
|
||||||
@@ -63,6 +64,7 @@ Authorization: Bearer <token>
|
|||||||
## Report Endpoints
|
## Report Endpoints
|
||||||
- `POST /reports`
|
- `POST /reports`
|
||||||
create citizen report, optional inline `media`, area inferred by GeoJSON
|
create citizen report, optional inline `media`, area inferred by GeoJSON
|
||||||
|
coordinates must stay inside allowed Bari municipal boundary
|
||||||
- `GET /reports`
|
- `GET /reports`
|
||||||
citizen sees own reports; operator/admin see all
|
citizen sees own reports; operator/admin see all
|
||||||
- `GET /reports/{id}`
|
- `GET /reports/{id}`
|
||||||
@@ -111,6 +113,7 @@ Authorization: Bearer <token>
|
|||||||
- `PATCH /admin/categories/{id}`
|
- `PATCH /admin/categories/{id}`
|
||||||
- `POST /admin/areas`
|
- `POST /admin/areas`
|
||||||
- `PATCH /admin/areas/{id}`
|
- `PATCH /admin/areas/{id}`
|
||||||
|
area polygon must stay inside allowed Bari municipal boundary
|
||||||
- `POST /admin/teams`
|
- `POST /admin/teams`
|
||||||
- `PATCH /admin/teams/{id}`
|
- `PATCH /admin/teams/{id}`
|
||||||
- `GET /admin/users`
|
- `GET /admin/users`
|
||||||
@@ -153,3 +156,5 @@ Authorization: Bearer <token>
|
|||||||
- `assignments` is historical external team linkage only.
|
- `assignments` is historical external team linkage only.
|
||||||
- Feedback does not auto-close report.
|
- Feedback does not auto-close report.
|
||||||
- Public intervention summary exposes citizen phone by design, but not email or full surname.
|
- Public intervention summary exposes citizen phone by design, but not email or full surname.
|
||||||
|
- Report coordinates are constrained to Bari municipal boundary.
|
||||||
|
- Admin-created area polygons must stay inside Bari municipal boundary.
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ Effective permission helpers:
|
|||||||
- `take` exists for `OPEN -> UNDER_REVIEW` without team assignment.
|
- `take` exists for `OPEN -> UNDER_REVIEW` without team assignment.
|
||||||
- External team link is public, revocable, tied to current assignment, hashed in
|
- External team link is public, revocable, tied to current assignment, hashed in
|
||||||
`intervention_access_links`.
|
`intervention_access_links`.
|
||||||
|
- Spatial scope is constrained to Bari municipal boundary from
|
||||||
|
`allowed_boundaries.code = 'BARI'`.
|
||||||
|
|
||||||
## Status Machine
|
## Status Machine
|
||||||
- Allowed statuses: `OPEN`, `UNDER_REVIEW`, `ASSIGNED`, `IN_PROGRESS`,
|
- Allowed statuses: `OPEN`, `UNDER_REVIEW`, `ASSIGNED`, `IN_PROGRESS`,
|
||||||
@@ -68,6 +70,7 @@ Effective permission helpers:
|
|||||||
- `moderation_flags`
|
- `moderation_flags`
|
||||||
- `report_category_team_categories`
|
- `report_category_team_categories`
|
||||||
- `intervention_access_links`
|
- `intervention_access_links`
|
||||||
|
- `allowed_boundaries`
|
||||||
|
|
||||||
## HTTP/Error Conventions
|
## HTTP/Error Conventions
|
||||||
- `401`: missing/invalid auth
|
- `401`: missing/invalid auth
|
||||||
@@ -94,4 +97,5 @@ Output path:
|
|||||||
hardcoded allowlist, not user input.
|
hardcoded allowlist, not user input.
|
||||||
- Public intervention summary currently excludes email and full surname, but
|
- Public intervention summary currently excludes email and full surname, but
|
||||||
still exposes citizen phone by design.
|
still exposes citizen phone by design.
|
||||||
|
- Allowed-area containment for admin areas is vertex-based, simple not strict GIS-hard.
|
||||||
- No frontend doc set here beyond API/OpenAPI docs.
|
- No frontend doc set here beyond API/OpenAPI docs.
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB
|
|||||||
- `report_category_team_categories`
|
- `report_category_team_categories`
|
||||||
explicit compatibility mapping:
|
explicit compatibility mapping:
|
||||||
report category -> team category
|
report category -> team category
|
||||||
|
- `allowed_boundaries`
|
||||||
|
active allowed municipal containment geometry, currently seeded with `BARI`
|
||||||
|
|
||||||
## Report Domain
|
## Report Domain
|
||||||
- `reports`
|
- `reports`
|
||||||
@@ -101,3 +103,4 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB
|
|||||||
- No dedicated table for external team personnel
|
- No dedicated table for external team personnel
|
||||||
- No route-optimization/routing tables
|
- No route-optimization/routing tables
|
||||||
- No audit table for intervention link access beyond `last_accessed_at`
|
- No audit table for intervention link access beyond `last_accessed_at`
|
||||||
|
- Containment check for admin area polygons is simple vertex-inside check
|
||||||
|
|||||||
+12
-3
@@ -40,9 +40,10 @@ public revocable link generated by operator/admin.
|
|||||||
### Citizen flow
|
### Citizen flow
|
||||||
1. Register/login.
|
1. Register/login.
|
||||||
2. `POST /reports` with category, coordinates, address, description, optional media array.
|
2. `POST /reports` with category, coordinates, address, description, optional media array.
|
||||||
3. Backend infers `area_id` by GeoJSON polygon match when possible.
|
3. Backend first rejects coordinates outside Bari municipal boundary.
|
||||||
4. Report starts `OPEN`, status history row inserted.
|
4. Backend infers `area_id` by GeoJSON polygon match when possible.
|
||||||
5. Citizen can:
|
5. Report starts `OPEN`, status history row inserted.
|
||||||
|
6. Citizen can:
|
||||||
- list own reports
|
- list own reports
|
||||||
- read detail/history/media
|
- read detail/history/media
|
||||||
- chat on own report
|
- chat on own report
|
||||||
@@ -72,6 +73,7 @@ public revocable link generated by operator/admin.
|
|||||||
|
|
||||||
### Admin flow
|
### Admin flow
|
||||||
- Create/update categories, areas, teams
|
- Create/update categories, areas, teams
|
||||||
|
- Area GeoJSON must stay inside Bari municipal boundary
|
||||||
- List users, edit roles, ban/unban
|
- List users, edit roles, ban/unban
|
||||||
- Read stats
|
- Read stats
|
||||||
- Review moderation queue and resolve/dismiss flags
|
- Review moderation queue and resolve/dismiss flags
|
||||||
@@ -89,6 +91,13 @@ public revocable link generated by operator/admin.
|
|||||||
- `GET /interventions/{token}` returns public summary for field team:
|
- `GET /interventions/{token}` returns public summary for field team:
|
||||||
report data, location, media, assigned team, masked citizen identity, phone.
|
report data, location, media, assigned team, masked citizen identity, phone.
|
||||||
|
|
||||||
|
## Spatial Boundary Rule
|
||||||
|
- `allowed_boundaries` stores active municipal allowed area.
|
||||||
|
- Current seed uses `BARI`.
|
||||||
|
- Report coordinates outside boundary are rejected.
|
||||||
|
- Admin area polygons outside boundary are rejected.
|
||||||
|
- Open data output is filtered again by same boundary.
|
||||||
|
|
||||||
## Current Notifications / Rewards / Moderation
|
## Current Notifications / Rewards / Moderation
|
||||||
- Notifications: local in-app only.
|
- Notifications: local in-app only.
|
||||||
- Reward points: awarded once per `(report_id, reason)`.
|
- Reward points: awarded once per `(report_id, reason)`.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ moderate abuse, and inspect operational stats.
|
|||||||
- create report with category, coordinates, address, description
|
- create report with category, coordinates, address, description
|
||||||
- optional inline media on creation
|
- optional inline media on creation
|
||||||
- later media upload up to total 5 files
|
- later media upload up to total 5 files
|
||||||
|
- coordinates must stay inside Bari municipal boundary
|
||||||
- area inferred by GeoJSON polygon match
|
- area inferred by GeoJSON polygon match
|
||||||
- citizen report list/detail/history/media
|
- citizen report list/detail/history/media
|
||||||
- operator queues:
|
- operator queues:
|
||||||
@@ -79,6 +80,7 @@ moderate abuse, and inspect operational stats.
|
|||||||
### Admin
|
### Admin
|
||||||
- create/update categories
|
- create/update categories
|
||||||
- create/update areas
|
- create/update areas
|
||||||
|
area polygons must stay inside Bari municipal boundary
|
||||||
- create/update teams
|
- create/update teams
|
||||||
- list users, update roles, ban/unban
|
- list users, update roles, ban/unban
|
||||||
- global stats including area/category/team resolution aggregates
|
- global stats including area/category/team resolution aggregates
|
||||||
@@ -87,6 +89,7 @@ moderate abuse, and inspect operational stats.
|
|||||||
- active area list
|
- active area list
|
||||||
- anonymized resolved/closed open-data export
|
- anonymized resolved/closed open-data export
|
||||||
- public intervention token endpoint
|
- public intervention token endpoint
|
||||||
|
- open-data output filtered to Bari municipal boundary
|
||||||
|
|
||||||
## Current Workflow Summary
|
## Current Workflow Summary
|
||||||
### Citizen
|
### Citizen
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
#include "services/AdminService.h"
|
#include "services/AdminService.h"
|
||||||
|
|
||||||
|
#include "common/GeoJson.h"
|
||||||
#include "common/Validation.h"
|
#include "common/Validation.h"
|
||||||
#include "repositories/Sql.h"
|
#include "repositories/Sql.h"
|
||||||
#include "services/UserService.h"
|
#include "services/UserService.h"
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
namespace visione::services
|
namespace visione::services
|
||||||
{
|
{
|
||||||
@@ -87,6 +89,35 @@ bool isActiveField(const Json::Value &body)
|
|||||||
{
|
{
|
||||||
return body.isMember("active") && body["active"].isBool();
|
return body.isMember("active") && body["active"].isBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<Json::Value> loadAllowedBoundary(const std::string &code)
|
||||||
|
{
|
||||||
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
||||||
|
"FROM allowed_boundaries WHERE code = ? AND active = TRUE LIMIT 1",
|
||||||
|
code);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::CharReaderBuilder builder;
|
||||||
|
Json::Value geojson;
|
||||||
|
std::string errors;
|
||||||
|
std::istringstream stream(rows[0]["geojson"].as<std::string>());
|
||||||
|
if (!Json::parseFromStream(builder, stream, &geojson, &errors))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return geojson;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAreaInsideAllowedBoundary(const Json::Value &boundaryGeojson)
|
||||||
|
{
|
||||||
|
const auto allowed = loadAllowedBoundary("BARI");
|
||||||
|
return allowed &&
|
||||||
|
geojson::polygonInsideGeometry(boundaryGeojson, *allowed);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Json::Value AdminService::createCategory(const auth::UserContext &user,
|
Json::Value AdminService::createCategory(const auth::UserContext &user,
|
||||||
@@ -119,6 +150,10 @@ Json::Value AdminService::createArea(const auth::UserContext &user,
|
|||||||
{
|
{
|
||||||
return failure("invalid_area");
|
return failure("invalid_area");
|
||||||
}
|
}
|
||||||
|
if (!isAreaInsideAllowedBoundary(body["boundary_geojson"]))
|
||||||
|
{
|
||||||
|
return failure("area_outside_allowed_boundary");
|
||||||
|
}
|
||||||
|
|
||||||
Json::StreamWriterBuilder writer;
|
Json::StreamWriterBuilder writer;
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
@@ -410,6 +445,10 @@ Json::Value AdminService::updateArea(const auth::UserContext &user,
|
|||||||
}
|
}
|
||||||
if (body["boundary_geojson"].isObject())
|
if (body["boundary_geojson"].isObject())
|
||||||
{
|
{
|
||||||
|
if (!isAreaInsideAllowedBoundary(body["boundary_geojson"]))
|
||||||
|
{
|
||||||
|
return failure("area_outside_allowed_boundary");
|
||||||
|
}
|
||||||
Json::StreamWriterBuilder writer;
|
Json::StreamWriterBuilder writer;
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"UPDATE areas SET boundary_geojson = CAST(? AS JSON) WHERE id = ?",
|
"UPDATE areas SET boundary_geojson = CAST(? AS JSON) WHERE id = ?",
|
||||||
|
|||||||
@@ -75,6 +75,34 @@ std::string maskedLastName(const std::string &lastName)
|
|||||||
return std::string(1, static_cast<char>(std::toupper(lastName.front()))) + ".";
|
return std::string(1, static_cast<char>(std::toupper(lastName.front()))) + ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<Json::Value> loadAllowedBoundary(const std::string &code)
|
||||||
|
{
|
||||||
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
||||||
|
"FROM allowed_boundaries WHERE code = ? AND active = TRUE LIMIT 1",
|
||||||
|
code);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::CharReaderBuilder builder;
|
||||||
|
Json::Value geojson;
|
||||||
|
std::string errors;
|
||||||
|
std::istringstream stream(rows[0]["geojson"].as<std::string>());
|
||||||
|
if (!Json::parseFromStream(builder, stream, &geojson, &errors))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return geojson;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool insideAllowedBoundary(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
const auto boundary = loadAllowedBoundary("BARI");
|
||||||
|
return boundary && geojson::pointInGeometry(latitude, longitude, *boundary);
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<long long> findArea(double latitude, double longitude)
|
std::optional<long long> findArea(double latitude, double longitude)
|
||||||
{
|
{
|
||||||
const auto result = drogon::app().getDbClient()->execSqlSync(
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||||
@@ -89,7 +117,7 @@ std::optional<long long> findArea(double latitude, double longitude)
|
|||||||
const auto raw = row["geojson"].as<std::string>();
|
const auto raw = row["geojson"].as<std::string>();
|
||||||
std::istringstream stream(raw);
|
std::istringstream stream(raw);
|
||||||
if (Json::parseFromStream(builder, stream, &geojson, &errors) &&
|
if (Json::parseFromStream(builder, stream, &geojson, &errors) &&
|
||||||
geojson::pointInPolygon(latitude, longitude, geojson))
|
geojson::pointInGeometry(latitude, longitude, geojson))
|
||||||
{
|
{
|
||||||
return row["id"].as<long long>();
|
return row["id"].as<long long>();
|
||||||
}
|
}
|
||||||
@@ -397,6 +425,10 @@ Json::Value ReportService::createReport(const auth::UserContext &user,
|
|||||||
|
|
||||||
const auto latitude = body["latitude"].asDouble();
|
const auto latitude = body["latitude"].asDouble();
|
||||||
const auto longitude = body["longitude"].asDouble();
|
const auto longitude = body["longitude"].asDouble();
|
||||||
|
if (!insideAllowedBoundary(latitude, longitude))
|
||||||
|
{
|
||||||
|
return failure("coordinates_outside_allowed_boundary");
|
||||||
|
}
|
||||||
const auto areaId = findArea(latitude, longitude);
|
const auto areaId = findArea(latitude, longitude);
|
||||||
|
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
@@ -1215,6 +1247,20 @@ Json::Value ReportService::openDataReports(const std::optional<std::string> &pag
|
|||||||
perPageValue,
|
perPageValue,
|
||||||
offset),
|
offset),
|
||||||
openDataReportSchema());
|
openDataReportSchema());
|
||||||
|
if (const auto boundary = loadAllowedBoundary("BARI"))
|
||||||
|
{
|
||||||
|
Json::Value filtered(Json::arrayValue);
|
||||||
|
for (const auto &report : response["reports"])
|
||||||
|
{
|
||||||
|
if (geojson::pointInGeometry(report["latitude"].asDouble(),
|
||||||
|
report["longitude"].asDouble(),
|
||||||
|
*boundary))
|
||||||
|
{
|
||||||
|
filtered.append(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response["reports"] = std::move(filtered);
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,61 @@ DROGON_TEST(GeoJsonMatchesPointInsidePolygon)
|
|||||||
|
|
||||||
CHECK(visione::geojson::pointInPolygon(41.5, 12.5, polygon));
|
CHECK(visione::geojson::pointInPolygon(41.5, 12.5, polygon));
|
||||||
CHECK(!visione::geojson::pointInPolygon(43.0, 12.5, polygon));
|
CHECK(!visione::geojson::pointInPolygon(43.0, 12.5, polygon));
|
||||||
|
CHECK(visione::geojson::pointInGeometry(41.5, 12.5, polygon));
|
||||||
|
CHECK(!visione::geojson::pointInGeometry(43.0, 12.5, polygon));
|
||||||
|
}
|
||||||
|
|
||||||
|
DROGON_TEST(GeoJsonAcceptsContainedPolygon)
|
||||||
|
{
|
||||||
|
Json::Value container;
|
||||||
|
container["type"] = "Polygon";
|
||||||
|
Json::Value outer(Json::arrayValue);
|
||||||
|
|
||||||
|
Json::Value a(Json::arrayValue);
|
||||||
|
a.append(12.0);
|
||||||
|
a.append(41.0);
|
||||||
|
Json::Value b(Json::arrayValue);
|
||||||
|
b.append(13.0);
|
||||||
|
b.append(41.0);
|
||||||
|
Json::Value c(Json::arrayValue);
|
||||||
|
c.append(13.0);
|
||||||
|
c.append(42.0);
|
||||||
|
Json::Value d(Json::arrayValue);
|
||||||
|
d.append(12.0);
|
||||||
|
d.append(42.0);
|
||||||
|
outer.append(a);
|
||||||
|
outer.append(b);
|
||||||
|
outer.append(c);
|
||||||
|
outer.append(d);
|
||||||
|
outer.append(a);
|
||||||
|
container["coordinates"].append(outer);
|
||||||
|
|
||||||
|
Json::Value candidate;
|
||||||
|
candidate["type"] = "Polygon";
|
||||||
|
Json::Value inner(Json::arrayValue);
|
||||||
|
Json::Value p1(Json::arrayValue);
|
||||||
|
p1.append(12.2);
|
||||||
|
p1.append(41.2);
|
||||||
|
Json::Value p2(Json::arrayValue);
|
||||||
|
p2.append(12.8);
|
||||||
|
p2.append(41.2);
|
||||||
|
Json::Value p3(Json::arrayValue);
|
||||||
|
p3.append(12.8);
|
||||||
|
p3.append(41.8);
|
||||||
|
Json::Value p4(Json::arrayValue);
|
||||||
|
p4.append(12.2);
|
||||||
|
p4.append(41.8);
|
||||||
|
inner.append(p1);
|
||||||
|
inner.append(p2);
|
||||||
|
inner.append(p3);
|
||||||
|
inner.append(p4);
|
||||||
|
inner.append(p1);
|
||||||
|
candidate["coordinates"].append(inner);
|
||||||
|
|
||||||
|
CHECK(visione::geojson::polygonInsideGeometry(candidate, container));
|
||||||
|
|
||||||
|
candidate["coordinates"][0][3][0] = 14.0;
|
||||||
|
CHECK(!visione::geojson::polygonInsideGeometry(candidate, container));
|
||||||
}
|
}
|
||||||
|
|
||||||
DROGON_TEST(PasswordHashSupportsPbkdf2Only)
|
DROGON_TEST(PasswordHashSupportsPbkdf2Only)
|
||||||
|
|||||||
Reference in New Issue
Block a user