Pull to refresh

Редактирование пространственных данных в Leaflet

Reading time 7 min
Views 19K
Несмотря на развитие картографических веб-фреймворков, редактирование векторных географических данных всё ещё, по большей части, происходит в настольных приложениях, но пора бы уже переходить к редактированию в браузерах.

Для отрисовки веб-карт существует несколько открытых библиотек, например, OpenLayers и Leaflet. Довольно давно наш выбор пал на Leaflet и мы продолжаем его активно использовать при реализации проектов. Для редактирования геоданных хотелось бы использовать его же и, при этом, иметь возможность интегрироваться с существующими хранилищами пространственных данных.

Для достижения последней цели как правило используются ГИС-сервера (geoserver, mapserver), которые умеют публиковать большое количество разнообразных форматов данных по стандартам OGC. Так, WMS протокол прекрасно справляется с функцией визуализации готовой карты, но не предполагает функции редактирования, для которой резонно использовать WFS-протокол с возможностью изменения данных. Запросы к WMS возвращают уже отрисованные тайлы — картинки, а к WFS — сырую информацию, «исходный код» за этими тайлами. Leaflet поддерживает модули расширения, соответственно, можно поискать готовый компонент, либо написать свой. Т.к. поиск готовых модулей для Leaflet удовлетворяющих результатов не дал, мы приступили к собственной реализации.

По статистике запросов на leaflet.uservoice.com понятно, что данный модуль интересен не только нам.

Начнем с описания WFS-T и для чего он используется


Стандарт OGC Web Feature Service позволяет запрашивать и редактировать (в случае с припиской “-T" — transaction) пространственные данные с помощью запросов к серверу. Функции CRUD стандартом разделены на запросы GetFeature для чтения и Transaction для остальных.

Для взаимодействия клиента и сервера могут использоваться два способа: первый использует XML и POST запросы, второй — пары ключ\значение и GET запросы.

Получить данные можно с помощью GET запросов вида:
%WFSServerURL%?service=WFS&version=1.0.0&request=GetFeature&typeName=osm_perm_region:perm_water_polygon&maxFeatures=50&outputFormat=application/json,
service=WFS тип сервиса, всегда одинаковый
version=1.0.0 версия стандарта WFS. На данный момент существует 3 версии: 1.0.0, 1.1.0, 2.0.0. Мы будем использовать версию 1.1.0, т.к. 2.0.0 реализована не всеми производителями серверной части
request=GetFeature тип запроса
typeName=osm_perm_region:perm_water_polygon тип опубликованных WFS-сервером данных
maxFeature=50 количество объектов, которое будет возвращено сервером
outputFormat=application/json формат данных, которые будут возвращены сервером. Стандартом регламентируется только один формат данных — GML, но некоторые реализации могут использовать и отличные от GML, например, geoserver умеет отдавать данные в geoJson

Для создания\изменения\удаления данных в некоторых случаях также можно воспользоваться парами ключ-значение в GET запросе, но выполнение POST запроса на %WFSServerURL% с данными в формате XML даёт больше возможностей.
Примеры изменения объектов:

Cоздание
<wfs:Transaction service="WFS" version="1.0.0"
  xmlns:wfs="http://www.opengis.net/wfs"
  xmlns:topp="http://www.openplans.org/topp"
  xmlns:gml="http://www.opengis.net/gml"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.0.0/WFS-transaction.xsd http://www.openplans.org/topp http://localhost:8080/geoserver/wfs/DescribeFeatureType?typename=topp:tasmania_roads">
  <wfs:Insert>
    <topp:tasmania_roads>
      <topp:the_geom>
        <gml:MultiLineString srsName="http://www.opengis.net/gml/srs/epsg.xml#4326">
          <gml:lineStringMember>
            <gml:LineString>
              <gml:coordinates decimal="." cs="," ts=" ">
494475.71056415,5433016.8189323 494982.70115662,5435041.95096618
              </gml:coordinates>
            </gml:LineString>
          </gml:lineStringMember>
        </gml:MultiLineString>
      </topp:the_geom>
      <topp:TYPE>alley</topp:TYPE>
    </topp:tasmania_roads>
  </wfs:Insert>
</wfs:Transaction>


Обновление
<wfs:Transaction service="WFS" version="1.0.0"
  xmlns:topp="http://www.openplans.org/topp"
  xmlns:ogc="http://www.opengis.net/ogc"
  xmlns:wfs="http://www.opengis.net/wfs"
  xmlns:gml="http://www.opengis.net/gml"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.0.0/WFS-transaction.xsd">
  <wfs:Update typeName="topp:tasmania_roads">
    <wfs:Property>
      <wfs:Name>the_geom</wfs:Name>
      <wfs:Value>
        <gml:MultiLineString srsName="http://www.opengis.net/gml/srs/epsg.xml#4326">
          <gml:lineStringMember>
              <gml:LineString>
                <gml:coordinates>500000,5450000,0 540000,5450000,0</gml:coordinates>
              </gml:LineString>
          </gml:lineStringMember>
        </gml:MultiLineString>
      </wfs:Value>
    </wfs:Property>
    <ogc:Filter>
      <ogc:FeatureId fid="tasmania_roads.1"/>
    </ogc:Filter>
  </wfs:Update>
</wfs:Transaction>


Удаление
<wfs:Transaction service="WFS" version="1.0.0"
  xmlns:cdf="http://www.opengis.net/cite/data"
  xmlns:ogc="http://www.opengis.net/ogc"
  xmlns:wfs="http://www.opengis.net/wfs"
  xmlns:topp="http://www.openplans.org/topp">
  <wfs:Delete typeName="topp:tasmania_roads">
    <ogc:Filter>
      <ogc:PropertyIsEqualTo>
        <ogc:PropertyName>topp:TYPE</ogc:PropertyName>
        <ogc:Literal>alley</ogc:Literal>
      </ogc:PropertyIsEqualTo>
    </ogc:Filter>
  </wfs:Delete>
</wfs:Transaction>


Для описания пространственных данных используется GML. В этом формате по умолчанию сервер отдаёт данные (при незаданном флаге outputFormat) и только в нём принимаются запросы при изменении данных. Например, точка [0, 0] в GML может быть представлена следующим образом:

<gml:Point srsName="http://www.opengis.net/def/crs/EPSG/0/4326">
    <gml:pos srsDimension="2">0.0 0.0</gml:pos>
</gml:Point>

Для ограничения запросов по удалению и изменению данных используются фильтры — ещё один из стандартов OGC. Вначале будет достаточно — GmlObjectId, он используется для обновления\удаления объектов. В дальнейшем потребуются и остальные фильтры.

Пример для ID=1:

<ogc:Filter>
    <ogc:GmlFeatureId gml:id=1/>
</ogc:Filter>

Создание плагина Leaflet


Как было сказано выше, у Leaflet хорошо продуманная модульная инфраструктура, поэтому надо уметь писать эти самые модули. Вводная по созданию плагина есть на сайте Leaflet, и пример реализации ILayer. Также есть несколько статей на хабре.

Нам потребуется создать свой слой, который при загрузке будет получать данные от сервиса и отрисовывать их. Для чтения WFS было найдено несколько плагинов, все они наследовались от L.GeoJSON слоя и вычитывали данные сразу в нужном формате. Но стандарт не обязывает производителей серверной части предоставлять данные в geoJson (я видел такую возможность только для geoserver'a), обязательным же является формат GML. Подсмотрев, как чтение сделано в OpenLayers, мы взяли идею оттуда: для чтения использовать отдельный класс, который умеет понимать нужный формат. Как и L.GeoJSON, свою реализацию мы унаследовали от L.FeatureGroup. Было реализовано 2 формата чтения GML и GeoJSON

Получение данных


выполяется AJAX запросом и отдается на обработку классу чтения, здесь мы просто передаем трансформацию geoJson в маркеры\полигоны\полилинии в недра самого Leaflet:

var layers = [];
var geoJson = JSON.parse(response.rawData);
for (var i = 0; i < geoJson.features.length; i++) {
   var layer = L.GeoJSON.geometryToLayer(geoJson.features[i], options.pointToLayer || null, options.coordsToLatLng, null);
   layer.feature = geoJson.features[i];
   layers.push(layer);
}
return layers;

Или парсим GML — на выходе те же маркеры\полигоны\полилинии:

var layers = [];
var xmlDoc = L.XmlUtil.parseXml(rawData);
var featureCollection = xmlDoc.documentElement;
var featureMemberNodes = featureCollection.getElementsByTagNameNS(L.XmlUtil.namespaces.gml, 'featureMember');
for (var i = 0; i < featureMemberNodes.length; i++) {
  var feature = featureMemberNodes[i].firstChild;
  layers.push(this.processFeature(feature));
}

var featureMembersNode = featureCollection.getElementsByTagNameNS(L.XmlUtil.namespaces.gml, 'featureMembers');
if (featureMembersNode.length > 0) {
  var features = featureMembersNode[0].childNodes;
  for (var j = 0; j < features.length; j++) {
    var node = features[j];
    if (node.nodeType === document.ELEMENT_NODE) {
      layers.push(this.processFeature(node));
    }
  }
}

return layers;

Редактирование объектов


Написаны фукнции, которые, взаимодействуя с плагинами визуального редактирования объектов Leaflet (leaflet.draw, Leaflet.Editable), запоминают произведенные изменения. После того как редактирование закончено надо вызывать метод save(), который сформирует GML описание изменений — элемент “wfs:Transaction", и, на каждый из измененных объектов, соответствующее действие (Action): wfs:Insert, wfs:Update, wfs:Delete. После этого выполняется AJAX запрос.

Пример подписки на события плагина Leaflet.Editable:

map.on('editable:created', function (e) {
   wfst.addLayer(e.layer);
});

map.on('editable:editing', function (e) {
   wfst.editLayer(e.layer);
});

map.on('editable:delete', function (e) {
   wfst.removeLayer(e.layer);
});

Для каждого примитива Leaflet (Marker, Polyline, Polygon и др.) была написана функция его перевода в описание геометрии GML, например, для маркера она выглядит так:

L.Marker.include({
   toGml: function (crs) {
       var node = L.XmlUtil.createElementNS('gml:Point', {srsName: crs.code});
       node.appendChild(L.GMLUtil.posNode(L.Util.project(crs, this.getLatLng())));
       return node;
   }
});

Примеры использования


Только чтение


var map = L.map('map').setView([0, 0], 2);

var boundaries = new L.WFS({
    url: 'http://demo.opengeo.org/geoserver/ows',
    typeNS: 'topp',
    typeName: 'tasmania_state_boundaries',
    crs: L.CRS.EPSG4326,
    style: {
        color: 'blue',
        weight: 2
    }
}).addTo(map)
        .on('load', function () {
            map.fitBounds(boundaries);
        })

ссылка

Редактирование


var wfst = new L.WFS.Transaction({
    url: 'http://myserver/geoserver/ows',
    typeNS: 'myns',
    typeName: 'POIPOINT',
    style: {
        color: 'blue',
        weight: 2
    }
}).addTo(map).once('load', function () {
            map.fitBounds(wfst);
            wfst.enableEdit();
        });

map.on('editable:created', function (e) {
    wfst.addLayer(e.layer);
});

map.on('editable:editing', function (e) {
    wfst.editLayer(e.layer);
});

 L.easyButton('fa-save', function () {
     wfst.save();
 }, 'Save changes');

ссылка

В планах по развитию плагина


— Миграция на Leaflet 0.8 (изменились внутренности Multi-классов, да и полилинии с полигонами обзавелись ring’s);
— Возможность использования разных версий WFS;
— Поддержка других пунктов стандарта OGC Filter Encoding.

Исходники и разработка


Проект живет на GitHub. Для автоматизации используется Grunt. Для тестирования используется связка karma+mocha+chai+sinon. Желающие принять участие — добро пожаловать.

Ссылки


  1. Стандарты OGC: WMS, WFS, GML, Filter Encoding
  2. Описание некоторых стандартов на русском — live.osgeo.org
  3. leaflet.wfs-t — нашедшийся wfst плагин для leaflet, из минусов — заброшен, alpha версия, сборка запросов(xml) конкатенацией, только маркеры и полигоны, только geojson.
  4. geoserver.org — один из открытых гис-серверов с поддержкой WFST, на нём опубликованы демонстрационные данные.
Tags:
Hubs:
+13
Comments 5
Comments Comments 5

Articles