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 как показано на изображении ниже:

Создание консольного приложения GDALDemoUpdate
Создание консольного приложения GDALDemoUpdate

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

Установка NuGet-пакета Gdal.Native
Установка NuGet-пакета Gdal.Native

Чтобы воспользоваться всеми преимуществами архитектуры x64, в свойствах проекта я укажу целевую платформу x64.

Изменение целевой платформы на 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. В качестве объекта для демонстрации механизмов обновления я выбрал остров Мадагаскар, поэтому именно он выделен на скриншоте ниже.

Вид слоя tm_world_borders_simpl перед обновлением в программе QGIS
Вид слоя tm_world_borders_simpl перед обновлением в программе 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 после обновления в программе QGIS
Вид слоя tm_world_borders_simpl после обновления в программе 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: