Введение
В данной статье я расскажу как я делал тайлер на основе openstreetmaps на С++/Qt. Задача была написать картографический модуль приложению для поисково-спасательных отрядов, которые работают в условиях недоступного интернет соединения и возможно целые сутки, поэтому требования к картографическому модулю стояли следующие:- работа в оффлайн режиме
- насколько это возможно быстрый рендеринг определённой области на карте
- высокая энергоэффективность загрузки и отображения тайлов на карте
Переделывание и ускорение базового тайлера
Изначально тайлер имел внешний вид стандартной консольной утилиты, через 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 "<<filesVector.at(i)->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;
};
Класс информации о тайлах с операторами сериализации
Код сохранения в бинарный файл
void SaveToFileClass::run()
{
QFile file("file.bin");
if(file.open(QIODevice::WriteOnly))
{
QDataStream stream(&file);
for(int i =0; i<constants.size(); i++)//запись констант в файл
{
stream<<constants.at(i).countOfTiles;
stream<<constants.at(i).xTileStart;
stream<<constants.at(i).yTileStart;
stream<<constants.at(i).xTileCount;
stream<<constants.at(i).yTileCount;
}
int countInputTiles = 0;
for(int i=0;i<files.size();i++)//запись структур с данными о тайлах
{
files.at(i)->open();
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х человек)
- размер картинки тайла в байтах
- стартовая позиция картинки в этом же бинарном файле
Ну и последнее это загрузка картинки тайла из папки в файл, картинка ложится в конец файла и указтель возвращается к структуре с инфой об этом тайле и записывается его стартовая позиция в файле и размер для считывания в будущем.
Получение нужного тайла в модуле картографии
Для понимания дальнейших действий покажу как тайлы располагаются на сетке меркатора.Покажу нахождение тайла на примере, без голых формул.
Тоесть в нашем случае 1 - 0, таким образом по X у нас лежит 1 тайл перед требуемым, так же рассчитываем по y получается 2. StartPosX взято из структуры с константами.
Где
- XtileCount - количество тайлов X в одном столбце Y
- CountY - предыдущие вычисления
Далее находим количество тайлов на предыдущем зуме для того, чтобы через 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 по файлу, ну и на загрузку любого зума теперь уходит не более секунды.
Благодарности
Хотелось бы поблагодарить Сахарука Андрея за кураторство в проекте и друзей из Чехии(Framstag и Karry) за отзывчивость и помощь с APIИсточник статьи: https://habr.com/ru/post/567936/