Tile server на коленке: навигация по старинным картам
Вступление
Я full stack разработчик на культурно-историческом IT портале Königsland, который успешно начал свою работу примерно месяц назад. Этот ресурс посвящается культуре и истории Восточной Пруссии и является своеобразной летописью времен, которая больше всего напоминает вирутальный музей, где можно получить довольно полную информацию об истории этого великого края, а эта информация пополняется по мере возникновения у меня свободного времени.
Страницы этой летописи приоткрывают завесу тайны и позволяют получить пользу от современных технологий тем, кто увлекается стариной.
Как у меня украли GPS навигатор
Идея реализовать подобные механики возникла у меня еще очень давно, со времен моего первого pet-проекта. Тогда мне не хватало знаний, и для его реализации потребовалась огромная куча времени. Тем не менее, добиться цели и сделать что-то плюс-минус рабочее получилось даже тогда. На тот момент, в качестве альтернативного программированию хобби я выбирал рыбную ловлю, по этой причине картография применялась для агрегации данных о водоемах области. Сейчас я переключился на поиск кладов и соответственно в приоритет вышли совершенно другие локации.
На одном из рыболовных соревнований мы с напарником взяли самую крупную рыбу, и получили в подарок GPS навигатор, на который я в дальнейшем загрузил карты области 1893 года, и использовал его для того, тобы ориентироваться на местности.
И вот этот навигатор пропал. Его конечно можно отследить по GPS, но все доступы к аккаунтам утеряны, и вообще я уже смирился с тем, что спустя 8 лет нашей крепкой с ним дружбы, его у меня украли. Это приносило мне душевные страдания на фоне повышенного стресса — я даже поставил себе мобильное приложение с платными картами, чтобы вновь получить возможность контролировать мое местонахождение в прострастве и времени. Но это было не очень удобно, да и деньги платить — не те времена сейчас.
Tile server
Честно признаюсь, эту часть я делал в самом конце (вся платформа в целом имеет довольно большую инфраструктуру, в которой сервер тайлов является небольшим микросервисом), потому как именно она оказалась наименее прозрачной для понимания. Хотя казалось бы — что может быть сложного в системе координат — есть точка, у нее две координаты — нет ничего проще. Не смотря на подробную документацию вот тут, это оказалось несколько сложнее, чем я думал, и сейчас я постараюсь рассказать почему.
Изначально я попробовал найти открытые tile серверы со старинными картами, по аналогии с серверами google
и arcgis
, но только чтобы они рендерили тайлы старинной карты. Этих карт много в открытом доступе, но вот форматы данных могут иной раз ввести в заблуждение. Когда мои попытки успехом не увенчались, я пришел к логичному выводу, что мне нужн свой сервер тайлов.
Развернуть свой apache2 tile server труда никакого не составило, но возникли проблемы с картами — я никак не мог найти способа конвертировать карты в нужный формат. В примере используется утилита osm2pgsql
, которая позволяет конвертировать карты из формата pbf
и положить их в postgres
. И все отлично работало с картами из докумментации osm
. Но где мне найти нужные старинные карты (восточная пруссия) в таком формате? Это оказалось весьма трудно.
Однако оказалось просто найти карты в формате готовой базы данных sqlite3 — этих карт много в сети и они есть почти для любого региона. Карты всей нужной мне области были разбиты на две базы данных — центральная часть и восточная часть. В базах по сути была одна нужная мне табличка tiles. Эта табличка в которой не было id, но были индексированные колонки: x, y, z. В последней четвертой колонке хранился мой заветный tile с кусочком старой карты в формате Blob. Если x (долгота) и y (широта) были понятны , то с z (zoom) осознание пришло чуть позже.
Давайте теперь посмотрим, как выглядит экземпляр компонента TileLayer
из react-leaflet библиотеки:
import {TileLayer} from "react-leaflet";
В этом фрагменте мы видим, что компонент использует в запросе к серверу тайлов те же самые параметры, что у нас находятся в базе, а занчит обработав их, мы сможем получить нужный нам тайл и вернуть его по запросу клиенту. Для этого нам даже не нужен apache2 — достаточно просто обработать параметры http запроса и сделать по ним правильное обращение в базу данных. Ух, теперь это не кажется таким сложным, ведь дело за малым.
Чтобы не тратить время на настройку и подготовку, используем django:
urls.py
from django.contrib import admin
from django.urls import path
from app.views import IndexView
urlpatterns = [
path('admin/', admin.site.urls),
path(
'tile///.jpeg', IndexView.as_view(), name='endpoint'
),
]
views.py
import io
from PIL import Image
from django.http import HttpResponse
from django.views import generic
from app.models import Tiles
class IndexView(generic.View):
def get(self, request, x, y):
if Tiles.objects.filter(x=x, y=y).first():
response = HttpResponse(content_type="image/jpeg")
Image.open(io.BytesIO(tile.image)).save(response, "JPEG")
else:
response = HttpResponse(200)
return response
models.py
import base64
from django.db import models
class BlobField(models.Field):
description = "Blob"
def db_type(self, connection):
return 'blob'
class Tiles(models.Model):
x = models.IntegerField(primary_key=True)
y = models.IntegerField()
z = models.IntegerField()
s = models.IntegerField()
image = BlobField(db_column='image', blank=True)
def set_data(self, data):
self._data = base64.encodestring(data)
def get_data(self):
return base64.decodestring(self._data)
data = property(get_data, set_data)
class Meta:
db_table = 'tiles'
unique_together = (('x', 'y', 'z', 's'),)
Если человеческим языком, то берем широту и долготу из запроса, находим по ним байткод картинки, конвертируем его в HttpResponse
с {content-type: image/jpeg}
и отдаем нашему клиенту. Тут нам пригодится любимая библиотека для раьбты с картинками: Pillow
Для того чтобы две sqlite3 базы объединить в одну, достаточно добавить настройки для обеих, и написать простенький скрипт по миграции данных из одной базы в другую:
settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
},
'second_db_name': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db2.sqlite3',
},
}
from app.models import *
second_db_tiles = Tiles.objects.using('second_db_name').all()
i=0
for sdt in second_db_tiles:
try:
sdt.save(using='default', force_insert=True)
except Exception as e:
print(e.__class__.__name__)
i+=1
nginx.conf
location /tile/ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://127.0.0.1:8080/tiles/;
}
Следующий шаг — настрйока диррективы ALLOWED_HOSTS, чтобы ограничить доступ к нашему серверу только нашимм доменом. Потом достаточно просто запустить gunicorn --daemon
и указать правильный url (https://yourdomain/tile/{x}/{y}/{z}.jpeg)
для своего новенького tile сервера в клиентском приложении. И словно магию можно увидеть очертания старинных улиц и домов с документов вековой давности.
Переключение между слоями в мобильном приложении
Заключение
Я не очень силен в картографии, но вроде как получилось сделал что-то полезное — сам я активно пользуюсь этим приложением, что позволяет поплнять базу данных сайта интереснейшими материалами, которые доступны на интерактивной карте. К слову, любой пользователь, который прошел аутентификацию, может добавлять на карту информацию, которой бы хотел поделиться и внести вклад в историю Восточной Пруссии.
Пусть кому-нибудь еще пригодится эта информация, чтобы процентное соотношение пользы ко времени, которое я на все это потратил, увеличилось.