#include "configuration.h"
#include "feature_author.h"
#include "profiler.h"
#include "tile_renderer.h"
#include "tools.h"

#include <library/cpp/resource/resource.h>
#include <util/generic/iterator_range.h>

#include <maps/libs/chrono/include/days.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/std.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/libs/tile/include/const.h>
#include <maps/libs/tile/include/geometry.h>
#include <maps/libs/tile/include/utils.h>
#include <maps/renderer/libs/api_params/include/parsers.h>
#include <maps/renderer/libs/base/include/zoom_range.h>
#include <maps/renderer/libs/data_sets/data_set/include/data_set.h>
#include <maps/renderer/libs/data_sets/data_set/include/feature_iterable.h>
#include <maps/renderer/libs/data_sets/data_set/include/view_queriable.h>
#include <maps/renderer/libs/style2_renderer/include/hotspot_render_params.h>
#include <maps/renderer/libs/style2_renderer/include/render.h>
#include <maps/renderer/libs/style2_renderer/include/vec_render.h>
#include <maps/renderer/libs/style2_renderer/include/vec3_render.h>
#include <maps/renderer/libs/style2_renderer/include/vec_render_params.h>
#include <maps/renderer/libs/style2_renderer/include/vec3_render_params.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/test_tools/io_operations.h>
#include <yandex/maps/rapidjsonhelper/rapidjsonhelper.h>
#include <maps/renderer/libs/base/include/geom/box.h>
#include <maps/renderer/libs/base/include/geom/path_builder.h>
#include <maps/renderer/libs/base/include/json_fwd.h>
#include <maps/renderer/libs/base/include/stl_util.h>
#include <yandex/maps/renderer/feature/attributes.h>
#include <yandex/maps/renderer/feature/feature.h>
#include <yandex/maps/renderer/feature/feature_iter.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/collection.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/geometry.h>

#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/algorithm/copy.hpp>
#include <boost/range/algorithm_ext/erase.hpp>

#include <algorithm>
#include <functional>
#include <iterator>
#include <sstream>
#include <unordered_map>

namespace ba = boost::adaptors;

namespace maps::mrc::browser {

namespace {

using StyleSetPtr = std::shared_ptr<renderer::style2::StyleSet>;
using tile::Zoom;

// this attribute is used by maps::renderer5::hotspots::Hotspots
// https://a.yandex-team.ru/arc/trunk/arcadia/maps/renderer/renderer/hotspots/HotspotsRenderer.cpp?rev=5512110#L1126
const std::string LAYER_ATTR_NAME = "layer";
const std::string HOTSPOT_ENABLED_ATTR_NAME = "hotspotEnabled";

constexpr auto FAKE_ID = db::TId(-1);
constexpr auto MIN_OVERVIEW_COVERAGE_FRACTION = 0.2f;

renderer::base::Vertices asVertices(const geolib3::Polyline2& geom)
{
    ASSERT(geom.pointsNumber() >= 2);
    renderer::base::Vertices result;
    result.reserve(geom.pointsNumber());
    result.addMoveTo({geom.pointAt(0).x(), geom.pointAt(0).y()});
    for(const auto& point : MakeIteratorRange(geom.points().begin() + 1, geom.points().end())) {
        result.addLineTo({point.x(), point.y()});
    }

    return result;
}

renderer::base::Vertices asVertices(const geolib3::Point2& geom)
{
    renderer::base::Vertices result;
    result.addGeomPoint({geom.x(), geom.y()});
    return result;
}

struct EdgeWithCoverageInfo {
    road_graph::LongEdgeId longEdgeId;
    geolib3::Polyline2 geom;
    std::vector<fb::CoveredSubPolyline> subpolylines;
    chrono::TimePoint actualizationDate;
};

struct GraphEdgePhotoFeature {
    db::Feature feature;
    geolib3::Polyline2 polyline;
};

using GraphEdgePhotoFeatures = std::vector<GraphEdgePhotoFeature>;

template<class Object>
class FeatureSet : public renderer::feature::FeatureIter {
public:
    using RendererFeatureFormatter = std::function<void(renderer::feature::Feature&, const Object&)>;

    FeatureSet(const std::vector<Object>& objects, RendererFeatureFormatter formatter)
        : objects_(objects)
        , idx_(0)
        , feature_(renderer::feature::FeatureType::Polyline)
        , formatter_(formatter)
    {}

    bool hasNext() const override { return idx_ < objects_.size(); }

    renderer::feature::Feature& next() override {
        const Object& object = objects_.at(idx_++);
        formatter_(feature_, object);
        return feature_;
    }

private:
    const std::vector<Object>& objects_;
    size_t idx_;
    renderer::feature::Feature feature_;
    RendererFeatureFormatter formatter_;
};


template<class Object>
class FeatureIterable : public renderer::data_set::FeatureIterable
{
public:

    FeatureIterable(std::vector<Object> objects, typename FeatureSet<Object>::RendererFeatureFormatter formatter)
        : objects_(std::move(objects))
        , formatter_(formatter)
    {}

    std::unique_ptr<renderer::feature::FeatureIter> iterator() const override
    {
        return std::make_unique<FeatureSet<Object>>(objects_, formatter_);
    }

private:
    std::vector<Object> objects_;
    typename FeatureSet<Object>::RendererFeatureFormatter formatter_;
};


geolib3::BoundingBox toGeolibBoundingBox(const renderer::base::BoxD& bbox)
{
    return geolib3::BoundingBox({bbox.x1, bbox.y1}, {bbox.x2, bbox.y2});
}

int maxRoadFcByZoom(int zoom) {
    if (zoom >= 15) {
        return 10;
    } else if (zoom >= 14) {
        return 7;
    } else if (zoom >= 12) {
        return 6;
    } else if (zoom >= 10) {
        return 5;
    } else if (zoom >= 8) {
        return 4;
    } else if (zoom >= 7) {
        return 3;
    } else if (zoom >= 5) {
        return 2;
    }

    return 1;
}

/// At this time there is a bug in maps::renderer library: it does not take into account
/// that visualized object could be bigger than original one.
renderer::base::BoxD
extendBox(renderer::base::BoxD box, int32_t zoom)
{
    /// This contant should exceed pixel line width of each layer in design
    static constexpr double BOX_EXTENSION_PX = 10;
    double extensionMeters = tile::zoomToResolution(zoom) * BOX_EXTENSION_PX;
    box.extend(extensionMeters);
    return box;
}

std::vector<EdgeWithCoverageInfo>
loadLimitedCoveredEdgesInTile(IDataAccessPtr dataAccess,
                              IGraphPtr graph,
                              std::optional<blackbox_client::Uid> uid,
                              std::optional<db::CameraDeviation> cameraDeviation,
                              const renderer::base::BoxD& bbox,
                              int maxDrawableFc)
{
    constexpr size_t GRID_ROWS = 55;
    constexpr size_t GRID_COLS = 55;
    constexpr size_t NEAREST_LIMIT = 1;

    auto edgeIds = std::unordered_set<road_graph::EdgeId>{};
    auto geoPoints = convertMercatorToGeodetic(
        makeGridPoints(toGeolibBoundingBox(bbox), GRID_ROWS, GRID_COLS));
    for (const auto& geoPos : geoPoints) {
        boost::range::copy(
            graph->getNearestCoveredEdges(geoPos, NEAREST_LIMIT, maxDrawableFc),
            std::inserter(edgeIds, edgeIds.end()));
    }

    auto result = std::vector<EdgeWithCoverageInfo>{};
    for (auto edgeId : edgeIds) {
        auto edge = graph->getCoveredEdgeById(edgeId);
        auto longEdgeId = graph->persistentIndex().findLongId(edgeId);
        REQUIRE(edge && longEdgeId, "Edge " << edgeId << " is not loaded");
        for (auto& coverage : edge->coverages) {
            if (cameraDeviation &&
                    cameraDeviation.value() != coverage.cameraDeviation ||
                !userMayViewPhotos(uid, coverage.privacy)) {
                continue;
            }

            dataAccess->removeUnpublishedFeatures(coverage.coveredSubpolylines);
            if (coverage.coveredSubpolylines.empty()) {
                continue;
            }

            result.push_back(EdgeWithCoverageInfo{
                .longEdgeId = *longEdgeId,
                .geom = geolib3::Polyline2(
                    graph->graph().edgeData(edgeId).geometry()),
                .subpolylines = std::move(coverage.coveredSubpolylines),
                .actualizationDate = coverage.actualizationDate});
            break;
        }
    }
    return result;
}

using SubpolylineWithFeatureId =
    common::geometry::SubPolylineWithValue<db::TId>;

/// zero overhead merger, when photo priority is not important
/// - on an overview scale
/// - for calculating uncovered edge parts
struct MaxIdSubpolylineMerger {
    std::vector<SubpolylineWithFeatureId>
    operator()(const std::vector<SubpolylineWithFeatureId>& sublines) const
    {
        return common::geometry::merge(sublines, std::less<db::TId>{});
    }
};

/// To select the most recent photos when combining subpolylines.
/// Pre-loads all photos inside tile.
class RecentSubpolylineMerger {
    std::unordered_map<db::TId, db::Feature> featureById_;

    auto lessFn() const
    {
        return [this](db::TId lhs, db::TId rhs) {
            auto lhsIt = featureById_.find(lhs);
            auto rhsIt = featureById_.find(rhs);
            return lhsIt != featureById_.end() && rhsIt != featureById_.end()
                       ? lhsIt->second.timestamp() < rhsIt->second.timestamp()
                       : rhsIt != featureById_.end();
        };
    }

public:
    RecentSubpolylineMerger(IDataAccess& dataAccess,
                            const IGraph& graph,
                            const renderer::base::BoxD& bbox,
                            int maxDrawableFc)
    {
        auto ids = db::TIds{};
        auto geoBbox = convertMercatorToGeodetic(toGeolibBoundingBox(bbox));
        auto edgeIds = graph.getCoveredEdgeIdsByBbox(geoBbox, maxDrawableFc);
        for (auto edgeId : edgeIds) {
            auto edge = graph.getCoveredEdgeById(edgeId);
            for (const auto& coverage : edge->coverages) {
                for (const auto& subline : coverage.coveredSubpolylines) {
                    ids.push_back(subline.featureId());
                }
            }
        }
        common::sortUnique(ids);
        featureById_ = common::byId(dataAccess.getFeaturesByIds(ids));
    }

    const std::unordered_map<db::TId, db::Feature>& featureById() const
    {
        return featureById_;
    }

    std::vector<SubpolylineWithFeatureId>
    operator()(const std::vector<SubpolylineWithFeatureId>& sublines) const
    {
        return common::geometry::merge(sublines, lessFn());
    }
};

SubpolylineWithFeatureId
convertToSubpolylineWithFeatureId(const fb::CoveredSubPolyline& from)
{
    return SubpolylineWithFeatureId{convertToCommonSubpolyline(from),
                                    from.featureId()};
}

fb::CoveredSubPolyline
convertToCoveredSubPolyline(const SubpolylineWithFeatureId& from)
{
    return fb::CoveredSubPolyline(
        from.value,
        fb::PolylinePosition(from.subPolyline.begin().segmentIdx(),
                             from.subPolyline.begin().segmentRelPosition()),
        fb::PolylinePosition(from.subPolyline.end().segmentIdx(),
                             from.subPolyline.end().segmentRelPosition()));
}

template <class SubpolylineMerger>
std::vector<fb::CoveredSubPolyline>
mergeSubpolylines(const std::vector<fb::TEdgeCoverage>& coverages,
                  const SubpolylineMerger& merger)
{
    auto subpolylines = std::vector<SubpolylineWithFeatureId>{};
    for (const auto& coverage : coverages) {
        for (const auto& subpolyline : coverage.coveredSubpolylines) {
            subpolylines.push_back(
                convertToSubpolylineWithFeatureId(subpolyline));
        }
    }
    auto result = std::vector<fb::CoveredSubPolyline>{};
    for (const auto& subpolyline : merger(subpolylines)) {
        result.push_back(convertToCoveredSubPolyline(subpolyline));
    }
    return result;
}

template <class SubpolylineMerger>
std::vector<EdgeWithCoverageInfo>
loadCoveredEdgesInTile(IDataAccessPtr dataAccess,
                       IGraphPtr graph,
                       const SubpolylineMerger& merger,
                       std::optional<blackbox_client::Uid> uid,
                       std::optional<db::CameraDeviation> cameraDeviation,
                       const renderer::base::BoxD& bbox,
                       int maxDrawableFc,
                       float minCoverageFraction = 0.f,
                       std::optional<chrono::TimePoint> actualizedAfter = std::nullopt,
                       std::optional<chrono::TimePoint> actualizedBefore = std::nullopt)
{
    auto geoBbox = convertMercatorToGeodetic(toGeolibBoundingBox(bbox));
    std::vector<EdgeWithCoverageInfo> result;

    auto edgeIds = graph->getCoveredEdgeIdsByBbox(geoBbox, maxDrawableFc);
    for (auto edgeId : edgeIds) {
        auto edge = graph->getCoveredEdgeById(edgeId);
        auto longEdgeId = graph->persistentIndex().findLongId(edgeId);
        REQUIRE(edge && longEdgeId, "Edge " << edgeId << " is not loaded");
        std::vector<fb::TEdgeCoverage> coverageInfos;
        for (auto& coverageInfo : edge->coverages) {
            if (cameraDeviation &&
                    cameraDeviation.value() != coverageInfo.cameraDeviation ||
                !userMayViewPhotos(uid, coverageInfo.privacy) ||
                coverageInfo.coverageFraction < minCoverageFraction) {
                continue;
            }

            if (actualizedAfter.has_value() && coverageInfo.actualizationDate < actualizedAfter.value()) {
                continue;
            }

            if (actualizedBefore.has_value() && coverageInfo.actualizationDate > actualizedBefore.value()) {
                continue;
            }

            dataAccess->removeUnpublishedFeatures(coverageInfo.coveredSubpolylines);
            if (!coverageInfo.coveredSubpolylines.empty()) {
                coverageInfos.push_back(std::move(coverageInfo));
            }
        }
        if (!coverageInfos.empty()) {
            auto it = std::max_element(coverageInfos.begin(),
                                       coverageInfos.end(),
                                       [](const auto& lhs, const auto& rhs) {
                                           return lhs.actualizationDate <
                                                  rhs.actualizationDate;
                                       });
            result.push_back(EdgeWithCoverageInfo{
                .longEdgeId = *longEdgeId,
                .geom = geolib3::Polyline2(
                    graph->graph().edgeData(edgeId).geometry()),
                .subpolylines =
                    coverageInfos.size() == 1
                        ? std::move(it->coveredSubpolylines)
                        : mergeSubpolylines(coverageInfos, merger),
                .actualizationDate = it->actualizationDate});
        }
    }
    return result;
}

void append(const fb::PolylinePosition& first,
            const fb::PolylinePosition& last,
            std::vector<fb::CoveredSubPolyline>& parts)
{
    if (first < last) {
        parts.emplace_back(FAKE_ID, first, last);
    }
}

/// @return subpolylines instead of gaps and vice versa
std::vector<fb::CoveredSubPolyline>
invert(const std::vector<fb::CoveredSubPolyline>& parts, size_t segmentsNumber)
{
    std::vector<fb::CoveredSubPolyline> result;
    if (!segmentsNumber) {
        return result;
    }

    const auto first = fb::PolylinePosition(0, fb::MIN_REL_POS);
    const auto last = fb::PolylinePosition(segmentsNumber - 1, fb::MAX_REL_POS);
    if (parts.empty()) {
        append(first, last, result);
        return result;
    }

    append(first, parts.front().begin(), result);
    for (size_t i = 1; i < parts.size(); ++i) {
        append(parts[i - 1].end(), parts[i].begin(), result);
    }
    append(parts.back().end(), last, result);
    return result;
}

std::vector<EdgeWithCoverageInfo>
loadUncoveredEdgesInTile(IDataAccessPtr dataAccess,
                         IGraphPtr graph,
                         std::optional<blackbox_client::Uid> uid,
                         std::optional<db::CameraDeviation> cameraDeviation,
                         const renderer::base::BoxD& bbox,
                         int maxDrawableFc,
                         float minCoverageFraction = 0.f)
{
    std::shared_ptr<privacy::RegionPrivacy> regionPrivacy =
        Configuration::instance()->regionPrivacy();
    auto geoBbox = convertMercatorToGeodetic(toGeolibBoundingBox(bbox));
    db::FeaturePrivacy minPrivacy = regionPrivacy->evalMinFeaturePrivacy(geoBbox);

    if (!userMayViewPhotos(uid, minPrivacy)) {
        return {};
    }

    std::vector<EdgeWithCoverageInfo> result;
    std::unordered_set<road_graph::LongEdgeId> coveredIds;
    for (auto edge : loadCoveredEdgesInTile(dataAccess,
                                            graph,
                                            MaxIdSubpolylineMerger{},
                                            uid,
                                            cameraDeviation,
                                            bbox,
                                            maxDrawableFc,
                                            minCoverageFraction)) {
        coveredIds.insert(edge.longEdgeId);
        edge.subpolylines =
            invert(edge.subpolylines, edge.geom.segmentsNumber());
        if (!edge.subpolylines.empty()) {
            result.push_back(std::move(edge));
        }
    }

    for (auto edgeId : graph->rtree().baseEdgesInWindow(geoBbox)) {
        auto longEdgeId = graph->persistentIndex().findLongId(edgeId);
        REQUIRE(longEdgeId, "Edge " << edgeId << " is not loaded");
        if (coveredIds.count(*longEdgeId)) {
            continue;
        }
        auto edgeData = graph->graph().edgeData(edgeId);
        if (db::TFc(edgeData.category()) > maxDrawableFc) {
            continue;
        }
        auto line = geolib3::Polyline2(edgeData.geometry());
        if (!line.segmentsNumber()) {
            continue;
        }

        auto subLine = fb::CoveredSubPolyline(
            FAKE_ID,
            fb::PolylinePosition(0, fb::MIN_REL_POS),
            fb::PolylinePosition(line.segmentsNumber() - 1, fb::MAX_REL_POS));
        result.push_back(
            EdgeWithCoverageInfo{.longEdgeId = *longEdgeId,
                                 .geom = std::move(line),
                                 .subpolylines = {std::move(subLine)}});
    }

    db::FeaturePrivacy maxPrivacy = regionPrivacy->evalMaxFeaturePrivacy(geoBbox);

    if (!userMayViewPhotos(uid, maxPrivacy)) {
        boost::range::remove_erase_if(
            result,
            [&](const EdgeWithCoverageInfo& edge) {
                db::FeaturePrivacy edgePrivacy =
                    regionPrivacy->evalMaxFeaturePrivacy(edge.geom.boundingBox());
                return !userMayViewPhotos(uid, edgePrivacy);
            }
        );
    }

    return result;
}

double calcCoverageLength(const geolib3::Polyline2& mercatorGeom,
                          const fb::CoveredSubPolyline& subpolyline)
{
    const auto ratio = geolib3::MercatorRatio::fromMercatorPoint(mercatorGeom.pointAt(0));
    double length = 0.;
    if (subpolyline.begin().segmentIdx() == subpolyline.end().segmentIdx()) {
        length += ratio.toMeters(geolib3::length(mercatorGeom.segmentAt(
                      subpolyline.begin().segmentIdx()))) *
                  (subpolyline.end().segmentRelPosition() -
                   subpolyline.begin().segmentRelPosition());
    }
    else {
        length += ratio.toMeters(geolib3::length(mercatorGeom.segmentAt(
                      subpolyline.begin().segmentIdx()))) *
                  (1. - subpolyline.begin().segmentRelPosition());
        for (size_t i = subpolyline.begin().segmentIdx() + 1;
             i < subpolyline.end().segmentIdx();
             ++i) {
            length +=
                ratio.toMeters(geolib3::length(mercatorGeom.segmentAt(i)));
        }
        length += ratio.toMeters(geolib3::length(
                      mercatorGeom.segmentAt(subpolyline.end().segmentIdx()))) *
                  subpolyline.end().segmentRelPosition();
    }
    return length;
}

chrono::Days days(chrono::TimePoint timePoint)
{
    return std::chrono::duration_cast<chrono::Days>(
        chrono::TimePoint::clock::now() - timePoint);
}

void orderByTime(db::Features& features)
{
    std::sort(features.begin(),
              features.end(),
              [](const db::Feature& lhs, const db::Feature& rhs) {
                  return lhs.timestamp() < rhs.timestamp();
              });
}

auto lessTimeFn(const std::unordered_map<db::TId, db::Feature>& featureById)
{
    return [&](db::TId lhsId, db::TId rhsId) {
        auto lhsIt = featureById.find(lhsId);
        auto rhsIt = featureById.find(rhsId);
        return (lhsIt != featureById.end() && rhsIt != featureById.end())
                   ? lhsIt->second.timestamp() < rhsIt->second.timestamp()
                   : rhsIt != featureById.end();
    };
}

template <class T>
std::vector<T> makeUnion(std::vector<T>&& lhs, std::vector<T>&& rhs)
{
    lhs.insert(lhs.end(),
               std::make_move_iterator(rhs.begin()),
               std::make_move_iterator(rhs.end()));
    return lhs;
}

struct OverviewFormatter {
    std::string layer;

    void operator()(renderer::feature::Feature& feature,
                    const EdgeWithCoverageInfo& edge) const
    {
        feature.setType(renderer::feature::FeatureType::Polyline);
        feature.geom().shapes() =
            asVertices(geolib3::convertGeodeticToMercator(edge.geom));

        json::Builder jsonBuilder;
        jsonBuilder << [&](json::ObjectBuilder obj) {
            obj[HOTSPOT_ENABLED_ATTR_NAME] = false;
            obj[LAYER_ATTR_NAME] = layer;
            obj[FIELD_ID] = std::to_string(edge.longEdgeId.value());
            obj[FIELD_PROPERTIES] << [&](json::ObjectBuilder obj) {
                obj[FIELD_ID] = std::to_string(edge.longEdgeId.value());
            };
            obj[FIELD_DAYS] = days(edge.actualizationDate).count();
        };

        feature.attr.set(jsonBuilder.str().c_str());
    }
};

const std::shared_ptr<renderer::data_set::FeatureIterable> NO_DATA
        = std::make_shared<renderer::data_set::EmptyFeatureSet>();

class DataAccessDataSet : public renderer::data_set::DataSetBase,
                          public renderer::data_set::ViewQueriable
{

    using FeaturesLoader = std::function<std::shared_ptr<renderer::data_set::FeatureIterable>(const renderer::data_set::ViewQueryContext&)>;

    struct LayerInfo {
        renderer::feature::FeatureType featureType;
        FeaturesLoader loader;
    };

public:
    DataAccessDataSet(
            IDataAccessPtr dataAccess,
            IGraphPtr graph,
            IGraphPtr pedestrianGraph,
            const FeatureFilter& featureFilter,
            const std::string& baseUrl = {},
            bool withAuthors = false
    )
        : dataAccess_(dataAccess)
        , roadGraph_(graph)
        , pedestrianGraph_(pedestrianGraph)
        , featureFilter_(featureFilter)
        , baseUrl_(baseUrl)
        , withAuthors_(withAuthors)
    {
        initLayersInfoMap();
        for (auto& [name, layerInfo] : layersInfoMap_) {
            datasetInfo_.layers.emplace_back(name, layerInfo.featureType);
        }
    }

    std::vector<renderer::data_set::LayerView>
    queryView(renderer::data_set::ViewQueryContext& ctx) const override
    {
        REN_THROW_IF_CANCELED(ctx.cancelToken);

        std::vector<renderer::data_set::LayerView> result;

        if (ctx.params.layers) {
            for (const auto& layer : *ctx.params.layers) {
                result.push_back(queryLayer(layer, ctx));
            }
        }
        return result;
    }

    const renderer::data_set::DataSetInfo& info() const override { return datasetInfo_; }

private:
    auto loadCovered(const renderer::data_set::ViewQueryContext& ctx,
                     std::initializer_list<IGraphPtr> graphs,
                     const std::string& layer)
        -> std::shared_ptr<renderer::data_set::FeatureIterable>
    {
        constexpr Zoom EDGE_COVERED_PARTS_MIN_ZOOM = 12;
        constexpr Zoom COVERED_EDGES_MIN_ZOOM = 6;
        if (ctx.params.zoomRange.max() >= EDGE_COVERED_PARTS_MIN_ZOOM) {
            return this->loadEdgeCoveredParts(ctx, graphs, layer);
        }
        else if (ctx.params.zoomRange.max() >= COVERED_EDGES_MIN_ZOOM) {
            return this->loadCoveredEdges(ctx, graphs, layer);
        }
        return NO_DATA;
    }

    void initLayersInfoMap()
    {
        layersInfoMap_.emplace(
            "edge",
            LayerInfo{renderer::feature::FeatureType::Polyline,
                      [&](const renderer::data_set::ViewQueryContext& ctx) {
                          return loadCovered(ctx, {roadGraph_}, LAYER_EDGES);
                      }});

        layersInfoMap_.emplace(
            "pedestrian_edge",
            LayerInfo{renderer::feature::FeatureType::Polyline,
                      [&](const renderer::data_set::ViewQueryContext& ctx) {
                          return loadCovered(
                              ctx, {pedestrianGraph_}, LAYER_EDGES);
                      }});

        layersInfoMap_.emplace(
            "pedestrian_road_edge",
            LayerInfo{renderer::feature::FeatureType::Polyline,
                      [&](const renderer::data_set::ViewQueryContext& ctx) {
                          return loadCovered(
                              ctx, {roadGraph_, pedestrianGraph_}, LAYER_EDGES);
                      }});

        layersInfoMap_.emplace(
            "ride_photo",
            LayerInfo{
                renderer::feature::FeatureType::Point,
                [&](const renderer::data_set::ViewQueryContext& ctx) {
                    return this->loadRidePhotos(ctx);
                }
            }
        );

        layersInfoMap_.emplace(
            "walk_photo",
            LayerInfo{
                renderer::feature::FeatureType::Point,
                [&](const renderer::data_set::ViewQueryContext& ctx) {
                    return this->loadWalkPhotos(ctx);
                }
            }
        );

        layersInfoMap_.emplace(
            "ride",
            LayerInfo{
                renderer::feature::FeatureType::Polyline,
                [&](const renderer::data_set::ViewQueryContext& ctx) {
                    return this->loadRides(ctx);
                }
            }
        );

        layersInfoMap_.emplace(
            "covered_pedestrian",
            LayerInfo{renderer::feature::FeatureType::Polyline,
                      [&](const renderer::data_set::ViewQueryContext& ctx) {
                          constexpr Zoom COVERED_PARTS_MIN_ZOOM = 15;
                          constexpr Zoom COVERED_EDGES_MIN_ZOOM = 12;
                          constexpr Zoom LIMITED_EDGES_MIN_ZOOM = 5;

                          if (ctx.params.zoomRange.max() >= COVERED_PARTS_MIN_ZOOM) {
                              return this->loadEdgeCoveredParts(
                                  ctx, {pedestrianGraph_}, LAYER_PEDESTRIAN_EDGES);

                          } else if (ctx.params.zoomRange.max() >= COVERED_EDGES_MIN_ZOOM) {
                              return this->loadCoveredEdges(
                                  ctx, {pedestrianGraph_}, LAYER_PEDESTRIAN_EDGES);

                          } else if (ctx.params.zoomRange.max() >= LIMITED_EDGES_MIN_ZOOM) {
                              return this->loadLimitedCoveredEdges(
                                  ctx, pedestrianGraph_, LAYER_PEDESTRIAN_EDGES);
                          }

                          return NO_DATA;
                      }});

        layersInfoMap_.emplace(
            "uncovered_pedestrian",
            LayerInfo{renderer::feature::FeatureType::Polyline,
                      [&](const renderer::data_set::ViewQueryContext& ctx) {
                          constexpr Zoom UNCOVERED_PARTS_MIN_ZOOM = 15;

                          if (ctx.params.zoomRange.max() < UNCOVERED_PARTS_MIN_ZOOM)
                            return NO_DATA;

                          return this->loadEdgeUncoveredParts(
                              ctx, pedestrianGraph_, LAYER_PEDESTRIAN_EDGES);
                      }});

    }

    void formatDbFeature(json::ObjectBuilder obj, const db::Feature& dbFeature,
            const FeatureIdToUserInfo& featureIdToUserInfo = {}, const IdToPointMap& targetPoints = {}) const
    {
        formatFeatureHotspotProperties(this->baseUrl_, obj, dbFeature, targetPoints);

        if (withAuthors_) {
            toFeatureAuthorJson(obj, dbFeature.id(), featureIdToUserInfo);
        }
    }

    std::shared_ptr<renderer::data_set::FeatureIterable>
    loadLimitedCoveredEdges(const renderer::data_set::ViewQueryContext& ctx,
                            IGraphPtr graph,
                            const std::string& layer)
    {
        TimeProfiler timer("loadLimitedCoveredEdges", 200);
        std::vector<EdgeWithCoverageInfo> edges = loadLimitedCoveredEdgesInTile(
            dataAccess_,
            graph,
            featureFilter_.uid,
            featureFilter_.cameraDeviation,
            ctx.params.bbox,
            maxDrawableFcByZoom(ctx.params.zoomRange.max(), layer));
        return std::make_shared<FeatureIterable<EdgeWithCoverageInfo>>(
            std::move(edges), OverviewFormatter{.layer = layer});
    }

    std::shared_ptr<renderer::data_set::FeatureIterable>
    loadCoveredEdges(const renderer::data_set::ViewQueryContext& ctx,
                     std::initializer_list<IGraphPtr> graphs,
                     const std::string& layer)
    {
        auto edges = std::vector<EdgeWithCoverageInfo>{};
        for (auto graph : graphs) {
            edges = makeUnion(
                std::move(edges),
                loadCoveredEdgesInTile(
                    dataAccess_,
                    graph,
                    MaxIdSubpolylineMerger{},
                    featureFilter_.uid,
                    featureFilter_.cameraDeviation,
                    extendBox(ctx.params.bbox, ctx.params.zoomRange.min()),
                    maxDrawableFcByZoom(ctx.params.zoomRange.max(), layer),
                    MIN_OVERVIEW_COVERAGE_FRACTION,
                    featureFilter_.actualizedAfter,
                    featureFilter_.actualizedBefore));
        }

        return std::make_shared<FeatureIterable<EdgeWithCoverageInfo>>(
            std::move(edges), OverviewFormatter{.layer = layer});
    }

    float mergeDistanceMetersForZoom(int zoom, const geolib3::Point2& geodeticPoint)
    {
        constexpr double MERGE_DISTANCE_PX = 10.;
        const auto ratio = geolib3::MercatorRatio::fromGeodeticPoint(geodeticPoint);
        return ratio.toMeters(tile::zoomToResolution(zoom) * MERGE_DISTANCE_PX);
    }

    std::vector<fb::CoveredSubPolyline>
    mergeAdjacentCoverages(const std::vector<fb::CoveredSubPolyline>& coveredSubpolylines,
                           const geolib3::Polyline2& geodeticGeom,
                           const std::unordered_map<db::TId, db::Feature>& featureById,
                           int zoom)
    {
        std::vector<fb::CoveredSubPolyline> result;
        if (coveredSubpolylines.empty()) {
            return result;
        }
        const float mergeDistance = mergeDistanceMetersForZoom(zoom, geodeticGeom.pointAt(0));
        geolib3::Polyline2 mercatorGeom = convertGeodeticToMercator(geodeticGeom);
        fb::CoveredSubPolyline coveredSubpolyline = coveredSubpolylines.front();
        float length = calcCoverageLength(mercatorGeom, coveredSubpolyline);

        for (size_t i = 1; i < coveredSubpolylines.size(); ++i) {
            const fb::CoveredSubPolyline& curSubpolyline = coveredSubpolylines[i];
            float curLength = calcCoverageLength(mercatorGeom, curSubpolyline);
            if (length > mergeDistance) {
                result.push_back(coveredSubpolyline);
                coveredSubpolyline = curSubpolyline;
                length = curLength;
            } else if (coveredSubpolyline.end() == curSubpolyline.begin()) {
                coveredSubpolyline.mutable_end() = curSubpolyline.end();
                coveredSubpolyline.mutate_featureId(
                    std::max(coveredSubpolyline.featureId(),
                             curSubpolyline.featureId(),
                             lessTimeFn(featureById)));
                length += curLength;
            } else {
                result.push_back(coveredSubpolyline);
                coveredSubpolyline = curSubpolyline;
                length = curLength;
            }
        }
        result.push_back(coveredSubpolyline);
        return result;
    }

    void loadEdgeFeatures(const renderer::data_set::ViewQueryContext& ctx,
                          IGraphPtr graph,
                          const std::string& layer,
                          GraphEdgePhotoFeatures& edgeFeatures,
                          FeatureIdToUserInfo& featureIdToUserInfo)
    {
        TimeProfiler timer("loadEdgeFeatures", 200);

        auto bbox = extendBox(ctx.params.bbox, ctx.params.zoomRange.min());
        auto fc = maxDrawableFcByZoom(ctx.params.zoomRange.max(), layer);
        auto merger = RecentSubpolylineMerger{*dataAccess_, *graph, bbox, fc};

        std::vector<EdgeWithCoverageInfo> edges = loadCoveredEdgesInTile(
            dataAccess_,
            graph,
            merger,
            featureFilter_.uid,
            featureFilter_.cameraDeviation,
            bbox,
            fc,
            0.f,
            featureFilter_.actualizedAfter,
            featureFilter_.actualizedBefore);

        edgeFeatures.reserve(edgeFeatures.size() + edges.size());
        INFO() << "Loaded " << edges.size() << " edges";

        const auto& featureById = merger.featureById();

        for (auto& edge : edges) {
                auto mergedCoveredSubpolylines = mergeAdjacentCoverages(
                    edge.subpolylines, edge.geom, featureById, ctx.params.zoomRange.max());
                for (const fb::CoveredSubPolyline& coveredSubpolyline : mergedCoveredSubpolylines) {

                    auto featureIt = featureById.find(coveredSubpolyline.featureId());
                    if (featureIt == featureById.end()) {
                        continue;
                    }
                    auto feature = featureIt->second;

                    geolib3::Polyline2 polyline =
                        partition(edge.geom,
                                  coveredSubpolyline.begin(),
                                  coveredSubpolyline.end());
                    if (polyline.pointsNumber() == 1) {
                        continue;
                    }
                    feature.setGeodeticPos(polyline.pointAt(0));
                    feature.setHeading(
                        geolib3::Direction2(
                            geolib3::convertGeodeticToMercator(polyline.segmentAt(0))
                        ).heading()
                    );
                    if (withAuthors_) {
                        featureIdToUserInfo[feature.id()] = getBlackboxUserInfo(feature);
                    }
                    edgeFeatures.push_back(GraphEdgePhotoFeature{.feature = std::move(feature),
                                                                 .polyline = std::move(polyline)});
                }
        }
    }

    auto makeDetailFormatter(FeatureIdToUserInfo&& featureIdToUserInfo, const std::string& layer)
    {
        return
            [this, featureIdToUserInfo = std::move(featureIdToUserInfo), layer]
            (renderer::feature::Feature& feature, const GraphEdgePhotoFeature& edgeFeature)
            {
                feature.setType(renderer::feature::FeatureType::Polyline);
                feature.geom().shapes() = asVertices(geolib3::convertGeodeticToMercator(edgeFeature.polyline));

                json::Builder jsonBuilder;
                jsonBuilder << [&](json::ObjectBuilder obj) {
                    obj[HOTSPOT_ENABLED_ATTR_NAME] = true;
                    obj[LAYER_ATTR_NAME] = layer;
                    obj[FIELD_ID] = std::to_string(edgeFeature.feature.id());
                    obj[FIELD_PROPERTIES] << [&](json::ObjectBuilder obj) {
                        formatDbFeature(obj, edgeFeature.feature, featureIdToUserInfo);
                    };
                    obj[FIELD_DAYS] =
                        days(edgeFeature.feature.timestamp()).count();
                };

                feature.attr.set(jsonBuilder.str().c_str());
            };
    }

    std::shared_ptr<renderer::data_set::FeatureIterable>
    loadEdgeCoveredParts(const renderer::data_set::ViewQueryContext& ctx,
                     std::initializer_list<IGraphPtr> graphs,
                     const std::string& layer)
    {
        GraphEdgePhotoFeatures edgeFeatures;
        FeatureIdToUserInfo featureIdToUserInfo;
        for (auto graph : graphs) {
            loadEdgeFeatures(
                ctx, graph, layer, edgeFeatures, featureIdToUserInfo);
        }
        INFO() << "Loaded " << edgeFeatures.size() << " objects";
        return std::make_shared<FeatureIterable<GraphEdgePhotoFeature>>(
            std::move(edgeFeatures),
            makeDetailFormatter(std::move(featureIdToUserInfo), layer));
    }

    std::shared_ptr<renderer::data_set::FeatureIterable>
    loadEdgeUncoveredParts(const renderer::data_set::ViewQueryContext& ctx,
                           IGraphPtr graph,
                           const std::string& layer)
    {
        TimeProfiler timer("loadEdgeUncoveredParts", 200);
        std::vector<EdgeWithCoverageInfo> edges = loadUncoveredEdgesInTile(
            dataAccess_,
            graph,
            featureFilter_.uid,
            featureFilter_.cameraDeviation,
            extendBox(ctx.params.bbox, ctx.params.zoomRange.min()),
            maxDrawableFcByZoom(ctx.params.zoomRange.max(), layer));

        std::vector<geolib3::Polyline2> lines;
        lines.reserve(edges.size());
        INFO() << "Loaded " << edges.size() << " uncovered edges";
        for (const auto& edge : edges) {
            for (const auto& subLine : edge.subpolylines) {
                auto line =
                    partition(edge.geom, subLine.begin(), subLine.end());
                if (line.pointsNumber() == 1) {
                    continue;
                }
                lines.push_back(line);
            }
        }

        auto formatter = [layer](renderer::feature::Feature& feature,
                                 const geolib3::Polyline2& line) {
            feature.setType(renderer::feature::FeatureType::Polyline);
            feature.geom().shapes() =
                asVertices(geolib3::convertGeodeticToMercator(line));

            json::Builder jsonBuilder;
            jsonBuilder << [&](json::ObjectBuilder obj) {
                obj[HOTSPOT_ENABLED_ATTR_NAME] = true;
                obj[LAYER_ATTR_NAME] = layer;
            };

            feature.attr.set(jsonBuilder.str().c_str());
        };

        INFO() << "Loaded " << lines.size() << " uncovered lines";
        return std::make_shared<FeatureIterable<geolib3::Polyline2>>(
            std::move(lines), formatter);
    }

    std::shared_ptr<renderer::data_set::FeatureIterable>
    loadRidePhotos(const renderer::data_set::ViewQueryContext& ctx)
    {
        TimeProfiler timer("loadRidePhotos", 200);
        FeatureFilter filter = featureFilter_;
        filter.datasets = db::ridePhotosDatasets();

        db::Features features =
            loadFeaturesWithMargins(dataAccess_, toGeolibBoundingBox(extendBox(ctx.params.bbox, ctx.params.zoomRange.min())),
                                        2 * GAP_DRIVING_LIMIT_METERS, filter);

        FeatureIdToUserInfo featureIdToUserInfo;
        if (withAuthors_) {
            featureIdToUserInfo = getBlackboxUserInfos(features);
        }

        auto featureFormatter =
            [this, featureIdToUserInfo = std::move(featureIdToUserInfo)]
            (renderer::feature::Feature& rdrFeature, const db::Feature& dbFeature)
            {
                rdrFeature.setType(renderer::feature::FeatureType::Text);
                rdrFeature.geom().shapes() = asVertices(dbFeature.mercatorPos());
                rdrFeature.zOrder() = days(dbFeature.timestamp()).count();

                json::Builder jsonBuilder;
                jsonBuilder << [&](json::ObjectBuilder obj) {
                    obj[HOTSPOT_ENABLED_ATTR_NAME] = true;
                    obj[LAYER_ATTR_NAME] = LAYER_EDGES;
                    obj[FIELD_ID] = std::to_string(dbFeature.id());
                    obj[FIELD_PROPERTIES] << [&](json::ObjectBuilder obj) {
                        formatDbFeature(obj, dbFeature, featureIdToUserInfo);
                    };
                };

                rdrFeature.attr.set(jsonBuilder.str().c_str());
            };
        orderByTime(features);
        return std::make_shared<FeatureIterable<db::Feature>>(std::move(features), std::move(featureFormatter));
    }

    std::shared_ptr<renderer::data_set::FeatureIterable>
    loadWalkPhotos(const renderer::data_set::ViewQueryContext& ctx)
    {
        TimeProfiler timer("loadWalkPhotos", 200);
        FeatureFilter filter = featureFilter_;
        filter.datasets = db::standalonePhotosDatasets();

        auto mercatorBbox = toGeolibBoundingBox(
            extendBox(ctx.params.bbox, ctx.params.zoomRange.min()));
        auto geoBbox = geolib3::convertMercatorToGeodetic(mercatorBbox);

        db::Features features =
            loadFeaturesWithMargins(dataAccess_, mercatorBbox,
                                        2 * GAP_DRIVING_LIMIT_METERS, filter);

        auto featureIds = db::TIdSet{};
        for (const auto& feature : features) {
            featureIds.insert(feature.id());
        }
        auto walkObjectFeatures =
            dataAccess_->getWalkFeaturesByWalkObjectsBbox(geoBbox);
        for (auto& feature : walkObjectFeatures) {
            if (!featureIds.contains(feature.id()) && !filter(feature)) {
                features.push_back(std::move(feature));
            }
        }

        auto targetPoints = dataAccess_->getWalkObjectsByFeatures(features);

        FeatureIdToUserInfo featureIdToUserInfo;
        if (withAuthors_) {
            featureIdToUserInfo = getBlackboxUserInfos(features);
        }

        auto featureFormatter =
            [this, targetPoints = std::move(targetPoints), featureIdToUserInfo = std::move(featureIdToUserInfo)]
                    (renderer::feature::Feature& rdrFeature, const db::Feature& dbFeature)
            {
                auto it = targetPoints.find(dbFeature.id());
                auto pos = (it == targetPoints.end())
                               ? dbFeature.mercatorPos()
                               : geolib3::convertGeodeticToMercator(it->second);

                rdrFeature.setType(renderer::feature::FeatureType::Text);
                rdrFeature.geom().shapes() = asVertices(pos);
                rdrFeature.zOrder() = days(dbFeature.timestamp()).count();

                json::Builder jsonBuilder;
                jsonBuilder << [&](json::ObjectBuilder obj) {
                    obj[HOTSPOT_ENABLED_ATTR_NAME] = true;
                    // Attribute LAYER_ATTR_NAME must be defined, but value is not significant
                    obj[LAYER_ATTR_NAME] = LAYER_EDGES;
                    obj[FIELD_ID] = std::to_string(dbFeature.id());
                    obj[FIELD_PROPERTIES] << [&](json::ObjectBuilder obj) {
                        formatDbFeature(obj, dbFeature, featureIdToUserInfo, targetPoints);
                    };
                };

                rdrFeature.attr.set(jsonBuilder.str().c_str());
            };
        orderByTime(features);
        return std::make_shared<FeatureIterable<db::Feature>>(std::move(features), std::move(featureFormatter));
    }

    std::shared_ptr<renderer::data_set::FeatureIterable>
    loadRides(const renderer::data_set::ViewQueryContext& ctx)
    {
        TimeProfiler timer("loadRides", 200);
        FeatureFilter filter = featureFilter_;
        filter.datasets = db::ridePhotosDatasets();

        db::Features features;
        features = loadFeaturesWithMargins(dataAccess_, toGeolibBoundingBox(extendBox(ctx.params.bbox, ctx.params.zoomRange.min())),
                                        2 * GAP_DRIVING_LIMIT_METERS, filter);

        std::vector<geolib3::Polyline2> lines;
        forEachPassage(
            FeaturesRange(features.begin(), features.end()),
            [&](db::Features::iterator begin, db::Features::iterator end) {
                geolib3::Polyline2 line;
                for (auto it = begin; it != end; ++it)
                    line.add(it->mercatorPos());
                if (line.pointsNumber() > 1) {
                    lines.push_back(std::move(line));
                }
                return LoopControl::Continue;
            }
        );

        auto featureFormatter =
            [](renderer::feature::Feature& feature, const geolib3::Polyline2& line)
            {
                feature.setType(renderer::feature::FeatureType::Polyline);
                feature.geom().shapes() = asVertices(line);

                auto attr = feature.attr.init();
                renderer::json::ObjectBuilder bld{attr.get()};
                bld.Put(HOTSPOT_ENABLED_ATTR_NAME, false);
                // Attribute LAYER_ATTR_NAME must be defined, but value is not significant
                bld.Put(LAYER_ATTR_NAME, LAYER_EDGES);
            };

        return std::make_shared<FeatureIterable<geolib3::Polyline2>>(std::move(lines), featureFormatter);
    }

    renderer::data_set::LayerView
    queryLayer(const std::string& layerName, const renderer::data_set::ViewQueryContext& ctx) const
    {
        const LayerInfo& layerInfo = layersInfoMap_.at(layerName);
        return renderer::data_set::LayerView{
            renderer::data_set::LayerInfo(layerName, layerInfo.featureType),
            layerInfo.loader(ctx)
        };
    }

    IDataAccessPtr dataAccess_;
    IGraphPtr roadGraph_;
    IGraphPtr pedestrianGraph_;
    const FeatureFilter featureFilter_;

    std::unordered_map<std::string, LayerInfo> layersInfoMap_;
    renderer::data_set::DataSetInfo datasetInfo_;
    const std::string baseUrl_;
    const bool withAuthors_;

}; // class DataAccessDataSet


class HotspotGenerator : public renderer::hotspots::HotspotGenerator {
public:
    std::string layer;

    // Generates attributive part for "properties" value
    // {
    //     "type": "Feature",
    //     "properties": {
    //         // required field
    //         "HotspotMetaData": {
    //             "id": $id,
    //         }
    //     }
    // }
    // Parameter source holds loaded from database objects attributes
    rapidjson::Value generate(
        const rapidjson::Value& source,
        const std::string& /* locale */,
        rapidjson::Value::AllocatorType* alloc) const override
    {
        if (!source.IsObject() || !source[HOTSPOT_ENABLED_ATTR_NAME.c_str()].GetBool()) {
            return {};
        }

        rapidjson::Value result(rapidjson::kObjectType);
        rjhelper::ObjectBuilder builder{&result, alloc};

        builder.Put("HotspotMetaData", [&](rjhelper::ObjectBuilder hsmd) {
            if (source.HasMember(FIELD_ID.c_str())) {
                hsmd.Put(FIELD_ID.c_str(), source[FIELD_ID.c_str()]);
            }
            hsmd.Put("layer", layer);
        });
        if (source.HasMember(FIELD_PROPERTIES.c_str())) {
            builder.Put(FIELD_PROPERTIES.c_str(), source[FIELD_PROPERTIES.c_str()]);
        }
        return result;
    }

    const std::string& settings(const std::string& /* layerName */) const override
    {
        static const std::string SETTINGS =
            R"(
                {
                    "enableRenderedGeometry": true,
                    "enableGeometry": true,
                    "geometryCoordinateOrder": "LonLat"
                }
            )";
        return SETTINGS;
    }
};

renderer::data_set::LayerFilter makeLayerFilter(
    std::shared_ptr<std::unordered_set<std::string>> acceptedLayers)
{
    return [acceptedLayers](const renderer::data_set::LayerInfo& info) {
        return acceptedLayers->count(info.id());
    };
}

void setLayerFilter(
    renderer::style2_renderer::RenderParams& params,
    const std::string& layer)
{
    auto dataSetLayers = std::make_shared<std::unordered_set<std::string>>();

    if (layer == LAYER_EDGES) {
        dataSetLayers->insert("edge");
    } else if (layer == LAYER_PEDESTRIAN_EDGES) {
        dataSetLayers->insert("uncovered_pedestrian");
        dataSetLayers->insert("covered_pedestrian");
    } else {
        throw yacare::errors::NotFound() << layer << " not found";
    }

    params.layerFilter = makeLayerFilter(dataSetLayers);
}

} // anonymous namespace

int maxDrawableFcByZoom(int zoom, const std::string& layer)
{
    if (layer == LAYER_PEDESTRIAN_EDGES ||
        layer == LAYER_PEDESTRIAN_EDGES_NIGHT ||
        layer == LAYER_PHOTO_PEDESTRIAN) {
        return 10;
    }
    else {
        return maxRoadFcByZoom(zoom);
    }
}

std::optional<std::vector<char>>
renderRasterTile(const TileRequestCacheKey& tileRequest,
                 IDataAccessPtr dataAccess,
                 IGraphPtr graph,
                 IGraphPtr pedestrianGraph,
                 std::shared_ptr<renderer::image::ImageStorage> imageStorage,
                 const renderer::design::Design& design)
{
    const auto& designMode = design.mode(
        renderer::api_params::parseTheme(tileRequest.theme));

    renderer::style2_renderer::RenderParams renderParams(
        tileRequest.x, tileRequest.y, tileRequest.z,
        designMode.styleSet, designMode.resources
    );

    renderer::style2_renderer::DataSets datasets;
    datasets.emplace(tileRequest.layer,
                     std::make_unique<DataAccessDataSet>(
                         dataAccess, graph, pedestrianGraph,
                         tileRequest.filter));

    renderParams.scale = tileRequest.scale;
    auto renderResponse = renderer::style2_renderer::MapRenderer(
        imageStorage).renderPng(datasets, renderParams);
    if (renderResponse.hasData) {
        return renderResponse.png;
    } else {
        return std::nullopt;
    }
}

// Render tile in vector 2 format
template<>
std::optional<std::vector<char>>
renderVectorTile<renderer::vecdata::ResultFormat>(
    const TileRequestCacheKey& tileRequest,
    IDataAccessPtr dataAccess,
    IGraphPtr graph,
    IGraphPtr pedestrianGraph,
    renderer::vecdata::ResultFormat format,
    std::shared_ptr<renderer::image::ImageStorage> imageStorage,
    const renderer::design::Design& design)
{
    renderer::style2_renderer::VecRenderParams renderParams(
        renderer::style2_renderer::RenderParams(
            tileRequest.x, tileRequest.y, tileRequest.z,
            design.basicMode.styleSet, design.basicMode.resources
        )
    );
    renderParams.format = format;
    renderParams.extraModes.push_back(design.nightMode);

    renderParams.equidistantFormat = tileRequest.equidistantFormat;

    renderer::style2_renderer::DataSets datasets;
    datasets.emplace(tileRequest.layer,
                     std::make_unique<DataAccessDataSet>(
                         dataAccess, graph, pedestrianGraph,
                         tileRequest.filter));

    renderParams.ftZoomRange = {tileRequest.zmin.value_or(tileRequest.z),
                                tileRequest.zmax.value_or(tileRequest.z)};
    return renderer::style2_renderer::VecRenderer(
        imageStorage).render(datasets, renderParams);
}

// Render tile in vector 3 format
template<>
std::optional<std::vector<char>>
renderVectorTile<renderer::vecdata3::ResultFormat>(
    const TileRequestCacheKey& tileRequest,
    IDataAccessPtr dataAccess,
    IGraphPtr graph,
    IGraphPtr pedestrianGraph,
    renderer::vecdata3::ResultFormat format,
    std::shared_ptr<renderer::image::ImageStorage>,
    const renderer::design::Design& design)
{
    renderer::style2_renderer::Vec3RenderParams renderParams(
        renderer::style2_renderer::RenderParams(
            tileRequest.x, tileRequest.y, tileRequest.z,
            design.basicMode.styleSet, design.basicMode.resources
        )
    );
    renderParams.format = format;
    setLayerFilter(renderParams, tileRequest.layer);

    renderParams.bitness = renderer::vecdata3::TileBitness{10};

    renderer::style2_renderer::DataSets datasets;
    datasets.emplace(tileRequest.layer,
                     std::make_unique<DataAccessDataSet>(
                         dataAccess, graph, pedestrianGraph,
                         tileRequest.filter));

    renderParams.ftZoomRange = {tileRequest.zmin.value_or(tileRequest.z),
                                tileRequest.zmax.value_or(tileRequest.z)};

    return renderer::style2_renderer::Vec3Renderer{}
        .render(datasets, renderParams);
}

std::string
renderHotspotTile(const TileRequestCacheKey& tileRequest,
                  IDataAccessPtr dataAccess,
                  IGraphPtr graph,
                  IGraphPtr pedestrianGraph,
                  const std::string& baseUrl,
                  const std::string& callback,
                  std::shared_ptr<renderer::image::ImageStorage> imageStorage,
                  const renderer::design::Design& design)
{
    HotspotGenerator hotspotGenerator;
    hotspotGenerator.layer = tileRequest.layer;

    renderer::style2_renderer::RenderParams renderParams(
        tileRequest.x, tileRequest.y, tileRequest.z,
        design.basicMode.styleSet, design.basicMode.resources
    );

    renderer::style2_renderer::HotspotRenderParams hotspotRenderParams(renderParams);
    if (!callback.empty()) {
        hotspotRenderParams.jsCallback = callback;
    }

    renderer::style2_renderer::DataSets datasets;
    datasets.emplace(tileRequest.layer,
                     std::make_unique<DataAccessDataSet>(dataAccess,
                                                         graph,
                                                         pedestrianGraph,
                                                         tileRequest.filter,
                                                         baseUrl,
                                                         tileRequest.withAuthors));

    return renderer::style2_renderer::MapRenderer(imageStorage)
        .renderHotspots(datasets, hotspotRenderParams, hotspotGenerator);
}

} // namespace maps::mrc::browser
