Все мы привыкли, рассматривая классические базовые подложки в интернете, видеть населённые пункты, дороги и их названия, дома с их номером. Но даже у этих объектов свойств куда больше чем просто имя или номер. У зданий это этажность, у дорог количество полос, а у городов количество жителей. Но это только верхушка айсберга — 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.: Возможно в дополнительной статье я поведаю как я добивался расположения подписи с названием лесничества на группе кварталов в него входящих.