django

Tile server на коленке: навигация по старинным картам

  • пятница, 4 марта 2022 г. в 00:37:40
https://habr.com/ru/post/654281/
  • Python
  • Django
  • Nginx
  • OpenStreetMap
  • ReactJS


Вступление

Я 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";

<TileLayer
  minZoom={2}
  maxZoom={20}
  attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
  url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>

В этом фрагменте мы видим, что компонент использует в запросе к серверу тайлов те же самые параметры, что у нас находятся в базе, а занчит обработав их, мы сможем получить нужный нам тайл и вернуть его по запросу клиенту. Для этого нам даже не нужен 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/<int:x>/<int:y>/<int:z>.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 сервера в клиентском приложении. И словно магию можно увидеть очертания старинных улиц и домов с документов вековой давности.

Переключение между слоями в мобильном приложении
Переключение между слоями в мобильном приложении

Заключение

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

Пусть кому-нибудь еще пригодится эта информация, чтобы процентное соотношение пользы ко времени, которое я на все это потратил, увеличилось.