Skip to content

Instantly share code, notes, and snippets.

@Tkachenko-Ivan
Last active April 12, 2024 03:54
Show Gist options
  • Save Tkachenko-Ivan/c2418a09c887e0baa0a823944d76e343 to your computer and use it in GitHub Desktop.
Save Tkachenko-Ivan/c2418a09c887e0baa0a823944d76e343 to your computer and use it in GitHub Desktop.
Сервис для обработки новых дорог, с целью корректно встроить их в граф
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