
На связи разработчики продукта Аврора Центр компании Открытая мобильная платформа. Сегодня мы расскажем как реализовать сервис self-hosted карт в закрытом контуре.
Наша компания активно развивается и добавляет новый функционал в продукт по удалённому управлению устройствами — Аврора Центр (UEM-решение, которое позволяет управлять устройствами и жизненным циклом приложений на ОС Аврора, Android и Linux). Так по запросам заказчиков было решено добавить отображение геопозиции мобильного устройства на карте территории России. И вот перед нами встаёт задача по работе с картами в АЦ.
Требования к карте:
должна работать офлайн, без наличия доступа к сети;
не должна вызывать лишних вопросов в свете геополитических событий;
должна быть актуальной.
Введение в карты
Существует много картографических сервисов с различными подходами. Так, например, есть сервисы, предоставляющие механизм (инструмент) по получению картографических данных через запросы (API). Например, это сервисы 2GIS и Yandex.
Также существуют сервисы, поставляющие географические данные, предназначенные для обработки и предоставления в желаемом формате. Такие сервисы представляют собой комплексные и разнородные системы, в которых необходимо выбрать подход к тому, как хранить, обновлять и поставлять географические данные. Например, они могут предоставлять их в «портативном» виде mbtiles или хранить в формате PostGIS.
Самым известным поставщиком географических данных является OpenStreetMap — открытая компания, которая предоставляет набор инструментов, необходимых для взаимодействия с картографической информацией, а также публикует их документацию на собственном Wiki. Как несложно догадаться, основной рабочей силой в OpenStreetMap являются волонтёры, которые пишут упомянутый софт, а также наполняют карту географическими данными.
Закатываем рукава
Начинаем работать над задачей и исследуем уже готовые решения. Заботливый аналитик подкидывает варианты и первым из них является использование карт Яндекса, 2GIS или OpenStreetMap.
Вариант с использованием внешних API, выглядит вполне закономерным решением — меньше трудозатрат. Использование сервисов 2GIS и Yandex требует предоставления конечному заказчику ключей и прав доступа к платному API. Возможно, некоторым заказчикам такое решение подходит, но мы стали изучать следующий вариант. Смотрим инструменты OpenStreetMap и понимаем, что их использование не соответствует одному из главных требований — карты должны работать без доступа в интернет. И аналитик предлагает ещё один вариант, чтобы соответствовать этому требованию, можно попробовать выгрузить необходимую нам территорию в виде изображений! Казалось бы — просто, что может пойти не так?
Обсудив решение с аналитиком и архитектором, приступили к реализации. Написали простенький скрипт, который выкачивает png-файлы тайлов по определённым координатам (x/y/z).
Вся карта разбита на небольшие кусочки, количество которых в сетке зависит от зума. Тайлы — это эти кусочки карты. x и y — координаты относительно левого верхнего угла сетки в направлении справа вниз. z — уровень зума.
К сожалению, попытка оказалась неудачной. Изображения одной только территории Москвы занимали очень много места. Но проблема объемов данных не является критичной, ведь HDD ёмкостью в десятки терабайт — обычное дело, и они вполне доступны. Суть проблемы кроется в обновлении этих данных — выкачивать каждый раз огромные объемы нецелесообразно, если, к примеру, изменилось только название улицы/парка/аптеки.
Не опускаем руки
Не опускаем руки и продолжаем изучать готовые решения, но уже не API, а сервера, которые могут хостить «наши данные» в пределах Аврора Центра. Выбор пал на два популярных инструмента: tileserver-gl и mbtileserver. Первый реализован на JS, второй на Go. Обрадовались, взяли самый удобный — tileserver-gl. Инструмент позволяет запрашивать и векторные тайлы, и растровые картинки из исходного файла mbtiles. Также есть возможность подгружать файлы стилей к карте, что тоже добавляет гибкости. Но тут пришёл безопасник и надавал по неопущенным рукам, ведь интерпретируемые языки (коим и является JS) сертифицировать весьма проблематично. Проблема заключается в том, что для сертификации продукта, написанного на интерпретируемом языке, как правило, необходимо сертифицировать его интерпретатор. Поэтому нельзя просто так взять и использовать какой-то интерпретируемый язык.
Пришли к решению на Go — mbtileserver. С одной стороны это решило проблему с сертификацией, с другой — реализация этого сервера не подходит под нашу архитектуру и его необходимо переписывать.
Первый этап пройден: создан инструмент, который умеет отдавать тайлы тестовой карты по запросу фронтенда и всё работает! Переходим к следующему этапу — где взять полную карту? И тут начинается тернистый путь и самое интересное.
Карты генерировали-генерировали, да не выгенерировали
Выяснилось, что есть готовые файлы mbtiles. Поискав в открытом доступе, нашли пару ресурсов, которые предлагают «на пробу» mbtiles небольших размеров, например Лихтенштейна, а если нужны конкретные регионы больших размеров — добро пожаловать в мир платных подписок. Такой вариант нам не подходит. Но ведь mbtiles как-то генерируются? Как? Начинаем изучать этот вопрос и находим ответ — некоторые сервисы предоставляют «сырые» данные, содержащие географическую информацию — osm.pbf. Из этих «ПБФок» мы можем сформировать mbtiles. Что является исходными данными мы выяснили, но как перевести один формат в другой? К сожалению, Wiki OpenStreetMap не даёт прямых ответов на этот вопрос (мы знаем, мы искали). Потратив кучу времени, мы нашли инструмент, который выглядел наиболее удовлетворяющим нашим потребностям — tilemaker.
«Исходники» карты (osm.pbf) есть, их можно скачать на сайте. Утилита для формирования mbtiles — есть! Приступаем к генерации. Для начала пробуем что-нибудь небольшое, а именно территорию Северного Кавказа (её размер около 100 Мб). Запускаем tilemaker.
$ tilemaker --input north-caucasus-fed-district-latest.osm.pbf --output north-caucasus-fed-district.mbtiles
Получаем следующий вывод:
Reading .pbf north-caucasus-fed-district-latest.osm.pbf
(Scanning for ways used in relations: 83%) (56 ms)
Block 2370/2371 (393 ms)
SortedNodeStore: 59102 groups, 407009 chunks, 18956414 nodes, 108239858 bytes (24% wasted)
Block 266/267 (3125 ms)
SortedWayStore: 14150 groups, 132594 chunks, 1565189 ways, 17370675 nodes, 56313586 bytes
only 6 relation blocks; subdividing for better parallelism
Block 95/96 (3384 ms)
Generated points: 936512, lines: 26, polygons: 536435
Attributes: 79817 sets from 3109888 objects (830464 uncached), 2691072 pairs (683008 uncached)
Creating mbtiles at north-caucasus-fed-district.mbtiles
indexed 207564 contended objects
osm: finalizing z6 tile 4096/4096 (165 ms)
osm: finalizing z6 tile 4096/4096 (0 ms)
indexed 0 contended objects
shp: finalizing z6 tile 4096/4096 (0 ms)
shp: finalizing z6 tile 4096/4096 (0 ms)
collecting tiles: 23ms, filtering tiles: z0 (1, 0ms) z1 (1, 0ms) z2 (1, 0ms) z3 (3, 0ms) z4 (3, 0ms) z5 (3, 0ms) z6 (8, 0ms) z7 (18, 0ms) z8 (51, 0ms) z9 (163, 0ms) z10 (554, 2ms) z11 (2121, 8ms) z12 (8030, 31ms) z13 (30947, 129ms) z14 (118776, 516ms)
z6/40/23, writing tile 160680 of 160680
Filled the tileset with good things at north-caucasus-fed-district.mbtiles
...и сгенерированный файл mbtiles!
Что ж, репетиция прошла успешно, пришло время сгенерировать карту всей России. Скачиваем исходники карты России и запускаем генерацию, счастливые наблюдаем за процессом... проходит полчаса и ловим зависание компьютера, т.к. заканчивается вся оперативная память. Локально протестировать не получилось, запрашиваем ресурсы, а именно виртуалку с 256 Гб ОЗУ и какое-то время экспериментируем на ней. Но в итоге находим другой выход — tilemaker умеет «скидывать» промежуточные этапы на диск, не храня всё в ОЗУ. Для включения этой опции используйте флаг --store
. Победа!
Стоит отметить, что размер необходимой оперативной памяти напрямую зависит от размера исходного файла (.pbf), без опции сохранения промежуточных этапов на диск. Так, например, для генерации карты из файла russia-latest.osm.pbf, размером 3,7 Гб, требуется до 32 Гб ОЗУ.
А это чьё?
На текущий момент в предоставляемых OpenStreetMap данных некоторых территорий могут находиться в разных исходных файлах. Для формирования карт, интересующих заказчика, может потребоваться объединение нескольких исходных файлов, что может привести к неправильному отображению границ территорий. Перед нами встаёт новая задача, или даже две: 1) решить, что с этим делать и 2) сделать. Пообщавшись с аналитиком, пришли к выводу, что можно позаимствовать идею у Яндекс карт и убрать границы. Таким образом, отображаем только границы административных регионов.
Для того чтобы сделать невидимыми границы стран, необходимо в файле стилей установить фильтр для boundary-land-level-2:
{
"id": "boundary-land-level-2",
"type": "line",
"source": "openmaptiles",
"source-layer": "boundary",
"filter": ["all", ["!=", "maritime", 1], ["!=", "disputed", 1]],
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
}
}
За границы регионов отвечает boundary-land-level-4, он остаётся без изменений. В итоге получилось как-то так:

Для того чтобы карта выглядела полноценной, были добавлены исходные данные Европы и Азии, а также на карте убрана отрисовка границ между странами. Конечно, после этого карта начала занимать десятки гигабайтов на диске и встал новый вопрос — как эти данные передавать конечному заказчику с каждым релизом? Ведь карта должна быть актуальной, а значит, регулярно обновляться. Для решения этой проблемы было придумано следующее — карты, интересующие заказчиков, были детализированы до 14 зума. Данные по остальным территориям были отфильтрованы, и оставлен набор элементов ways, relations, nodes, которые нужны для нормального отображения до 8 зума. На бОльших зумах pbf будут просто растягиваться. Для Азии такой фильтр может включать, например, основные магистрали, парки, ЖД пути, названия регионов и областей. Нужное подчеркнуть и добавить с помощью утилиты osmfilter. Например:
osmfilter asia-latest.o5m --keep= --keep-ways="highway=trunk or railway=rail or natural= or boundary=national_park or boundary=administrative and type=boundary" -o=asia-latest-filter.o5m
Также карты остальных территорий можно приправить добавлением названий столиц, областей или дополнительной информации на ваш вкус. Например, столицы можно скачать с ресурса overpass-turbo. Запрос будет выглядеть так:
[out:xml][timeout:10000];
(
node["capital"="yes"](4,4,-20,80.5,180);
);
out body;
>;
out skel qt;
Запрос для континентов, стран, областей, городов:
[out:xml][timeout:10000];
(
node["place"="continent"](4,4,-20,80.5,180);
node["place"="country"](4,4,-20,80.5,180);
node["place"="state"](4,4,-20,80.5,180);
node["place"="city"](4,4,-20,80.5,180);
);
out body;
>;
out skel qt;
Экспортируем результат в необработанные данные OSM. Добавить столицы и страны:
osmconvert eurasia-latest-filter.o5m capitals.osm country.osm -o=eurasia-filter.pbf
eurasia-latest-filter.o5m — файл для Евразии, отфильтрованный до нужного зума.
capitals.osm — скачанные столицы из шага выше.
country.osm — скачанные названия континентов, стран, областей и городов из шага выше.
eurasia-filter.pbf — результирующий файл.
Просто добавь воды
Карта, включающая территории РФ, Европы и Азии, может выглядеть не совсем законченной, т.к. не обрамляется океанами или морями. Чтобы «налить воды», можно внести изменения конфигурационный файл tilemaker'a /resources/process-openmaptiles.lua
, а именно в секции:
if natural=="water" or natural=="bay" or leisure=="swimming_pool" or landuse=="reservoir" or landuse=="basin" or waterClasses[waterway] then
if way:Find("covered")=="yes" or not isClosed then return end
local class="lake"; if natural=="bay" then class="ocean" elseif waterway~="" then class="river" end
if class=="lake" and way:Find("wikidata")=="Q192770" then return end
if class=="ocean" and isClosed and (way:AreaIntersecting("ocean")/way:Area() > 0.98) then return end
way:Layer("water",true)
way:MinZoom(0)
-- SetMinZoomByArea(way)
way:Attribute("class",class)
...
и
-- Set 'landcover' (from landuse, natural, leisure)
local l = landuse
if l=="" then l=natural end
if l=="" then l=leisure end
if landcoverKeys[l] then
way:Layer("landcover", true)
way:MinZoom(0)
-- SetMinZoomByArea(way)
way:Attribute("class", landcoverKeys[l])
if l=="wetland" then way:Attribute("subclass", way:Find("wetland"))
else way:Attribute("subclass", l) end
write_name = true
...
SetMinZoomByArea(way)
заменить на way:MinZoom(0)
, где 0 — нужный зум.
Однако необходимо учитывать, что добавление воды, как и полагается, будет влиять на конечный размер файла.
Обновления
И так, промежуточные итоги:
генерация карт — есть;
сервис для хостинга данных этих карт — есть;
фронт их отрисовывает — есть!
Можно праздновать победу? Не совсем. Как уже говорилось ранее, данные карт должны обновляться. Снова погружаемся в пучину Wiki OpenStreetMap.
Находим несколько интересных особенностей:
Обновления предоставляются geofabric.de в формате ocs.gz.
Обновления пропускать нельзя, иначе это поломает .osm.pbf.
Чтобы обновить, необходимо выяснить порядковый номер (sequence number) текущей и обновлённой pbf.
Высчитать URL для файлов обновлений и загрузить их, основываясь на порядковых номерах.
Объединить файлы обновлений в один.
Применить.
Для этого в дело вступает еще один инструмент — Osmium. С его помощью из pbf достаём replication info, в которой хранится текущий sequence number. Также из replication info достаём базовый URL, с которого можно подтянуть обновления. А теперь наиболее интересное в данном процессе — высчитываем путь до файлов обновления. Для этого сходим за файлом state.txt (например, сюда), в котором хранится последний sequence number для нашей pbf (например, текущая pbf имеет sn=3700
, а файл state.txt содержит в себе sn=3710
). После того как мы узнали текущий sn
и sn
в файле, начинаем считать. Да, стоит отметить особенность URL для файлов обновления, они выглядят примерно так. Проходимся в цикле от текущего sn+1
до sn
из state.txt, делим на 1000, чтобы получить часть URL (3701/1000 = 3) и делим с остатком, чтобы получить номер обновления (3701%1000 = 701). На основе упомянутых ранее особенностей формируем URL и получаем результат. Ниже приведён bash-скрипт, который используется для автоматизации процесса:
# get pbf header
# $1 argument -- name of the file in format name.osm.gz
function get_pbf_header() {
if [[ -z $REPLICATION_INFO ]]
then
REPLICATION_INFO=`$OSMIUM fileinfo osm-data/pbf/$1`
fi
}
# parse pbf header and get sequence number
# $1 argument -- name of the file in format name.osm.gz
function get_sequence_number() {
get_pbf_header $1
echo $(echo -e $REPLICATION_INFO | grep -E -o "replication_sequence_number=([0-9]+)" | cut -d '=' -f2)
}
# parse pbf header and get base url
# $1 argument -- name of the file in format name.osm.gz
function get_base_url() {
get_pbf_header $1
echo $(echo $REPLICATION_INFO | grep -E -o "replication_base_url=.*[^\s]" | cut -d ' ' -f1 | cut -d '=' -f2)
}
# get information about pbf, parse and prepare download url
# $1 argument -- pbf name
# $2 argument -- wanted sequence number
function calc_osc_update_path() {
get_pbf_header $1
local __seq_num=$2
local __base_url=$(get_base_url)
local __lead=$(( $__seq_num / 1000 ))
local __remain=$(( $__seq_num % 1000 ))
local i=${#__lead}
local __lead_prefix=""
while [ $i -lt $SUB_DIR_LEN ]
do
local __lead_prefix="0""$__lead_prefix"
local i=$(( $i + 1))
done
i=${#__remain}
local __remain_prefix=""
while [ $i -lt $SUB_DIR_LEN ]
do
local __remain_name="0""$__remain_prefix"
local i=$(( $i + 1))
done
echo "$__base_url/$UPDATES_PREFIX/$__lead_prefix$__lead/$__remain_prefix$__remain"
}
# get last change number from origin resourse
# $1 argument -- name of the file in format name.osm.gz
function get_latest_change_number() {
get_pbf_header $1
local __base_url=$(get_base_url)
echo $(curl -sL $__base_url/state.txt | grep -oE "sequenceNumber=(.*)" | cut -d"=" -f2)
}
# download needed change files
# from current sequence number to latest
# $1 argument -- file name in the "name.osm.pbf" format
function download_changes() {
local __fullname=$1
local __filename="${__fullname%.*.*}"
local cur_seq_num=$(get_sequence_number $__fullname)
local last_seq_num=$(get_latest_change_number $__fullname)
if [[ $cur_seq_num -gt $last_seq_num ]]; then
echo "[critical] bad sequence numbers for $__fullname. current $cur_seq_num, latest $last_seq_num"
return 1
elif [[ $cur_seq_num -eq $last_seq_num ]]; then
echo "[warn] no updates for $__fullname with latest $last_seq_num"
return 2
fi
echo "[info] downloading updates for $__fullname"
for i in $(seq $(( $cur_seq_num + 1 )) $last_seq_num)
do
local __url=$(calc_osc_update_path $__fullname $i)
local __file_date=$(curl -s $__url.state.txt | grep -oE "timestamp=([0-9]+-[0-9]+-[0-9]+)" | cut -d"=" -f2)
wget \
--quiet \
--progress=bar \
-e robots=off \
-np \
-nH \
-R "index.html*" \
-O "osm-data/changes/$__filename"_"$i"_"$__file_date.osc.gz" \
$__url.osc.gz
done
}
Файлы обновлений скачаны. Самое время их объединить в один. Для этого используем уже упомянутый Osmium:
# merge multiple osc.gz change files into one
# $1 argument -- file name in the "name.osm.pbf" format
function merge_changes() {
local __filename=$1
local __filename="${__filename%.*.*}"
echo "[info] merging changes for $1"
$OSMIUM merge-changes osm-data/changes/"$__filename"_*.gz \
--progress \
-O \
-o osm-data/changes/"$__filename"-merged.osc.gz
}
Ну и наконец, применяем наш файл к pbf'ке:
# apply change files to pbfs
function apply_changes() {
local __filename_ext=$1
local __filename="${__filename_ext%.*.*}"
local __seq_num=$(get_latest_change_number $__filename_ext)
local __base_url=$(get_base_url $__filename_ext)
echo "[info] applying changes for $1"
$OSMIUM apply-changes \
-O \
--progress \
--output-header=osmosis_replication_base_url=$__base_url \
--output-header=osmosis_replication_sequence_number=$(( $__seq_num )) \
-o osm-data/pbf/$__filename-updated.osm.pbf \
osm-data/pbf/$__filename.osm.pbf \
osm-data/changes/$__filename-merged.osc.gz
if [[ $? -eq 0 ]]; then
echo "[info] removing intermediate files"
mv osm-data/pbf/$__filename-updated.osm.pbf osm-data/pbf/$__filename.osm.pbf
echo $($OSMIUM fileinfo osm-data/pbf/$__filename.osm.pbf)
else
echo "[critical] something went wrong"
return $?
fi
}
Обновлять ли все файлы территорий конечной карты, решать вам. В соответствии с этим нужно будет скорректировать скрипты. Вот и всё. И с обновлениями мы успешно справились!
Мы подошли из-за угла
Использование данных, которые поддерживаются сообществом, несет в себе некоторые риски и опасности. Данные OpenStreetMap — не исключение. В один прекрасный день мы столкнулись с одной опасностью — неверные или даже намерено испорченные данные. В случае с картами — это нецензурная лексика и различные выражения оскорбительного характера в названиях улиц и «поломанные» дороги. Процесс валидации кажется простым: посмотри и поищи. НО! Нужно это как-то автоматизировать. Вдруг поменяют имя только одной улицы?
Для поиска нецензурных выражений мы составили список возможных слов и словосочетаний. Далее пробегаемся по тегам и элементам в поисках слов из списка. В случае обнаружения невалидных данных мы сообщаем о них. Пусть это и наивный способ, но максимально быстрый и достаточный для обнаружения.
Пусть и не идеально, но:
import atexit
import argparse
import pathlib
import threading
import time
from typing import TypeAlias
import osmium as osm
import osmium.osm.types as osm_types
# OSMElementType is a type alias for OSM types used in handler.
OSMElementType: TypeAlias = osm_types.Node | osm_types.Way | osm_types.Relation
# https://wiki.openstreetmap.org/wiki/Key:name
TagKeyName: str = 'name'
class OSMNameValidator:
def __init__(self, forbidden_words: list[str]):
self._forbidden_words = forbidden_words
def valid(self, line: str) -> bool:
for word in self._forbidden_words:
if word in line.lower():
return False
return True
class OSMHandler(osm.SimpleHandler):
def __init__(self, validator: OSMNameValidator):
osm.SimpleHandler.__init__(self)
self.__validator = validator
self.processed_elements = 0
self.processed_tags = 0
self.non_valid_count = 0
self.processed_files = 0
def tag_inventory(self, elem: OSMElementType):
for tag in elem.tags:
if (key := tag.k) == TagKeyName:
if not self.__validator.valid(tag.v):
self.non_valid_count += 1
print(f"{tag.v} contains forbidden words")
self.processed_tags += 1
self.processed_elements += 1
def node(self, node: osm_types.Node):
self.tag_inventory(node)
def way(self, w: osm_types.Way):
self.tag_inventory(w)
def relation(self, r: osm_types.Relation):
self.tag_inventory(r)
def apply_file(self, filename: str, locations: bool = False, idx: str = '') -> None:
self.processed_files += 1
return super().apply_file(filename, locations, idx)
class VerbosityHandlerThread:
'''Prints statistics if verbosity flag is set'''
def __init__(self, verbose: bool, handler: OSMHandler, forbidden_words_count: int):
self.__handler = handler
self.__verbose = verbose
self.__fwc = forbidden_words_count
self.__f_count = 0
self.__time_now = lambda : time.ctime(time.time())
self.__run()
atexit.register(self.__exit)
def add_files_count(self, count: int):
self.__f_count += count
def __fetch_stats(self) -> str:
elems = self.__handler.processed_elements
tags = self.__handler.processed_tags
non_valid = self.__handler.non_valid_count
processed_files = self.__handler.processed_files
now = self.__time_now()
return (
f'Processed elements: {elems} tags: {tags}'
f' processed files: {processed_files}/{self.__f_count}'
f' non valid: {non_valid} {now}'
)
def __print_stats(self):
stats = self.__fetch_stats()
print(stats, end='\r')
def __print_stats_forever(self):
while True:
self.__print_stats()
time.sleep(5)
def __run(self):
if self.__verbose:
print("started", self.__time_now())
print("forbidden words count", self.__fwc)
t = threading.Thread(
target=self.__print_stats_forever,
daemon=True,
)
t.start()
def __exit(self):
self.__print_stats()
print("\nfinished", self.__time_now())
def cli_args() -> argparse.Namespace:
ap = argparse.ArgumentParser()
ap.add_argument(
'-s',
'--source',
help='data source file to process',
type=str,
)
ap.add_argument(
'-d',
'--directory',
type=str,
)
ap.add_argument(
'-f',
'--forbidden',
default='./forbidden_words.data',
)
ap.add_argument(
'-v',
'--verbose',
default=False,
action='store_true',
)
return ap.parse_args()
def main():
args = cli_args()
if args.source is None and args.directory is None:
print("source or directory arguments must be provided")
exit(1)
try:
f = open(args.forbidden, 'r')
forbidden = f.read().split(',')
f.close()
except Exception as e:
print(e)
exit(1)
osm_name_validator = OSMNameValidator(forbidden)
osmhandler = OSMHandler(osm_name_validator)
verbosity_handler = VerbosityHandlerThread(
args.verbose,
osmhandler,
len(forbidden),
)
if args.source:
osmhandler.apply_file(args.source)
verbosity_handler.add_files_count(1)
if args.directory:
file_dir = pathlib.Path(args.directory)
globed_files = [f for f in file_dir.glob('**/*')]
verbosity_handler.add_files_count(len(globed_files))
for file in globed_files:
osmhandler.apply_file(file)
if __name__ == '__main__':
main()
Финал
Self-hosted карты — это весьма трудозатратное занятие. Построение такой инфраструктуры с нуля требует больших временных затрат и высокой квалификации сотрудников. Так как информация, доступная из открытых источников, неоднородная и неисчерпывающая, приходится собирать всё по крупицам методом проб и ошибок. Плюс не стоит забывать о возможных подводных камнях, которые могут встретиться как во время исследования и реализации, так и в процессе эксплуатации, поддержки и обновления данных для карт. Но то, насколько это увлекательно и какой прирост функциональности даёт — несравнимо ни с какими сложностями, которые мы повстречали на пути реализации. С момента создания статьи могли появиться новые возможности по работе с картами.
Комментарии (9)
rtnF
23.05.2025 06:29Меня интересует выбор Go вместо JavaScript с точки зрения требования верификации. Почему в вашем случае верификация является столь жёстким требованием? Значит ли это, что любые библиотеки на JavaScript запрещены в проекте из соображений безопасности? Как проводится верификация в Go?
У меня есть друг, который выбрал методы формальной верификации в качестве темы своей бакалаврской работы, но я всё ещё не понимаю, как это применяется в реальных условиях. Поэтому мне этот вопрос очень интересен
NikiN
Есть староватое, но работающее решение от OSM
https://github.com/Overv/openstreetmap-tile-server
Почему его не стали использовать?
freeExec
Передать mbtiles проще, чем разворачивать postgis и мучиться с импортом на стороне заказчика.
Собственно и поделки на node/go не к чему, когда есть небольшой модуль для apache.
Да и скрипты обновления самописные зачем, есть же osmupdate
NikiN
Передается pbf , тоже самое что mbtiles, postgres в докере, как и osmupdate , импорт одной командой.
freeExec
Если так рассуждать, то зачем тогда вообще эта прослойка, клиент и так всё сам может сделать.
omprussia Автор
pbf не совсем то же самое, что mbtiles. pdf используются в качестве исходных данных для mbtiles, которые затем передаются заказчику и могут использоваться в закрытом контуре без лишних манипуляций с postgres, а обычной заменой файла mbtiles на новый. Подобный подход к организации работы с гео-данными снимает необходимость с заказчика разворачивать Postgres там где он не нужен.
omprussia Автор
Собственно и поделки на node/go не к чему, когда есть небольшой модуль для apache.
В инфраструктуре Аврора Центр нет Apache, плюс его использование снижает гибкость в работе с mbtiles, если потребуются какие-то изменения. Поэтому мы не можем использовать его в качестве сервера для mbtiles-файлов. Но там, где есть возможность его использования - это хороший вариант.
Да и скрипты обновления самописные зачем, есть же osmupdate
Мы используем самописные скрипты, так как сами определяем какие именно обновления и каких регионов нам необходимы. У нас кастомная карта собранная из разных регионов и частей регионов. Это одна из причин, почему нам приходится кастомизировать тулчейн обновлений. Так же в файлах обновлений мы ищем нецензурную лексику или другой неприемлемый контент. Если такая тонкая работа с обновлениями не требуется, то osmupda - это отличный вариант.
freeExec
Доработать на го проще чем на си, ну может быть, хотя кому как.
Не понятно как это влияет на невозможность применить osmupdate
omprussia Автор
Мы искали решение, которое работает с mbtiles, написано на компилируемом языке и может быть адаптировано под нашу архитектуру. По этим причинам, мы не нашли предложенное вами решение, а модифицировали mbtiles-go и стали использовать его.