feat(geo): auto-derive geometry из data.lat/lon + backfill migration
Записи ground_station и similar dicts имеют lat/lon в data, но geometryWkt не задан. Spatial filter (AOI) WHERE geometry IS NOT NULL AND ST_Intersects(...) возвращал 0 records даже когда координаты есть. Три места починены: 1. DictionaryRecordService.resolveGeometry — для CRUD через REST. Если req.geometryWkt() задан — приоритет (explicit). Иначе читаем data.lat/lon (или latitude/longitude/lng), валидируем ranges, строим POINT(lon lat) с SRID 4326. Игнор out-of-range без throw — не ломаем legacy data. 2. CuodBundleImporter.deriveLatLonGeometry — bundle bypass'ит service, reuse'аем ту же логику локально (5 строк inline). Применяется при создании НОВЫХ records (existing skipped по businessKey). 3. Liquibase migration 0015-backfill-latlon-geometry — UPDATE existing records где geometry IS NULL AND data has lat/lon. Три changeset'а для разных aliases (lat/lon, latitude/longitude, lat/lng). Idempotent (только NULL→value), rollback supported (NULL обратно). После deploy: existing 3 ground_station записей (Отрадное и др.) и spacecraft с lat/lon получат POINT geometry. AOI bbox-фильтр заработает.
This commit is contained in:
+43
-1
@@ -106,7 +106,7 @@ public class DictionaryRecordService {
|
||||
d.getScope(),
|
||||
validFrom);
|
||||
record.setValidTo(validTo);
|
||||
record.setGeometry(parseWkt(req.geometryWkt()));
|
||||
record.setGeometry(resolveGeometry(req));
|
||||
|
||||
DictionaryRecord saved;
|
||||
try {
|
||||
@@ -314,6 +314,48 @@ public class DictionaryRecordService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve final geometry для записи: explicit geometryWkt из request имеет
|
||||
* приоритет; если не задан — автодеривируется POINT из числовых полей
|
||||
* data.lat/lon (или latitude/longitude). Это позволяет AOI spatial filter
|
||||
* работать на справочниках типа ground_station/spacecraft без ручного
|
||||
* дублирования lat/lon → WKT в каждом record.
|
||||
*
|
||||
* <p>Validation: lat ∈ [-90, 90], lon ∈ [-180, 180]. Если значения вне
|
||||
* диапазона — игнорируем (не throw, чтобы не ломать legacy records со
|
||||
* странными данными). Спорные значения validate'ятся schema-уровнем
|
||||
* через {@code minimum/maximum}.
|
||||
*/
|
||||
private Geometry resolveGeometry(CreateRecordRequest req) {
|
||||
if (req.geometryWkt() != null && !req.geometryWkt().isBlank()) {
|
||||
return parseWkt(req.geometryWkt());
|
||||
}
|
||||
var data = req.data();
|
||||
if (data == null) return null;
|
||||
Double lat = pickLatLon(data, "lat", "latitude");
|
||||
Double lon = pickLatLon(data, "lon", "longitude", "lng");
|
||||
if (lat == null || lon == null) return null;
|
||||
if (lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0) return null;
|
||||
String pointWkt = "POINT(" + lon + " " + lat + ")";
|
||||
try {
|
||||
Geometry g = new WKTReader().read(pointWkt);
|
||||
g.setSRID(4326);
|
||||
return g;
|
||||
} catch (ParseException e) {
|
||||
// Не должно случиться (мы сами sсонструировали WKT) — но лучше null
|
||||
// чем 500 если cosmic ray flip'нет double.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Double pickLatLon(com.fasterxml.jackson.databind.JsonNode data, String... keys) {
|
||||
for (String key : keys) {
|
||||
var node = data.get(key);
|
||||
if (node != null && node.isNumber()) return node.doubleValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void publishCreated(DictionaryDefinition d, DictionaryRecord r) {
|
||||
String wkt = r.getGeometry() == null ? null : new org.locationtech.jts.io.WKTWriter().write(r.getGeometry());
|
||||
var payload = new DictionaryRecordCreated(
|
||||
|
||||
Reference in New Issue
Block a user