Как вывести рендеринг карт на сверхзвук и не…
Введение
В данной статье я расскажу как я делал тайлер на основе 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х человек)
размер картинки тайла в байтах
стартовая позиция картинки в этом же бинарном файле
Ну и последнее это загрузка картинки тайла из папки в файл, картинка ложится в конец файла и указтель возвращается к структуре с инфой об этом тайле и записывается его стартовая позиция в файле и размер для считывания в будущем.
Структура бинарного файла
Получение нужного тайла в модуле картографии
Для понимания дальнейших действий покажу как тайлы располагаются на сетке меркатора.
Размещение тайлов на сетке меркатора
Покажу нахождение тайла на примере, без голых формул.
Тоесть в нашем случае 1 — 0, таким образом по X у нас лежит 1 тайл перед требуемым, так же рассчитываем по y получается 2. StartPosX взято из структуры с константами.
Где
Получаем 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 по файлу, ну и на загрузку любого зума теперь уходит не более секунды.