Заранее оговорюсь что данная статья не претендует на подробное изучение стандартов WMS или огромных возможностей набора библиотек Geotools. Это лишь простое HOWTO(или если хотите, считайте это учебно-демонстрационной задачей), которого для выполнения своих задач я не нашел ни на Хабре ни где либо еще в полноценном варианте.
Постановка задачи
Задача заключается в том чтобы реализовать WMS отдающий прозрачный слой в формате PNG с нанесенными на него точками координаты которых записаны где-то в базе данных(способ хранения не столь важен). Уже в WMS клиенте этот слой можно накладывать на изображение другой карты.
Кратко о WMS
WMS это Web-сервис предоставляющий доступ к картографической информации по HTTP протоколу. Описание стандарта можно найти здесь http://www.opengeospatial.org/standards/wms. Параметры сервису передаются в Get-запросе. Стандарт WMS предусматривает три типа запроса.
- GetCapabilities(обязательный) – Данный запрос возвращает информацию о возможностях сервиса, таких как набор слоев, стилей, возвращаемых форматах в виде XML-документа соответствующего XSD приведенному в описании стандарта.
- GetMap(обязательный) – Основной тип запроса для WMS. Возвращает изображение, заданных в параметрах запроса размеров и формата, географической информации в области координат(BBOX) заданной системы координат.
- GetFeatureInfo(опциональный) – Этот запрос возвращает более подробную информацию о географических объектах на карте полученной в предыдущем запросе. Не все WMS соответствующие стандарту обязаны поддерживать данный тип запроса. В случае если этот запрос не поддерживается сервис возвращает исключение OperationNotSupported.
Более подробную информацию ищите в документах по ссылке выше.
Приступаем к реализации
На реализации GetCapabilities останавливаться не буду. Думаю что создание XML документа тема не интересная. GetFeatureInfo не предусмотрена в моем тестовом примере, так что перейдем сразу к главному – GetMap.
Самым непростым вопросом в отображении географической информации является вопрос перевода географических координат, в координаты проекции на плоской картинке. В случае с большим приближением(так называемый Street View), эта задача решается по средствам достаточно простой математики, к несчастью когда мы видим карту мира целиком, эта задача становится значительно сложнее. Вместо того что бы изобретать велосипед реализовать алгоритм выполняющий сложные математические вычисления, воспользуемся готовой библиотекой Geotools обладающей большим набором функций для отображения картографической информации.
Достаем необходимые библиотеки
Для того что бы реализовать весь необходимый функционал, вовсе не необходимо тащить весь набор библиотек Geotools. Что бы не заморачиваться с зависимостями проще всего достать все необходимое с помощью Maven. Пример pom.xml можно найти в Geotools Quickstart http://docs.geotools.org/latest/userguide/tutorial/quickstart/eclipse.html. В данный pom.xml необходимо добавить зависимость от gt-epsg-hsql. В итоге мой pom.xml выглядел так:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.geotools</groupId>
<artifactId>tutorial</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>tutorial</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<geotools.version>8-SNAPSHOT</geotools.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-swing</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>${geotools.version}</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>maven2-repository.dev.java.net</id>
<name>Java.net repository</name>
<url>http://download.java.net/maven/2</url>
</repository>
<repository>
<id>osgeo</id>
<name>Open Source Geospatial Foundation Repository</name>
<url>http://download.osgeo.org/webdav/geotools/</url>
</repository>
<repository>
<id>maven2-repository.dev.java.net</id>
<name>Java.net repository</name>
<url>http://download.java.net/maven/2</url>
</repository>
<repository>
<id>osgeo</id>
<name>Open Source Geospatial Foundation Repository</name>
<url>http://download.osgeo.org/webdav/geotools/</url>
</repository>
<repository>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>opengeo</id>
<name>OpenGeo Maven Repository</name>
<url>http://repo.opengeo.org</url>
</repository>
</repositories>
</project>
Примечание: Здесь указана не последняя версия gt по причине сохранения совместимости с продуктами заказчика.
Создаем возвращаемую картинку
Географическая информация в geotools хранится в виде экземпляров класса Feature. Коллекция Фич создается следующим образом:
final SimpleFeatureType TYPE;
try {
TYPE = DataUtilities.createType("Location",
"location:Point:srid=4326," + // <- the geometry attribute:
// Point type
"name:String," + // <- a String attribute
"number:Integer" // a number attribute
);
} catch (SchemaException e) {
ServletException ex = new ServletException(e);
throw ex;
}
// create feature collection
DefaultFeatureCollection collection = new DefaultFeatureCollection(
null, TYPE);
// create geometry factory
GeometryFactory geometryFactory = JTSFactoryFinder
.getGeometryFactory(null);
// create feature builder
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE);
Когда создается коллекция фич, эти фичи должны иметь тип. По сути тип фичи, это набор атрибутов. Создавая тип, первым параметром мы указываем название типа, вторым параметром является строка определяющая набор и типы хранимых атрибутов. Между собой атрибуты разделены запятой. До двоеточия находится ваше имя атрибута, после тип данных. Вы можете использовать такие типы как String или Integer из java.lang. Для определения местоположения, необходим атрибут названый location. Тип этого атрбута это com.vividsolutions.jts.geom.Point;. После второго двоеточия стоит дополнительный параметр определяющий систему координат. После создается коллекция фич созданного нами типа, для нанесения на карту, и билдер фич этого типа. Так же создадим GeometryFactory. Этот класс нужен для получения экземпляров класса Point.
Далее в цикле, для каждой точки считанной из базы необходимо выполнить следующий код
Point point = geometryFactory.createPoint(new Coordinate(
longitude, latitude));//create point
//set new feature attributes in featureBuilder
featureBuilder.add(point);
featureBuilder.add(name);
featureBuilder.add(number);
//create new feature
SimpleFeature feature = featureBuilder.buildFeature(null);
//add feature to collection
collection.add(feature);
Здесь мы создаем точку из координат типа Double. Добавляем все новые атрибуты в featureBuilder, создаем фичу и добавляем ее в колеекцию.
После необходимо создать карту со слоем на котором были бы отражены все фичи.
MapContent map = new MapContent();
map.setTitle("Quickstart");
Style style = createPointStyle();
Layer layer = new FeatureLayer(collection, style);
map.addLayer(layer);
Здесь отдельного внимания стоит удостоить метод createPointStyle(). Как оказалось задать стиль дальнейшего отображения точки на слое не такая уж и тривиальная задача в Geotools.
static StyleFactory styleFactory = CommonFactoryFinder.getStyleFactory();
static FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory();
private Style createPointStyle() {
Graphic gr = styleFactory.createDefaultGraphic();
Mark mark = styleFactory.getCircleMark();
mark.setStroke(styleFactory.createStroke(
filterFactory.literal(Color.RED), filterFactory.literal(1)));
mark.setFill(styleFactory.createFill(filterFactory.literal(Color.CYAN)));
gr.graphicalSymbols().clear();
gr.graphicalSymbols().add(mark);
gr.setSize(filterFactory.literal(7));
/*
* Setting the geometryPropertyName arg to null signals that we want to
* draw the default geomettry of features
*/
PointSymbolizer sym = styleFactory.createPointSymbolizer(gr, null);
Rule rule = styleFactory.createRule();
rule.symbolizers().add(sym);
FeatureTypeStyle fts = styleFactory
.createFeatureTypeStyle(new Rule[] { rule });
Style style = styleFactory.createStyle();
style.featureTypeStyles().add(fts);
return style;
}
Здесь мы устанавливаем цвет бордера – красный, заливку циан, и размер 7 пикселей(такой размер будет вне зависимости от масштаба карты).
После этих операций у нас в памяти есть карта со всеми необходимыми нам точками на ней. Остается последний этап, превратить эту карту в картинку формата PNG размеров заданных в запросе.
private void saveImage(final MapContent map, HttpServletResponse response,
ParamsContainer paramsContainer) throws ServletException {
GTRenderer renderer = new StreamingRenderer();
renderer.setMapContent(map);
Rectangle imageBounds = null;
ReferencedEnvelope mapBounds = new ReferencedEnvelope();
try {
// mapBounds = map.getMaxBounds();
mapBounds.init(paramsContainer.minx, paramsContainer.maxx,
paramsContainer.miny, paramsContainer.maxy);
imageBounds = new Rectangle(0, 0, paramsContainer.width,
(int) Math.round(paramsContainer.height));
} catch (Exception e) {
// failed to access map layers
throw new ServletException(e);
}
BufferedImage image = new BufferedImage(imageBounds.width,
imageBounds.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D gr = image.createGraphics();
try {
renderer.paint(gr, imageBounds, mapBounds);
ImageIO.write(image, paramsContainer.imageFormat,
response.getOutputStream());
} catch (IOException e) {
throw new ServletException(e);
}
}
Тут нам поможет класс GTRenderer. Он позволяет записать в стандартный BufferedImage изображение где c с помощью imageBounds мы указываем размер изображения, а при помощи mapBounds указываем BBOX карты в координатах. Все эти параметры мы получаем из Get-запроса. После этого просто берем наш BufferedImage и c помощью ImageIO записываем изображение в нужном формате в выходной поток HttpServletResponse.
На этом небольшая учебно-демонстрационная задача выполнена. Стоит заметить, что хоть в этом примере мы получаем сервис полностью соответствующий стандарту, и который вполне может использоваться стандартными WMS клиентами, например такими как OpenGiS, до полноценного WMS тут еще очень и очень далеко, и это уже совсем не маленькая задача.
Автор: bazarnazar