Введение

В данной статье я расскажу как я делал тайлер на основе 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х человек)

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

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

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

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

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

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

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

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

CountX = X - StartPosX

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

TileCount = XtileCount*CountY + CountX

Где

  • 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 по файлу, ну и на загрузку любого зума теперь уходит не более секунды.

Благодарности

Хотелось бы поблагодарить Сахарука Андрея за кураторство в проекте и друзей из Чехии(Framstag и Karry) за отзывчивость и помощь с API :)

Комментарии (22)


  1. 0xd34df00d
    15.07.2021 20:58
    +4

    как определить количество потоков на текущем пк я так и не узнал (возможно кто то в комментах подскажет)


    std::thread::hardware_concurrency() в чистых плюсах, или QThread::idealThreadCount() в кутях. Но вообще в этой задаче лучше просто использовать QtConcurrent::run() и подобное, там всё само по всем ядрам раскидается.

    tileData = new TileDataClass(x,y,level.Get(),0,0,0,0);
    ...
    delete tileData;
    


    QTemporaryFile * file = new QTemporaryFile(QDir::tempPath() + "/TileQueue/" + fileName);
    



    Ну блин, C++11 уже десять лет, почему бы не использовать std::unique_ptr? Так же легко допустить утечки.

    Да и auto давно завезли, можно не повторять тип слева лишний раз.

    new TileDataClass(x,y,level.Get(),0,0,0,0)


    Слабо по этому коду вспомнить, что тут за что отвечает, и что никакие аргументы не перепутаны? Мне вот было бы слабо. Но об этом дальше.

    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);
    };
    



    Зачем, если можно использовать non-static data member initializers (доступные десять лет), избежать наличия конструктора, сделать тип более простым?

    Кроме того, тогда можно было бы написать в коде выше не new TileDataClass(x,y,level.Get(),0,0,0,0), а new TileDataClass { .x = x, .y = y, .zoom = level.Get() } (в clang и gcc — с начала времён даже с выключенными гнутыми расширениями, в C++20 — официально).

    TileDataClass *tiles = new TileDataClass();


    Забыли delete, получили утечку. unique_ptr, да.

    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);
    



    Извините, но это какой-то ад. Почему бы просто не писать в новый файл, а потом подменить им старый (заодно нахаляву получив невозможность испортить данные, если в процессе записи вы упали)? Вы экономите место на диске?

    И я так и не понял, зачем объединять тайлы в этот большой файл, если тайлы уже есть?

    Я уж не говорю о том, что, похоже, тут имеет смысл задуматься о перепроектировании структур данных — похоже, size и startPoint логически не настолько сильно связаны с прочими членами TileDataClass. Но это надо думать, а у меня уже глаз замылился, извините.


    1. Deymos_s Автор
      15.07.2021 21:07
      +1

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


      1. 0xd34df00d
        16.07.2021 02:30
        +2

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

        Я бы вместо поиска по папке сделал поиск по иерархии папок.


        Самый простой способ, чтобы вообще не вникать в предметную область, а просто перенести ваше решение ­— вместо того, чтобы свалить все тайлы в одну папку root/, свалить тайлы с номерами, например, 0—999 в папку root/0, с номерами 1000—1999 — root/1, и так далее. Итого у вас в каждой папке лежит, скажем, не более N тайлов. Тогда вы вычисляете номер n тайла как сейчас, а для доступа к картинке обращаетесь к root/<n / N>/<n % N>.png, где первое — целочисленное деление, второе — взятие остатка. Если ls root будет тормозить, то можно навернуть сверху ещё один уровень, и так далее.


        Но вообще у вас уже есть некоторая естественная иерархия — как минимум, это уровень зума и координаты. Соответственно, я бы делал иерархию root/<zoom>/<x>/<y>.png. Вероятно, на достаточно больших уровнях зума будет слишком много картинок. поэтому на них придётся совмещать с предыдущим подходом, сохранив, скажем, тайл с x=45917, y=53960 на зуме в 100 по пути root/100/45/917/53/960.png. Возможно, поменять x и y местами будет чуть эффективнее для типичных мониторов в альбомной ориентации, но это уже совсем экономия на спичках.


        Либо я что-то ключевое не понимаю в вашей задаче.


        1. InterceptorTSK
          16.07.2021 07:57
          +1

          "тайл с x=45917, y=53960 на зуме в 100 по пути root/100/45/917/53/960"

          Там квадранты которые можно и нужно держать в hex, зума 100 не будет, максимум 14-16, может быть под 18 но это редкость.

          "а для доступа к картинке обращаетесь к root/<n / N>/<n % N>.png, где первое — целочисленное деление, второе — взятие остатка"

          Своя алгебра у квадрантов, делений никаких тут не нужно, всё на сдвигах.

          Автору рекомендую сильно пережать картинки в jpeg и запаковать в архивы без сжатия. В моём варианте я в рар упаковал блоками по 256 (64 и т.д. по зуму) штук, без сжатия но с информацией для восстановления. Если коннектор до картинок колупает картинку с ошибкой, то запускается восстановление архива и иногда оно действительно помогало. Заодно напишите чекер этих баз, что бы проверять корректность всех картинок враз. Отказоустойчивость - хорошая и удобная штука.

          Если у автора нет квадрантов - рекомендую перейти на них, но вроде бы у автора они как раз таки и есть.

          Реализовывал такую же хрень лет 10 назад тестил на древних атомах первых - работало отлично, тормозов не было. Писалось на шарпе с почти готовой обёрткой вокруг unrar.dll и прикручено оно было естественно на обёртках OpenGL для отрисовки.

          Плюсы тут не совсем нужны, если есть возможность - юзайте попроще, разницы почти не будет. Ей неоткуда браться. Всё слишком просто тут и почти топорно. По оперативке можете чютка просесть, но и это решаемо.

          п.с.: Автор, усиленно на квадранты перейти рекомендую) Никаких xy тут нет, тут одно число, четверичное. Ваш x,y = {00, 01,10,11} = {0,1,2,3} в четверичной. Далее нужна алгебра от этого четверичного числа влево/вправо/вверх/вниз - это 4 операции условно два сложения два вычитания. Всё это не даёт коллизий. Картинки тоже так же переподпишите. Тайл x,y = 3FA0A7.jpeg, папки так же шестнадцатеричные. Это математика. Да да, без неё никак. Придётся кое что придумать и реализовать самостоятельно. Работать будет и работать будет хорошо.


          1. thousandsofthem
            16.07.2021 10:37
            +1

            Автору рекомендую сильно пережать картинки в jpeg и запаковать в архивы без сжатия. В моём варианте я в рар упаковал блоками по 256 (64 и т.д. по зуму) штук, без сжатия но с информацией для восстановления

            Лучше положить в базу sqlite. И использовать можно будет без распаковки, напрямую, и места занимает меньше чем архив


            1. Deymos_s Автор
              16.07.2021 10:44

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


              1. thousandsofthem
                16.07.2021 10:49

                Если вопрос "чтение файлов по одному с диска" и "чтение из sqlite" то второе точно выигрывает - проверялось много-много лет назад на андроид приложении (RMaps), которое умело и то и другое. Для кучи одиночных файлов файловая система работает как база данных, причем неоптимизированная с большим overhead


              1. InterceptorTSK
                17.07.2021 12:10

                Не совсем верный подход у вас, впрочем заказчику конечно же виднее. Однако же заказчик в этом всём ничерта не понимает, вот в чём проблема. Так что решать в любом случае вам, а не заказчику.

                Так вот, всё что вы делаете - должно работать, это очевидно. Но как работать? Вы пишете - максимально быстро. "Максимально быстро" и "быстро" - сильно отличаются. Максимально быстро - это значит быстрее и быть не может. Причём это самое "максимально быстро" придётся доказывать. Т.е. что бы утверждать что оно "максимально быстро" - нужно доказать то, что все прочие реализации будут медленнее. А как вы это докажете?) Проблема.

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

                Вы сами же пишете - изобрели велосипед, но стоило ли оно того, если сравнивать не с чем? Проблема))

                Это я всего лишь про то, на что как обычно влетают начинающие прогеры. Прогают не на том, прогают не туда, прогают не то. Всё как обычно, и конечно же в этом ничего страшного нет.

                Первое что вы должны получить - это валидный продукт, это значит напишите быстро и получите 100% работающее. С какой скоростью оно работает - это вообще не важно.

                Про скорости. Вам пол-жизни не хватит, что бы написать рендер на 30 картиночек, такой что максимально быстрый. Вовсе не шучу. Оно так и есть. Пишите лишь бы работало, а уже после причём не кому то а себе - напишете куски быстрых реализаций, но на это у вас пол-жизни уйдёт, ещё раз говорю) Не верите? А это не вопрос веры. Давайте по существу, почему нет?

                Как оно работает? Есть бд - пусть это файловая система [это тоже бд], или узкоспециализированная штука которые и называются бд - это не суть. Есть и есть. Как они работают? По каким то там запросам они отдают объекты. Объекты иногда кешируются, иногда нет, оно иногда настраивается, иногда нет. Бд может следить за собой ну например как файловая система и быть отказоустойчивой, а иногда бд за собой не следят как тот же скюель и в общем то это почти бесполезные бд [ну потому что бесполезные]. Все они работают как обычно на "вставку"/"удаление"/"изменение" и т.д. всё стандартно, иногда сами самовосстанавливаются и прочее прочее. Суть - они все динамические, а динамический - это изменяющийся по времени [по определению]. Далее вы тут сами напишите портянку про бд себе, и допишите сюда всё то что сейчас существует.

                А ТЕПЕРЬ ДАВАЙТЕ ПОСМОТРИМ НА ВСЁ ЭТО И НА ТО ЧТО ВАМ НУЖНО)))))

                Вам не нужна динамическая бд, вам не нужны объекты, вам не нужны запросы, вам не нужны поиски, вам не нужно от этого всего вообще ничего. А то что вам нужно - в этом всём НЕТУ)

                Что вам нужно? Максимально быстро, отказоустойчиво, просто и желательно иметь поверх такое апи, что бы оно было по вашей задаче максимально простым и даже тривиальным. Иначе говоря вы суёте число и получаете картинку. Вот что нужно.

                Теперь конкретно. Статических бд нет, статических бд отдающщих не объекты а указатели на них нет, статических отказоустойчивых бд нет, ничерта нет) Гонг вопрос: сколько это всё вы будете писать? И дело не в писанине тут. До писанины вы даже не дойдёте. Потому что перед тем как что то писать - нужно вообще то понимать что будет в итоге. А в итоге получается вообще принципиально иная архитектура, нежели имеющиеся. Ну вы же хотели максимально быстро? Хотели? Ну получите. У вас джиельобёртки напрямую и должны получать по hex-числу указатель на кусог бд И ВСЁ)))))) В случае чего бд должна самовосстановиться. Более ничего тут и не нужно.

                А теперь сколько по времени вы это всё собираетесь писать? Понятно или нет?) Никогда вы сие не напишете, потому что это очень сложно, почти невозможно это. Для вас во всяком случае сейчас и ещё лет десять сверху - это в принципе не реализуемая задачька. Так вот я всего лишь про то, что пишите как есть, как вы задумали - так и пишите. И разбирайтесь в алгоритмах, это очень важно. Под алгоритмами я вовсе не понимаю тут какие то мат.алгоритмы, это не то, про них можно почитать и потестить и всё такое. Разбирайтесь в алгоритмах как что работает на самом деле. Алгоритм в данном случае - это как раз таки пример того что написано выше про бд в целом. Ещё это всё называют архитектурой. Вот для вас это важно. Как и что работает на самом деле.

                И не слушайте брюзжащих старпёров, ибо да они сильно знают как и что работает на самом деле. Но писать то вам, а не мне) Так что пишите как задумали, и никого не слушайте. А вот когда напишете - тогда может быть [и то не факт] и поглядите что можно исправить и подправить. Даже могу объяснить почему это выгодно. Вы являетесь по сути программой, конечным автоматом с ножками и ручками. Вы как программа запущены в треде. Если кто-то лезет в тред - он тормозит тред) Нельзя лочить тред, от этого будет только хуже, причём сильно хуже. Вы как программа запущены и исполняетесь, ну и исполняйтесь. Вот когда ваше исполнение закончится, вот тогда можно анализировать результаты вашего исполнения, смотреть время, корректность результата и т.д. А до тех пор влезать в ваш тред никак нельзя, так что удачи вам а точнее вашему треду, как напишете - так и приходите с результатами) Оно будет конечно не интересно, глупо, ужасно, убого, дебильно, монструозно, велосипедно, ужасающе медленно - ну и что? Оно будет ваше и оно будет работать. Это главное.


          1. Deymos_s Автор
            16.07.2021 10:38

            Спасибо большое, я бы и не додумался до такого в силу слабости в вышмате, когда рисование маршрутов на прямиком на тайлах делал это было ещё то испытание, пока кто то не подсказал что линейная интерполяция существует)

            Я в шоке, столько интересной информации для изучения подкинули, думал будет сплошной хейт.


  1. thousandsofthem
    16.07.2021 10:29

    Тайлы векторные или растровые? Из текста статьи выходит что растровые, но явно не вижу где сказано. Сейчас везде стараются использовать первые, т к у вторых слишком много ограничений - скажем на экране с другой плотностью пикселей будут выглядеть плохо


    1. Deymos_s Автор
      16.07.2021 10:35

      растровые, у меня была бд откуда рендерились тайлы, тоесть я особо не мог выбрать какие мне нужны, а про векторные не слышал, погляжу, спасибо!
      Я думал вообще делать векторную карту, но она имеет слишком много минусов по сравнению с картой из тайлов.


      1. thousandsofthem
        16.07.2021 10:45

        https://openmaptiles.org/

        https://github.com/openmaptiles/

        https://www.maptiler.com/news/2021/06/maplibre-gl-native-open-source-mobile-sdk-for-android-and-ios/

        Можно начать поиск отсюда


        1. Deymos_s Автор
          16.07.2021 11:51

          Спасибо большое!


  1. black_list_man
    16.07.2021 10:38

    Я, честно говоря не совсем понимаю что значит "поиск файлов" и в чем суть оптимизации. Вот допустим у нас в локальной директории лежат готовые тайлы. Именование файлов соответствует их позиции на проекции веб-Меркатора, например {X}_{Y}_{Z}.png. Мы рендерим сцену, смотрим какие тайлы перекрывают текущую область видимости при текущем масштабе, и просто берем их с диска по имени. В одной сцене как правило всего 20-30 тайлов. По сути так и работает тайл-сервер, мы у него запрашиваем имя тайла, он нам его выдает. Только тут они все на диске.


    1. Deymos_s Автор
      16.07.2021 10:40
      +1

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


      1. black_list_man
        16.07.2021 11:07

        Я на 100% не уверен, но разве время доступа к файлу зависит от общего количества файлов данной директории? Не слышал о таком. Вот допустим у нас есть функция QImage* getTile(const QString& filename). Внутри она пытается открыть файл по имени, и естественно обрабатывает случаи, в который указанного файла нет на диске. Опять же, на одной сцене всего около 30 тайлов. Если одного из тайлов не окажется на диске, то он как бы и не будет отрисован. Т.е. 30 обращений к файловой системе за кадр(и то, если мы полностью изменили текущую область видимости не экране). Я в свое время делал приложение для растрирования векторных морских навигационных карт и разбиения их на тайлы. Все тайлы лежали в одной папке. В картографическом приложении я просто их от туда брал. Каждый раз при изменении уровня зума или или центра, я рассчитывал т.н. Tile Coverage - квадратная область [minX,maxX,minY,maxY]. И тайлы из этой области брал с диска.


        1. Deymos_s Автор
          16.07.2021 11:51

          Я точно не знаю, просто замечал что очень долго грузится при большом количестве тайлов в папке, не знаю точно изза чего такое происходило, но ждать одного экрана приходилось по 5-10 секунд


  1. Paiser
    18.07.2021 10:13

    Формат хранения тайлов в одном бинарном файле реализован в проекте PMTiles
    github.com/protomaps/PMTiles


    1. Deymos_s Автор
      18.07.2021 11:23

      Ого, спасибо, посмотрю как они реализовали это)


      1. InterceptorTSK
        18.07.2021 14:36

        Да просто реализовали, ибо чем проще - тем быстрее [почти всегда].

        Там условный хеадер должен быть и его длина, в хеадере офсеты на другие хеадеры. В свою очередь эти хеадеры содержат офсеты на начала картиночек. В итоге этот бинарник это немножко офсетов на начала данных и собственно данные.

        Библиотека читает всю бд с диска, кладёт целиком в память, далее "парсит" байтики, т.е. находит смещения по заголовкам на начала данных. Причём эти начала данных всмысле указатели на начала не нужно где то держать в очередном массиве. Всё должно быть
        "линейно" - подайте число на вход [номер вашего тайла], получите число на выходе [всмысле указатель]. Вот и всё. Более ничего и не нужно.

        Что можно и нужно добавить сюда. Опять и снова проблемы архитектуры. Межбиблиотечные взаимодействия губят производительность. Иначе говоря не плодите библиотеки на каждый чих, либо же делайте библиотеки так, что бы обмен данными между ними был минимальным.

        Пример: реализуйте собственные перечисления. Пихните это всё в библиотеку. Наследуйтесь от этой библиотеки и вы погубите производительность перечислений примерно до полутора раз.

        Смысл: если вы тащщите много данных из библиотеки - это не выход. ТащИте мало данных, библиотека должна "обсчитывать" всё что можно в пределах библиотеки и не выходить за эти пределы, и в итоге вы должны получать маленькие данные, которыми уже и пользуйтесь на выходе из библиотеки.

        Иначе говоря у вас есть библиотека "калупающая" эти самые бинарники, где лежат картиночки. Внимательно потестируйте выходное апи этого всего. И проведите тест безбиблиотечный. Код который вы запихаете в библиотеку иногда есть смысл не пихать в библиотеку, а прям "заинлайнить" "копипастой" туда где оно вызывается непосредственно. Межбиблиотечные вызовы иногда могут оказаться критичными, учитывайте это.

        Бинарники должны быть короткими. Выше написано что лучше всё читать с диска куском враз. Например кусок может содержать в себе 256 картиночек. Целиком читайте этот кусог в оперативку, из него берите то что нужно, а нужно из него будет многое, потому то и проще читануть приличный кусог, но не шибко большой конечно же. Тут нужны тесты.

        Далее. Вы же это всё обновлять как то будете. Ну не вы а юзер конечно же. Что юзеру надо - он то и обновит. Обновлять как обновляют нынче всё что есть гигантскими кусками - это вообще путь дегенерата. Но это почему то повсеместно практикуется. Почему? Ахз почему. Стильно модно маладёжно. Наверное. Правда это дичайшая дичандра, но кого это волнует?

        Проблемы с бг-мерским линуксом не забывайте. Они все настроены по разному, это не вёнда где всё всегда одинаково. В вёнде что либо затюнить под производительность почти пара пустяков, ну потому что всё у всех одинаково насторено. Линуксы же в этом плане это аццкий гимарой на сраку ибо они все разные настроено всё по разному и учесть все возможные конфигурации настроек в линуксе и подстроить под это производительность это тот ещё квест.

        И т.д. и т.п.


        1. Deymos_s Автор
          18.07.2021 16:21

          сейчас поясню немного

          1. На экране помещается 20 тайлов, 20 тайлов достать из бинарника это моментальная задача, это не просто быстро, это максимально быстро, если есть система быстрее я буду в шоке.

          2. При открытии файла он не загружается в оперативную память, поэтому файл может быть хоть 10гб размером, скорость считывания и нагрузка на систему не изменится

          3. Линукс это лучшая операционная система, хочется удобства, велком ту MAC OS, по производительности виндовс никогда не сравнится с линуксом.

          4. Если есть непонятные места, могу написать статью про плагин который управляет отрисовкой, там всё подробно обьясню как и где он загружает тайлы и почему это настолько быстро.


          1. InterceptorTSK
            20.07.2021 11:25

            1. На экране не известно сколько тайлов помещяется, всё зависит от разрешения экрана. Даже при условии что если ваши картиночки константны (256x256) то не факт что выводить вы их будете в родном разрешении. Практика показывает что картиночки лучче вывести в чуть ужатом виде, изначально они у вас 256x256, рендерьте их 192x192 и поглядите что получится. Проблемы и с рендерами имеются, иногда рендеры рендерят по дифолту с прозрачностью, но когда они рендерят - края полупрозрачные получатся, а это значит что вы увидите сеточку. В общем смысле я щяс говорю про то, что не факт что у вас будут константные сетки, совпадающие с родным разрешением тайлов. А отсюда следует что даже при константном разрешении экрана вы не можете утверждать что у вас константное кол-во тайлов будет. Учитывайте это. И т.д. и т.п. Рендеры тут не совсем интересны даже, это та ещё отдельная песня. В любом случае рендер должен иметь возможность получить указатель на картинку, если этого в рендере нет - это совсем пагримушка какая то а не рендер.

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

            2. Это как? Большинство систем настроены на виртуализацию памяти. И все файлики дифолтно читаются в память, а затем с ними можно что либо делать. Если вы используете иные механизмы - это такое себе... Потестируйте апи, я вот не знаю про линуксовые апи в случае с IO, но железки везде одинаковые. Обратите внимание на железо прежде всего. Никакое проганье не должно быть оторвано от железа, иначе это не проганье а порнография. У винта есть кеш. Кеш большой. Кеш больше вашей картинки. Причём нынче уже сильно больше, десятки мегабайт, уже иногда и сотни и более. Как вы читаете картинку с диска? Вы читаете её как то через кеш диска. Но если вы читаете по одной картинке, то вы не используете кеш по максимуму. Иначе говоря картинка у вас мегабайт (условно), вы читаете в кеш диска, из кеша в куда то там [в оперативку конечно же, но у вас не так, ну да ладно], однако же проще и эффективнее зачитывать куски примерно равные размеру кеша диска [или кратные ему]. Т.е. файлики и должны быть прилично большими, но не слишком большими.

              Возьмите и потестируйте IO-операции. Наплодите 256 файликов пофиг с чем каждый размером мегабайт, и один файлик размером 256 мегабайт. И читайте в оперативку и то и то. Или куда оно вам надо. Один большой кусок прочитается быстрее.

            3. Линукс это не лучшая ос. Для прогера - совершенно нет. Для прогера это ацкий ужас и кошмар. И вообще то линукс это не ос, это вроде бы как ядро. А вот то что поверх ядра накарячено в виде конечной ос - это уже ос и есть. Все ос разные, ядро вроде как одно. Как настроены ос - так они и работают. И все эти опирационки разные. Линкус вроде как один, а толку особо от этого нету. Ещё раз подчеркну - для прогера нету. Потому что операционки слишком отличаются. И запрогать под это всё что бы оно хотя бы работало везде - это проблема. А вот запрогать так, что бы оно везде хотя бы быстро работало - это очень сложно. А уж запрогать так что бы оно максимально быстро работало везде - это нерешаемая задача. Ну и как? Линукс лучшая ос? А для кого лучшая то?) Для дурачька торвальдса который двадцать лет ядро не меняет и векторы в ядро завести не может? Который не может уже накорячить прям в ядро линукса аналог GDI который работает вообще без видеокарты и выпилить нахрен никому не нужные бестолковые иксы? Вот потому то в этих ваших линуксах ничего и не работает. Для прогера. Потому что даже шрифт толком однообразно не прикрутишь. Потому что убожественное ядро линукса ничего не может. Потому и даже шрифты "у вас" никогда толком и не работали. А казалось бы это банальность вроде как. Ну да ну да. Везде это банальность, кроме линуксов. Отличная "ос" что тут скажешь. И т.д. и т.п.

            4. У вас не какой то непонятный плагин должен картиночки загружать. Он вообще ничего не должен. Боже упаси))) Потому что не должен. Ибо непонятно будет ли "постпроцессинг" с картиночками в дальнейшем.

              1. Нужен "калупатор" картиночек с диска. Это одно. С калупатора вам нужен указатель на картиночку и всё.

              2. Нужен рендер картиночек получающий пачку указателей на картиночки.

              3. Нужен "пробросчик" указателей из одного в другое. Это отдельный модуль. Смысл тут в том, что вы можете имея этот отдельный "пробросчик" - что либо сделать с картиночками ещё, дорисовать на них что-то изменить, балансы цвета подправить, анализировать это всё можно как то и т.д. и т.п. Не факт что оно будет юзаться в прадакшене, так оно вам должно быть нужно а не прадакшену)) Похоже у вас там как обычно набраны никчомные либы которые как обычно не соединяются) Впрочем нынче прогать так модно стильно маладёжно. Глючная фигня получается как обычно, ну да ладно. Пробросчик изменит картиночку, но она доложна в памяти как то лежать уже готовая, иначе как её изменить то?