Генерация тайлов своими мозгами

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

В данном случае получится не совсем полностью собственные алгоритмы, но тоже достаточно занимательно.

Тайл, сгенерированный на NodeJS + mapnik

С чего начать?

Первым делом требуется определиться что есть уже готовое. Одним из компонентов является mapnik. Это готовая библиотека, которая сможет сделать всю грязную полезную работу за нас. Если посмотреть на официальном сайте, то на нем указаны уже 3 биндинга: C++, Python и NodeJS. Я взял третий вариант, так как с ним все достаточно просто и мне знаком. На Python тоже можно сделать, но я лично использую именно Node. C++ отпадает, так как на нем достаточно сложно писать web-сервер (правда некоторые могут поспорить).

Следующим этапом нужно определиться с документацией. На самом деле с ней тоже все в порядке.

Каркас

Для работы со всем этим делом понадобится всего 4 модулю. Устанавливаем:


npm i express generic-pool mapnik mkdirp

Далее создаем файл libs/mapnik.js:


var mapnik = require('mapnik');

mapnik.register_fonts('/usr/share/fonts', { recurse: true });
mapnik.register_default_input_plugins();

module.exports = () => {
    let map = new mapnik.Map(256, 256);
    map.loadSync('./openstreetmap-carto/mapnik.xml');
    map.registerFonts('/usr/share/fonts', { recurse: true });
    map.loadFonts();
    map.zoomAll();
    return map;
}

После создаем пул в файле libs/pool.js:


const genericPool = require('generic-pool');
const mapnik = require('./mapnik');

const factory = {
    create: () => {
        return mapnik();
    },
    destroy: (client) => {
        console.log(`client destroyed`);
    }
};

const opts = {
    max: 10, // maximum size of the pool
    min: 1 // minimum size of the pool
};

const pool = genericPool.createPool(factory, opts);

module.exports = pool;

И, собственно, файл сервера index.js:


const mapnik = require('mapnik');
const mkdirp = require('mkdirp');
const fs = require('fs');

const pool = require('./libs/pool');
const path = require('path')

const express = require('express')
const app = express()
const port = 3000

app.get('/:z/:x/:y.png', (req, res, next) => {
    // TODO
});

app.use((err, req, res, next) => {
    if (err) {
        return res.status(500).send(err.message);
    }
    return next();
});

app.listen(port, () => {
    console.log(`App listening on port ${port}`)
});

libs/pool.js описывает пул экземпляров для mapnik, так как при конкурентном доступе к одному и тому же экземпляру будет возникать ошибка. Когда идет обращение к пулу, то он смотрит есть ли свободные экземпляры, и если их нет, то либо создает новые (если пул не заполнен), либо ждет, пока не будет возвращен занятый экземпляр.

Дополнительные требования

Так же понадобится настройка стилей генерации тайлов. Тут 2 варианта: писать xml-файл вручную либо использовать готовый образец. Я, опять же, выбрал второй вариант. Для этого идем на сервер нашего тайлового сервера, который мы настраивали ранее и копируем к себе в рабочую директорию всю папку openstreetmap-carto со всеми сгенерированными настройками. В этой директории уже все есть, включая настройки подключения к БД и все необходимые файлы стилей.

Генерация тайлов

Вот теперь можно заняться непосредственно кодом. В файле index.js в самом хэндлере get вместо TODO пишем такой код:


if (fs.existsSync(`./data/${req.params.z}/${req.params.x}/${req.params.y}.png`)) {
        res.setHeader('content-type', 'image/png');
        return res.sendFile(path.resolve(`./data/${req.params.z}/${req.params.x}/${req.params.y}.png`));
    }

    pool.acquire().then(map => {

        mkdirp.sync(`./data/${req.params.z}/${req.params.x}`);
        let vt;
        try {
            vt = new mapnik.VectorTile(+req.params.z, +req.params.x, +req.params.y);
        } catch (e) {
            pool.release(map);
            return next(e);
        }

        map.render(vt, (err, vt) => {
            if (err) {
                pool.release(map);
                return next(err);
            }
            let image = new mapnik.Image(256, 256);
            vt.render(map, image, (err, image) => {
                if (err) {
                    pool.release(map);
                    return next(err);
                }
                image.encode('png8', (err, buffer) => {
                    if (err) {
                        pool.release(map);
                        return next(err);
                    }
                    fs.writeFile(`./data/${req.params.z}/${req.params.x}/${req.params.y}.png`, buffer, (err) => {
                        if (err) return next(err);
                        res.setHeader('content-type', 'image/png');
                        pool.release(map);
                        return res.sendFile(path.resolve(`./data/${req.params.z}/${req.params.x}/${req.params.y}.png`));
                    });
                });
            });

        });
    });

Здесь все достаточно просто: проверяем есть ли запрошенный тайл на диске, если нет, получаем экземпляр мапника и генерируем тайл с сохранением его на диск и отправкой клиенту, иначе спазу отдаем тайл с диска. Вот и весь код.

Выполняем

Для выполнения озадачиваем командную строку такой конструкцией:


node index.js

После открываем браузер и вбиваем адрес http://localhost:3000/0/0/0.png. Должен открыться тайл. Если нет, то проверяем ошибки.

Что можно еще оптимизировать?

Теперь можно запускать сразу несколько экземпляров, например, через NodeJS Cluster, или упаковать в Docker и запустить массу контейнеров, например, через оркестратор.

Ко всему прочему можно дописать кэширование тайлов в ОЗУ через Redis, memcache и т.д. В таком варианте кластер все так же должен работать.

Итого

Вот в таком варианте можно развивать сервис так, как того требует задача.

Поделиться
Вы можете оставить комментарий, или ссылку на Ваш сайт.

Оставить комментарий

Вы должны быть авторизованы, чтобы разместить комментарий.