
Введение
В данной статье я расскажу как я делал тайлер на основе 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 "<<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; };
Класс информации о тайлах с операторами сериализации
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; 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 — предыдущие вычисления
Получаем 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 по файлу, ну и на загрузку любого зума теперь уходит не более секунды.
ссылка на оригинал статьи https://habr.com/ru/post/567936/
Добавить комментарий