27.04.2017 Создание простой веб-ГИС "VerySimpleGis"
Вступление
В этой статье мы объединим все наши знания и умения, которые были получены ранее в предыдущих статьях в рамках создания своей первой простой веб-ГИС. Поэтому ниже я перечислю все статьи, в которых описаные все шаги, которые необходимо выполнить для успешного освения материала.
- Установка и начальная настройка MapServer - здесь читатель узнает, что собой представляет MapServer, а также научится устанавливать его и публиковать свои геоданные.
- Подключаем MapServer к MS SQL - продолжение цикла статей о MapServer. После ознакомления с материалом, читатель получит практический опыт по настройке БД MS SQL Server как хранилища пространственных данных и сможет настроить подключение к MS SQL из MapServer.
- Настройка WMS-сервиса на платформе MapServer - статья дает представление о том, что такое WMS сервис, а также рассказывается о том, как выполнить настройку WMS на базе MapServer.
- Подключаемся к MapServer WMS с помощью Leaflet - рассказывается о том, как с помощью js-библиотеки Leaflet создать своего WMS клиента.
- Программируем с использованием GDAL/OGR на C# (часть 1). Установка и настройка - описан процесс настройки консольного проекта на C# для использования с библиотекой GDAL/OGR.
- Программируем с использованием GDAL/OGR на C# (часть 2). Получение пространственных данных - рассказывается о методах доступа к источнику геоданных и получение атрибутивной информации из объектов, а также их геометрии.
Предполагая, что читатель обладает всеми вышеуказанными знаниями, можно приступать к разработке нашей первой ГИС.
Несколько слов о проекте VerySimpleGis
VerySimpleGis - это обычное ASP.NET MVC веб-приложение, которое позволяет по клику мыши на картографическом объекте получить о нем определенную информацию. В данном случае я буду в качестве геоданных использовать слой стран мира, который будет храниться в БД MS SQL Server в таблице tm_world_borders_simpl в виде объектов типа geometry. При клике мыши на объекте с помощью jQuery ajax посылается запрос на сервер, содержащий координаты места клика, а серверный код с помощью библиотеки GDAL/OGR определяет объект по заданным координатам и возвращает объект, содержащий единственное поле - название страны. При создании приложения я использовал ручную установку GDAL/OGR.
Начальная настройка проекта VerySimpleGis
Первым делом запускаем Visual Studio 2017 (я использую Visual Studio 2017 Community, которая является бесплатной для учащихся, разработчиков открытого ПО и отдельных разработчиков), далее создаем новый проект по типу ASP.NET Web Application (.NET Framework) и выберем вид проекта - MVC, как показано на скриншотах ниже.
Я использую данный тип проекта, поскольку подключаемые библиотеки GDAL/OGR опираются именно на платформу .NET Framework, в то время как ASP.NET Core Web Application (.NET Core) базируется на новой среде выполнения .NET Core Runtime. При создании проекта по типу ASP.NET Core Web Application (.NET Framework) и смене целевой платформы на x64, у меня возникли проблемы с запуском веб-приложения, а именно - на этапе запуска веб-хоста методом host.Run() возникло исключение: BadImageFormatException: Была сделана попытка загрузить программу, имеющую неверный формат. (Исключение из HRESULT: 0x8007000B). Похоже на то, что пытающийся запуститься веб-сервер Kestrel, является x86 приложением.
Следующим шагом в свойствах проекта на вкладке Build меняем целевую платформу на x64. Далее, добавляем в проект ссылки на библиотеки GDAL/OGR с помощью контекстного меню проекта Add Reference. О процессе установки этой библиотеки посвящена статья Программируем с использованием GDAL/OGR на C# (часть 1). Установка и настройка.
О серверной части
В корне проекта в директории Models создадим файл GisAccess.cs, который будет содержать одноименный класс. Содержимое данного файла будет иметь следующий вид:
using OSGeo.OGR;
using System;
using System.Globalization;
using VerySimpleGis.Models.Entities;
namespace VerySimpleGis.Models
{
public class GisAccess : IDisposable
{
private const int GEOM_FIELD_IDX = 0;
private const int COUNTRY_ATTR_NAME_IDX = 4;
private NumberFormatInfo nfi = new NumberFormatInfo() { CurrencyDecimalSeparator = "." };
protected bool disposed = false;
protected DataSource ds;
protected Layer layer;
public GisAccess(string dataSource)
{
ds = Ogr.Open(dataSource, 0);
if (ds == null)
{
throw new GisException("DataSource object is null");
}
if (ds.GetLayerCount() == 0)
{
ds.Dispose();
ds = null;
throw new GisException("Datasource has no layers");
}
layer = ds.GetLayerByIndex(0);
}
public Country GetCountryByCoordinates(double x, double y)
{
x = normalize(x);
string wkt = $"POINT({x.ToString(nfi)} {y.ToString(nfi)})";
Geometry filter = Ogr.CreateGeometryFromWkt(ref wkt, layer.GetLayerDefn().GetGeomFieldDefn(GEOM_FIELD_IDX).GetSpatialRef());
layer.SetSpatialFilter(filter);
Feature feature = layer.GetNextFeature();
if (feature != null)
{
return new Country() { Name = feature.GetFieldAsString(COUNTRY_ATTR_NAME_IDX) };
}
return null;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// Защищенная реализация метода Dispose
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing)
{
// Очистить все управляемые объекты
if (layer != null)
{
layer.Dispose();
layer = null;
}
if (ds != null)
{
ds.Dispose();
ds = null;
}
}
// Освободить все неуправляемые ресурсы
// ...
disposed = true;
}
private double normalize(double x)
{
x = x % 360;
if (x > 180)
{
x -= 360;
}
else if (x < -180)
{
x += 360;
}
return x;
}
~GisAccess()
{
Dispose(false);
}
}
}
В конструкторе класса GisAccess происходит подключение к источнику пространственных данных с помощью метода Ogr.Open(dataSource, 0). Об этом методе я рассказывал в статье Программируем с использованием GDAL/OGR на C# (часть 2). Получение пространственных данных. Далее, выполнив проверку переменной ds на null, проверяем, есть ли слои в источнике данных, если слои существуют, то присваиваем ссылку на самый первый слой переменной layer.
Класс GisAccess реализует интерфейс IDisposable и стандартный шаблон Dispose, который хорошо описан в MSDN.
Метод GetCountryByCoordinates(double x, double y) осуществляет получение страны по указанным координатам. Вначале мы, в случае необходимости, корректируем координату долготы, чтобы она лежала в пределах от -180° до 180°. В этом нам помогает метод normalize(double x). В следующей строке формируется WKT представление точки, в которой нужно определить страну. Далее, по ссылке на WKT строку и по заданной системе координат создаем объект геометрии с помощью метода Ogr.CreateGeometryFromWkt(ref wkt, layer.GetLayerDefn().GetGeomFieldDefn(GEOM_FIELD_IDX).GetSpatialRef()). Метод layer.GetLayerDefn() позволяет получить объект класса FeatureDefn, содержащий информацию о слое. Метод GetGeomFieldDefn(GEOM_FIELD_IDX) предоставляет доступ к объекту, который содержит пространственную информацию по указанному индексу поля. Здесь я предполагаю, что источник данных содержит только одно поле геоданных. А методом GetSpatialRef() уже получаем ссылку на объект системы координат нашего слоя. В следующей строке layer.SetSpatialFilter(filter) устанавливается пространственный фильтр для слоя. Это приведет к тому, что метод GetNextFeature() будет возвращать только те объекты, которые пересекают объект геометрии, установленный в фильтре. А далее, собственно, мы получаем объект, который попадает в наш фильтр и, если такой объект существует, то создаем объект страны, в название мы заносим строку из атрибутивной информации объекта.
Класс Country располагается в файле Model/Entities/Country.cs и имеет очень простой вид:
namespace VerySimpleGis.Models.Entities
{
public class Country
{
public string Name { get; set; }
}
}
Также в проекте задействован использован класс исключения GisException, расположенный в каталоге Models.
using System;
namespace VerySimpleGis.Models
{
public class GisException : Exception
{
public GisException() { }
public GisException(string message) : base(message) { }
public GisException(string message, Exception innerException) : base(message, innerException) { }
}
}
Прежде чем рассмотреть код класса контроллера, давайте добавим в секцию appSettings файла Web.config следующую строку:
<add key="SpatialDataSource" value="MSSQL:server=.\SQLEXPRESS;uid=sa;pwd=12345;database=MySpatialDb;Integrated Security=false;tables=tm_world_borders_simpl(ogr_geometry)" />
В случае необходимости, измените значения параметров uid (логин пользователя для подключения к БД), pwd (пароль пользователя для подключения к БД) и server (имя вашего экземпляра MS SQL).
А теперь в каталоге Controllers создайте файл HomeController.cs, где мы разместим одноименный файл со следующим содержимым:
using System.Configuration;
using System.Web.Mvc;
using VerySimpleGis.Models;
using VerySimpleGis.Models.Entities;
namespace VerySimpleGis.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public JsonResult GetGeoData(double[] latlng)
{
using (GisAccess gis = new GisAccess(ConfigurationManager.AppSettings["SpatialDataSource"]))
{
Country country = gis.GetCountryByCoordinates(latlng[1], latlng[0]);
return country != null ? Json(country) : Json(new object());
}
}
}
}
Метод Index() просто вызывает метод View() без параметров, поэтому инфраструктура MVC будет использовать представление по умолчанию для данного метода контроллера.
Метод GetGeoData(double[] latlng) будет вызываться посредством ajax из js-файла. В методе извлекается из секции пользовательских настроек строка подключения к источнику данных и передается в конструктор класса GisAccess. Благодаря тому, что данный класс реализует интерфейс IDisposable, мы можем применить блок using. При выходе из блока автоматически будет вызван метод Dispose, который освободит необходимые ресурсы. В блоке, собственно, происходит получение объекта страны по указанным координатам и возвращение результата в виде объекта JsonResult.
Теперь приступим к настройке и программированию веб-ГИС со стороны клиента.
О клиентской части
Вначале добавим в наш проект необходимые библиотеки. Для этого в обозревателе решений (Solution Explorer) добавим в каталог Content новый каталог js, в котором будут храниться наши javascript-файлы, а также сторонние файлы javascript-библиотек. В созданном каталоге js создадим еще один каталог с именем leaflet, куда мы распакуем содержимое архива с одноименной библиотеки, которую можно скачать либо с официального сайта Leaflet, либо с моего ресурса. В каталоге Content/js создадим новый файл с именем main.js и поместим в него следующее содержимое:
$(function () {
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 mymap = L.map('map', {
minZoom: 2,
maxBounds: [[-90, -180],
[90, 180]]
}).setView([37.41, 8.82], 4);
var osm = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(mymap);
var wmsLayer = L.tileLayer.wms('http://localhost:8888/cgi-bin/mapserv.exe?map=../htdocs/mydemo/wms_ol.map&', {
layers: 'world_poly',
crs: L.CRS.EPSG4326,
version: '1.1.1',
opacity: 0.5
}).addTo(mymap);
mymap.on('click', function (e) {
$.ajax({
url: App.ROOT + 'Home/GetGeoData',
data: JSON.stringify([e.latlng.lat, e.latlng.lng])
}).done(function (data) {
showInfo(data);
});
});
}
function showInfo(obj) {
alert(typeof (obj.Name) != 'undefined' ? obj.Name : 'Нет объекта!');
}
});
Как можно заметить, на стороне клиента мы задействовали популярную библиотеку jQuery. После окончания загрузки документа вызывается функция по настройке объекта Ajax для выполнения асинхронных запросов к серверу (функция initAjax()), а также инициализация и отображение карты (функция initMap()). initAjax() устанавливает для всех ajax-запросов http-метод запросов типа POST, таймаут запроса в 30 сек., тип данных в виде json и заголовок запроса contentType в значение application/json; charset=utf-8. Также в функции настраивается глобальный обработчик ошибок.
Код функции initMap() во многом заимствован из предыдущего примера с Leaflet и описан в статье Подключаемся к MapServer WMS с помощью Leaflet. Новыми особенностями здесь являются только установка во втором аргументе L.map параметров minZoom и maxBounds, которые определяют минимальный масштаб и границы карты соответственно и подключение обработчика клика на карте. В обработчике с помощью библиотеки jQuery выполняется ajax-запрос на сервер для определения страны по координатам. В функции обратного вызова отображаем название страны благодаря вызову функции showInfo, в которую передается полученный от сервера объект страны. Если у объекта отсутствует свойство Name, то это означает, что по указанным координатам нет объекта.
Данный код является сердцем клиентской части, и нам остается только рассмотреть файл стилей Content/css/style.css и файл представления Views/Home/Index.cshtml. style.css имеет вид:
html, body {
margin: 0;
padding: 0;
height: 100%;
}
#info {
position: absolute;
z-index: 500;
background-color: #cce6ff;
right: 0;
}
#map {
height: 100%;
cursor: auto !important;
}
Стоит отметить строку cursor: auto !important;, которая устанавливает курсор мыши в виде указателя.
И, наконец, разметка файла представления Index.cshtml имеет следующий вид:
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/js/leaflet/leaflet.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/leaflet/leaflet.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>
Как можно заметить, здесь я решил не загружать библиотеку jQuery, а подключить ее из CDN. Также в секции <script> я присваиваю свойству ROOT объекта App строку, содержащую путь к корню веб-приложения.
Итоги
На данном этапе можно собирать приложение и запускать нашу первую веб-ГИС! Конечно, многие вещи при разработке архитектуры приложения были упрощены. В реальном веб-приложении вы скорей всего будете использовать какой-либо IoC-контейнер, например, Ninject. Кроме того, на клиенте вы, возможно, захотите подключить еще какой-нибудь framework и т.д. Однако, цель статьи - показать основные этапы в разработке ГИС, которая может обеспечить доступ к пользовательским картам, которые хранятся на собственном сервере в том или ином векторном формате.
Также вы можете загрузить с github исходники проекта VerySimpleGis.
Файлы для загрузки:
- Библиотека GDAL/OGR x64 - архив с библиотекой GDAL/OGR x64 версии 2.1.3.
- Leaflet v1.0.3 - архив с библиотекой Leaflet версии 1.0.3.
- TM_WORLD_BORDERS_SIMPL-0.3.zip (shape) - территориальные границы стран (полигоны).