06.08.2018 Улучшение веб-ГИС "VerySimpleGis". Выделение объектов на карте.
Перед тем как начать
Данная статья является логическим продолжением предыдущей статьи Создание простой веб-ГИС "VerySimpleGis", в которой мы создали нашу первую самую простую веб-ГИС с возможностью получения информации об объекте по клику на нем с использованием библиотеки GDAL/OGR. Что это за библиотека и как ее подключить к своему проекту на платформе .NET Framework, вы можете ознакомиться в статье Программируем с использованием GDAL/OGR на C# (часть 1). Установка и настройка. Ну а в этой статье мы дополним нашу интерактивную карту функцией выделения объекта.
Подготавливаем проект
Если вы хотите повторить все мои действия самостоятельно, то вам нужно клонировать репозиторий и перейти на предыдущую ревизию. Отмечу, что у вас должен быть установлен Git. Первым делом клонируем репозиторий с VerySimpleGis командой и переходим на нужную ревизию. Заходим в нужную директорию и выполняем следующие команды:
git clone https://github.com/freewindsv/VerySimpleGis.git VerySimpleGis
cd VerySimpleGis
git reset --hard 440f695f755e6e5cc2eb815e0ab95da0201dec20
Немного поясню. Первой строкой мы клонируем репозиторий себе на жесткий диск в директорию VerySimpleGis. Во второй строке заходим в созданную директорию и последней строкой сбрасываем состояние исходников к ревизии, хеш которой указан последним параметром.
Теперь открываем проект в Visual Studio 2017. Первым делом обновим все установленные nuget пакеты. Открываем консоль диспетчера пакетов командой из меню: Средства --> Диспетчер пактов NuGet --> Консоль диспетчера пакетов. И в консоли набираем команду:
Update-Package
После некоторого времени необходимые пакеты будут обновлены. Далее, удалим ссылки на внешние библиотеки. Для этого развернем объект "Ссылки" в обозревателе решений, выделим все указанные сборки (зажав клавишу Ctrl и кликая по ним мышью), как показано на скриншоте ниже, и жмем клавишу Delete
Теперь перейдем в консоль диспетчера пакетов и установим необходимый нам пакет GDAL Native. Для этого выполним команду:
Install-Package Gdal.Native -Version 1.11.1
Следующим действием заходим в свойства проекта VerySimpleGis, переходим на вкладку "Сборка" и в качестве целевой платформы указываем значение Any CPU:
Теперь открываем файл Global.asax.cs и приводим его к виду:
using System.Web.Mvc;
using System.Web.Routing;
namespace VerySimpleGis
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
GdalConfiguration.ConfigureOgr(); //register all vector drivers
}
}
}
Если зайти в данный метод, то можно увидеть, что он выполняем регистрацию всех доступных драйверов для доступа к векторным данным. Далее, пересобираем решение. Если все выполнено верно, то пересборка решения должна завершиться успешно. Запустим наш проект и мы увидим, что все работает как положено: при клике на страну мы получаем с сервера ее название. Т.е. пока что мы имеем функциональность, как и в самой первой статье о VerySimpleGis.
Теперь заменим используемую в проекте библиотеку Leaflet, на самую свежую версию библиотеки OpenLayers. Переход на OpenLayers я решил осуществить лишь по одной причине - она обладает большими возможностями, да и я уже успел привыкнуть к ней, если честно )) Перейдем на официальный сайт OpenLayers, откроем ссылку Get the Code и загрузим архив v5.1.3-dist.zip (последняя версия на момент публикации) Данную версию библиотеки можно также загрузить с моего ресурса по ссылке: v5.1.3-dist.zip. Распакуем архив в директорию проекта Content/js/ol-v5.1.3, а каталог Content/js/leaflet удалим вместе со всем его содержимым. Включим новые файлы в проект. В результате у вас должна получиться такая структура:
Теперь пришло время поправим ссылки на нашу библиотеку в файле Views/Home/Index.cshtml. Указанный файл теперь будет выглядеть таким образом:
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/js/ol-v5.1.3/ol.css" rel="stylesheet" />
<link href="~/Content/css/style.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script src="~/Content/js/ol-v5.1.3/ol.js"></script>
<script src="~/Content/js/main.js"></script>
<title>VerySimpleGis</title>
</head>
<body>
<div id="map" class="map">
<div id="info">VerySimpleGis</div>
</div>
<script type="text/javascript">
var App = {};
App.ROOT = '@Url.Content("~/")';
</script>
</body>
</html>
В нем мы заменили ссылку на новую таблицу стилей и javascript-файл библиотеки. Откроем файл Content/js/main.js и адаптируем его содержимое под новую библиотеку.
function initMap() {
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
}),
new ol.layer.Tile({
source: new ol.source.TileWMS({
projection: 'EPSG:4326',
url: 'http://localhost:8888/cgi-bin/mapserv.exe?map=../htdocs/mydemo/wms_ol.map&',
params: { 'LAYERS': 'world_poly', 'TILED': true, 'VERSION': '1.1.1' },
}),
opacity: 0.7
})
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
map.on('click', function (event) {
var coord = ol.proj.toLonLat(event.coordinate);
$.ajax({
url: App.ROOT + 'Home/GetGeoData',
data: JSON.stringify([coord[1], coord[0]])
}).done(function (data) {
showInfo(data);
});
});
}
В этом файле мы только обновим функцию initMap, которая отвечает за инициализацию карты и подключение обработчика клика мышки. Запустим проект на выполнение и убедимся, что мы имеем ту же самую функциональность.
Добавляем выделение объектов
Ну вот, все шаги по настройке и обновлению проекта уже позади и теперь мы приступаем к расширению функциональности нашего решения. Первым делом, внесем изменения в файл модели Models/Entities/Country.cs. Добавим к этому классу свойство, которое будет содержать координаты нашего объекта страны в формате WKT. WKT (Well-known text) - это текстовый формат описания геометрических объектов. В этом формате, например, точка будет описываться следующей строкой:
POINT(1 2)
namespace VerySimpleGis.Models.Entities
{
public class Country
{
public string Name { get; set; }
public string GeomWKT { get; set; }
}
}
В классе Country мы добавили свойство GeomWKT, которое будет хранить координаты выделяемой страны. Теперь откроем файл Models/GisAccess.cs и приведем содержимое метода public Country GetCountryByCoordinates(double x, double y) к виду:
public Country GetCountryByCoordinates(double x, double y)
{
x = Normalize(x);
string wkt = $"POINT({x.ToString(nfi)} {y.ToString(nfi)})";
Geometry filter = null;
Feature feature = null;
try
{
filter = Ogr.CreateGeometryFromWkt(ref wkt, layer.GetLayerDefn().GetGeomFieldDefn(GEOM_FIELD_IDX).GetSpatialRef());
layer.SetSpatialFilter(filter);
feature = layer.GetNextFeature();
if (feature != null)
{
string featureWkt = null;
int error = feature.GetGeomFieldRef(GEOM_FIELD_IDX).ExportToWkt(out featureWkt);
if (error == OGRERR_NONE)
{
return new Country() { Name = feature.GetFieldAsString(COUNTRY_ATTR_NAME_IDX), GeomWKT = featureWkt };
}
}
}
catch(Exception ex)
{
throw new GisException("Error getting feature by coordinates", ex);
}
finally
{
if (filter != null)
{
filter.Dispose();
}
if (feature != null)
{
feature.Dispose();
}
}
return null;
}
А в самом классе GisAccess добавим константу: private const int OGRERR_NONE = 0;, которая содержит значение, указывающее на то, что вызов метода ExportToWkt завершился успешно.
Итак, разберем обновленный метод GetCountryByCoordinates. Самым значимым событием является то, что мы добавили получение координат объекта в формате WKT с помощью строки int error = feature.GetGeomFieldRef(GEOM_FIELD_IDX).ExportToWkt(out featureWkt);. Здесь мы получаем ссылку на объект типа Geometry, по индексу поля, хранящего пространственные данные объекта. В нашем случае такое поле единственное, поэтому в вызов метода мы передаем 0. И у полученного объекта геометрии мы вызываем метод ExportToWkt, в который мы передаем строку, куда будет записано значение координат в WKT-формате. Затем проверяем наличие ошибки и, если все в порядке, то возвращаем объект страны, присвоив полю GeomWKT полученное ранее значение. Среди прочих изменений следует отметить тот факт, что вызов методов мы обернули в блок try - catch - finally. И в блоке finally теперь выполняем освобождение ресурсов более ненужных объектов filter и feature. Если исключение в процессе выполнения блока try будет выброшено, то оно будет обработано в блоке catch, который просто выбрасывает новое исключение типа GisException, снабжая его собственным описанием. А еще мы поправили название метода Normalize, сделав первую букву заглавной, чтобы Visual Studio не придиралась )
Для проверки временно добавим в функцию function showInfo(obj) вывод свойства obj.GeomWKT. Итак, обновленный метод будет выглядеть следующим образом:
function showInfo(obj) {
console.log(obj.GeomWKT);
alert(typeof (obj.Name) != 'undefined' ? obj.Name : 'Нет объекта!');
}
Пересоберем проект и запустим. Теперь кликнем на какую-нибудь страну и в консоли браузера увидим описание геометрии объекта в формате WKT:
Обновляем клиентскую часть
Мы вышли уже на финишную прямую и нам осталось только внести изменеия в файл Content/js/main.js, в которм собственно и будет происходить выделение нужного нам объекта. Но сперва давайте избавимся от надоедливого сообщения через alert и заменим отображение информации о выделенной стране через специальный слой div с id="info", который пылился у нас без дела в файле View/Home/Index.cshtml. В конец файла Content/js/main.js добавим объект UI, отвечающий за взаимодействие с интерфейсом. В нем определен единственный метод showInfo, который и выполняет отображение переданной ему строки в указанном выше div'е.
UI = {
showInfo: function (str) {
$('#info').text(str);
}
};
Далее, перед вызовом initAjax(); добавим код:
var siteSource = new ol.source.Vector();
var selectInteraction = new ol.interaction.Select({
condition: function (mapBrowserEvent) {
return false;
},
removeCondition: function (mapBrowserEvent) {
return false;
}
});
initAjax();
О том, что в нем происходит я расскажу чуть позже. А пока что внесем следующие изменения в функцию initMap():
function initMap() {
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
}),
new ol.layer.Tile({
source: new ol.source.TileWMS({
projection: 'EPSG:4326',
url: 'http://localhost:8888/cgi-bin/mapserv.exe?map=../htdocs/mydemo/wms_ol.map&',
params: { 'LAYERS': 'world_poly', 'TILED': true, 'VERSION': '1.1.1' },
}),
opacity: 0.7
}),
new ol.layer.Vector({
source: siteSource
}),
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
map.addInteraction(selectInteraction);
map.on('click', function (event) {
selectInteraction.getFeatures().clear();
var coord = ol.proj.toLonLat(event.coordinate);
$.ajax({
url: App.ROOT + 'Home/GetGeoData',
data: JSON.stringify([coord[1], coord[0]])
}).done(function (data) {
showInfo(data);
});
});
}
В данной функции мы добавили новый векторный слой, в качестве источника данных слоя используется ранее определенный объект var siteSource = new ol.source.Vector();. Строкой map.addInteraction(selectInteraction); мы добавляем к карте ранее созданный объект взаимодействия selectInteraction, который используется специально для выделения объектов. Чуть позже я несколько подробнее опишу параметры которые были использованы при его создании. И последнее изменение в данной функции - добавление строки selectInteraction.getFeatures().clear(); в обработчик события клика на карте. методом getFeatures() мы получаем коллекцию выделенных объектов, а последующим методом clear() - очищаем ее. Т.е. сбрасываем выделение со всех объектов.
И, наконец, последнее изменение в функции showInfo(obj):
function showInfo(obj) {
if (typeof (obj.Name) != 'undefined') {
var format = new ol.format.WKT();
var feature = format.readFeature(obj.GeomWKT, {
dataProjection: 'EPSG:4326',
featureProjection: 'EPSG:3857' });
feature.set('name', obj.Name);
siteSource.clear();
siteSource.addFeature(feature);
selectInteraction.getFeatures().push(feature);
UI.showInfo(obj.Name);
}
else {
UI.showInfo('Нет объекта!');
}
}
Убедившись, что объект существует, мы создаем объект для чтения и записи формата геометрии типа WKT. В нашем случае мы будем создавать картографический объект библиотеки OpenLayers (feature) из WKT-представления геометрии. Собственно, это и выполняет строка var feature = format.readFeature(obj.GeomWKT, { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' });. В первом аргументе мы передаем WKT-строку с описанием геометрии, а во втором - мы указываем идентификатор системы координат исходных данных - это EPSG:4326 и идентификатор системы координат нашего объекта - это 'EPSG:3857', т.к. подложка OSM, указанная как самый первый слой, представлена именно в данной проекции. Затем в качестве атрибутивных данных мы устанавливаем имя нашего объекта. (Хотя они нами здесь пока что не используются.) Строкой siteSource.clear(); мы очищаем наш векторный слой, а следующей строкой: siteSource.addFeature(feature); - мы добавляем наш объект к векторному слою. Таким образом, при каждом новом клике, старый объект удаляется, а новый - добавляется. Строкой selectInteraction.getFeatures().push(feature); мы добавляем наш объект к коллекции выделенных объектов, благодаря такому действию к объекту применяются специальные стили, которые визуально выделяют объект среди остальных. Ну и, наконец, строкой UI.showInfo(obj.Name); мы отображаем имя выделенного объекта в нашем специальном div'е, который располагается в правом верхнем углу.
Итак, если кто-то запутался, то обновленный файл Content/js/main.js будет выглядеть следующим образом:
$(function () {
var siteSource = new ol.source.Vector();
var selectInteraction = new ol.interaction.Select({
condition: function (mapBrowserEvent) {
return false;
},
removeCondition: function (mapBrowserEvent) {
return false;
}
});
initAjax();
initMap();
function initAjax() {
$.ajaxSetup({
global: true, //trigger global Ajax event handlers for requests
method: 'POST',
timeout: 30000,
dataType: 'json',
contentType: 'application/json; charset=utf-8',
error: function (jqXHR, textStatus, errorThrown) {
if (textStatus == 'timeout') {
alert('При выполнении запроса произошла ошибка.\r\nПроверьте соединение с интернет.');
return;
}
if (jqXHR.responseText) {
alert(jqXHR.responseText, 'Ошибка на сервере', [800, 800]);
}
}
});
}
function initMap() {
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
}),
new ol.layer.Tile({
source: new ol.source.TileWMS({
projection: 'EPSG:4326',
url: 'http://localhost:8888/cgi-bin/mapserv.exe?map=../htdocs/mydemo/wms_ol.map&',
params: { 'LAYERS': 'world_poly', 'TILED': true, 'VERSION': '1.1.1' },
}),
opacity: 0.7
}),
new ol.layer.Vector({
source: siteSource
}),
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
map.addInteraction(selectInteraction);
map.on('click', function (event) {
selectInteraction.getFeatures().clear();
var coord = ol.proj.toLonLat(event.coordinate);
$.ajax({
url: App.ROOT + 'Home/GetGeoData',
data: JSON.stringify([coord[1], coord[0]])
}).done(function (data) {
showInfo(data);
});
});
}
function showInfo(obj) {
if (typeof (obj.Name) != 'undefined') {
var format = new ol.format.WKT();
var feature = format.readFeature(obj.GeomWKT, {
dataProjection: 'EPSG:4326',
featureProjection: 'EPSG:3857' });
feature.set('name', obj.Name);
siteSource.clear();
siteSource.addFeature(feature);
selectInteraction.getFeatures().push(feature);
UI.showInfo(obj.Name);
}
else {
UI.showInfo('Нет объекта!');
}
}
});
UI = {
showInfo: function (str) {
$('#info').text(str);
}
};
А теперь, еще раз вернемся к созданию объекта selectInteraction. В качестве аргумента конструктору передается объект настроек, который задает функции, определяющие следует ли добавлять объект в коллекцию выделенных объектов (свойство condition) или удалять объект из этой коллекции (свойство removeCondition). Таким образом, возможно автоматическое выделение объектов при перемещении над ним мыши, например. Но в нашем случае, мы переопределяем механизм выделения объектов по умолчанию и будем самостоятельно добавлять объекты в коллекцию выделенных объектов и удалять их оттуда. По умолчанию объекты добавляются в коллекцию выделенных при клике на них мышью. Но это действие справедливо только для векторных слоев ol.layer.Vector, объекты которых существуют на клиенте. В то время как тайлы, получаемые с сервера в слоях ol.layer.Tile, являются, по сути, для библиотеки OpenLayers растровыми данными, хотя на сервере они могут хранится в векторном формате. Однако, библиотекой они подгружаются как изображение.
Таким образом, общий механизм выделения объектов работает таким образом. По клику на карте мы очищаем коллекцию выделенных объектов, получаем координаты места клика и передаем их на сервер. На стороне сервера код ищет объект, в который попадает эта координата и, в случае, если объект найден, возвращается его название и описание геометрии в формате WKT. По строке WKT создается объект библиотеки OpenLayers (feature) и добавляется к предварительно очищенному векторному слою путем добавления его к источнику данных (строка siteSource.addFeature(feature);), который был указан при создании этого слоя (фрагмент new ol.layer.Vector({ source: siteSource })). Теперь мы выделяем объект, добавляя его в коллекцию выделенных объектов строкой selectInteraction.getFeatures().push(feature);.
И финальный штрих. Чуть-чуть поправим стили нашей карты:
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial;
}
#info {
position: absolute;
z-index: 500;
background-color: #cce6ff;
right: 0;
padding:5px;
}
#map {
height: 100%;
cursor: auto !important;
}
Скриншот обновленной веб-ГИС представлен ниже.
В принципе, визуально эффект выделения может быть достигнут и без участия объекта selectInteraction. Для этого вы можете самостоятельно закомментировать нужные строки (//selectInteraction.getFeatures().push(feature); и //selectInteraction.getFeatures().clear();) и посмотреть на результат.
Если какие-то моменты вам остались непонятны, то можете просмотреть видеоролик, в котором я выполняю те же самые действия. Записывал одним дублем без подготовки, поэтому не судите строго ))
Файлы для загрузки:
Предыдущие статьи по теме разработки веб-ГИС VerySimpleGis с использованием GDAL/OGR: