Как вывести рендеринг карт на сверхзвук и не…

image-loader.svg

Введение

В данной статье я расскажу как я делал тайлер на основе openstreetmaps на С++/Qt. Задача была написать картографический модуль приложению для поисково-спасательных отрядов, которые работают в условиях недоступного интернет соединения и возможно целые сутки, поэтому требования к картографическому модулю стояли следующие:

  • работа в оффлайн режиме

  • насколько это возможно быстрый рендеринг определённой области на карте

  • высокая энергоэффективность загрузки и отображения тайлов на карте

OpenStreetMaps был выбран банально из-за open source, да и модулей к нему в свободном доступе было много. Основу тайлера я взял у libosmscout, но для меня он имел множество проблем, о которых расскажу далее.

Переделывание и ускорение базового тайлера

Изначально тайлер имел внешний вид стандартной консольной утилиты, через args задавались параметры рендеринга и он начинал жужать. Для удобства использования, я решил переделать его под ООП и прикрутить минимальный графический интерфейс, в качестве решения проблем с быстродействием сделал его многопоточным. В итоге получилось что то такое:

Структура обновлённого тайлераСтруктура обновлённого тайлера

После ввода всех данных для отрисовки, стартует интерфейс, который проверяет введены ли все параметры и если да, начинает построение очереди на рендеринг. Класс построения очереди (QueueBuilder) стартует в отдельном потоке и служит для того, чтобы иметь представление о том, сколько тайлов всего, сколько осталось, и чтобы на этапе рендеринга не собиралась инфа о тайле, а сразу переходило к делу по готовым данным. Информацию о тайлах в очереди я решил размещать во временные файлы, для того, чтобы они не лежали в оперативной памяти, потому как её не хватит даже на 18 зумов Беларуси, а когда очередь лежит в файлах по 30 миллионов тайлов, при загрузке вектор с ними занимает 2 гб оперативы, что было в переделах разумного для моего пк.

Код формирования очереди

for (quint32 y=yTileStart; y<=yTileEnd; y++) {
    for (quint32 x=xTileStart; x<=xTileEnd; x++) {
        tileData = new TileDataClass(x,y,level.Get(),0,0,0,0);
        countLatLon(x,y,level.Get());
        if(counterOfTiles>=30000000)
        {
            filesVector.at(i)->flush();
            i++;
            counterOfTiles = 0;

            QTemporaryFile * file = new QTemporaryFile(QDir::tempPath() + "/TileQueue/" + fileName);
            filesVector.push_back(file);
            if(filesVector.at(i)->open())
            {
                qDebug()<<"Opened "<fileName();
            }
            else
            {
                qDebug()<<"Not opened";
            }
            dataStream.setDevice(filesVector.at(i));
        }

        counterOfTiles++;
        dataStream << TileDataClass(tileData->x,tileData->y,tileData->zoom,stepLongitude, stepLattitude, 0,0);
        delete tileData;
    }
}

После того как очередь создана, QueueBuilder не завершает свою работу, а остаётся до конца, для выдачи каждому потоку рендера следующего тайла. И здесь стартуют потоки рендера, как определить количество потоков на текущем пк я так и не узнал (возможно кто то в комментах подскажет), поэтому создаю 4 потока, рендерер ничего интересного не делает, просто создаёт директорию в которую будут сохраняться тайлы и начинает свои тёмные дела (полное описание рендеринга займёт ещё одну статью), после окончания отрисовки тайла, запрашивает следующий и так пока солнце не зашло. По окончанию отрисовки всех тайлов уходит сигнал в интерфейс и интерфейс стартует класс пересохранения тайлов.

Как ускорить загрузку карт и другие изобретения велосипедов

По идее всё сделано, тайлы отрендерены, лежат в папке, бери модуль карт и запускай. Но всегда что то пойдёт не так, после запуска карты и скролла туда обратно можно заметить, что чем больше тайлов отрендерено, тем дольше грузится зум, и при числе картинок 256×256 в полтора миллиарда, поиск в папке нужной занимает неприлично большое время и ресурсы.

Решение этой проблемы пришло не сразу, но пришло, я создал бинарный файл в который поместил константы, константы представляют собой структуру для каждого зума в которой содержится:

  • общее количество тайлов

  • стартовые номера тайлов по x и по y на сетке меркатора

  • количество тайлов по x и по y, для чего это нужно покажу позже.

Структура констант

struct ConstantStruct
{
    uint32_t countOfTiles;
    uint32_t xTileStart;
    uint32_t yTileStart;
    uint32_t xTileCount;
    uint32_t yTileCount;
};
Класс информации о тайлах с операторами сериализации
class TileDataClass
{
public:
    TileDataClass() : x( 0 ), y( 0 ), zoom(0), size(0), startPoint(0) { }
    uint32_t x;
    uint32_t y;
    uint8_t zoom;
    double stepLattitude;
    double stepLongitude;
    uint32_t size;
    uint32_t startPoint;
    friend QDataStream& operator>>(QDataStream &stream, TileDataClass &data);
    friend QDataStream& operator<<(QDataStream &stream, TileDataClass data);
};
void SaveToFileClass::run()
{
    QFile file("file.bin");
    if(file.open(QIODevice::WriteOnly))
    {

        QDataStream stream(&file);
        for(int i =0; iopen();
            QDataStream dataStream(files.at(i));
            while(!dataStream.atEnd())
            {
                TileDataClass *tiles = new TileDataClass();
                dataStream>>*tiles;
                countInputTiles++;
                stream<<*tiles;
                delete tiles;
            }
            files.at(i)->close();
        }
        file.close();
        file.open(QIODevice::ReadWrite);
        file.seek(sizeof(constants.at(0))*constants.size());
        QDataStream dataStream(&file);
        int countOutputTiles = 0;
        while(countOutputTiles!=countInputTiles)//вывод и редактирование структур с учётом информации о размещении самой картинки
        {
            TileDataClass *tiles = new TileDataClass();
            dataStream>>*tiles;

            QString a = "offline_tiles/osm_custom_100-l-1-"+QString::number(tiles->zoom)+
                    +"-"+QString::number(tiles->x)+"-"+QString::number(tiles->y)+".png";
            QFile tilePic(a);
            tilePic.open(QIODevice::ReadOnly);
            tiles->size = tilePic.size();

            tiles->startPoint = file.size();
            file.seek(sizeof(constants.at(0))*constants.size()+sizeof(TileDataClass)*countOutputTiles);
            dataStream<<*tiles;
            file.seek(tiles->startPoint); 
            file.write(tilePic.readAll());
            countOutputTiles++;
            file.seek(sizeof(constants.at(0))*constants.size()+sizeof(TileDataClass)*countOutputTiles);

        }
        if(stream.status() != QDataStream::Ok)
        {
            qDebug() << "Ошибка записи";
        }//отправить сигнал который оповестит о завершении записи в файл, после этого запросить картинку из интерфейса и пробросить её в виджет для вывода.
        QElapsedTimer timer;
        timer.start();
        getTile(147,82,8);
        qDebug() << "The slow operation took " << timer.nsecsElapsed() << " nanoseconds";
        exit(0);
    }
    else
    {
        qDebug()<<"Файл не открыт";
    }
    this->exec();

}

После констант я положил в файл структуры с информацией о тайлах, на каждый тайл своя структура, она содержит в себе:

  • x y тайла

  • уровень приближения

  • количество долготы широты в пикселе (для отрисовки маршрутов, об этом в следующей статье, если эту прочтёт более 4х человек)

  • размер картинки тайла в байтах

  • стартовая позиция картинки в этом же бинарном файле

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

Структура бинарного файлаСтруктура бинарного файла

Получение нужного тайла в модуле картографии

Для понимания дальнейших действий покажу как тайлы располагаются на сетке меркатора.

Размещение тайлов на сетке меркатораРазмещение тайлов на сетке меркатора

Покажу нахождение тайла на примере, без голых формул.

CountX = X - StartPosX

Тоесть в нашем случае 1 — 0, таким образом по X у нас лежит 1 тайл перед требуемым, так же рассчитываем по y получается 2. StartPosX взято из структуры с константами.

TileCount = XtileCount*CountY + CountX

Где

Получаем 4×2+1 = 9, как видно на картинке, всё верно.

Далее находим количество тайлов на предыдущем зуме для того, чтобы через seek перескочить на нужный. Просто берём константы предыдущих зумов и забираем количество тайлов прибавляя к TileCount. В итоге получается 14 тайлов лежит перед необходимым.

И одно из последних действий это перенести указатель на структуру нужного тайла и считать её.

    if(file.open(QIODevice::ReadOnly))
    {
        file.seek(sizeof(constants)*20 + sizeof(QTileDataClass)*(countTls));
        QDataStream dataStream(&file);
        dataStream>>*tile;
    }

После этого из структуры берём начальную позицию картинки и размер её и забираем искомый тайл.

    QPixmap pixmap;
    QByteArray arr;
    QDataStream stream(&file);
    file.seek(tile->startPoint);
    arr = file.read(tile->size);
    QPixmap img;
    img.loadFromData(arr);
    QImage image(img.toImage());

Что же в итоге? В итоге реализовав поддержку файла в модуле картографии с помощью пары формул, получаем поиск нужного тайла за несколько seek по файлу, ну и на загрузку любого зума теперь уходит не более секунды.

© Habrahabr.ru