feat(geo): polygon GeoJSON spatial filter
Дополнение к bbox: ?polygon=<GeoJSON> для произвольных полигонов
(ground stations coverage, custom AOI). Один из bbox/polygon —
взаимоисключающие; передача обоих → 400.
Repository:
- findActiveByPolygonGeoJson native query через PostGIS
ST_GeomFromGeoJSON(polygon) → ST_SetSRID(_, 4326). DB-side parsing,
не нужен jts-io-common.
GeoJsonPolygon validator (app-side, structural):
- Парсит JSON, проверяет {"type":"Polygon","coordinates":[[...]]}
- RFC 7946 closed-ring check: first point == last
- >= 4 points в каждом ring (3 вертекса + closing point)
- Поддержка holes (multiple rings)
- Coordinate range validation: lon ∈ [-180,180], lat ∈ [-90,90]
- DoS guards: MAX_JSON_BYTES = 1MB, MAX_POINTS = 50_000
- Canonical re-serialize (whitespace stripped) перед DB
Tests (13 в GeoJsonPolygonTest):
- Valid square + polygon with hole
- Reject empty/null/non-JSON/non-object/wrong-type/missing-coords
- Reject too-few points / unclosed ring
- Reject lon/lat out of range
- Reject malformed point [no lat]
- Reject too-large JSON
Total project tests: 161 → 174.
API now supports:
GET /api/v1/{dict}/records?bbox=west,south,east,north
GET /api/v1/{dict}/records?polygon={"type":"Polygon",...}
Альтум-задача (rect AOI) и геопортал (polygon AOI) разблокированы
от client-side full-fetch + filter.
This commit is contained in:
+27
@@ -119,4 +119,31 @@ public interface DictionaryRecordRepository extends JpaRepository<DictionaryReco
|
||||
@Param("south") double south,
|
||||
@Param("east") double east,
|
||||
@Param("north") double north);
|
||||
|
||||
/**
|
||||
* Spatial filter по произвольному polygon в формате GeoJSON. PostGIS
|
||||
* {@code ST_GeomFromGeoJSON} парсит на DB-side, ST_SetSRID вешает 4326
|
||||
* (GeoJSON по spec не имеет SRID, нужен explicit cast). GiST index
|
||||
* на geometry колонке используется для ST_Intersects.
|
||||
*
|
||||
* <p>Если GeoJSON malformed, Postgres вернёт SQL error → Hibernate
|
||||
* выбросит DataAccessException → перехватывается в exception handler
|
||||
* как 400 Bad Request.
|
||||
*/
|
||||
@Query(value = """
|
||||
SELECT * FROM dictionary_records r
|
||||
WHERE r.dictionary_id = :dictionaryId
|
||||
AND r.data_scope = ANY(CAST(:scopesCsv AS TEXT[]))
|
||||
AND r.valid_from <= :at
|
||||
AND r.valid_to > :at
|
||||
AND r.geometry IS NOT NULL
|
||||
AND ST_Intersects(
|
||||
r.geometry,
|
||||
ST_SetSRID(ST_GeomFromGeoJSON(:polygonGeoJson), 4326))
|
||||
""", nativeQuery = true)
|
||||
List<DictionaryRecord> findActiveByPolygonGeoJson(
|
||||
@Param("dictionaryId") UUID dictionaryId,
|
||||
@Param("scopesCsv") String[] scopesCsv,
|
||||
@Param("at") OffsetDateTime at,
|
||||
@Param("polygonGeoJson") String polygonGeoJson);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user