Экспортируем данные OpenStreetMap с помощью визуального редактора на rete.js

от автора

В своей работе я часто сталкиваюсь с задачей по экспорту данных из OpenStreetMap. OSM — это восхитительный источник данных, откуда можно вытащить хоть достопримечательности, хоть районы города, хоть улицы для исследований пешеходной доступности, и вообще что угодно.

Вот только процесс работы с ними в какой-то момент начал меня утомлять. Чтобы вытащить данные по какому-то нетривиальному запросу, нужно или изучать язык запросов Overpass, или писать скрипты и ковыряться в OSM XML формате.

Проделывая эти манипуляции в сотый раз, я задумался о создании какого-нибудь более простого и удобного инструмента. И вот он готов — https://yourmaps.io, визуальный редактор описаний экспорта OpenStreetMap. В редакторе можно мышкой натыкать граф, каждый узел которого будет представлять операцию или фильтр над потоком OSM объектов, а затем скачать результат в GeoJSON.

Вот пример графа, который выбирает все школы в границах заданного муниципального округа, и затем строит 300-метровые буферы вокруг них:

В результате работы получим вот такой набор полигонов в GeoJSON формате, которые затем можно импортировать в QGIS или еще какой-либо софт.

Под катом — немного про функционал сервиса, а также мой опыт работы с библиотекой Rete.js, которая позволяет легко вставлять визуальное программирование и редактирование графов в свой веб-проект.

Rete.js

Rete.js — JS библиотека для рисования и редактирования графов, с упором именно на визуальное программирование и создание схем обработки данных. К сожалению, документация там не отличается полнотой и до некоторых вещей мне пришлось додумываться самостоятельно.

В этом разделе я приведу примеры кода, как на Rete сделать графы со сложными узлами с различными видами пользовательских контролов. Если вам интересно только про экспорт из OpenStreetMap — сразу переходите к следующему разделу.

image

Граф состоит из узлов (node), которые создаются на основе компонент (component). При этом у каждого узла есть входы, выходы и контролы (элементы пользовательского ввода). Также к каждому узлу привязано поле data, хранящее его состояние (например, данные, введенные пользователем)

В документации по Rete есть простой пример с полем для ввода чисел. Однако мне быстро потребовались и более сложные варианты: например, селекты для выбора режима работы узла или кнопка для добавления новых полей ввода (чтобы можно было менять количество значений в фильтрах) или входов узла.

Мне пришлось самому додумываться до того, как сделать такие сложные элементы, так что на всякий случай приведу тут код того, что у меня получилось, если вдруг кому-то придется решать схожую задачу.

Код ниже — для компонента фильтра по значению тега, позволяющего добавлять новые значения по нажатию на кнопку, и имеющего селект для выбора режима сравнения (совпадение или несовпадение значения тега). Сразу скажу, что веб-программирование это не мой конек, кто-то может меня тут побить ногами за использование jquery в 2к20, но по другому я не умею. Да и полезен тут принцип работы с контролами рете, а не нюансы джаваскрипта.

Код InputControl взят из примера Rete, это просто текстовое поле ввода.

Код контрола со списком

var SelectComponent = {     // Шаблон - это HTML элементы, которые будут добавляться к нашему узлу графа, и на которые можно ссылаться как на this.root дальше по коду     template: '<select></select>',     data() {         return {             value: ""         };     },     methods: {         update() {             // сохраняем данные о состоянии в наш узел графа             this.putData(this.ikey, $(this.root).val())         }     },     // метод вызовется при привязке компонента к реальному узлу графа     mounted() {         // this.root - это html элемент, созданный по нашему template, т.е. select в данном случае         let jqueryRoot = $(this.root)         // накидаем в селект нужных значений         for (let idx = 0; idx < this.values.length; ++idx) {             let v = this.values[idx]             jqueryRoot.append($("<option></option>")                 .attr("value", v[0])                 .text(v[1]));         }         // если мы загружаем уже готовый граф - в данных нашего узла уже будет выбранное значение, восстановим его         let currentVal = this.getData(this.ikey)         if (currentVal === undefined) {             currentVal = this.defaultValue             this.putData(this.ikey, this.defaultValue)         }         jqueryRoot.val(currentVal);          const _self = this;         // на каждое изменение значения селекта будем сохранять его в data         jqueryRoot.change(function() {             _self.root.update()         })     } } // Дальше этот контрол можно добавлять к узлу графа как node.addControl(new SelectControl(...)) class SelectControl extends Rete.Control {     constructor(emitter, key, values, defaultValue) {         super(key);         this.key = key;         this.component = SelectComponent         // к этим полям можно получить доступ из кода компоненты контрола, в них можно хранить данные конкретного инстанса         this.props = { emitter, ikey: key, values: values, defaultValue: defaultValue};     } }

Код контрола, добавляющего новые поля ввода в узел графа

var AddTextFieldComponent = {    // наш шаблон - это кнопка, по нажатию на которую будем добавлять новый InputControl     template: '<button type="button" class="btn btn-outline-light">' +         '<i class="fa fa-plus-circle"></i>&nbsp;Add Value</button>',     data() {         return {             value: ""         };     },     methods: {         // метод для подсчета того, сколько контролов уже есть, считаем InputControlы, у которых id начинается с заданного префикса         getCount(node, prefix) {             let count = 0;             node.controls.forEach((value, key, map) => {                 if (key.startsWith(prefix) && value instanceof InputControl) {                     ++count;                 }             });              return count;         },         // по клику на кнопку добавляем новый контрол с именем, состоящем из префикса и индекса         update(e) {             let count = this.methods.getCount(this.node, this.prefix)             this.node.addControl(new InputControl(this.editor, this.prefix + count))             // следующие два метода надо пнуть, чтобы заставить Rete перерисовать узел графа с новым контролом             this.node.update()             this.emitter.view.updateConnections(this)             // дополнительно сохраняем в данные узла графа общее количество контролов, чтобы при загрузке графа из json было ясно, сколько надо полей ввода создать             this.putData(this.iKey, count + 1)         }     },     mounted() {         const _self = this;         this.root.onclick = function(event) {             _self.root.update()         }     } };  class AddTextFieldControl extends Rete.Control {     constructor(emitter, key, prefix, node, inputPlaceholder) {         super(key);         this.key = key;         this.component = AddTextFieldComponent         this.props = { emitter, iKey: key, prefix: prefix, node: node, inputPlaceholder: inputPlaceholder};     } }

Код компонента узла графа

class FilterByTagValueComponent extends Rete.Component {     constructor(){         super("Filter_by_Tag_Value");     }      builder(node) {         // наш узел фильтрации принимает и выдает потоки объектов карты, для этого у меня заведен тип osm.          // Механизм сокетов позволяет в Rete ограничивать то, какие входы и выходы можно соединять друг с другом         var input = new Rete.Input('osm',"Map Data", osmSocket);         var output = new Rete.Output('osm', "Filtered Map Data", osmSocket);         // контрол для ввода названия тега         var tagNameInput = new InputControl(this.editor, 'tag_name')         // контрол с выбором режима сравнения значения тега         var  modeControl = new SelectControl(this.editor,             "mode",             [["EQUAL", "=="], ["NOT_EQUAL", "!="], ["GREATER", ">"], ["LESS", "<"], ["GE", ">="], ["LE", ">="]],             "EQUAL")         // добавляем наши инпуты         node.addInput(input)             .addControl(tagNameInput)             .addControl(modeControl)             .addControl(new AddTextFieldControl(this.editor, "tag_valueCount", "tag_value", node, "Tag Value"))         // Если мы восстанавливаем узел графа из json - надо прочитать, сколько инпутов в нем было, и добавить нужное количество        // Значение data.tag_valueCount записывает AddTextFieldControl, описанный выше         let valuesCount = 1;         if (node.data.tag_valueCount !== undefined) {             valuesCount = node.data.tag_valueCount         }         // Добавляем нужное количество InputControlов         node.addControl(new InputControl(this.editor, 'tag_value'))         for (let i = 1; i < valuesCount; ++i) {             node.addControl(new InputControl(this.editor, 'tag_value' + i))         }          return node             .addOutput(output);     } }

В итоге мне понадобились дополнительные контролы для:

  • Селектов
  • Добавления полей ввода
  • Добавления входов узла
  • Выбора области на карте (для этого в моем контроле по нажатию кнопки открывался поп-ап с картой, нарисованной на leaflet.js и плагином по выбору области). А, еще я использовал апи статических карт Here Maps для отображения превью карты

С их помощью можно сделать узлы графа для решения всех типичных задач обработки данных карт.

После нажатия пользователем кнопки запуска, граф сериализуется в JSON (в Rete уже есть сохранение и загрузка графов), отправляется на сервер, там парсится и обрабатывается.

Примеры экспорта OSM данных

В этом разделе я приведу несколько примеров того, как можно использовать нарисованный граф для решения задач экспорта картографических данных.

Начнем с простого: выберем все парки (объекты с тегом leisure=park, все популярные значения тегов можно найти на вики OSM):

В графе у нас слева — узел, скачивающий OSM данные для указанного района, затем узел, фильтрующий по наличию тега и наконец узел с результатом. Первый узел создает поток картографических объектов (кто хоть немного разбирается с функциональным программированиям и всякими стримами (в терминах Java) — тот легко поймет как оно работает), второй его фильтрует, а третий сохраняет результат, который потом можно скачать или просмотреть.

Результат

Полученные объекты выделены синим, можно просмотреть их значения тегов:

Пример посложнее: хотим построить 500-метровые круглые зоны доступности вокруг школ:

Тут мы сперва получаем поток объектов для области, затем фильтруем его по тегу amenity=school, затем для каждой школы от ее геометрии переходим к центроиду (точка — центр масс), затем вокруг центроида строим буфер нужной толщины.

Можно было бы строить буфер сразу вокруг школы, но тогда его форма зависела бы от формы здания школы. А буфер вокруг точки-центроида всегда будет круглым.

Что делать, если мы хотим получить не только буфера, но и сами здания школ? Все просто: разделяем поток после фильтра по тегу на два (оба потока будут копиями друг друга и будут содержать те же значения), один обрабатываем буфером, другой оставляем как есть, затем объединяем их с помощью узла Union. Этот узел просто сливает все входные потоки в один выходной:

Получаем результат… упс. Некоторые школы показаны полигонами-зданиями, а некоторые — маркерами, т.е. точками. Оказывается, некоторые объекты с amenity=school это не здания школ, а точки, находящиеся внутри полигонов зданий. Так обычно мапят тогда, когда объект не занимает все здание целиком.

В зависимости от того, что нам нужно, мы можем либо отбросить такие точечные объекты вообще с помощью узла-фильтра по геометрии. Или можем немного извратиться вот так:

Это довольно сложный пример с переносом тегов с точечных объектов на здания. Похожий пример я подробно описал в документации по нашему проекту. Вкратце — мы оставляем только те здания из ветки 4, которые пересекаются хотя бы с одной школой из ветки 3. Потом сливаем их в один поток вместе с этими школами. И затем объединяем в этом потоке пересекающиеся
объекты в один. Т.е. мы объединим точки-школы и полигоны-здания, в которые они попадают.

В результате получаем полигоны зданий и зон доступности вокруг них:

Заключение

Вот так с помощью простого визуального редактора на Rete.js наш сервис YourMaps позволяет просто выполнять достаточно сложные задачи экспорта и преобразования картографических объектов.

В дальнейшем я планирую туда добавить еще больше всего — например, возможность загружать данные не только из OSM, но и из своих GeoJSON файлов, больше типов операций и фильтров и т.п.

Мне лично этот сервис уже неплохо помогает. Например, когда надо студенту что-то быстро показать на OSM карте — мне не надо больше запускать QGIS и вспоминать сложный язык запросов Overpass, я в пару движений мышкой накликиваю нужный граф, за несколько секунд он обрабатывается и можно сразу там же увидеть результат.

Надеюсь, он окажется полезным и кому-то из вас. Как всегда, готов выслушать предложения и пожелания или тут в комментариях, или можете прислать на почту evsmirnov@itmo.ru

ссылка на оригинал статьи https://habr.com/ru/post/502714/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *