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

Шаг 1. Изыскания.
Можно конечно угадать пальцем в небе как они могли бы обозначаться, но надёжней отправиться в вики-osm. И там мы можем найти следующее: boundary=forest_compartment
Следовательно лесные кварталы обозначаются полигонами с тегом boundary=forest_compartment. Правда там есть уточнение, что первоначально это обозначалось как boundary=forestry_compartment, но являлось менее грамотным. А так как количество использований со старым обозначением существенно (по данным taginfo около 4 тыс. раз) не будем сбрасывать его со счетов.
Шаг 2. Данные.
Данные возьмём с Geofabrik. Скачиваем файл на всю Россию — russia-latest.osm.pbf. С помощью osmconvert получим данные в формате o5m для последующей фильтрации.
osmconvert russia-latest.osm.pbf -o=russia-latest.o5m
Теперь фильтруем только нужные нам данные с помощью osmfilter
osmfilter russia-latest.o5m --keep="boundary=forest_compartment =forestry_compartment" -o=forest_compartment-local.o5m
Шаг 3. Векторные тайлы.
Немного кратко теории. Старый подход — из большой базы запросить немного данных, получить из них картинку, сохранить её, чтобы в будущем отдать клиенту. В новом — из большой базы запросить немного данных и сохранить их для последующей передачи клиенту. А клиент пусть сам превращает их в картинку. Профит как бы на лицо — нагрузку по рендеру картинки мы перенесли на плечи клиента. Из минусов — на кофеварке возможно не удастся увидеть карту, нужна поддержка WebGL.
И так Mapbox предложил формат векторных тайлов и контейнер для них в виде sqlite базы. Поэтому теперь это не россыпь файлов по папкам, а аккуратный одинокий файл. Векторный тайл содержит в себе логические слои (дома, дороги и т.д.), которые состоят из геометрии и атрибутов.
Вот их мы и будем готовить для наших лесных кварталов. Я буду использовать инструмент TileMaker. На вход он принимает данные OSM в формате pbf, поэтому после фильтрации нам нужно сконвертировать обратно в этот формат.
osmconvert forest_compartment-local.o5m -o=forest_compartment-local.pbf
Теперь нужно объяснить TileMaker какие слои и с какими атрибутами нам нужны, согласно документации.
Шаг 4. Слои?
А какие же нам нужны слои? А это зависит от того, что мы будем показывать. Т.е. прежде всего мы должны уже как-то представлять себе визуальную часть. И как её можно добиться из имеющихся данных. Из данных OSM у нас есть сетка многоугольников и их атрибуты. В атрибутах есть название лесничества и номер квартала.

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

Поэтому для векторных тайлов готовят отдельный слой с надписями, когда ещё есть вся необходимая информация о геометрии.
Итог: нам нужно два слоя, полигональный для заливки и точечный для подписи. Создаём файл config.json.
{ "layers": { }, "settings": { "minzoom": 11, "maxzoom": 11, "basezoom": 14, "include_ids": false, "author": "freeExec", "name": "Forest Compartment RUS", "license": "ODbL 1.0", "version": "0.1", "description": "Forest compartment from OpenStreetMap", "compress": "gzip", "metadata": { "attribution": "<a href=\"http://www.openstreetmap.org/copyright/\" target=\"_blank\">© Участники OpenStreetMap</a>", "json": { "vector_layers": [ ] } } } }
В разделе слои указываем что нам нужно
"layers": { "forest_compartment": { "minzoom": 11, "maxzoom": 11 }, "forest_compartment_label": { "minzoom": 11, "maxzoom": 11 } },
Указаны названия слоёв и на каких масштабах их будем показывать.
"json": { "vector_layers": [ { "id": "forest_compartment", "description": "Compartment", "fields": {}}, { "id": "forest_compartment_label", "description": "Compartment", "fields": {"ref":"String"}} ] }
В метаданных мы подсказываем для будущего визуализатора какие атрибуты у нас доступны. Для слоя с метками у нас будет номер квартала в ref.
Шаг 5. Обработка данных.
Для этой цели служит скрипт на языке lua, который и будет решать какие объекты из данных OSM нам нужны, в какой слой их отправить и с какими атрибутами.
Начнём с шаблона файла process.lua.
-- Nodes will only be processed if one of these keys is present node_keys = { } -- Initialize Lua logic function init_function() end -- Finalize Lua logic() function exit_function() end -- Assign nodes to a layer, and set attributes, based on OSM tags function node_function(node) end -- Similarly for ways function way_function(way) end
Что мы здесь имеем:
node_keys — точек в данных OSM очень и очень много, если мы с каждой будем тыкаться в этот скрипт, то обработка продлится очень долго. Это некий фильтр, говорящий нам, точки с какими ключами нам интересны.
function node_function(node) — функция будет вызываться на каждую интересную нам из предыдущего пункта точку. Тут мы должны решить что с ней делать.
function way_function(way) — функция, которую будут вызывать на любую линию и на отношения с типом multipolygon и boundary, т.к. они считаются площадными объектами.
Начинаем писать код. Первым делом укажем какие точки нам нужны:
node_keys = { "boundary" }
Теперь напишем функцию их обработки:
function node_function(node) local boundary = node:Find("boundary") if boundary == "forestry_compartment" or boundary == "forest_compartment" then local ref = node:Find("ref") if ref ~= "" then node:Layer("forest_compartment_label", false) node:Attribute("ref", ref) end end end
Что тут происходит: читаем значение ключа boundary через node:Find("ключ"). Если это forest_compartment, то читаем номер квартала из тега ref. Если он не пустой, то этот объект добавляем на наш слой с метками, через Layer("название_слоя", нет_объект_не_полигон). В атрибут слоя ref сохраняем номер квартала.
Почти так же просто и для площадных кварталов:
function way_function(way) local boundary = way:Find("boundary") if way:IsClosed() and ( boundary == "forestry_compartment" or boundary == "forest_compartment" ) then way:Layer("forest_compartment", true) way:AttributeNumeric("nomerge", way:Id()) local ref = way:Find("ref") if ref ~= "" then way:LayerAsCentroid("forest_compartment_label", false) way:Attribute("ref", ref) end end end
Тут дополнительно проверяем, что линия замкнута, т.к. бывает, что теги присутствуют просто на отрезках. Стоит обратить внимание, что слой forest_compartment площадный (поэтому второй аргумент в функции Layer("", true)), а место для подписи берём как центр фигуры LayerAsCentroid.
Так же стоит обратить внимание на атрибут который мы добавляем, хотя и не указывали его в конфиге — nomerge. Он нужен, чтобы победить другую особенность, на этот раз уже конвертера TileMaker (хотя в новой версии появился параметр для её отключения).
Особенность в том, что для оптимизации, когда в одном слое есть много объектов с одинаковыми атрибутами, конвертер для них объединяет геометрии в одну. Например у нас есть улица состоящая из трех отдельных сегментов, которые в итоге будут три раза отправлены на рендер. Это дольше, по сравнению стем, что мы бы отправили на рендер один объект, но с чуть более сложной (объединяющая их всех) геометрией.
В нашем же случае все смежные кварталы объединились бы в один большой многоугольник, а нам этого не нужно. Поэтому мы и добавляем номер объекта, чтобы они отличались и не объединялись.
Теперь пришла пора запустить процесс создания векторных тайлов.
tilemaker forest_compartment-local.pbf --output forest_compartment-local.mbtiles
В результате у нас должен появиться файл forest_compartment-local.mbtiles
Шаг 6. Создаём стиль.
Заводим аккаунт на mapbox.com. В Mapbox Studio в разделе Tileset создаём новый tileset, перетаскивая в окошко загрузки наш созданный ранее файл. В течении минуты он должен обработаться и добавиться в список.
Теперь переходим в раздел Styles и создаём новый на основе готового Light, чтобы у нас были видны основные элементы карты, как то дороги, населённые пункты и т.д. Отправляемся в Чебоксары ибо там были замечены лесные кварталы.
Спускаемся на 11 уровень масштаба (мы ведь только для него создали тайлы) и нажимаем кнопку Add layer. В закладке data source находим наш источник данных forest_compartment-local-XXXXX, в нём выбираем полигональный слой. Он должен подсвечиваться справа зелёным.

Далее на вкладке style задаём цвет заливки — зелёный, а обводки коричневый.

Теперь осталось добавить подписи. Добавляем новый слой, только на этот раз выбираем в данных forest_compartment_label, а тип выбираем symbol, справа должны появиться номера.

В закладке стиля укажем что нужно выводить наш атрибут ref.

Вот как бы и всё, жмём в правой части экрана publish и можем делиться ссылкой, чтобы другие могли посмотреть на наше творение. НО показ карт не бесплатно, как и везде, поэтому я вам свою ссылку не дам, чтобы не попасть на хабрэффект.
P.S.: Возможно в дополнительной статье я поведаю как я добивался расположения подписи с названием лесничества на группе кварталов в него входящих.
