PostGIS — открытое программное обеспечение, добавляющее поддержку географических объектов в реляционную базу данных PostgreSQL.
В этой небольшой статье будет рассмотрено использование его в Java. В частности — задача нахождения географических объектов по их координатам.
PostGIS был создан еще в 2001 году. Он является хорошим бесплатным решением для хранения картографических данных в базе. Но статья не совсем о нем, а только о частном случае — удобной работе с PostGIS средствами JPA.
Зависимости
Для нашей задачи важны такие библиотеки:
- Hibernate 5.3.7
- hibernate-spatial — той же версии. Теоретически, можно использовать более старые. Начиная с пятой, hibernate-spatial совпадает с hibernate. Ранее: Hibernate Spatial 1.1.x для Hibernate 3.6.x, Hibernate Spatial 1.0 для Hibernate 3.2.x — 3.5.x.
- postgresql 42.2.4. Бралась такая версия, потому что в более новых ужесточены требования к SSL. Подбирайте версию драйвера, подходящую к версии БД.
Ну и все, что вам еще потребуется для JPA — Spring или контейнер.
Диалекты
Hibernate Spatial предоставляет геометрические абстракции для работы с пространственными базами. Как и в JPA, в первом приближении нас не интересует, какая база данных используется на сервере.
Официально поддерживаются PostgresSQL, Oracle, MySQL, MS SQLServer, GeoDB (H2), DB2. Детали поддержки функций. Может показаться, что Мускул — аутсайдер. Но в 8-й версии поддержку пространственных данных прилично улучшили.
Мы используем Postgres. Но нужно указать Hibernate диалект
"org.hibernate.spatial.dialect.postgis.PostgisDialect"
вместо стандартного постгресовского.Пора кодить
Таблица в PostGIS может иметь какие угодно поля. Просто стандартно одно из них будет типа geometry. А еще есть geography (не поддерживается сейчас в Hibernate). Если не научить Java работать с этим типом, он будет восприниматься как blob или String вида «01010000207B7F0000188D594CC9B22541BC4E56674F2C5541».
Конечно, можно работать с PostGis'ом на чистом JBDC. Пример. Но это требует отдельной кропотливой работы с
org.postgis.PGgeometry
. Это совсем не те классы, про какие будет статья. И никакой переносимости уже не будет.Мы идем в JPA и создаем простой класс:
@Entity
public class AdressBuilding implements Serializable {
@Id
private Integer id;
private Point geom;
...
Остальные поля опущены (географический объект может хранить любую информацию). Здесь ничего необычного — стандартный класс сущности. Интересен только объект класса Point — точка трехмерного пространства.
Тут и далее используются классы из пакета com.vividsolutions.jts.geom.
JTS стал стандартом де-факто для представления геопространственных данных. Он реализует спецификацию Simple Feature Specification / Simple Feature Access, созданную OpenGIS еще в 90е.
Уточнение. Point наследуется от абстрактного класса Geometry. Он содержит такие нестатические поля:
protected Envelope envelope;
protected final GeometryFactory factory;
protected int SRID;
private Object userData;
Envelope — минимальная ограничительная рамка для этой геометрии. Но она может возвращаться в виде геометрии. И тогда у вас будет бесконечная попытка сериализации.
SRID — номер системы координат. Их существует очень много. Основные различия: формат координат (метры, градусы...), точка отсчета и форма Земли (Земля не круглая). PostGis знает много систем координат и умеет их преобразовывать.
Как я уже сказал, в базе у нас тип geometry. Я же сразу использовал конкретный класс Point для удобства, ведь в этой таблице у меня только точечные объекты. Но PostGIS теоретические может хранить несколько типов геометрии сразу. Просто в каждой геометрии указан ее тип:
"geometry":{"type":"MultiPolygon","coordinates":...
Если верить StackOverflow, использование нескольких геометрий в одной таблице замедляет запросы. Геометрии могут быть и вложенными. Типы:
Запросы к БД
С реализацией класса разобрались. теперь пора получить их из базы. Наши точки — дома, точнее их адреса. Вы можете делать привычные SQL запросы: получать домики по id, номеру, количеству бабушек…
Нас же сейчас интересуют пространственные запросы. Например, найти дом по координатам. Пусть искомые координаты x,y, а +-delta — желаемая область поиска. Основные запросы в STS выполняются на соотношение геометрий. Поэтому нам нужно ее создать:
Coordinate c1 = new Coordinate(x - delta, y - delta);
...
Coordinate[] coordinates = new Coordinate[]{c1, c2, c3, c4, c1};
GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();//static заранее
Polygon square_window = GEOMETRY_FACTORY.createPolygon(coordinates);
square_window.setSRID(32635);
Если мы не укажем систему координат, PostGis откажется их сравнивать. Вы или знаете код вашей системы, или получите его из любой точки кодом
.getGeom().getSRID()
.Дальше посылаем запрос вида:
"select a "
+ "from AdressBuilding a "
+ "where within(a.geom, :window) = true"
Запрос within означает проверку, находится ли геометрия внутри другой. Не пугайтесь, если ваша IDE скажет, что в JPA не может быть таких запросов. Hibernate Spatial преобразует его в:
where
st_within(adressbuil0_.geom, ?)=true
Где
st_within
— уже функция PostGis.Есть еще несколько вариантов, как получить тот же результат — точка попала в квадрат.
contains(:window, a.geom) / intersects(a.geom, :window)...
Детальное описание спецификаций здесь.
Послесловие
Точки мы получили — делайте теперь с ними что пожелаете.
Я тестировал случай небольшой базы на сервере с относительно большим объемом оперативы. Если грузить по максимуму и забыть про индексы, задача поиска упрется в процессор.
В Postgres есть много разных индексов. И часть из них помогают Постгису. Исследование показало, что для точек подходит только GIST(?)
CREATE INDEX [indexname] ON [tablename] USING GIST ( [geometryfield] );
Но чаще всего, когда вы импортируете данные в PostGis, индексы создаются автоматом…
Уточнения и дополнительная информация приветствуется.
Использованный мануал:
Для Hibernate 5
Для Hibernate 4