Last active
April 12, 2024 03:54
-
-
Save Tkachenko-Ivan/c2418a09c887e0baa0a823944d76e343 to your computer and use it in GitHub Desktop.
Сервис для обработки новых дорог, с целью корректно встроить их в граф
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package loader.service; | |
import java.sql.Connection; | |
import java.sql.PreparedStatement; | |
import java.sql.SQLException; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.Map; | |
import loader.dto.Bidi; | |
import org.apache.commons.lang3.StringUtils; | |
import org.postgis.*; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.jdbc.core.JdbcTemplate; | |
import org.springframework.jdbc.support.GeneratedKeyHolder; | |
import org.springframework.jdbc.support.KeyHolder; | |
import org.springframework.stereotype.Component; | |
/** | |
* Модификация линий для создания общих точек. Работа с сервисами PostGIS | |
* | |
* @author Иван | |
*/ | |
@Component | |
public class PostGisGeometry { | |
@Autowired | |
JdbcTemplate jdbcTemplate; | |
String layerName = "gis_roads"; | |
/** | |
* Вставка данных | |
* | |
* @param geom геометрия мультилинии | |
* @return идентификаторы созданных объектов | |
*/ | |
public List<Long> insertTempLines(Geometry geom) { | |
if (geom.numPoints() == 0) { | |
return new ArrayList<>(); | |
} | |
String sql = String.format("INSERT INTO %s (geom, date_load) VALUES (?, current_date)", layerName); | |
return insert(sql, geom); | |
} | |
/** | |
* Вставка данных с копированием атрибутики | |
* | |
* @param geom геометрия мультилинии | |
* @param prevId идентификатор объкта атрибуты которого надо копировать | |
* @return идентификаторы созданных объектов | |
*/ | |
public List<Long> insertTempLines(Geometry geom, Long prevId) { | |
if (geom.numPoints() == 0) { | |
return new ArrayList<>(); | |
} | |
// Атрибутивные поля для копирования | |
List<String> fields = getFields(); | |
if (fields.isEmpty()) { | |
// Если их нет, то решение сводится к другой задаче | |
return insertTempLines(geom); | |
} | |
String fieldStr = String.join(",", fields); | |
String sql = String.format( | |
""" | |
INSERT INTO %s (geom,date_load,%s) | |
SELECT ?,current_date,%s | |
FROM %s | |
WHERE gid = %d | |
""", | |
layerName, fieldStr, fieldStr, layerName, prevId); | |
return insert(sql, geom); | |
} | |
private List<Long> insert(String sql, Geometry geom) { | |
List<Long> result = new ArrayList<>(); | |
KeyHolder keyHolder = new GeneratedKeyHolder(); | |
MultiLineString multiLine = (org.postgis.MultiLineString) geom; | |
for (org.postgis.LineString line : multiLine.getLines()) { | |
jdbcTemplate.update( | |
(Connection connection) -> { | |
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"gid"}); | |
ps.setObject(1, new PGgeometry(line)); | |
return ps; | |
}, | |
keyHolder); | |
result.add(keyHolder.getKey().longValue()); | |
} | |
return result; | |
} | |
/** | |
* Получает список колонок таблицы | |
* | |
* @return список названий и типов колонок | |
*/ | |
private List<String> getFields() { | |
String sql | |
= "SELECT column_name, udt_name " | |
+ "FROM information_schema.columns " | |
+ "WHERE table_name = ? AND column_name NOT IN ('geom', 'gid', 'osm_id', 'date_load')"; | |
return jdbcTemplate.query(sql, (rs, rowNum) -> { | |
return rs.getString("column_name"); | |
}, layerName); | |
} | |
/** | |
* Добавляет общие точки в местах пересечений | |
* | |
* @param tempIds идентификаторы временных объектов | |
* @param deleteList список линий которые были удалены в процессе разделения | |
* (простаскивается через все этапы) | |
*/ | |
public void putIntersectionPoint(List<Long> tempIds, List<Long> deleteList) { | |
List<Bidi> map = new ArrayList<>(); | |
// Поиск пересечений | |
List<Map<String, Object>> intersects = findIntersect(tempIds); | |
for (Map<String, Object> intersect : intersects) { | |
// Идентификатор рассматриваемых линии | |
Long tempId = ((Number) intersect.get("temps_id")).longValue(); | |
Long mainId = ((Number) intersect.get("mains_id")).longValue(); | |
if (deleteList.contains(tempId) || deleteList.contains(mainId)) { | |
// Одна из этих линий была удалена на предыдущем шаге | |
// найденное пересечение уже не актуально | |
continue; | |
} | |
if (map.contains(new Bidi(mainId, tempId))) { | |
// Это пересечение уже было обработано в другой комбинации | |
continue; | |
} | |
// Сохранить признак того что эта пара линий уже обработана | |
// Она может быть обработана несколько раз, - по количеству пересечений | |
Bidi bidi = new Bidi(tempId, mainId); | |
if (!map.contains(bidi)) { | |
map.add(bidi); | |
} | |
// Геометрия пересечения | |
PGgeometry pgGeom = (PGgeometry) intersect.get("intersects"); | |
Geometry geom = pgGeom.getGeometry(); | |
switch (geom.getTypeString()) { | |
case "MULTILINESTRING", "LINESTRING" -> { | |
// Если геометрией пересечения является линия значит временная линия будет поделена на несколько других | |
List<Long> newTempIds = lineSpliter(tempId, mainId); | |
// Удалить лишнюю линию | |
deleteList.add(tempId); | |
deleteFromTemp(Arrays.asList(tempId)); | |
putIntersectionPoint(newTempIds, deleteList); | |
} | |
case "POINT" -> { | |
// Модифицируем временную линию | |
lineModificate(tempId, (Point) geom); | |
// Модифицируем постоянную линию | |
lineModificate(mainId, (Point) geom); | |
} | |
case "MULTIPOINT" -> { | |
MultiPoint multiPoint = (MultiPoint) geom; | |
for (Point point : multiPoint.getPoints()) { | |
// Модифицируем временную линию | |
lineModificate(tempId, point); | |
// Модифицируем постоянную линию | |
lineModificate(mainId, point); | |
} | |
} | |
} | |
} | |
} | |
private List<Map<String, Object>> findIntersect(List<Long> tempIds) { | |
if (tempIds.isEmpty()) { | |
return new ArrayList<>(); | |
} | |
String ids = StringUtils.join(tempIds, ','); | |
// https://gis.stackexchange.com/questions/236712/change-st-intersects-default-tolerance | |
String sql = String.format( | |
""" | |
SELECT | |
CASE WHEN ST_IsEmpty(ST_Intersection(temps.geom, mains.geom)) | |
THEN ST_ClosestPoint(temps.geom, mains.geom) | |
ELSE ST_Intersection(temps.geom, mains.geom) | |
END intersects, | |
temps.gid temps_id, | |
mains.gid mains_id, | |
temps.geom temps_way, | |
mains.geom mains_way | |
FROM | |
%s AS temps, | |
%s AS mains | |
WHERE temps.gid IN (%s) | |
AND temps.gid <> mains.gid | |
AND ST_Intersects(temps.geom, mains.geom, 0.01) IS TRUE | |
AND (mains.bridge IS null OR mains.bridge = 'F') | |
AND (mains.tunnel IS null OR mains.tunnel = 'F') | |
AND (temps.bridge IS null OR temps.bridge = 'F') | |
AND (temps.tunnel IS null OR temps.tunnel = 'F') | |
""", layerName, layerName, ids); | |
return jdbcTemplate.queryForList(sql); | |
} | |
/** | |
* Удаляет линии из временного слоя | |
* | |
* @param deletesIds список линий | |
*/ | |
private void deleteFromTemp(List<Long> deletesIds) { | |
if (!deletesIds.isEmpty()) { | |
String sql = String.format("DELETE FROM %s WHERE gid IN (%s)", layerName, StringUtils.join(deletesIds, ',')); | |
jdbcTemplate.update(sql); | |
} | |
} | |
/** | |
* Получить охват xmin, ymin, xmax, ymax | |
* | |
* @param ids идентификаторы геометрий | |
* @return охват | |
*/ | |
public PGbox2d getExtent(List<Long> ids) throws SQLException { | |
String sql = String.format("SELECT ST_Extent(geom) as bextent FROM %s WHERE gid IN (%s)", layerName, StringUtils.join(ids, ',')); | |
String bbox = jdbcTemplate.queryForObject(sql, String.class); | |
return new PGbox2d(bbox); | |
} | |
/** | |
* Сохраняем различия линий (в виде новых линий) имеющих повторяющиеся куски | |
* | |
* @param tempId идентификатор временной линии | |
* @param mainId идентификатор уже существующей линии | |
* @return список идентифкаторов созданных линий | |
*/ | |
private List<Long> lineSpliter(long tempId, long mainId) { | |
List<Long> result = new ArrayList<>(); | |
// Получаем разницу | |
String sql = String.format( | |
""" | |
SELECT diff FROM | |
( | |
SELECT ST_Difference(temps.geom, mains.geom) AS diff | |
FROM | |
%s AS temps, | |
%s AS mains | |
WHERE temps.gid = %d AND mains.gid = %d | |
) dif | |
WHERE ST_IsEmpty(diff) = false | |
""", layerName, layerName, tempId, mainId); | |
List<Map<String, Object>> differences = jdbcTemplate.queryForList(sql); | |
for (Map<String, Object> difference : differences) { | |
PGgeometry pgGeom = (PGgeometry) difference.get("diff"); | |
Geometry geom = pgGeom.getGeometry(); | |
// Вставить в ту же самую таблицу | |
result.addAll(insertTempLines(geom, tempId)); | |
} | |
return result; | |
} | |
/** | |
* Модифицирует линию добавляя к ней точку | |
* | |
* @param lineId идентификатор обрабатываемой линии в указанном слое | |
* @param intersectPoint точка, которую необходимо добавить в линию | |
*/ | |
private void lineModificate(Long lineId, Point intersectPoint) { | |
// Получить длину линии от начала до точки, в долях от 0 до 1 | |
String sql = String.format("SELECT ST_LineLocatePoint(geom, ?) FROM %s WHERE gid = %d", layerName, lineId); | |
double part = jdbcTemplate.queryForObject(sql, Double.class, new PGgeometry(intersectPoint)); | |
int pointCount = 1; | |
if (part != 0) { | |
// Количество точек в этой части линии | |
sql = String.format("SELECT ST_NumPoints(ST_LineSubstring(geom, 0, %s)) FROM %s WHERE gid = %d", part + "", layerName, lineId); | |
pointCount = jdbcTemplate.queryForObject(sql, Integer.class); | |
} | |
// Получить из БД эту линию | |
// Линию придётся получить каждый раз заново потому что она постоянно в состоянии модификации | |
// и то, что было ранее получено в запросе, - очень быстро потеряет актуальность | |
sql = String.format("SELECT geom FROM %s WHERE gid = %d", layerName, lineId); | |
LineString tempWay = (LineString) jdbcTemplate.queryForObject(sql, PGgeometry.class).getGeometry(); | |
// Проверить наличие этой точки в существующей линии | |
// (полагаю что выход за границу массива невозможен) | |
Point existPoint = tempWay.getPoints()[pointCount - 1]; | |
if (existPoint.x != intersectPoint.x || existPoint.y != intersectPoint.y) { | |
// Если отсутствует, - то создать | |
sql = String.format("UPDATE %s SET geom = ST_AddPoint(geom, ?, %d) WHERE gid = %d", layerName, pointCount - 1, lineId); | |
jdbcTemplate.update(sql, new PGgeometry(intersectPoint)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
см. Как хранить сеть дорог в БД для построения маршрута?