Экспортируем данные 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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 7

      0
      Блин, это вы что, сделали аналог overpass с визуальным редактором запросов? По описанию это очень круто.

      А как оно себя поведет на чем-то достаточно большом? Скажем, выбрать все банкоматы внутри России?
        0
        аналог overpass с визуальным редактором

        Да, хотя мой сервис в теории будет мощнее. Так как можно создавать сложные операции в виде узлов графа, не ограничиваясь всякими выборками.


        Сейчас у меня ограничение на размер области стоит до 60х60 км. Так как сперва скачивается вся область и затем к объектам в ней применяются фильтры. А уже такая область весит несколько сотен Мб.

          0
          >Да, хотя мой сервис в теории будет мощнее.
          На глаз не скажу, но вполне допускаю.

          >Сейчас у меня ограничение на размер области стоит до 60х60 км.
          А, ну то есть Россия не прокатит по определению? А просто список банкоматов в Москве, и сравнить с оверпассом — это сложно?
            0

            Промазал с ответом, он ниже.

        0

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


        Про банкоматы в Москве — вот для центра Москвы пайплайн экспорта, который вытаскивает amenity=atm и заодно еще amenity=bank с atm=yes


        Граф экспорта


        Результат выглядит вот так. Надо бы наверное на эту страницу тоже добавить ссылку "скачать".

          0
          Такое впечатление, что результат захватил только часть Москвы. Я не верю, что скажем районы Аэропорта или Сокола не имеют банкоматов вовсе?

          И еще — типовая проблема с таким UI, если много точек (маркеров) — нужна кластеризация. Иначе на эту карту вообще невозможно смотреть, маркеры закрывают ее целиком.

          >оверпасс тоже просто может взять и не выдать данные, если решит что запрос слишком
          общий
          Да, это факт. Мне кажется, лучшее что тут можно сделать — это вменяемо ругаться :)
            0

            Да, только часть. Вся Москва это долго, так что я не стал ее запускать.


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

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

        Самое читаемое