26.06.2018 Программируем с использованием GDAL/OGR на C# (часть 3). Обновление векторных данных
Что нужно знать?
После длительного перерыва я вновь возвращаюсь к нашей замечательной ГИС-библиотеке GDAL/OGR, которая предоставляет единый интерфейс для доступа к различным хранилищам геоданных. Перед прочтением данной статьи я рекомендую ознакомиться с предыдущим материалом по этой теме: Установка и настройка GDAL/OGR и Получение пространственных данных с помощью GDAL/OGR. Как и в предыдущей статье по данной теме, я буду опираться на источник геоданных MS SQL Server и мою базу данных MySpatialDb, содержащую таблицу tm_world_borders_simpl. О том, как создать необходимую БД и таблицу, рассказано в статье Подключаем MapServer к MS SQL. В одной из следующих статей я планирую дополнить наше учебное веб-ГИС приложение VerySimpleGis функциями редактирования, которые сегодня изучим. А сейчас приступаем!
Подготавливаем ГИС-проект
Создадим консольное приложение GDALDemoUpdate на базе платформы .NET Framework как показано на изображении ниже:

Конечно же путь к решению и версию платформы вы можете выбрать на ваш вкус. Далее, воспользуемся диспетчером пакетов NuGet (Меню "Средства" --> "Диспетчер пакетов NuGet" --> "Управление пакетами NuGet для решения") и установим пакет Gdal.Native:
Чтобы воспользоваться всеми преимуществами архитектуры x64, в свойствах проекта я укажу целевую платформу x64.

Сохраняем наши изменения и для проверки работоспособности изменим наш файл Program.cs таким образом, чтобы он выглядел так, как показано ниже.
using System;
namespace GDALDemoUpdate
{
class Program
{
static void Main(string[] args)
{
GdalConfiguration.ConfigureOgr();
Console.ReadLine();
}
}
}
Поскольку в данной статье я буду использовать только доступ к векторным данным, то строкой GdalConfiguration.ConfigureOgr(); я регистрирую все доступные драйвера для доступа именно к векторным данным. При этом метод ConfigureOgr() осуществяет уже знакомый нам по предыдущим статьям библоитечный вызов Gdal.AllRegister();.
Обновляем геометрию пространственных объектов
Перед тем как мы приступим, я рекомендую сделать резервную копию БД MySpatialDb, т.к. некоторые данные в таблице tm_world_borders_simpl будут изменены. Хотя, если подобный факт вас не смущает, то бэкап можно и не делать. :)
Обновление координат объекта и атрибутивной информации
Сперва приведу текст программы, а затем уже подробно расскажу, что же в ней происходит. Итак, листинг в студию!
using OSGeo.OGR;
using System;
using System.Globalization;
namespace GDALDemoUpdate
{
class Config
{
public string DataSource { get; set; }
public string LayerName { get; set; }
public double Lng { get; set; }
public double Lat { get; set; }
public static Config FromArgsArray(string[] args, NumberFormatInfo nfi)
{
string dataSource = args[0];
string layerName = args[1];
string[] coords = args[2].Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
double lng = 0, lat = 0.0;
if (!(coords.Length >= 2 && double.TryParse(coords[0], NumberStyles.Number, nfi, out lng) &&
double.TryParse(coords[1], NumberStyles.Number, nfi, out lat)))
{
throw new ApplicationException("Lng & Lat coords not specifed");
}
return new Config()
{
DataSource = dataSource,
LayerName = layerName,
Lat = lat,
Lng = lng
};
}
}
class Program
{
private const int GEOM_FIELD_IDX = 0;
private const string COUNTRY_ATTR_NAME = "name";
private const double COORDS_OFFSET = 20.0;
private static NumberFormatInfo nfi = new NumberFormatInfo() { CurrencyDecimalSeparator = "." };
static void Main(string[] args)
{
if (args.Length < 3)
{
Console.WriteLine("Usage: GDALDemoUpdate <datasource> <layername> <lng;lat>");
PauseAndExit(0);
}
Config config = null;
try
{
config = Config.FromArgsArray(args, nfi);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
PauseAndExit(1);
}
GdalConfiguration.ConfigureOgr();
DataSource ds = Ogr.Open(config.DataSource, 1);
if (ds == null)
{
Console.WriteLine("DataSource open failed");
PauseAndExit(1);
}
Layer layer = ds.GetLayerByName(config.LayerName);
if (layer == null)
{
Console.WriteLine("Layer not found");
ds.Dispose();
PauseAndExit(1);
}
Feature feature = GetFeatureByCoordinates(layer, config.Lng, config.Lat);
if (feature != null)
{
Console.WriteLine($"Modifying: {feature.GetFieldAsString(COUNTRY_ATTR_NAME)}");
UpdateFeature(feature);
layer.SetFeature(feature);
ds.SyncToDisk();
feature.Dispose();
}
else
{
Console.WriteLine($"No feature found");
}
layer.Dispose();
ds.Dispose();
PauseAndExit(0);
}
static Feature GetFeatureByCoordinates(Layer layer, double x, double y)
{
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();
filter.Dispose();
return feature;
}
static void UpdateFeature(Feature feature)
{
string newName = $"{feature.GetFieldAsString(COUNTRY_ATTR_NAME)} is so amazing!";
Console.WriteLine($"New name: {newName}");
feature.SetField(COUNTRY_ATTR_NAME, newName);
UpdateGeometry(feature.GetGeometryRef(), " ");
}
static void UpdateGeometry(Geometry geom, string padding)
{
Console.WriteLine($"{padding}GeometryName:{geom.GetGeometryName()}, GeometryType:{geom.GetGeometryType()}");
int pointCount = geom.GetPointCount();
for (int j = 0; j < pointCount; j++)
{
double[] points = new double[3];
geom.GetPoint(j, points);
for (int i = 0; i < points.Length - 1; i++)
{
points[i] += COORDS_OFFSET;
Console.WriteLine($"{padding}New coords: [x:{points[0]}, y:{points[1]}]");
}
geom.SetPoint(j, points[0], points[1], points[2]);
}
int geomCount = geom.GetGeometryCount();
for (int i = 0; i < geomCount; i++)
{
UpdateGeometry(geom.GetGeometryRef(i), padding + " ");
}
}
static void PauseAndExit(int errCode)
{
Console.ReadLine();
Environment.Exit(errCode);
}
}
}
В самом начале пространства имен программы объявлен класс Config, представляющий собой набор параметров, которые в дальнейшем будут использоваться в работе программы. Вот эти параметры:
- DataSource - источник данных. В нашем случае это будет строка подключения к MS SQL Server;
- LayerName - имя пространственного слоя. В нашем случае это будет таблица tm_world_borders_simpl;
- Lng - Долгота;
- Lat - Широта. По данным координатам будет предпринята попытка поиска объекта, и, если объект будет найден, то он соответствующим образом будет модифицирован.
Также в классе определен статический метод FromArgsArray, который позволяет получить экземпляр данного класса из набора параметров командной строки.
Далее, в классе Program, определены некоторые константы, назначение которых я объясню на этапе их непосредственного использования. Собственно, в начале программы происходит проверка количества аргументов и, если их число менее необходимого, то на консоль выводится сообщение о правильном запуске программы. Если же количество аргументов больше необходимого, то происходит попытка получить экземпляр класса Config. Если все прошло успешно, то далее вызывается метод GdalConfiguration.ConfigureOgr(), который был сгенерирован в процессе установки NuGet-пакета GDAL.Native. Этот метод регистрирует все доступные драйвера доступа к векторным данным, а также в случае, если программа запущена в отладочном режиме, выводит на консоль список доступных драйверов.
Далее происходит попытка получить доступ к источнику геоданных с помощью вызова метода Ogr.Open(config.DataSource, 1). Обратите внимание на второй параметр метода Open. Согласно документации значение 0 определяет доступ только на чтение, а 1 - на чтение и запись. Поскольку мы собираемся изменять геоданные, то мы передаем 1. В случае успешного доступа к источнику геоданных, мы пытаемся получить слой по его имени с помощью метода ds.GetLayerByName(config.LayerName) экземпляра класса DataSource. В случае успеха, предпринимается попытка получить пространственный объект в указанном слое по координатам, которые были получены из командной строки, с помощью собственного метода GetFeatureByCoordinates(layer, config.Lng, config.Lat).
Рассмотрим этот метод более подробно. Вначале, по координатам мы создаем WKT-строку (Well-Known Text - язык разметки для представления векторных данных), которая описывает точечный объект. Следующей строкой Geometry filter = Ogr.CreateGeometryFromWkt(ref wkt, layer.GetLayerDefn().GetGeomFieldDefn(GEOM_FIELD_IDX).GetSpatialRef()); мы создаем объект типа Geometry, который будет выступать в роли фильтра для поиска нужного пространственного объекта - Feature. В метод CreateGeometryFromWkt мы первым аргументом по ссылке передаем WKT-строку, а во втором - объект пространственной привязки (объект, описывающий систему координат нашего слоя). Чтобы получить этот объект, вначале мы получаем объект типа FeatureDefn, который содержит информацию о схеме слоя, с помощью метода GetLayerDefn(). Далее, с помощью вызова метода GetGeomFieldDefn(GEOM_FIELD_IDX) мы получаем объект, который содержит характеристики пространственных данных по указанному индексу. Индекс необходим, т.к. полей в пространственной таблице может быть несколько. Но в нашем случае поле, содержащее векторные данные только одно, поэтому указываем нулевой индекс, который хранится в константе GEOM_FIELD_IDX. Ну и наконец, вызов GetSpatialRef() позволяет получить объект, описывающий пространственную привязку. Строка layer.SetSpatialFilter(filter); устанавливает пространственный фильтр, который будет применятся для поиска объектов. В нашем случае - это объект типа точки. Вызов layer.GetNextFeature() позволяет получить очередной объект, который попадает в установленный пространственный фильтр. Нас интересует только единственный объект, который мы и возвращаем из метода, предварительно очистив ресурсы созданного фильтра.
Если объект найден, то затем вызывается метод UpdateFeature(feature), в котором и происходит собственно обновление координат и атрибутивной информации. Вначале мы модифицируем поле name, вызывая метод feature.SetField(COUNTRY_ATTR_NAME, newName). Константа COUNTRY_ATTR_NAME содержит название поля, которое необходимо установить, а переменная newName - новое значение.
Затем вызывается рекурсивная функция UpdateGeometry(feature.GetGeometryRef(), " "), в которую передается объект геометрии и начальный отступ для структурированного вывода информации на консоль. Внутри функции мы получаем кол-во точек у объекта геометрии, выполняем итерацию по ним, получаем координаты каждой точки по ее индексу в массив points строкой geom.GetPoint(j, points) и к каждой точке добавляем смещение, заданное константой COORDS_OFFSET. Таким образом, мы смещаем объект на северо-восток. В конце каждой итерации мы обновляем координаты каждой точки вызовом geom.SetPoint(j, points[0], points[1], points[2]). Стоит отметить, что мы модифицируем только координаты широты и долготы. Координату Z (высоту) мы не трогаем. Также внутри метода мы получаем количество дочерних объектов геометрии и для них рекурсивно вызываем этот же метод. Таким образом, каждая координата объекта будет смещена на 20° к северу и к востоку.
После того, как имя объекта и каждая координата была обновлена, необходимо ОБЯЗАТЕЛЬНО вызвать метод layer.SetFeature(feature), иначе изменения не сохранятся. Когда я впервые столкнулся с задачей обновления координат, то я не вызывал вышеуказанный метод, предполагая, что библиотека отслеживает изменения геометрии и атрибутивной информации у объекта. В результате, после выполнения команды ds.SyncToDisk(), которая выполняет фиксацию изменений в источнике данных, я обнаруживал, что никаких изменений не происходило. Поэтому не забывайте про вызов layer.SetFeature(feature)! На самом деле, я даже экспериментировал и закомментировал строку //ds.SyncToDisk(), все равно изменения были записаны в хранилище даже до вызова методов освобождения ресурсов, т.е. фактически сразу же после выполнения строки layer.SetFeature(feature). Но я все же рекомендую вызывать метод SyncToDisk.
Ну и далее, вызываются методы Dispose, которые освобождают ресурсы, и происходит собственно завершение программы.
Теперь давайте взглянем на наш слой до обновления с помощью программы QGIS. В качестве объекта для демонстрации механизмов обновления я выбрал остров Мадагаскар, поэтому именно он выделен на скриншоте ниже.

А теперь давайте вызовем нашу программу из командной строки, передав ей необходимые параметры:
GDALDemoUpdate "MSSQL:server=.\SQLEXPRESS;uid=sa;pwd=12345;database=MySpatialDb;Integrated Security=false;tables=tm_world_borders_simpl(ogr_geometry)" tm_world_borders_simpl 47;-18
Строку подключения к СУБД MS SQL Server модифицируйте согласно вашим параметрам. Координаты (долгота: 47° и широта: -18°) я подобрал таким образом, чтобы они попадали в остров Мадагаскар. После того как программа отработает, давайте еще раз взглянем на наш слой в QGIS.

Мы видим, что координаты объекта изменились, а также изменилось и название объекта.
Давайте теперь попробуем заменить наш источник данных файлом TM_WORLD_BORDERS_SIMPL-0.3.shp, из которого мы ранее и загружали данные в нашу базу. Итак, в моем случае вызов программы будет выглядеть следующим образом:
GDALDemoUpdate "D:\Stas\mapserver\ms4w\Apache\htdocs\mydemo\shp\TM_WORLD_BORDERS_SIMPL-0.3.shp" TM_WORLD_BORDERS_SIMPL-0.3 47;-18
В вашем случае, скорей всего, потребуется подкорректировать путь к shape-файлу. А сам результат работы программы будет идентичным. Таким образом, мы можем с легкостью менять источники геоданных, совершенно не внося никаких изменений в код программы.
Файлы для загрузки:
Предыдущие статьи по теме разработки ГИС с использованием GDAL/OGR: