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

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

Граф состоит из узлов (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> 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
