From ea29932e9075ff1239ed7bf68b37ba0291a2ccb4 Mon Sep 17 00:00:00 2001 From: alasram Date: Wed, 26 Mar 2025 04:36:44 -0700 Subject: [PATCH] Support for routes and vanishing routes rendering --- CMakeLists.txt | 8 + include/mbgl/gfx/renderer_backend.hpp | 12 +- include/mbgl/gfx/rendering_stats.hpp | 1 + include/mbgl/gl/renderer_backend.hpp | 1 + include/mbgl/route/id_pool.hpp | 253 +++++ include/mbgl/route/id_types.hpp | 37 + include/mbgl/route/route.hpp | 85 ++ include/mbgl/route/route_manager.hpp | 88 ++ include/mbgl/route/route_segment.hpp | 34 + .../shaders/gl/drawable_line_gradient.hpp | 13 +- include/mbgl/shaders/line_layer_ubo.hpp | 6 +- include/mbgl/style/layers/line_layer.hpp | 10 + include/mbgl/style/types.hpp | 5 + .../MapLibreAndroid/src/cpp/map_renderer.cpp | 9 + .../MapLibreAndroid/src/cpp/map_renderer.hpp | 3 + .../src/cpp/native_map_view.cpp | 291 +++++- .../src/cpp/native_map_view.hpp | 50 + .../org/maplibre/android/maps/MapView.java | 137 +++ .../org/maplibre/android/maps/NativeMap.java | 29 + .../maplibre/android/maps/NativeMapView.java | 157 +++ .../org/maplibre/android/maps/RouteID.java | 14 + .../maplibre/android/maps/RouteOptions.java | 23 + .../android/maps/RouteSegmentOptions.java | 9 + .../activity/style/GradientLineActivity.kt | 1 + platform/glfw/glfw_renderer_frontend.cpp | 7 + platform/glfw/glfw_renderer_frontend.hpp | 4 +- platform/glfw/glfw_view.cpp | 905 ++++++++++++++++-- platform/glfw/glfw_view.hpp | 62 ++ src/mbgl/gfx/renderer_backend.cpp | 6 + src/mbgl/gfx/rendering_stats.cpp | 46 + src/mbgl/gl/renderer_backend.cpp | 7 + src/mbgl/programs/attributes.hpp | 2 + src/mbgl/programs/uniforms.hpp | 2 + .../renderer/layers/line_layer_tweaker.cpp | 15 +- .../renderer/layers/line_layer_tweaker.hpp | 5 + .../renderer/layers/render_line_layer.cpp | 28 +- src/mbgl/route/route.cpp | 302 ++++++ src/mbgl/route/route_manager.cpp | 774 +++++++++++++++ src/mbgl/route/route_segment.cpp | 93 ++ src/mbgl/style/layers/line_layer.cpp | 44 + src/mbgl/style/layers/line_layer_impl.hpp | 1 + .../style/layers/line_layer_properties.hpp | 13 +- 42 files changed, 3519 insertions(+), 73 deletions(-) create mode 100644 include/mbgl/route/id_pool.hpp create mode 100644 include/mbgl/route/id_types.hpp create mode 100644 include/mbgl/route/route.hpp create mode 100644 include/mbgl/route/route_manager.hpp create mode 100644 include/mbgl/route/route_segment.hpp create mode 100644 platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteID.java create mode 100644 platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteOptions.java create mode 100644 platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteSegmentOptions.java create mode 100644 src/mbgl/route/route.cpp create mode 100644 src/mbgl/route/route_manager.cpp create mode 100644 src/mbgl/route/route_segment.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d56e47855ef..4c8a6093c65b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -443,6 +443,11 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/util/work_request.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/work_task.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/util/work_task_impl.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/route/id_pool.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/route/id_types.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/route/route_manager.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/route/route.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/route/route_segment.hpp ) list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/actor/mailbox.cpp @@ -1010,6 +1015,9 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/util/version.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/version.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/work_request.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/route/route_manager.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/route/route_segment.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/route/route.cpp ) if(MLN_WITH_OPENGL) diff --git a/include/mbgl/gfx/renderer_backend.hpp b/include/mbgl/gfx/renderer_backend.hpp index 5c6e760573da..c0591fe15ed0 100644 --- a/include/mbgl/gfx/renderer_backend.hpp +++ b/include/mbgl/gfx/renderer_backend.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -36,10 +37,17 @@ class RendererBackend { // Return the background thread pool assigned to this backend TaggedScheduler& getThreadPool() noexcept { return threadPool; } - /// Returns the device's context. Context& getContext(); + /** + * @brief this method is to be used when the context is needed outside of the rendering scope and should + * be called only at such times. An example of this would be to get the RenderingStats from the context. + * + * @return the context for the backend + */ + Context& getContextOutsideRenderingScope(); + template T& getContext() { return static_cast(getContext()); @@ -50,6 +58,8 @@ class RendererBackend { /// Returns a reference to the default surface that should be rendered on. virtual Renderable& getDefaultRenderable() = 0; + virtual mbgl::gfx::RenderingStats getRenderingStats() { return {}; }; + #if MLN_DRAWABLE_RENDERER /// One-time shader initialization virtual void initShaders(gfx::ShaderRegistry&, const ProgramParameters&) = 0; diff --git a/include/mbgl/gfx/rendering_stats.hpp b/include/mbgl/gfx/rendering_stats.hpp index 22918e28ae0d..50d2301a5a92 100644 --- a/include/mbgl/gfx/rendering_stats.hpp +++ b/include/mbgl/gfx/rendering_stats.hpp @@ -63,6 +63,7 @@ struct RenderingStats { int stencilUpdates = 0; RenderingStats& operator+=(const RenderingStats&); + std::string toJSONString() const; #if !defined(NDEBUG) std::string toString(std::string_view separator) const; diff --git a/include/mbgl/gl/renderer_backend.hpp b/include/mbgl/gl/renderer_backend.hpp index 145c1f78f224..8786d827c657 100644 --- a/include/mbgl/gl/renderer_backend.hpp +++ b/include/mbgl/gl/renderer_backend.hpp @@ -22,6 +22,7 @@ class RendererBackend : public gfx::RendererBackend { /// Called prior to rendering to update the internally assumed OpenGL state. virtual void updateAssumedState() = 0; + mbgl::gfx::RenderingStats getRenderingStats() override; #if MLN_DRAWABLE_RENDERER /// One-time shader initialization diff --git a/include/mbgl/route/id_pool.hpp b/include/mbgl/route/id_pool.hpp new file mode 100644 index 000000000000..189bb2a734da --- /dev/null +++ b/include/mbgl/route/id_pool.hpp @@ -0,0 +1,253 @@ +#pragma once + +#include // uint32_t +#include +#include +#include +namespace mbgl { +namespace gfx { + +/*** + ** An IDpool is a pool of IDs from which you can create consecutive IDs. This is very similary to graphics APIs such as + * opengl or vulkan where you an ID is used to represent a resource. Every ID constructed is consecutive and when an + * ID is disposed, it returns back to the pool for re-use. Every invocation to create an ID will return the smallest + * contiguous ID. + */ +class IDpool { +private: + // Change to uint16_t here for a more compact implementation if 16bit or less IDs work for you. + typedef uint32_t uint; + + struct Range { + uint first_; + uint last_; + }; + + Range* ranges_; // Sorted array of ranges of free IDs + uint count_; // Number of ranges in list + uint capacity_; // Total capacity of range list + + IDpool& operator=(const IDpool&); + IDpool(const IDpool&); + +public: + explicit IDpool(const uint max_id) { + // Start with a single range, from 0 to max allowed ID (specified) + ranges_ = static_cast(::malloc(sizeof(Range))); + capacity_ = 1; + reset(max_id); + } + + ~IDpool() { ::free(ranges_); } + + void reset(const uint max_id) { + ranges_[0].first_ = 0; + ranges_[0].last_ = max_id; + count_ = 1; + } + + bool createID(uint& id) { + if (ranges_[0].first_ <= ranges_[0].last_) { + id = ranges_[0].first_; + + // If current range is full and there is another one, that will become the new current range + if (ranges_[0].first_ == ranges_[0].last_ && count_ > 1) { + destroyRange(0); + } else { + ++ranges_[0].first_; + } + return true; + } + + // No availble ID left + return false; + } + + bool createRangeID(uint& id, const uint count) { + uint i = 0; + do { + const uint range_count = 1 + ranges_[i].last_ - ranges_[i].first_; + if (count <= range_count) { + id = ranges_[i].first_; + + // If current range is full and there is another one, that will become the new current range + if (count == range_count && i + 1 < count_) { + destroyRange(i); + } else { + ranges_[i].first_ += count; + } + return true; + } + ++i; + } while (i < count_); + + // No range of free IDs was large enough to create the requested continuous ID sequence + return false; + } + + bool destroyID(const uint id) { return destroyRangeID(id, 1); } + + bool destroyRangeID(const uint id, const uint count) { + const uint end_id = id + count; + + // Binary search of the range list + uint i0 = 0; + uint i1 = count_ - 1; + + for (;;) { + const uint i = (i0 + i1) / 2; + + if (id < ranges_[i].first_) { + // Before current range, check if neighboring + if (end_id >= ranges_[i].first_) { + if (end_id != ranges_[i].first_) + return false; // Overlaps a range of free IDs, thus (at least partially) invalid IDs + + // Neighbor id, check if neighboring previous range too + if (i > i0 && id - 1 == ranges_[i - 1].last_) { + // Merge with previous range + ranges_[i - 1].last_ = ranges_[i].last_; + destroyRange(i); + } else { + // Just grow range + ranges_[i].first_ = id; + } + return true; + } else { + // Non-neighbor id + if (i != i0) { + // Cull upper half of list + i1 = i - 1; + } else { + // Found our position in the list, insert the deleted range here + insertRange(i); + ranges_[i].first_ = id; + ranges_[i].last_ = end_id - 1; + return true; + } + } + } else if (id > ranges_[i].last_) { + // After current range, check if neighboring + if (id - 1 == ranges_[i].last_) { + // Neighbor id, check if neighboring next range too + if (i < i1 && end_id == ranges_[i + 1].first_) { + // Merge with next range + ranges_[i].last_ = ranges_[i + 1].last_; + destroyRange(i + 1); + } else { + // Just grow range + ranges_[i].last_ += count; + } + return true; + } else { + // Non-neighbor id + if (i != i1) { + // Cull bottom half of list + i0 = i + 1; + } else { + // Found our position in the list, insert the deleted range here + insertRange(i + 1); + ranges_[i + 1].first_ = id; + ranges_[i + 1].last_ = end_id - 1; + return true; + } + } + } else { + // Inside a free block, not a valid ID + return false; + } + } + } + + bool isID(const uint id) const { + // Binary search of the range list + uint i0 = 0; + uint i1 = count_ - 1; + + for (;;) { + const uint i = (i0 + i1) / 2; + + if (id < ranges_[i].first_) { + if (i == i0) return true; + + // Cull upper half of list + i1 = i - 1; + } else if (id > ranges_[i].last_) { + if (i == i1) return true; + + // Cull bottom half of list + i0 = i + 1; + } else { + // Inside a free block, not a valid ID + return false; + } + } + } + + uint getAvailableIDs() const { + uint count = count_; + uint i = 0; + + do { + count += ranges_[i].last_ - ranges_[i].first_; + ++i; + } while (i < count_); + + return count; + } + + uint getLargestContinuousRange() const { + uint max_count = 0; + uint i = 0; + + do { + uint count = ranges_[i].last_ - ranges_[i].first_ + 1; + if (count > max_count) max_count = count; + + ++i; + } while (i < count_); + + return max_count; + } + + void printRanges() const { + uint i = 0; + for (;;) { + std::stringstream log; + if (ranges_[i].first_ < ranges_[i].last_) + log << ranges_[i].first_ << "-" << ranges_[i].last_; + else if (ranges_[i].first_ == ranges_[i].last_) + log << ranges_[i].first_; + else + log << "-"; + + ++i; + if (i >= count_) { + log << "\n"; + return; + } + + log << ", "; + mbgl::Log::Info(mbgl::Event::General, log.str()); + } + } + +private: + void insertRange(const uint index) { + if (count_ >= capacity_) { + capacity_ += capacity_; + ranges_ = (Range*)realloc(ranges_, capacity_ * sizeof(Range)); + } + + ::memmove(ranges_ + index + 1, ranges_ + index, (count_ - index) * sizeof(Range)); + ++count_; + } + + void destroyRange(const uint index) { + --count_; + ::memmove(ranges_ + index, ranges_ + index + 1, (count_ - index) * sizeof(Range)); + } +}; + +} // namespace gfx +} // namespace mbgl diff --git a/include/mbgl/route/id_types.hpp b/include/mbgl/route/id_types.hpp new file mode 100644 index 000000000000..afe00f52f3d9 --- /dev/null +++ b/include/mbgl/route/id_types.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#define INVALID_ID (uint32_t(~0)) + +#define MAKEID_TYPE(name) \ + typedef struct name { \ + uint32_t id; \ + name() { \ + id = INVALID_ID; \ + } \ + name(uint32_t nid) { \ + id = nid; \ + } \ + bool isValid() const { \ + return id != INVALID_ID; \ + } \ + friend bool operator<(const name& lhs, const name& rhs) { \ + return lhs.id < rhs.id; \ + } \ + friend bool operator==(const name& lhs, const name& rhs) { \ + return lhs.id == rhs.id; \ + } \ + friend bool operator!=(const name& lhs, const name& rhs) { \ + return lhs.id != rhs.id; \ + } \ + } name; + +template +struct IDHasher { + size_t operator()(const EntityID& eid) const { return eid.id; } +}; + +MAKEID_TYPE(RouteID); +MAKEID_TYPE(RouteSegmentID); diff --git a/include/mbgl/route/route.hpp b/include/mbgl/route/route.hpp new file mode 100644 index 000000000000..89917965c876 --- /dev/null +++ b/include/mbgl/route/route.hpp @@ -0,0 +1,85 @@ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace mbgl { +namespace route { + +struct RouteSegmentOptions; +class RouteSegment; + +struct RouteOptions { + Color outerColor = Color(1, 1, 1, 1); + Color innerColor = Color(0, 0, 1, 1); + Color outerClipColor = Color(0, 0, 0, 0); + Color innerClipColor = Color(0, 0, 0, 0); + float outerWidth = 16; + float innerWidth = 14; + std::map outerWidthZoomStops; + std::map innerWidthZoomStops; + bool useDynamicWidths = false; + std::string layerBefore; +}; + +/*** + * A route is a polyline that represents a road between two locations. There can be multiple routes in the case of + * multiple stops. Each route can have route segments. Routes segments represents a traffic zone. A route must have a + * base route segment (aka casing) that contains all the vertices of the polyline that makes it. + */ +class Route { +public: + Route() = default; + Route(const LineString& geometry, const RouteOptions& ropts); + void routeSegmentCreate(const RouteSegmentOptions&); + std::map getRouteSegmentColorStops(const mbgl::Color& routeColor); + std::map getRouteColorStops(const mbgl::Color& routeColor) const; + std::vector getRouteSegmentDistances() const; + void routeSetProgress(const double t); + double routeGetProgress() const; + double getTotalDistance() const; + double getProgressPercent(const Point& progressPoint) const; + mbgl::LineString getGeometry() const; + bool hasRouteSegments() const; + const RouteOptions& getRouteOptions() const; + bool routeSegmentsClear(); + Route& operator=(const Route& other) noexcept; + uint32_t getNumRouteSegments() const; + + std::string segmentsToString(uint32_t tabcount) const; + +private: + struct SegmentRange { + std::pair range; + Color color; + }; + + struct SegmentComparator { + bool operator()(const RouteSegment& lhs, const RouteSegment& rhs) const { + if (lhs.getNormalizedPositions().empty() || rhs.getNormalizedPositions().empty()) { + return true; + } + + return lhs.getNormalizedPositions()[0] < rhs.getNormalizedPositions()[0]; + } + }; + + std::vector compactSegments() const; + + RouteOptions routeOptions_; + double progress_ = 0.0; + std::vector segDistances_; + std::set segments_; + mbgl::LineString geometry_; + std::map segGradient_; + double totalDistance_ = 0.0; +}; + +} // namespace route +} // namespace mbgl diff --git a/include/mbgl/route/route_manager.hpp b/include/mbgl/route/route_manager.hpp new file mode 100644 index 000000000000..3e8fbd154ac5 --- /dev/null +++ b/include/mbgl/route/route_manager.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +namespace style { +class Style; +} // namespace style + +namespace route { + +struct RouteMgrStats { + uint32_t numFinalizedInvoked = 0; + uint32_t numRoutes = 0; + uint32_t numRouteSegments = 0; + std::string finalizeMillis; + bool inconsistentAPIusage = false; + double avgRouteCreationInterval = 0.0; + double avgRouteSegmentCreationInterval = 0.0; +}; + +/*** + * A route manager manages construction, disposal and updating of one or more routes. It is the API facade and is 1:1 + *with a map view. You can create and mutate multiple routes as many times and after you're done with mutating routes, + *the client code needs to call finalize() on the route manager for it to create needed resources underneath the hood + *for rendering. + **/ +class RouteManager final { +public: + RouteManager(); + void setStyle(style::Style&); + const std::string getStats(); + bool hasStyle() const; + RouteID routeCreate(const LineString& geometry, const RouteOptions& ropts); + bool routeSegmentCreate(const RouteID&, const RouteSegmentOptions&); + bool routeSetProgress(const RouteID&, const double progress); + bool routeSetProgress(const RouteID&, const mbgl::Point& progressPoint); + void routeClearSegments(const RouteID&); + bool routeDispose(const RouteID&); + std::vector getAllRoutes() const; + std::string getActiveRouteLayerName(const RouteID& routeID) const; + std::string getBaseRouteLayerName(const RouteID& routeID) const; + std::string getActiveGeoJSONsourceName(const RouteID& routeID) const; + std::string getBaseGeoJSONsourceName(const RouteID& routeID) const; + std::string captureSnapshot() const; + int getTopMost(const std::vector& routeList) const; + + bool hasRoutes() const; + void finalize(); + + ~RouteManager(); + +private: + static const std::string BASE_ROUTE_LAYER; + static const std::string ACTIVE_ROUTE_LAYER; + static const std::string GEOJSON_BASE_ROUTE_SOURCE_ID; + static const std::string GEOJSON_ACTIVE_ROUTE_SOURCE_ID; + + enum class DirtyType { + dtRouteSegments, + dtRouteProgress, + dtRouteGeometry, + // TODO: may be route puck position + }; + std::string dirtyTypeToString(const DirtyType& dt) const; + + std::unordered_map>> dirtyRouteMap_; + std::vector apiCalls_; + + RouteMgrStats stats_; + gfx::IDpool routeIDpool_ = gfx::IDpool(100); + + void finalizeRoute(const RouteID& routeID, const DirtyType& dt); + void validateAddToDirtyBin(const RouteID& routeID, const DirtyType& dirtyBin); + // TODO: change this to weak reference + style::Style* style_ = nullptr; + std::unordered_map> routeMap_; +}; +}; // namespace route + +} // namespace mbgl diff --git a/include/mbgl/route/route_segment.hpp b/include/mbgl/route/route_segment.hpp new file mode 100644 index 000000000000..a6af7c849c57 --- /dev/null +++ b/include/mbgl/route/route_segment.hpp @@ -0,0 +1,34 @@ + +#include +#include +#include + +#pragma once + +namespace mbgl { +namespace route { + +struct RouteSegmentOptions { + Color color = Color(1.0f, 1.f, 1.0f, 1.0f); + LineString geometry; + uint32_t priority = 0; +}; + +class RouteSegment { +public: + RouteSegment() = default; + RouteSegment(const RouteSegmentOptions& routeSegOptions, + const LineString& routeGeometry, + const std::vector& routeGeomDistances, + double routeTotalDistance); + std::vector getNormalizedPositions() const; + RouteSegmentOptions getRouteSegmentOptions() const; + + ~RouteSegment(); + +private: + RouteSegmentOptions options_; + std::vector normalizedPositions_; +}; +} // namespace route +} // namespace mbgl diff --git a/include/mbgl/shaders/gl/drawable_line_gradient.hpp b/include/mbgl/shaders/gl/drawable_line_gradient.hpp index c77e020c44ab..3054d218c7df 100644 --- a/include/mbgl/shaders/gl/drawable_line_gradient.hpp +++ b/include/mbgl/shaders/gl/drawable_line_gradient.hpp @@ -44,8 +44,9 @@ layout (std140) uniform LineGradientDrawableUBO { lowp float u_gapwidth_t; lowp float u_offset_t; lowp float u_width_t; - lowp float drawable_pad1; + highp float u_line_clip_t; lowp float drawable_pad2; + highp vec4 u_clip_color_t; }; layout (std140) uniform LineEvaluatedPropsUBO { @@ -64,6 +65,8 @@ out vec2 v_normal; out vec2 v_width2; out float v_gamma_scale; out highp float v_lineprogress; +flat out highp float v_line_clip; +flat out vec4 v_clip_color; #ifndef HAS_UNIFORM_u_blur layout (location = 2) in lowp vec2 a_blur; @@ -158,6 +161,8 @@ mediump float width = u_width; v_gamma_scale = extrude_length_without_perspective / extrude_length_with_perspective; v_width2 = vec2(outset, inset); + v_line_clip = u_line_clip_t; + v_clip_color = u_clip_color_t; } )"; static constexpr const char* fragment = R"(layout (std140) uniform LineEvaluatedPropsUBO { @@ -178,6 +183,8 @@ in vec2 v_width2; in vec2 v_normal; in float v_gamma_scale; in highp float v_lineprogress; +flat in highp float v_line_clip; +flat in vec4 v_clip_color; #ifndef HAS_UNIFORM_u_blur in lowp float blur; @@ -207,6 +214,10 @@ lowp float opacity = u_opacity; // scaled to [0, 2^15), and the gradient ramp is stored in a texture. vec4 color = texture(u_image, vec2(v_lineprogress, 0.5)); + if(v_lineprogress < v_line_clip) { + color = v_clip_color; + } + fragColor = color * (alpha * opacity); #ifdef OVERDRAW_INSPECTOR diff --git a/include/mbgl/shaders/line_layer_ubo.hpp b/include/mbgl/shaders/line_layer_ubo.hpp index 34a713f40abe..64a8f8689304 100644 --- a/include/mbgl/shaders/line_layer_ubo.hpp +++ b/include/mbgl/shaders/line_layer_ubo.hpp @@ -43,11 +43,11 @@ struct alignas(16) LineGradientDrawableUBO { /* 76 */ float gapwidth_t; /* 80 */ float offset_t; /* 84 */ float width_t; - /* 88 */ float pad1; + /* 88 */ float lineclip_t; /* 92 */ float pad2; - /* 96 */ + /* 96 */ std::array clip_color_t; }; -static_assert(sizeof(LineGradientDrawableUBO) == 6 * 16); +static_assert(sizeof(LineGradientDrawableUBO) == 7 * 16); // // Line pattern diff --git a/include/mbgl/style/layers/line_layer.hpp b/include/mbgl/style/layers/line_layer.hpp index 1a84b9a987b5..79058945ef0c 100644 --- a/include/mbgl/style/layers/line_layer.hpp +++ b/include/mbgl/style/layers/line_layer.hpp @@ -44,6 +44,9 @@ class LineLayer final : public Layer { const PropertyValue& getLineSortKey() const; void setLineSortKey(const PropertyValue&); + void setGradientLineFilter(const LineGradientFilterType&); + LineGradientFilterType getGradientLineFilter() const; + // Paint properties static PropertyValue getDefaultLineBlur(); @@ -112,6 +115,13 @@ class LineLayer final : public Layer { void setLineWidthTransition(const TransitionOptions&); TransitionOptions getLineWidthTransition() const; + void setGradientLineClip(const PropertyValue&); + const PropertyValue& getGradientLineClip() const; + + void setGradientLineClipColor(const PropertyValue&); + const PropertyValue& getGradientLineClipColor() const; + + // Private implementation class Impl; diff --git a/include/mbgl/style/types.hpp b/include/mbgl/style/types.hpp index 3d5780935a89..543960e04066 100644 --- a/include/mbgl/style/types.hpp +++ b/include/mbgl/style/types.hpp @@ -125,5 +125,10 @@ enum class LightAnchorType : bool { Viewport }; +enum class LineGradientFilterType : uint8_t { + Linear, + Nearest +}; + } // namespace style } // namespace mbgl diff --git a/platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp b/platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp index 9fd889e05ef6..5b9c072f8c17 100644 --- a/platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -227,10 +228,18 @@ void MapRenderer::render(JNIEnv&) { } } +gfx::RenderingStats MapRenderer::getRenderingStats() { + return backend->getImpl().getRenderingStats(); +} + void MapRenderer::onSurfaceCreated(JNIEnv& env, const jni::Object& surface) { // Lock as the initialization can come from the main thread or the GL thread first std::lock_guard lock(initialisationMutex); + platform::makeThreadHighPriority(); + Log::Debug(Event::Android, + "Setting render thread priority to " + std::to_string(platform::getCurrentThreadPriority())); + // The android system will have already destroyed the underlying // GL resources if this is not the first initialization and an // attempt to clean them up will fail diff --git a/platform/android/MapLibreAndroid/src/cpp/map_renderer.hpp b/platform/android/MapLibreAndroid/src/cpp/map_renderer.hpp index 9fc962c0d353..7861d8618388 100644 --- a/platform/android/MapLibreAndroid/src/cpp/map_renderer.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/map_renderer.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -70,6 +71,8 @@ class MapRenderer : public Scheduler { // any thread. ActorRef actor() const; + gfx::RenderingStats getRenderingStats(); + // From Scheduler. Schedules by using callbacks to the // JVM to process the mailbox on the right thread. void schedule(std::function&& scheduled) override; diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index 082443b98efa..070a73abbf2e 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -22,12 +22,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include // Java -> C++ conversion #include "style/android_conversion.hpp" @@ -37,6 +39,7 @@ // C++ -> Java conversion #include "conversion/conversion.hpp" #include "conversion/collection.hpp" +#include "conversion/color.hpp" #include "style/conversion/filter.hpp" #include "geojson/feature.hpp" @@ -54,6 +57,8 @@ #include "run_loop_impl.hpp" #include "style/light.hpp" #include "tile/tile_operation.hpp" +#include "mbgl/route/route_manager.hpp" +#include "mbgl/route/route.hpp" namespace mbgl { namespace android { @@ -75,7 +80,7 @@ NativeMapView::NativeMapView(jni::JNIEnv& _env, // Create a renderer frontend rendererFrontend = std::make_unique(mapRenderer); - + routeMgr = std::make_unique(); // Create Map options MapOptions options; options.withMapMode(MapMode::Continuous) @@ -304,6 +309,9 @@ jni::Local NativeMapView::getStyleUrl(jni::JNIEnv& env) { void NativeMapView::setStyleUrl(jni::JNIEnv& env, const jni::String& url) { map->getStyle().loadURL(jni::Make(env, url)); + if (routeMgr) { + routeMgr->setStyle(map->getStyle()); + } } jni::Local NativeMapView::getStyleJson(jni::JNIEnv& env) { @@ -312,6 +320,9 @@ jni::Local NativeMapView::getStyleJson(jni::JNIEnv& env) { void NativeMapView::setStyleJson(jni::JNIEnv& env, const jni::String& json) { map->getStyle().loadJSON(jni::Make(env, json)); + if (routeMgr) { + routeMgr->setStyle(map->getStyle()); + } } void NativeMapView::setLatLngBounds(jni::JNIEnv& env, const jni::Object& jBounds) { @@ -925,6 +936,92 @@ jni::Local> NativeMapView::queryShapeAnnotations(JNIEnv& env, return result; } +jint NativeMapView::routeQueryRendered(JNIEnv& env, + jni::jdouble screenSpaceX, + jni::jdouble screenSpaceY, + jni::jint radius) { + if (routeMgr) { + if (!routeMgr->hasRoutes()) { + return -1; + } + + std::vector routeIDs = routeMgr->getAllRoutes(); + std::vector baseLayers; + // we specifically create caches for base layer since base route is wider than the active layer. + // we also check against source name as well as source layer name, since I've seen cases where the source layer + // name is not set in Feature. + std::unordered_map baseLayerMapCache; + std::unordered_map baseSourceMapCache; + for (const auto& routeID : routeIDs) { + std::string baseLayer = routeMgr->getBaseRouteLayerName(routeID); + assert(!baseLayer.empty() && "base layer cannot be empty!"); + if (!baseLayer.empty()) { + baseLayers.push_back(baseLayer); + baseLayerMapCache[baseLayer] = routeID; + } + std::string baseSource = routeMgr->getBaseGeoJSONsourceName(routeID); + assert(!baseSource.empty() && "base source cannot be empty"); + if (!baseSource.empty()) { + baseSourceMapCache[baseSource] = routeID; + } + } + + std::unordered_map> routeCoverage; + // sample multiple ray picks over an radius + for (int i = -radius; i < radius; i++) { + for (int j = -radius; j < radius; j++) { + mbgl::ScreenCoordinate screenpoint = {screenSpaceX + i, screenSpaceY + j}; + std::vector features = rendererFrontend->queryRenderedFeatures(screenpoint, + {baseLayers}); + for (const auto& feature : features) { + if (baseLayerMapCache.find(feature.sourceLayer) != baseLayerMapCache.end()) { + RouteID baseRouteID = baseLayerMapCache[feature.sourceLayer]; + routeCoverage[baseRouteID]++; + } + + // also check cache of geojson source names if the source layer is not set. + if (baseSourceMapCache.find(feature.source) != baseSourceMapCache.end()) { + RouteID baseRouteID = baseSourceMapCache[feature.source]; + routeCoverage[baseRouteID]++; + } + } + } + } + + // when you do a touch at a location, the radius can cover multiple routes. + // find the RouteID that has the maximum touch weight value + int maxTouchWeight = 0; + std::vector maxRouteIDs; + for (const auto& [routeID, weight] : routeCoverage) { + if (weight > maxTouchWeight) { + maxTouchWeight = weight; + } + } + + for (const auto& [routeID, weight] : routeCoverage) { + if (weight == maxTouchWeight) { + maxRouteIDs.push_back(routeID); + } + } + + if (maxRouteIDs.size() == 1) { + return maxRouteIDs[0].id; + } + + if (!maxRouteIDs.empty()) { + int top = routeMgr->getTopMost(maxRouteIDs); + RouteID topRouteID; + if (top >= 0 && top < static_cast(maxRouteIDs.size())) { + topRouteID = maxRouteIDs[top]; + } + + return topRouteID.id; + } + } + + return -1; +} + jni::Local>> NativeMapView::queryRenderedFeaturesForPoint( JNIEnv& env, jni::jfloat x, @@ -1339,7 +1436,197 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::getPrefetchZoomDelta, "nativeGetPrefetchZoomDelta"), METHOD(&NativeMapView::setTileCacheEnabled, "nativeSetTileCacheEnabled"), METHOD(&NativeMapView::getTileCacheEnabled, "nativeGetTileCacheEnabled"), - METHOD(&NativeMapView::triggerRepaint, "nativeTriggerRepaint")); + METHOD(&NativeMapView::triggerRepaint, "nativeTriggerRepaint"), + METHOD(&NativeMapView::setTileLodMinRadius, "nativeSetTileLodMinRadius"), + METHOD(&NativeMapView::getTileLodMinRadius, "nativeGetTileLodMinRadius"), + METHOD(&NativeMapView::setTileLodScale, "nativeSetTileLodScale"), + METHOD(&NativeMapView::getTileLodScale, "nativeGetTileLodScale"), + METHOD(&NativeMapView::setTileLodPitchThreshold, "nativeSetTileLodPitchThreshold"), + METHOD(&NativeMapView::getTileLodPitchThreshold, "nativeGetTileLodPitchThreshold"), + METHOD(&NativeMapView::setTileLodZoomShift, "nativeSetTileLodZoomShift"), + METHOD(&NativeMapView::getTileLodZoomShift, "nativeGetTileLodZoomShift"), + METHOD(&NativeMapView::getLastRenderedTileCount, "nativeGetLastRenderedTileCount"), + METHOD(&NativeMapView::routeCreate, "nativeRouteCreate"), + METHOD(&NativeMapView::routeGetActiveLayerName, "nativeRouteGetActiveLayerName"), + METHOD(&NativeMapView::routeGetBaseLayerName, "nativeRouteGetBaseLayerName"), + METHOD(&NativeMapView::routeDispose, "nativeRouteDispose"), + METHOD(&NativeMapView::routeProgressSet, "nativeRouteSetProgress"), + METHOD(&NativeMapView::routeProgressSetPoint, "nativeRouteSetProgressPoint"), + METHOD(&NativeMapView::routeSegmentsClear, "nativeRouteClearSegments"), + METHOD(&NativeMapView::routeSegmentCreate, "nativeRouteSegmentCreate"), + METHOD(&NativeMapView::getRenderingStats, "nativeGetRenderingStats"), + METHOD(&NativeMapView::routeQueryRendered, "nativeRouteQuery"), + METHOD(&NativeMapView::routesGetCaptureSnapshot, "nativeRoutesCaptureSnapshot"), + METHOD(&NativeMapView::routesFinalize, "nativeRoutesFinalize")); +} + +std::map NativeMapView::convert(JNIEnv& env, + const jni::Array& keys, + const jni::Array& values) { + std::map ret; + size_t keyslen = keys.Length(env); + size_t valueslen = values.Length(env); + assert(keyslen == valueslen && "invalid map data"); + if (keyslen == valueslen) { + for (size_t i = 0; i < keyslen; i++) { + double key = keys.Get(env, i); + double value = values.Get(env, i); + ret[key] = value; + } + } + + return ret; +} + +jint NativeMapView::routeCreate(JNIEnv& env, + const jni::Object& routeGeom, + jint outerColor, + jint innerColor, + jdouble outerWidth, + jdouble innerWidth, + const jni::String& layerbefore, + jboolean useDynamicWidths, + const jni::Array& outerDynamicWidthZooms, + const jni::Array& outerDynamicWidths, + const jni::Array& innerDynamicWidthZooms, + const jni::Array& innerDynamicWidths) { + if (!routeMgr->hasStyle()) { + routeMgr->setStyle(map->getStyle()); + } + + assert(routeMgr->hasStyle() && "style object has not been set"); + RouteID routeID; + if (routeMgr) { + if (routeMgr->hasStyle()) { + using namespace mbgl::android::conversion; + const auto& linestring = mbgl::android::geojson::LineString::convert(env, routeGeom); + mbgl::route::RouteOptions routeOptions; + Converter colorConverter; + Result outerColorRes = colorConverter(env, outerColor); + Result innerColorRes = colorConverter(env, innerColor); + if (outerColorRes) { + routeOptions.outerColor = *outerColorRes; + } + if (innerColorRes) { + routeOptions.innerColor = *innerColorRes; + } + routeOptions.outerWidth = outerWidth; + routeOptions.innerWidth = innerWidth; + routeOptions.useDynamicWidths = useDynamicWidths; + if (layerbefore) { + routeOptions.layerBefore = jni::Make(env, layerbefore); + } + + if (outerDynamicWidthZooms && outerDynamicWidths && innerDynamicWidthZooms && innerDynamicWidths) { + routeOptions.outerWidthZoomStops = convert(env, outerDynamicWidthZooms, outerDynamicWidths); + routeOptions.innerWidthZoomStops = convert(env, innerDynamicWidthZooms, innerDynamicWidths); + } + + routeID = routeMgr->routeCreate(linestring, routeOptions); + } + } + + return routeID.id; +} + +jni::Local NativeMapView::routeGetActiveLayerName(JNIEnv& env, const jint routeID) { + std::string layerName; + if (routeMgr) { + layerName = routeMgr->getActiveRouteLayerName(RouteID(routeID)); + } + + return jni::Make(env, layerName); +} + +jni::Local NativeMapView::routeGetBaseLayerName(JNIEnv& env, const jint& routeID) { + std::string layerName; + if (routeMgr) { + layerName = routeMgr->getBaseRouteLayerName(RouteID(routeID)); + } + + return jni::Make(env, layerName); +} + +jni::Local NativeMapView::routesGetCaptureSnapshot(JNIEnv& env) { + std::string captureStr; + if (routeMgr) { + captureStr = routeMgr->captureSnapshot(); + } + + return jni::Make(env, captureStr); +} + +jboolean NativeMapView::routeDispose(JNIEnv& env, jint routeID) { + if (routeMgr) { + return routeMgr->routeDispose(RouteID(routeID)); + } + + return false; +} + +jboolean NativeMapView::routeSegmentCreate(JNIEnv& env, + jint routeID, + const jni::Object& segmentGeom, + jint color, + jint priority) { + if (routeMgr) { + using namespace mbgl::android::conversion; + const auto& linestring = mbgl::android::geojson::LineString::convert(env, segmentGeom); + mbgl::route::RouteSegmentOptions rsegopts; + rsegopts.geometry = linestring; + rsegopts.priority = static_cast(priority); + Converter colorConverter; + Result segmentColorRes = colorConverter(env, color); + if (segmentColorRes) { + rsegopts.color = *segmentColorRes; + } + + return routeMgr->routeSegmentCreate(RouteID(routeID), rsegopts); + } + + return false; +} + +jboolean NativeMapView::routeProgressSet(JNIEnv& env, jint routeID, jdouble progress) { + if (routeMgr) { + return routeMgr->routeSetProgress(RouteID(routeID), progress); + } + + return false; +} + +jboolean NativeMapView::routeProgressSetPoint(JNIEnv& env, jint routeID, jdouble x, jdouble y) { + if (routeMgr) { + return routeMgr->routeSetProgress(RouteID(routeID), mbgl::Point(x, y)); + } + + return false; +} + +void NativeMapView::routeSegmentsClear(JNIEnv& env, jint routeID) { + if (routeMgr) { + routeMgr->routeClearSegments(RouteID(routeID)); + } +} + +jboolean NativeMapView::routesFinalize(JNIEnv& env) { + if (routeMgr) { + routeMgr->finalize(); + + return true; + } + + return false; +} + +jni::Local NativeMapView::getRenderingStats(JNIEnv& env) { + std::stringstream ss; + gfx::RenderingStats stats = mapRenderer.getRenderingStats(); + ss << stats.toJSONString(); + if (routeMgr) { + ss << routeMgr->getStats(); + } + return jni::Make(env, ss.str()); } void NativeMapView::onRegisterShaders(gfx::ShaderRegistry&) {}; diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp index 7ec2a4dd3596..cb18171affd7 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.hpp @@ -15,6 +15,7 @@ #include "graphics/rectf.hpp" #include "geojson/feature.hpp" #include "geojson/geometry.hpp" +#include "geojson/line_string.hpp" #include "geometry/lat_lng.hpp" #include "geometry/projected_meters.hpp" #include "style/layers/layer_manager.hpp" @@ -24,6 +25,7 @@ #include "map/image.hpp" #include "style/light.hpp" #include "bitmap.hpp" +#include "mbgl/route/route_manager.hpp" #include #include @@ -229,6 +231,51 @@ class NativeMapView : public MapObserver { void removeAnnotationIcon(JNIEnv&, const jni::String&); + //--------------- route APIs --------------- + jint routeCreate(JNIEnv& env, + const jni::Object& routeGeom, + jint outerColor, + jint innerColor, + jdouble outerWidth, + jdouble innerWidth, + const jni::String& layerbefore, + jboolean useDyanmicWidths, + const jni::Array& outerDynamicWidthZooms, + const jni::Array& outerDynamicWidths, + const jni::Array& innerDynamicWidthZooms, + const jni::Array& innerDynamicWidths); + + jboolean routeDispose(JNIEnv& env, jint routeID); + + jni::Local routeGetActiveLayerName(JNIEnv& env, const jint routeID); + + jni::Local routeGetBaseLayerName(JNIEnv& env, const jint& routeID); + + jboolean routeSegmentCreate(JNIEnv& env, + jint routeID, + const jni::Object& segmentGeom, + jint color, + jint priority); + + jboolean routeProgressSet(JNIEnv& env, jint routeID, jdouble progress); + + jboolean routeProgressSetPoint(JNIEnv& env, jint routeID, jdouble x, jdouble y); + + void routeSegmentsClear(JNIEnv& env, jint routeID); + + jni::Local routesGetStats(JNIEnv& env); + + jni::Local routesGetCaptureSnapshot(JNIEnv& env); + + void routesClearStats(JNIEnv& env); + + jint routeQueryRendered(JNIEnv& env, jni::jdouble screenSpaceX, jni::jdouble screenSpaceY, jni::jint radius); + + jboolean routesFinalize(JNIEnv& env); + //------------------------------------------------ + + jni::Local getRenderingStats(JNIEnv& env); + jni::jdouble getTopOffsetPixelsForAnnotationSymbol(JNIEnv&, const jni::String&); jni::Local> getTransitionOptions(JNIEnv&); @@ -320,6 +367,7 @@ class NativeMapView : public MapObserver { private: std::unique_ptr rendererFrontend; + std::unique_ptr routeMgr; JavaVM* vm = nullptr; jni::WeakReference> javaPeer; @@ -336,6 +384,8 @@ class NativeMapView : public MapObserver { // Ensure these are initialised last std::unique_ptr map; + + std::map convert(JNIEnv& env, const jni::Array& keys, const jni::Array& values); }; } // namespace android diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapView.java index 4c62ff47ec91..511c55357ce2 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/MapView.java @@ -34,6 +34,8 @@ import org.maplibre.android.storage.FileSource; import org.maplibre.android.utils.BitmapUtils; import org.maplibre.android.tile.TileOperation; +import org.maplibre.geojson.LineString; +import org.maplibre.geojson.Point; import java.util.ArrayList; import java.util.Iterator; @@ -439,6 +441,141 @@ public void onDestroy() { } } + /*** + * Create a new route on the map view. Once all routes are created and updated , one must call finalizeRoutes(). + * One should not create a RouteID manually and should only be used what is returned by this method. + * + * @param routeGeom specified linestring of points in physical coordinates or lat longs + * @param routeOptions specified visual appearance of the route geometry + * @return a unique id for the route + */ + public RouteID createRoute(LineString routeGeom, RouteOptions routeOptions) { + return nativeMapView.createRoute(routeGeom, routeOptions); + } + + /*** + * Gets the active layer name for a route. An active layer is a layer in the style that contains the + * route's blue line (not casing) + * + * @param routeID the specified routeID that was returned by createRoute() + * @return the active layer name for the specified routeID + */ + String getRouteActiveLayerName(RouteID routeID) { + return nativeMapView.getRouteActiveLayerName(routeID); + } + + /*** + * Gets the base layer name for a route. A base layer is a layer in the style that contains the + * route's casing. + * + * @param routeID the specified routeID that was returned by createRoute() + * @return the base/casing layer name for the specified routeID + */ + String getRouteBaseLayerName(RouteID routeID) { + return nativeMapView.getRouteBaseLayerName(routeID); + } + + + /*** + * Disposes a route given a route ID. The route ID must have been previous created via createRoute(). + * Once a route is disposed, the routeID can be recycled and reused. Remember to call finalizeRoutes(). + * + * @param routeID the specified routeID for the corresponding route to be disposed + * @return true if disposal was successful, false otherwise + */ + public boolean disposeRoute(RouteID routeID) { + return nativeMapView.disposeRoute(routeID); + } + + /*** + * Sets the progress of a route on the map view. Remember to call finalizeRoutes(). + * + * @param routeID the specified routeID for the corresponding route. If the route does not exist, + * this method will return false. + * @param progress the specified progress which is a value between 0 and 1. + * @return true if setting the progress was a success + */ + public boolean setRouteProgress(RouteID routeID, double progress) { + return nativeMapView.setRouteProgress(routeID, progress); + } + + /*** + * Sets the progress point which lies close enough on a route in the map view. Remember to call finalizeRoutes(). + * + * @param routeID the specified routeID for the corresponding route. + * @param point the specified point which lies close enough on the route + * @return true if successful, false otherwise. If the route does not exist, false is returned. + */ + public boolean setRouteProgressPoint(RouteID routeID, Point point) { + return nativeMapView.setRouteProgressPoint(routeID, point); + } + + /*** + * Removes all the route segments for the corresponding route. Remember to call finalizeRoutes(). + * + * @param routeID the specified route ID for the corresponding route. If the route does not exist, + * this operation is a no-op. + */ + public void clearRouteSegments(RouteID routeID) { + nativeMapView.clearRouteSegments(routeID); + } + + /*** + * Creates a new route segment on the map view. A route segment is a set of points that exists on + * the route. These points may be a subset of points that makee the route geometry or it may + * be composed on points that are interpolated between points in the route geometry. + * A route segment is typically used to visualize traffic zones and is customizable by the client + * code. Remember to call finalizeRoutes(). + * + * @param routeID the specified routeID for the corresponding route. + * @param rsopts the specified visual appearance of the route segment + * @return true if the route segment was created successfully, false otherwise. If the route + * does not exist, false is returned. + */ + public boolean createRouteSegment(RouteID routeID, RouteSegmentOptions rsopts) { + return nativeMapView.createRouteSegment(routeID, rsopts); + } + + /*** + * Finalizes the routes on the map view. This method must be called if any route or route segments + * calls were made. This method can get fairly expensive and hence we do not want to call this for + * every mutation of route or route segment. The client code is free to create/mutate the routes/routesegments + * as many times as needed but call the finalizeRoutes() method once (or as few times as possible). + * + * It is during this time , that map libre layers, expressions and images are constructed. + * + * @return true if the routes were finalized successfully, false otherwise + */ + public boolean finalizeRoutes() { + return nativeMapView.finalizeRoutes(); + } + + /*** + * Gets statistics of the underlying health of routes management and includes stats on various + * constructs built in native code, such as memory and time take for such constructs. + * + * @return a formatted string containing the statistics + */ + public String getRenderingStats() { + return nativeMapView.getRenderingStats(); + } + + public String getSnapshotCapture() { + return nativeMapView.getSnapshotCapture(); + } + + /*** + * Queries map libre native is a screen space point is over a route. + * + * @param x the screen space x coordinate + * @param y the screen space y coordinate + * @return a route ID. The routeID will always be non null but client needs to check if its + * valid by calling isValid() on it. + */ + public RouteID queryRoute(double x, double y, int radius) { + return nativeMapView.queryRoute(x, y, radius); + } + /** * Queue a runnable to be executed on the map renderer thread. * diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java index 31fd4f6bafa5..c0c6e14a2d05 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMap.java @@ -22,6 +22,8 @@ import org.maplibre.android.style.layers.TransitionOptions; import org.maplibre.android.style.light.Light; import org.maplibre.android.style.sources.Source; +import org.maplibre.geojson.LineString; +import org.maplibre.geojson.Point; import java.util.List; @@ -246,6 +248,33 @@ List queryRenderedFeatures(@NonNull RectF coordinates, void setSwapBehaviorFlush(boolean flush); + // + // Route API + // + RouteID createRoute(LineString routeGeom, RouteOptions routeOptions); + + String getRouteActiveLayerName(RouteID routeID); + + String getRouteBaseLayerName(RouteID routeID); + + boolean disposeRoute(RouteID routeID); + + RouteID queryRoute(double x, double y, int radius); + + boolean createRouteSegment(RouteID routeID, RouteSegmentOptions rsopts); + + boolean setRouteProgress(RouteID routeID, double progress); + + boolean setRouteProgressPoint(RouteID routeID, Point point); + + void clearRouteSegments(RouteID routeID); + + boolean finalizeRoutes(); + + String getRenderingStats(); + + String getSnapshotCapture(); + // // Deprecated Annotations API // diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java index 4ed013c50dcb..398e9e51f99d 100755 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/NativeMapView.java @@ -38,10 +38,13 @@ import org.maplibre.android.style.sources.Source; import org.maplibre.android.utils.BitmapUtils; import org.maplibre.android.tile.TileOperation; +import org.maplibre.geojson.LineString; +import org.maplibre.geojson.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.TreeMap; // Class that wraps the native methods for convenience final class NativeMapView implements NativeMap { @@ -114,6 +117,7 @@ public NativeMapView(final Context context, final float pixelRatio, final boolea // Methods // + private boolean checkState(String callingMethod) { // validate if invocation has occurred on the main thread if (thread != Thread.currentThread()) { @@ -1050,6 +1054,113 @@ public void setSwapBehaviorFlush(boolean flush) { mapRenderer.setSwapBehaviorFlush(flush); } + private static double[] getDoubleArrayKeys(TreeMap map) { + List keyList = new ArrayList<>(map.keySet()); + double[] keys = new double[keyList.size()]; + for (int i = 0; i < keyList.size(); i++) { + keys[i] = keyList.get(i); + } + return keys; + } + + private static double[] getDoubleArrayValues(TreeMap map) { + List valueList = new ArrayList<>(map.values()); + double[] values = new double[valueList.size()]; + for (int i = 0; i < valueList.size(); i++) { + values[i] = valueList.get(i); + } + return values; + } + + @Override + public RouteID createRoute(LineString routeGeom, RouteOptions routeOptions) { + double[] outerDynamicWidthZooms = getDoubleArrayKeys(routeOptions.outerDynamicWidthZoomStops); + double[] outerDynamicWidths = getDoubleArrayValues(routeOptions.outerDynamicWidthZoomStops); + double[] innerDynamicWidthZooms = getDoubleArrayKeys(routeOptions.innerDynamicWidthZoomStops); + double[] innerDynamicWidths = getDoubleArrayValues(routeOptions.innerDynamicWidthZoomStops); + + RouteID routeID = new RouteID(nativeRouteCreate(routeGeom, routeOptions.outerColor, routeOptions.innerColor, + routeOptions.outerWidth, routeOptions.innerWidth, routeOptions.layerBefore, routeOptions.useDynamicWidths, + outerDynamicWidthZooms, outerDynamicWidths, innerDynamicWidthZooms, innerDynamicWidths)); + return routeID; + } + + @Override + public String getRouteActiveLayerName(RouteID routeID) { + return nativeRouteGetActiveLayerName(routeID.getId()); + } + + @Override + public String getRouteBaseLayerName(RouteID routeID) { + return nativeRouteGetBaseLayerName(routeID.getId()); + } + + @Override + public boolean disposeRoute(RouteID routeID) { + boolean success = false; + if(routeID.isValid()) { + success = nativeRouteDispose(routeID.getId()); + } + + return success; + } + + @Override + public RouteID queryRoute(double x, double y, int radius) { + RouteID routeID = new RouteID(nativeRouteQuery(x, y, radius)); + return routeID; + } + + @Override + public boolean createRouteSegment(RouteID routeID, RouteSegmentOptions rsopts) { + if(routeID.isValid()) { + return nativeRouteSegmentCreate(routeID.getId(), rsopts.geometry, rsopts.color, rsopts.priority); + } + + return false; + } + + @Override + public boolean setRouteProgress(RouteID routeID, double progress) { + if(routeID.isValid()) { + return nativeRouteSetProgress(routeID.getId(), progress); + } + + return false; + } + + @Override + public boolean setRouteProgressPoint(RouteID routeID, Point point) { + if(routeID.isValid()) { + return nativeRouteSetProgressPoint(routeID.getId(), point.latitude(), point.longitude()); + } + + return false; + } + + @Override + public void clearRouteSegments(RouteID routeID) { + if(routeID.isValid()) { + nativeRouteClearSegments(routeID.getId()); + } + } + + @Override + public boolean finalizeRoutes() { + return nativeRoutesFinalize(); + } + + @Override + public String getRenderingStats() { + return nativeGetRenderingStats(); + } + + @Override + public String getSnapshotCapture() { + return nativeRoutesCaptureSnapshot(); + } + + @NonNull @Override public RectF getDensityDependantRectangle(final RectF rectangle) { @@ -1420,6 +1531,52 @@ private native CameraPosition nativeGetCameraForGeometry( private native void nativeSetVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, double direction, long duration); + //---------------------Native route APIs--------------------- + @Keep + private native int nativeRouteCreate(LineString routeGeometry, int outerColor, int innerColor, + double outerWidth, double innerWidth, String layerBefore, + boolean useDynamicWidths, double[] outerDynamicWidthZooms, + double[] outerDynamicWidths, double[] innerDynamicWidthZooms, + double[] innerDynamicWidths); + + @Keep + private native int nativeRouteQuery(double x, double y, int radius); + + @Keep + private native String nativeRouteGetActiveLayerName(int routeID); + + @Keep + private native String nativeRouteGetBaseLayerName(int routeID); + + @Keep + private native boolean nativeRouteDispose(int routeID); + + @Keep + private native boolean nativeRouteSegmentCreate(int routeID, LineString segmentGeometry, int color, int priority); + + @Keep + private native boolean nativeRouteSetProgress(int routeID, double progress); + + @Keep + private native boolean nativeRouteSetProgressPoint(int routeID, double x, double y); + + @Keep + private native void nativeRouteClearSegments(int routeID); + + @Keep + private native boolean nativeRoutesFinalize(); + + @Keep + private native String nativeRoutesCaptureSnapshot(); + + @Keep + native void nativeRoutesClearStats(); + + //--------------------------------------------------------- + + @Keep + private native String nativeGetRenderingStats(); + @Keep private native void nativeOnLowMemory(); diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteID.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteID.java new file mode 100644 index 000000000000..fa290ad782a4 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteID.java @@ -0,0 +1,14 @@ +package org.maplibre.android.maps; + +final public class RouteID { + private Integer id; + public RouteID(int id) { + this.id = id; + } + public int getId() { + return id; + } + public boolean isValid() { + return id != -1; + } +} diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteOptions.java new file mode 100644 index 000000000000..944e2ffa0f08 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteOptions.java @@ -0,0 +1,23 @@ +package org.maplibre.android.maps; + +import java.util.TreeMap; + +final public class RouteOptions { + public int outerColor; + public int innerColor; + public double outerWidth = 10; + public double innerWidth = 6; + public String layerBefore; + public boolean useDynamicWidths = false; + /*** + * outer dynamic width zoom stops is a mapping from zoom levels => route line width for the casing + */ + public TreeMap outerDynamicWidthZoomStops = new TreeMap(); + + /*** + * inner dynamic width zoom stops is a mapping from zoom levels => route line width for the + * blue line + */ + public TreeMap innerDynamicWidthZoomStops = new TreeMap(); + +} diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteSegmentOptions.java b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteSegmentOptions.java new file mode 100644 index 000000000000..3084a74c3821 --- /dev/null +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/RouteSegmentOptions.java @@ -0,0 +1,9 @@ +package org.maplibre.android.maps; + +import org.maplibre.geojson.LineString; + +final public class RouteSegmentOptions { + public LineString geometry; + public int color; + public int priority = 0; +} diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/GradientLineActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/GradientLineActivity.kt index 4bae21deae07..34d5d8b6cb6e 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/GradientLineActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/style/GradientLineActivity.kt @@ -21,6 +21,7 @@ import java.io.IOException */ class GradientLineActivity : AppCompatActivity(), OnMapReadyCallback { private lateinit var mapView: MapView + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_gradient_line) diff --git a/platform/glfw/glfw_renderer_frontend.cpp b/platform/glfw/glfw_renderer_frontend.cpp index a79814f013f3..255b33c49bb3 100644 --- a/platform/glfw/glfw_renderer_frontend.cpp +++ b/platform/glfw/glfw_renderer_frontend.cpp @@ -4,6 +4,7 @@ #include #include #include +#include GLFWRendererFrontend::GLFWRendererFrontend(std::unique_ptr renderer_, GLFWView& glfwView_) : glfwView(glfwView_), @@ -18,6 +19,12 @@ void GLFWRendererFrontend::reset() { renderer.reset(); } +std::vector GLFWRendererFrontend::queryFeatures(double screenSpaceX, double screenSpaceY) { + mbgl::ScreenCoordinate screen_point(screenSpaceX, screenSpaceY); + std::vector features = renderer->queryRenderedFeatures(screen_point); + return features; +} + void GLFWRendererFrontend::setObserver(mbgl::RendererObserver& observer) { assert(renderer); renderer->setObserver(&observer); diff --git a/platform/glfw/glfw_renderer_frontend.hpp b/platform/glfw/glfw_renderer_frontend.hpp index 252e26676c4b..c8f937b1fb02 100644 --- a/platform/glfw/glfw_renderer_frontend.hpp +++ b/platform/glfw/glfw_renderer_frontend.hpp @@ -2,7 +2,7 @@ #include "glfw_view.hpp" #include - +#include #include namespace mbgl { @@ -21,6 +21,8 @@ class GLFWRendererFrontend : public mbgl::RendererFrontend { const mbgl::TaggedScheduler& getThreadPool() const override; void render(); + std::vector queryFeatures(double screenspaceX, double screenspaceY); + mbgl::Renderer* getRenderer(); private: diff --git a/platform/glfw/glfw_view.cpp b/platform/glfw/glfw_view.cpp index 4c11ccf27348..678989578124 100644 --- a/platform/glfw/glfw_view.cpp +++ b/platform/glfw/glfw_view.cpp @@ -3,6 +3,8 @@ #include "glfw_renderer_frontend.hpp" #include "ny_route.hpp" #include "test_writer.hpp" +#include "rapidjson/document.h" +#include "rapidjson/error/en.h" #include #include @@ -27,6 +29,9 @@ #include #include #include +#include +#include +#include #if !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) && MLN_DRAWABLE_RENDERER #include "example_custom_drawable_style_layer.hpp" @@ -55,6 +60,8 @@ #endif #define GL_GLEXT_PROTOTYPES +#include "mbgl/gfx/renderer_backend.hpp" + #include #include @@ -64,6 +71,11 @@ #include #include #include +#include +#include +#include +#include +#include using namespace std::numbers; @@ -85,6 +97,9 @@ std::array toArray(const mbgl::LatLng &crd) { } // namespace #endif +namespace { +const double ROUTE_PROGRESS_STEP = 0.001; +} class SnapshotObserver final : public mbgl::MapSnapshotterObserver { public: ~SnapshotObserver() override = default; @@ -132,6 +147,59 @@ void addFillExtrusionLayer(mbgl::style::Style &style, bool visible) { extrusionLayer->setFillExtrusionBase(PropertyExpression(get("min_height"))); style.addLayer(std::move(extrusionLayer)); } + +mbgl::Color convert(std::string hexcolor) { + std::stringstream ss; + ss << std::hex << hexcolor; + int color; + ss >> color; + float r = (color >> 16) & 0xFF; + float g = (color >> 8) & 0xFF; + float b = (color) & 0xFF; + float a = 1.0f; + return {mbgl::Color(r / 255.0f, g / 255.0f, b / 255.0f, a)}; +} + +enum RouteColorType { + RouteMapAlternative, + RouteMapAlternativeCasing, + RouteMapAlternativeLowTrafficColor, + RouteMapAlternativeModerateTrafficColor, + RouteMapAlternativeHeavyTrafficColor, + RouteMapAlternativeSevereTrafficColor, + RouteMapColor, + RouteMapCasingColor, + RouteMapLowTrafficColor, + RouteMapModerateTrafficColor, + RouteMapHeavyTrafficColor, + RouteMapSevereTrafficColor, + InactiveLegRouteColor, + InactiveRouteLowTrafficColor, + InactiveRouteModerateTrafficColor, + InactiveRouteHeavyTrafficColor, + InactiveRouteSevereTrafficColor +}; + +const std::unordered_map routeColorTable = { + {RouteMapAlternative, convert("7A7A7A")}, + {RouteMapAlternativeCasing, convert("FFFFFF")}, + {RouteMapAlternativeLowTrafficColor, convert("FFCC5B")}, + {RouteMapAlternativeModerateTrafficColor, convert("F0691D")}, + {RouteMapAlternativeHeavyTrafficColor, convert("DB0000")}, + {RouteMapAlternativeSevereTrafficColor, convert("9B0000")}, + {RouteMapColor, convert("2F70A9")}, + {RouteMapCasingColor, convert("FFFFFF")}, + {RouteMapLowTrafficColor, convert("FFBC2D")}, + {RouteMapModerateTrafficColor, convert("ED6D4A")}, + {RouteMapHeavyTrafficColor, convert("DB0000")}, + {RouteMapSevereTrafficColor, convert("9B0000")}, + {InactiveLegRouteColor, convert("76A7D1")}, + {InactiveRouteLowTrafficColor, convert("FFE5AD")}, + {InactiveRouteModerateTrafficColor, convert("F39F7E")}, + {InactiveRouteHeavyTrafficColor, convert("EE7676")}, + {InactiveRouteSevereTrafficColor, convert("E64747")} + +}; } // namespace void glfwError(int error, const char *description) { @@ -150,7 +218,7 @@ GLFWView::GLFWView(bool fullscreen_, MLN_TRACE_FUNC(); glfwSetErrorCallback(glfwError); - + rmptr_ = std::make_unique(); std::srand(static_cast(std::time(nullptr))); if (!glfwInit()) { @@ -324,6 +392,7 @@ void GLFWView::onKey(GLFWwindow *window, int key, int /*scancode*/, int action, MLN_TRACE_FUNC(); auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); + bool isLeftShift = glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS; if (action == GLFW_RELEASE) { if (key != GLFW_KEY_R || key != GLFW_KEY_S) view->animateRouteCallback = nullptr; @@ -566,43 +635,98 @@ void GLFWView::onKey(GLFWwindow *window, int key, int /*scancode*/, int action, } if (action == GLFW_RELEASE || action == GLFW_REPEAT) { - switch (key) { - case GLFW_KEY_W: - view->popAnnotation(); - break; - case GLFW_KEY_1: - view->addRandomPointAnnotations(1); - break; - case GLFW_KEY_2: - view->addRandomPointAnnotations(10); - break; - case GLFW_KEY_3: - view->addRandomPointAnnotations(100); - break; - case GLFW_KEY_4: - view->addRandomPointAnnotations(1000); - break; - case GLFW_KEY_5: - view->addRandomPointAnnotations(10000); - break; - case GLFW_KEY_6: - view->addRandomPointAnnotations(100000); - break; - case GLFW_KEY_7: - view->addRandomShapeAnnotations(1); - break; - case GLFW_KEY_8: - view->addRandomShapeAnnotations(10); - break; - case GLFW_KEY_9: - view->addRandomShapeAnnotations(100); - break; - case GLFW_KEY_0: - view->addRandomShapeAnnotations(1000); - break; - case GLFW_KEY_M: - view->addAnimatedAnnotation(); - break; + if (isLeftShift) { + switch (key) { + case GLFW_KEY_1: + std::cout << "Adding Route" << std::endl; + view->addRoute(); + break; + case GLFW_KEY_2: + std::cout << "Disposing Route" << std::endl; + view->disposeRoute(); + break; + case GLFW_KEY_3: + std::cout << "Adding Route Traffic Viz" << std::endl; + view->addTrafficSegments(); + break; + case GLFW_KEY_4: + std::cout << "Removing Route Traffic Viz" << std::endl; + view->removeTrafficViz(); + break; + case GLFW_KEY_5: + std::cout << "Set Route Pick Mode" << std::endl; + view->setRoutePickMode(); + break; + case GLFW_KEY_6: + view->incrementRouteProgress(); + break; + case GLFW_KEY_7: + view->decrementRouteProgress(); + break; + + case GLFW_KEY_P: + view->setRouteProgressUsage(); + break; + + case GLFW_KEY_S: { + view->captureSnapshot(); + std::cout << "captured snapshot" << std::endl; + } break; + + case GLFW_KEY_L: { + int lastCapturedIdx = view->getCaptureIdx() - 1; + if (lastCapturedIdx == -1) lastCapturedIdx = 0; + std::string capture_file_name = "snapshot" + std::to_string(lastCapturedIdx) + ".json"; + view->readAndLoadCapture(capture_file_name); + } break; + + case GLFW_KEY_Q: + view->writeStats(); + break; + + case GLFW_KEY_0: + view->setRouteProgressUsage(); + break; + } + } else { + switch (key) { + case GLFW_KEY_W: + view->popAnnotation(); + break; + case GLFW_KEY_1: + view->addRandomPointAnnotations(1); + break; + case GLFW_KEY_2: + view->addRandomPointAnnotations(10); + break; + case GLFW_KEY_3: + view->addRandomPointAnnotations(100); + break; + case GLFW_KEY_4: + view->addRandomPointAnnotations(1000); + break; + case GLFW_KEY_5: + view->addRandomPointAnnotations(10000); + break; + case GLFW_KEY_6: + view->addRandomPointAnnotations(100000); + break; + case GLFW_KEY_7: + view->addRandomShapeAnnotations(1); + break; + case GLFW_KEY_8: + view->addRandomShapeAnnotations(10); + break; + case GLFW_KEY_9: + view->addRandomShapeAnnotations(100); + break; + case GLFW_KEY_0: + view->addRandomShapeAnnotations(1000); + break; + case GLFW_KEY_M: + view->addAnimatedAnnotation(); + break; + } } } } @@ -739,6 +863,585 @@ void GLFWView::addRandomPointAnnotations(int count) { } } +mbgl::Point GLFWView::RouteCircle::getPoint(double percent) const { + if (points.empty()) { + return {0.0, 0.0}; + } + + if (percent <= 0.0) { + return points.front(); + } + + if (percent >= 1.0) { + return points.back(); + } + + double totalLength = 0.0; + std::vector segmentLengths(points.size() - 1); + + // Calculate the length of each segment and the total length of the polyline + for (size_t i = 0; i < points.size() - 1; ++i) { + double dx = points[i + 1].x - points[i].x; + double dy = points[i + 1].y - points[i].y; + segmentLengths[i] = std::sqrt(dx * dx + dy * dy); + totalLength += segmentLengths[i]; + } + + double targetLength = percent * totalLength; + double accumulatedLength = 0.0; + + // Find the segment where the target length falls + for (size_t i = 0; i < segmentLengths.size(); ++i) { + if (accumulatedLength + segmentLengths[i] >= targetLength) { + double segmentPercent = (targetLength - accumulatedLength) / segmentLengths[i]; + double x = points[i].x + segmentPercent * (points[i + 1].x - points[i].x); + double y = points[i].y + segmentPercent * (points[i + 1].y - points[i].y); + return {x, y}; + } + accumulatedLength += segmentLengths[i]; + } + + return points.back(); +} + +void GLFWView::writeStats() { + std::stringstream ss; + std::string renderingStats = backend->getRendererBackend().getRenderingStats().toJSONString(); + std::string routeStats = rmptr_->getStats(); + rapidjson::Document renderStatsDoc; + renderStatsDoc.Parse(renderingStats.c_str()); + rapidjson::Document routeStatsDoc; + routeStatsDoc.Parse(routeStats.c_str()); + + rapidjson::Document combined; + combined.SetObject(); + + rapidjson::Document::AllocatorType &combinedAllocator = combined.GetAllocator(); + + rapidjson::Value copiedRenderingStats(renderStatsDoc, combinedAllocator); + rapidjson::Value copiedRouteStats(routeStatsDoc, combinedAllocator); + + combined.AddMember("rendering_stats", copiedRenderingStats, combinedAllocator); + combined.AddMember("route_stats", copiedRouteStats, combinedAllocator); + + rapidjson::StringBuffer buffer; + rapidjson::PrettyWriter writer(buffer); + combined.Accept(writer); + + std::cout << buffer.GetString() << std::endl; +} + +std::vector GLFWView::getAllRoutes() const { + return rmptr_->getAllRoutes(); +} + +std::string GLFWView::getBaseRouteLayerName(const RouteID &routeID) const { + return rmptr_->getBaseRouteLayerName(routeID); +} + +std::string GLFWView::getBaseGeoJSONsourceName(const RouteID &routeID) const { + return rmptr_->getBaseGeoJSONsourceName(routeID); +} + +int GLFWView::getTopMost(const std::vector &routeList) const { + return rmptr_->getTopMost(routeList); +} + +void GLFWView::addRoute() { + using namespace mbgl::route; + + mbgl::Color color0 = routeColorTable.at(RouteColorType::RouteMapColor); + mbgl::Color color1 = routeColorTable.at(RouteColorType::RouteMapAlternative); + std::vector colors = {color0, color1}; + + auto getRouteGeom = [](const RouteCircle &route) -> mbgl::LineString { + mbgl::LineString linestring; + float radius = route.radius; + for (int i = 0; i < route.resolution; i++) { + float anglerad = (float(i) / float(route.resolution - 1)) * 2 * 3.14f; + mbgl::Point pt{route.xlate + radius * sin(anglerad), radius * cos(anglerad)}; + linestring.push_back(pt); + } + + return linestring; + }; + + rmptr_->setStyle(map->getStyle()); + RouteCircle route; + route.resolution = 30.0; + route.xlate = routeMap_.size() * route.radius * 2.0; + mbgl::LineString geom = getRouteGeom(route); + route.points = geom; + assert(route.points.size() == route.resolution && "invalid number of points generated"); + RouteOptions routeOpts; + int colorIdx = routeMap_.size() == 0 ? 0 : 1; + routeOpts.innerColor = colors[colorIdx]; + routeOpts.outerColor = mbgl::Color(0.2, 0.2, 0.2, 1); + routeOpts.useDynamicWidths = false; + routeOpts.outerClipColor = mbgl::Color(0.5, 0.5, 0.5, 1.0); + routeOpts.innerClipColor = mbgl::Color(0.5, 0.5, 0.5, 1.0); + // Testing the layerBefore option + // if(lastRouteID_.isValid()) { + // routeOpts.layerBefore = getBaseRouteLayerName(lastRouteID_); + // } + + auto routeID = rmptr_->routeCreate(geom, routeOpts); + routeMap_[routeID] = route; + rmptr_->finalize(); + lastRouteID_ = routeID; +} + +std::vector GLFWView::testCases(const RouteSegmentTestCases &testcase, + const GLFWView::RouteCircle &route) const { + TrafficBlock block1; + TrafficBlock block2; + + std::vector fixture; + switch (testcase) { + case RouteSegmentTestCases::Blk1LowPriorityIntersecting: { + block1.block = {route.getPoint(0.0), route.getPoint(0.25), route.getPoint(0.5)}; + block1.priority = 0; + block1.color = routeColorTable.at(RouteMapLowTrafficColor); + + block2.block = {route.getPoint(0.2), route.getPoint(0.7), route.getPoint(0.8)}; + block2.priority = 1; + block2.color = routeColorTable.at(RouteMapModerateTrafficColor); + } break; + + case RouteSegmentTestCases::Blk1HighPriorityIntersecting: { + block1.block = {route.getPoint(0.0), route.getPoint(0.25), route.getPoint(0.5)}; + block1.priority = 1; + block1.color = routeColorTable.at(RouteMapLowTrafficColor); + + block2.block = {route.getPoint(0.2), route.getPoint(0.7), route.getPoint(0.8)}; + block2.priority = 0; + block2.color = routeColorTable.at(RouteMapModerateTrafficColor); + } break; + + case RouteSegmentTestCases::Blk12SameColorIntersecting: { + block1.block = {route.getPoint(0.0), route.getPoint(0.25), route.getPoint(0.5)}; + block1.priority = 0; + block1.color = routeColorTable.at(RouteMapLowTrafficColor); + + block2.block = {route.getPoint(0.2), route.getPoint(0.7), route.getPoint(0.8)}; + block2.priority = 1; + block2.color = routeColorTable.at(RouteMapLowTrafficColor); + + } break; + + case RouteSegmentTestCases::Blk12NonIntersecting: { + block1.block = {route.getPoint(0.0), route.getPoint(0.25), route.getPoint(0.5)}; + block1.priority = 0; + block1.color = routeColorTable.at(RouteMapLowTrafficColor); + + block2.block = {route.getPoint(0.6), route.getPoint(0.7), route.getPoint(0.8)}; + block2.priority = 0; + block2.color = routeColorTable.at(RouteMapModerateTrafficColor); + + } break; + + default: + break; + } + + fixture.push_back(block1); + fixture.push_back(block2); + + return fixture; +} + +void GLFWView::addTrafficSegments() { + const auto &getActiveColors = []() -> std::vector { + return {routeColorTable.at(RouteColorType::RouteMapLowTrafficColor), + routeColorTable.at(RouteColorType::RouteMapModerateTrafficColor), + routeColorTable.at(RouteColorType::RouteMapHeavyTrafficColor), + routeColorTable.at(RouteColorType::RouteMapSevereTrafficColor)}; + }; + + const auto &getAlternativeColors = []() -> std::vector { + return {routeColorTable.at(RouteColorType::InactiveRouteLowTrafficColor), + routeColorTable.at(RouteColorType::InactiveRouteModerateTrafficColor), + routeColorTable.at(RouteColorType::InactiveRouteHeavyTrafficColor), + routeColorTable.at(RouteColorType::InactiveRouteHeavyTrafficColor)}; + }; + + for (const auto &iter : routeMap_) { + const auto &routeID = iter.first; + const auto &route = iter.second; + std::vector colors = routeID == routeMap_.begin()->first ? getActiveColors() + : getAlternativeColors(); + std::vector trafficBlks; + + // TODO: we have run out of hot keys :( . one of these days, need to create graphics tests for nav. + bool useTestCode = false; + if (useTestCode) { + trafficBlks = testCases(RouteSegmentTestCases::Blk12SameColorIntersecting, route); + } else { + size_t blockSize = floor(float(route.resolution) / float(route.numTrafficZones)); + size_t innerBlockSize = ceil((float(blockSize) / 2.0f)); + + auto &routePts = route.points; + TrafficBlock currTrafficBlk; + for (size_t i = 0; i < routePts.size(); i++) { + if (i % blockSize == 0 && !currTrafficBlk.block.empty()) { + trafficBlks.push_back(currTrafficBlk); + currTrafficBlk.block.clear(); + } + + if (i % blockSize < innerBlockSize) { + currTrafficBlk.block.push_back(mbgl::Point(routePts.at(i).x, routePts.at(i).y)); + } + } + if (!currTrafficBlk.block.empty()) { + trafficBlks.push_back(currTrafficBlk); + } + } + + // clear the route segments and create new ones from the traffic blocks + rmptr_->routeClearSegments(routeID); + for (size_t i = 0; i < trafficBlks.size(); i++) { + mbgl::route::RouteSegmentOptions rsegopts; + rsegopts.color = trafficBlks[i].color; + rsegopts.geometry = trafficBlks[i].block; + rsegopts.priority = trafficBlks[i].priority; + + rmptr_->routeSegmentCreate(routeID, rsegopts); + } + } + rmptr_->finalize(); +} + +void GLFWView::modifyTrafficViz() { + const auto &getColorTable = [](uint32_t numColors) -> std::vector { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> distrib(0.0, 1.0); + std::vector colors; + for (uint32_t i = 0; i < numColors; i++) { + double rand_r = distrib(gen); + double rand_g = distrib(gen); + double rand_b = distrib(gen); + mbgl::Color color(rand_r, rand_g, rand_b, 1.0); + // mbgl::Color color(1.0, 0.0, 0.0, 1.0); + colors.push_back(color); + } + return colors; + }; + + for (const auto &iter : routeMap_) { + const auto &routeID = iter.first; + const auto &route = iter.second; + rmptr_->routeClearSegments(routeID); + + std::vector colors = getColorTable(route.numTrafficZones); + + size_t blockSize = floor(float(route.resolution) / float(route.numTrafficZones)); + size_t innerBlockSize = ceil((float(blockSize) / 2.0f)); + + auto &routePts = route.points; + std::vector> trafficBlks; + mbgl::LineString currTrafficBlk; + if (route.resolution > route.numTrafficZones) { + for (size_t i = 1; i < routePts.size() - 1; i++) { + if (i % blockSize == 0 && !currTrafficBlk.empty()) { + trafficBlks.push_back(currTrafficBlk); + currTrafficBlk.clear(); + } + + if (i % blockSize < innerBlockSize) { + mbgl::Point non_aligned_pt; + if (i == 0 || i == routePts.size() - 1) { + non_aligned_pt = routePts.at(i); + } else { + mbgl::Point prevpt = routePts.at(i - 1); + mbgl::Point currpt = routePts.at(i); + non_aligned_pt = (currpt - prevpt) * 0.5; + } + currTrafficBlk.push_back(non_aligned_pt); + } + } + } else { + std::cout << "need to have more number of sample points in the route!" << std::endl; + exit(1); + } + if (!currTrafficBlk.empty()) { + trafficBlks.push_back(currTrafficBlk); + } + + for (size_t i = 0; i < trafficBlks.size(); i++) { + mbgl::route::RouteSegmentOptions rsegopts; + rsegopts.color = colors[i]; + rsegopts.geometry = trafficBlks[i]; + rsegopts.priority = i; + + rmptr_->routeSegmentCreate(routeID, rsegopts); + } + } +} + +void GLFWView::setRouteProgressUsage() { + useRouteProgressPercent_ = !useRouteProgressPercent_; + if (useRouteProgressPercent_) { + std::cout << "Using route progress percent" << std::endl; + } else { + std::cout << "Using route progress point" << std::endl; + } +} + +void GLFWView::setRoutePickMode() { + routePickMode_ = !routePickMode_; + std::string routePickModeStr = routePickMode_ ? "routePickMode ON" : "routePickMode OFF"; + std::cout << routePickModeStr << std::endl; +} + +bool GLFWView::getRoutePickMode() const { + return routePickMode_; +} + +GLFWRendererFrontend *GLFWView::getRenderFrontend() const { + return rendererFrontend; +} + +void GLFWView::incrementRouteProgress() { + routeProgress_ += ROUTE_PROGRESS_STEP; + std::clamp(routeProgress_, 0.0, 1.0f); + // std::cout<<"Route progress: "<routeSetProgress(routeID, routeProgress_); + } else { + mbgl::Point progressPoint = routeMap_[routeID].getPoint(routeProgress_); + rmptr_->routeSetProgress(routeID, progressPoint); + } + } + rmptr_->finalize(); +} + +void GLFWView::decrementRouteProgress() { + routeProgress_ -= ROUTE_PROGRESS_STEP; + std::clamp(routeProgress_, 0.0, 1.0f); + std::cout << "Route progress: " << routeProgress_ << std::endl; + for (const auto &iter : routeMap_) { + const auto &routeID = iter.first; + rmptr_->routeSetProgress(routeID, routeProgress_); + } + rmptr_->finalize(); +} + +void GLFWView::removeTrafficViz() { + for (const auto &iter : routeMap_) { + rmptr_->routeClearSegments(iter.first); + } + rmptr_->finalize(); +} + +void GLFWView::disposeRoute() { + if (!routeMap_.empty()) { + auto &routeID = routeMap_.begin()->first; + bool success = rmptr_->routeDispose(routeID); + if (success) { + routeMap_.erase(routeID); + } + rmptr_->finalize(); + } +} + +void GLFWView::captureSnapshot() { + if (rmptr_) { + std::string snapshot = rmptr_->captureSnapshot(); + std::string snapshot_file_name = "snapshot" + std::to_string(lastCaptureIdx_++) + ".json"; + writeCapture(snapshot, snapshot_file_name); + + std::cout << "Snapshot created: " << snapshot_file_name << std::endl; + } +} + +int GLFWView::getCaptureIdx() const { + return lastCaptureIdx_; +} + +void GLFWView::writeCapture(const std::string &capture, const std::string &capture_file_name) const { + std::ofstream outfile(capture_file_name); + + if (outfile.is_open()) { + outfile << capture; + outfile.close(); + std::cout << "Successfully wrote capture to file: " << capture_file_name << std::endl; + } else { + std::cout << "Failed to write capture to file: " << capture_file_name << std::endl; + } +} + +void GLFWView::readAndLoadCapture(const std::string &capture_file_name) { + for (size_t i = 0; i < routeMap_.size(); i++) { + disposeRoute(); + } + lastRouteID_ = RouteID(); + rmptr_->setStyle(map->getStyle()); + + std::ifstream jsonfile(capture_file_name); + if (!jsonfile.is_open()) { + std::cerr << "Failed to open the file." << std::endl; + return; + } + + std::stringstream buffer; + buffer << jsonfile.rdbuf(); + + rapidjson::Document document; + rapidjson::ParseResult result = document.Parse(buffer.str()); + + if (document.HasParseError()) { + const auto errorCode = document.GetParseError(); + std::cerr << "JSON parse error: " << rapidjson::GetParseError_En(result.Code()) << " (" << result.Offset() + << ")" << std::endl; + + std::cerr << "Error parsing JSON: " << errorCode << std::endl; + + return; + } + + uint32_t numRoutes = 0; + if (document.HasMember("num_routes") && document["num_routes"].IsInt()) { + numRoutes = document["num_routes"].GetInt(); + std::cout << "De-serializing " << numRoutes << " routes" << std::endl; + } + + if (document.HasMember("routes") && document["routes"].IsArray()) { + const rapidjson::Value &routes = document["routes"]; + for (rapidjson::SizeType i = 0; i < routes.Size(); i++) { + const rapidjson::Value &route = routes[i]; + + if (route.HasMember("route") && route["route"].IsObject()) { + const rapidjson::Value &route_obj = route["route"]; + + mbgl::route::RouteOptions routeOpts; + const auto &mbglColor = [](const rapidjson::Value &jsonColor) -> mbgl::Color { + std::array innerColorArr; + for (rapidjson::SizeType j = 0; j < jsonColor.Size(); j++) { + innerColorArr[j] = jsonColor[j].GetFloat(); + } + mbgl::Color color(innerColorArr[0], innerColorArr[1], innerColorArr[2], innerColorArr[3]); + + return color; + }; + + if (route_obj.HasMember("route_options") && route_obj["route_options"].IsObject()) { + const rapidjson::Value &route_options = route_obj["route_options"]; + + // innerColor + if (route_options.HasMember("innerColor") && route_options["innerColor"].IsArray()) { + const rapidjson::Value &innerColor = route_options["innerColor"]; + routeOpts.innerColor = mbglColor(innerColor); + } + + // outerColor + if (route_options.HasMember("outerColor") && route_options["outerColor"].IsArray()) { + const rapidjson::Value &outerColor = route_options["outerColor"]; + routeOpts.outerColor = mbglColor(outerColor); + } + + // innerClipColor + if (route_options.HasMember("innerClipColor") && route_options["innerClipColor"].IsArray()) { + const rapidjson::Value &innerClipColor = route_options["innerClipColor"]; + routeOpts.innerClipColor = mbglColor(innerClipColor); + } + + // outerClipColor + if (route_options.HasMember("outerClipColor") && route_options["outerClipColor"].IsArray()) { + const rapidjson::Value &outerClipColor = route_options["outerClipColor"]; + routeOpts.outerClipColor = mbglColor(outerClipColor); + } + + // innerWidth + if (route_options.HasMember("innerWidth") && route_options["innerWidth"].IsArray()) { + const rapidjson::Value &innerWidth = route_options["innerWidth"]; + routeOpts.innerWidth = innerWidth.GetFloat(); + } + + // outerWidth + if (route_options.HasMember("outerWidth") && route_options["outerWidth"].IsArray()) { + const rapidjson::Value &outerWidth = route_options["outerWidth"]; + routeOpts.outerWidth = outerWidth.GetFloat(); + } + + // layerBefore + if (route_options.HasMember("layerBefore") && route_options["layerBefore"].IsArray()) { + const rapidjson::Value &layerBefore = route_options["layerBefore"]; + routeOpts.layerBefore = layerBefore.GetString(); + } + + // useDynamicWidths + if (route_options.HasMember("useDynamicWidths") && route_options["useDynamicWidths"].IsArray()) { + const rapidjson::Value &useDynamicWidths = route_options["useDynamicWidths"]; + routeOpts.useDynamicWidths = useDynamicWidths.GetBool(); + } + } + + mbgl::LineString route_geom; + if (route_obj.HasMember("geometry") && route_obj["geometry"].IsArray()) { + const rapidjson::Value &geometry = route_obj["geometry"]; + for (rapidjson::SizeType j = 0; j < geometry.Size(); j++) { + const rapidjson::Value &point = geometry[j]; + if (point.IsArray() && point.Size() == 2) { + double x = point[0].GetDouble(); + double y = point[1].GetDouble(); + route_geom.push_back({x, y}); + } + } + } + + auto routeID = rmptr_->routeCreate(route_geom, routeOpts); + RouteCircle routeCircle; + routeCircle.points = route_geom; + routeCircle.resolution = route_geom.size(); + routeCircle.radius = 50; + routeCircle.xlate = 0; // TODO fixme later + routeMap_[routeID] = routeCircle; + + rmptr_->finalize(); + lastRouteID_ = routeID; + + // create route segments + if (route_obj.HasMember("route_segments") && route_obj["route_segments"].IsArray()) { + const rapidjson::Value &route_segments = route_obj["route_segments"]; + for (rapidjson::SizeType j = 0; j < route_segments.Size(); j++) { + mbgl::route::RouteSegmentOptions rsopts; + const rapidjson::Value &segment = route_segments[j]; + if (segment.HasMember("route_segment_options") && segment["route_segment_options"].IsObject()) { + const rapidjson::Value &segment_options = segment["route_segment_options"]; + if (segment_options.HasMember("color") && segment_options["color"].IsArray()) { + const rapidjson::Value &color = segment_options["color"]; + rsopts.color = mbglColor(color); + } + + if (segment_options.HasMember("geometry") && segment_options["geometry"].IsArray()) { + const rapidjson::Value &geometry = segment_options["geometry"]; + for (rapidjson::SizeType k = 0; k < geometry.Size(); k++) { + const rapidjson::Value &point = geometry[k]; + if (point.IsArray() && point.Size() == 2) { + double x = point[0].GetDouble(); + double y = point[1].GetDouble(); + rsopts.geometry.push_back({x, y}); + } + } + } + + if (segment_options.HasMember("priority")) { + rsopts.priority = segment_options["priority"].GetInt(); + } + } + + rmptr_->routeSegmentCreate(routeID, rsopts); + } + } + rmptr_->finalize(); + } + } + } +} + void GLFWView::addRandomLineAnnotations(int count) { MLN_TRACE_FUNC(); @@ -959,31 +1662,113 @@ void GLFWView::onMouseClick(GLFWwindow *window, int button, int action, int modi MLN_TRACE_FUNC(); auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); + if (view->getRoutePickMode()) { + std::cout << "onClick(): last: " << std::to_string(view->lastX) << ", " << std::to_string(view->lastY) + << std::endl; + std::vector routeIDs = view->getAllRoutes(); + std::vector layers; + // we specifically create caches for base layer since base route is wider than the active layer. + // we also check against source name as well as source layer name, since I've seen cases where the source layer + // name is not set. + std::unordered_map baseLayerMapCache; + std::unordered_map baseSourceMapCache; + for (const auto &routeID : routeIDs) { + if (routeID.isValid()) { + std::string baseLayer = view->getBaseRouteLayerName(routeID); + assert(!baseLayer.empty() && "base layer cannot be empty!"); + if (!baseLayer.empty()) { + layers.push_back(baseLayer); + baseLayerMapCache[baseLayer] = routeID; + } + std::string baseSource = view->getBaseGeoJSONsourceName(routeID); + assert(!baseSource.empty() && "base source cannot be empty"); + if (!baseSource.empty()) { + baseSourceMapCache[baseSource] = routeID; + } + } + } - if (button == GLFW_MOUSE_BUTTON_RIGHT || (button == GLFW_MOUSE_BUTTON_LEFT && modifiers & GLFW_MOD_CONTROL)) { - view->rotating = action == GLFW_PRESS; - view->map->setGestureInProgress(view->rotating); - } else if (button == GLFW_MOUSE_BUTTON_LEFT && (modifiers & GLFW_MOD_SHIFT)) { - view->pitching = action == GLFW_PRESS; - view->map->setGestureInProgress(view->pitching); - } else if (button == GLFW_MOUSE_BUTTON_LEFT) { - view->tracking = action == GLFW_PRESS; - view->map->setGestureInProgress(view->tracking); - - if (action == GLFW_RELEASE) { - double now = glfwGetTime(); - if (now - view->lastClick < 0.4 /* ms */) { - if (modifiers & GLFW_MOD_SHIFT) { - view->map->scaleBy(0.5, - mbgl::ScreenCoordinate{view->lastX, view->lastY}, - mbgl::AnimationOptions{{mbgl::Milliseconds(500)}}); - } else { - view->map->scaleBy(2.0, - mbgl::ScreenCoordinate{view->lastX, view->lastY}, - mbgl::AnimationOptions{{mbgl::Milliseconds(500)}}); + double screenSpaceX = view->lastX, screenSpaceY = view->lastY; + int radius = 5; + std::unordered_map> routeCoverage; + // sample multiple ray picks over an radius + for (int i = -radius; i < radius; i++) { + for (int j = -radius; j < radius; j++) { + mapbox::geometry::point screenpoint = {screenSpaceX + i, screenSpaceY + j}; + std::vector features = view->getRenderFrontend()->queryFeatures(screenpoint.x, + screenpoint.y); + for (const auto &feature : features) { + if (baseLayerMapCache.find(feature.sourceLayer) != baseLayerMapCache.end()) { + RouteID baseRouteID = baseLayerMapCache[feature.sourceLayer]; + routeCoverage[baseRouteID]++; + } + + // also check cache of geojson source names if the source layer is not set. + if (baseSourceMapCache.find(feature.source) != baseSourceMapCache.end()) { + RouteID baseRouteID = baseSourceMapCache[feature.source]; + routeCoverage[baseRouteID]++; + } + } + } + } + + // when you do a touch at a location, the radius can cover multiple routes. + // find the RouteID that has the maximum touch weight value + int maxTouchWeight = 0; + std::vector maxRouteIDs; + for (const auto &[routeID, weight] : routeCoverage) { + if (weight > maxTouchWeight) { + maxTouchWeight = weight; + } + } + for (const auto &[routeID, weight] : routeCoverage) { + if (weight == maxTouchWeight) { + maxRouteIDs.push_back(routeID); + } + } + + if (maxRouteIDs.size() == 1) { + std::cout << "Clicked routeID: " << std::to_string(maxRouteIDs[0].id) << std::endl; + } + + if (!maxRouteIDs.empty()) { + int top = view->getTopMost(maxRouteIDs); + RouteID topRouteID; + if (top >= 0 && top < static_cast(maxRouteIDs.size())) { + topRouteID = maxRouteIDs[top]; + } + + std::cout << "Clicked routeID: " << std::to_string(topRouteID.id) << std::endl; + } else { + std::cout << "Clicked on no routes" << std::endl; + } + + } else { + if (button == GLFW_MOUSE_BUTTON_RIGHT || (button == GLFW_MOUSE_BUTTON_LEFT && modifiers & GLFW_MOD_CONTROL)) { + view->rotating = action == GLFW_PRESS; + view->map->setGestureInProgress(view->rotating); + } else if (button == GLFW_MOUSE_BUTTON_LEFT && (modifiers & GLFW_MOD_SHIFT)) { + view->pitching = action == GLFW_PRESS; + view->map->setGestureInProgress(view->pitching); + } else if (button == GLFW_MOUSE_BUTTON_LEFT) { + view->tracking = action == GLFW_PRESS; + view->map->setGestureInProgress(view->tracking); + + if (action == GLFW_RELEASE) { + double now = glfwGetTime(); + if (now - view->lastClick < 0.4 /* ms */) { + if (modifiers & GLFW_MOD_SHIFT) { + view->map->scaleBy(0.5, + mbgl::ScreenCoordinate{view->lastX, view->lastY}, + mbgl::AnimationOptions{{mbgl::Milliseconds(500)}}); + } else { + view->map->scaleBy(2.0, + mbgl::ScreenCoordinate{view->lastX, view->lastY}, + mbgl::AnimationOptions{{mbgl::Milliseconds(500)}}); + } } + view->lastClick = now; } - view->lastClick = now; } } } diff --git a/platform/glfw/glfw_view.hpp b/platform/glfw/glfw_view.hpp index ffea1020a26d..52e25c3d02af 100644 --- a/platform/glfw/glfw_view.hpp +++ b/platform/glfw/glfw_view.hpp @@ -8,6 +8,9 @@ #include #include +#include +#include +#include #if (defined(MLN_RENDER_BACKEND_OPENGL) || defined(MLN_RENDER_BACKEND_VULKAN)) && \ !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) @@ -67,6 +70,9 @@ class GLFWView : public mbgl::MapObserver { mbgl::Size getSize() const; + bool getRoutePickMode() const; + GLFWRendererFrontend *getRenderFrontend() const; + // mbgl::MapObserver implementation void onDidFinishLoadingStyle() override; void onWillStartRenderingFrame() override; @@ -108,6 +114,19 @@ class GLFWView : public mbgl::MapObserver { void updateAnimatedAnnotations(); void toggleCustomSource(); void toggleLocationIndicatorLayer(); + std::vector routeIDlist; + std::unique_ptr rmptr_; + void addRoute(); + void modifyRoute(); + void disposeRoute(); + void addTrafficSegments(); + void modifyTrafficViz(); + void removeTrafficViz(); + void incrementRouteProgress(); + void decrementRouteProgress(); + void captureSnapshot(); + void setRouteProgressUsage(); + void setRoutePickMode(); void cycleDebugOptions(); void clearAnnotations(); @@ -140,6 +159,49 @@ class GLFWView : public mbgl::MapObserver { bool pitching = false; bool show3DExtrusions = false; + struct RouteCircle { + double resolution = 30; + double xlate = 0; + double radius = 50; + int numTrafficZones = 5; + bool trafficZonesGridAligned = true; + mbgl::LineString points; + + mbgl::Point getPoint(double percent) const; + }; + + struct TrafficBlock { + mbgl::LineString block; + uint32_t priority = 0; + mbgl::Color color; + }; + + enum RouteSegmentTestCases { + Blk1LowPriorityIntersecting, + Blk1HighPriorityIntersecting, + Blk12SameColorIntersecting, + Blk12NonIntersecting, + Invalid + }; + + std::vector testCases(const RouteSegmentTestCases &testcase, + const GLFWView::RouteCircle &route) const; + void writeCapture(const std::string &capture, const std::string &capture_file_name) const; + void readAndLoadCapture(const std::string &capture_file_name); + int getCaptureIdx() const; + void writeStats(); + std::vector getAllRoutes() const; + std::string getBaseRouteLayerName(const RouteID &routeID) const; + std::string getBaseGeoJSONsourceName(const RouteID &routeID) const; + int getTopMost(const std::vector &routeList) const; + + std::unordered_map> routeMap_; + int lastCaptureIdx_ = 0; + RouteID lastRouteID_; + double routeProgress_ = 0.0; + bool useRouteProgressPercent_ = false; + bool routePickMode_ = false; + // Frame timer int frames = 0; float frameTime = 0; diff --git a/src/mbgl/gfx/renderer_backend.cpp b/src/mbgl/gfx/renderer_backend.cpp index 20d2b85d3403..bdb360a1a779 100644 --- a/src/mbgl/gfx/renderer_backend.cpp +++ b/src/mbgl/gfx/renderer_backend.cpp @@ -22,5 +22,11 @@ gfx::Context& RendererBackend::getContext() { return *context; } +Context& RendererBackend::getContextOutsideRenderingScope() { + std::call_once(initialized, [this] { context = createContext(); }); + assert(context); + return *context; +} + } // namespace gfx } // namespace mbgl diff --git a/src/mbgl/gfx/rendering_stats.cpp b/src/mbgl/gfx/rendering_stats.cpp index 20b20c21d17d..49331b2cb1ac 100644 --- a/src/mbgl/gfx/rendering_stats.cpp +++ b/src/mbgl/gfx/rendering_stats.cpp @@ -3,6 +3,11 @@ #include #include #include +#include +#include +#include +#include +#include namespace mbgl { namespace gfx { @@ -56,6 +61,47 @@ RenderingStats& RenderingStats::operator+=(const RenderingStats& r) { return *this; } +std::string RenderingStats::toJSONString() const { + const auto& formatWithCommas = [](long long number) -> std::string { // Using long long for broader applicability + std::ostringstream oss; + bool localeSet = false; + try { + oss.imbue(std::locale("")); + localeSet = true; + } catch (const std::runtime_error& err) { + localeSet = false; + } + if (!localeSet) { + const char* fallbackLocale = "en_US.UTF-8"; // Common on Linux/macOS + oss.imbue(std::locale(fallbackLocale)); + } + oss << number; + return oss.str(); + }; + + rapidjson::Document document; + document.SetObject(); + + rapidjson::Document::AllocatorType& allocator = document.GetAllocator(); + rapidjson::Value rendering_stats(rapidjson::kObjectType); + rendering_stats.AddMember("numDrawCalls", formatWithCommas(numDrawCalls), allocator); + rendering_stats.AddMember("numCreatedTextures", formatWithCommas(numCreatedTextures), allocator); + rendering_stats.AddMember("numActiveTextures", formatWithCommas(numActiveTextures), allocator); + rendering_stats.AddMember("numBuffers", formatWithCommas(numBuffers), allocator); + rendering_stats.AddMember("memTextures", formatWithCommas(memTextures), allocator); + rendering_stats.AddMember("memIndexBuffers", formatWithCommas(memIndexBuffers), allocator); + rendering_stats.AddMember("memVertexBuffers", formatWithCommas(memVertexBuffers), allocator); + rendering_stats.AddMember("stencilUpdates", formatWithCommas(stencilUpdates), allocator); + + document.AddMember("rendering_stats", rendering_stats, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::PrettyWriter writer(buffer); + document.Accept(writer); + + return buffer.GetString(); +} + #if !defined(NDEBUG) template std::ostream& optionalStatLine(std::ostream& stream, T value, std::string_view label, std::string_view sep) { diff --git a/src/mbgl/gl/renderer_backend.cpp b/src/mbgl/gl/renderer_backend.cpp index f43f4ccb50d2..20991ddf83c6 100644 --- a/src/mbgl/gl/renderer_backend.cpp +++ b/src/mbgl/gl/renderer_backend.cpp @@ -38,6 +38,13 @@ PremultipliedImage RendererBackend::readFramebuffer(const Size& size) { return getContext().readFramebuffer(size); } +mbgl::gfx::RenderingStats RendererBackend::getRenderingStats() { + MLN_TRACE_FUNC(); + + auto renderingStats = getContextOutsideRenderingScope().renderingStats(); + return renderingStats; +} + void RendererBackend::assumeFramebufferBinding(const gl::FramebufferID fbo) { MLN_TRACE_FUNC(); diff --git a/src/mbgl/programs/attributes.hpp b/src/mbgl/programs/attributes.hpp index df8f7cef6c8d..b24e83bf05fe 100644 --- a/src/mbgl/programs/attributes.hpp +++ b/src/mbgl/programs/attributes.hpp @@ -36,9 +36,11 @@ MBGL_DEFINE_ATTRIBUTE(float, 2, fill_color); MBGL_DEFINE_ATTRIBUTE(float, 2, halo_color); MBGL_DEFINE_ATTRIBUTE(float, 2, stroke_color); MBGL_DEFINE_ATTRIBUTE(float, 2, outline_color); +MBGL_DEFINE_ATTRIBUTE(float, 2, line_clip_color); MBGL_DEFINE_ATTRIBUTE(float, 1, opacity); MBGL_DEFINE_ATTRIBUTE(float, 1, stroke_opacity); MBGL_DEFINE_ATTRIBUTE(float, 1, blur); +MBGL_DEFINE_ATTRIBUTE(float, 1, line_clip); MBGL_DEFINE_ATTRIBUTE(float, 1, radius); MBGL_DEFINE_ATTRIBUTE(float, 1, width); MBGL_DEFINE_ATTRIBUTE(float, 1, floorwidth); diff --git a/src/mbgl/programs/uniforms.hpp b/src/mbgl/programs/uniforms.hpp index bf928cde5e0c..d2a019a22925 100644 --- a/src/mbgl/programs/uniforms.hpp +++ b/src/mbgl/programs/uniforms.hpp @@ -13,6 +13,8 @@ MBGL_DEFINE_UNIFORM_MATRIX(double, 4, matrix); MBGL_DEFINE_UNIFORM_SCALAR(float, opacity); MBGL_DEFINE_UNIFORM_SCALAR(Color, color); MBGL_DEFINE_UNIFORM_SCALAR(float, blur); +MBGL_DEFINE_UNIFORM_SCALAR(float, line_clip); +MBGL_DEFINE_UNIFORM_SCALAR(Color, line_clip_color); MBGL_DEFINE_UNIFORM_SCALAR(float, zoom); MBGL_DEFINE_UNIFORM_SCALAR(float, collision_y_stretch); diff --git a/src/mbgl/renderer/layers/line_layer_tweaker.cpp b/src/mbgl/renderer/layers/line_layer_tweaker.cpp index 8b1fa69c6b11..8f73dc9814a8 100644 --- a/src/mbgl/renderer/layers/line_layer_tweaker.cpp +++ b/src/mbgl/renderer/layers/line_layer_tweaker.cpp @@ -65,6 +65,14 @@ auto LineLayerTweaker::evaluate([[maybe_unused]] const PaintParameters& paramete return evaluated.get().constantOr(Property::defaultValue()); } +void LineLayerTweaker::setGradientLineClip(double clip) { + line_clip_ = clip; +} + +void LineLayerTweaker::setGradientLineClipColor(const mbgl::Color& color) { + line_clip_color = color; +} + void LineLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParameters& parameters) { if (layerGroup.empty()) { return; @@ -223,6 +231,8 @@ void LineLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParameters } break; case LineType::Gradient: { + std::array linecolor = { + line_clip_color.r, line_clip_color.g, line_clip_color.b, line_clip_color.a}; #if MLN_UBO_CONSOLIDATION drawableUBOVector[i].lineGradientDrawableUBO = { #else @@ -236,8 +246,9 @@ void LineLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParameters .gapwidth_t = std::get<0>(binders->get()->interpolationFactor(zoom)), .offset_t = std::get<0>(binders->get()->interpolationFactor(zoom)), .width_t = std::get<0>(binders->get()->interpolationFactor(zoom)), - .pad1 = 0, - .pad2 = 0 + .lineclip_t = static_cast(line_clip_), + .pad2 = 0, + .clip_color_t = util::cast(linecolor) }; #if !MLN_UBO_CONSOLIDATION diff --git a/src/mbgl/renderer/layers/line_layer_tweaker.hpp b/src/mbgl/renderer/layers/line_layer_tweaker.hpp index 511f18ac7c8a..da79e07d0056 100644 --- a/src/mbgl/renderer/layers/line_layer_tweaker.hpp +++ b/src/mbgl/renderer/layers/line_layer_tweaker.hpp @@ -33,6 +33,9 @@ class LineLayerTweaker : public LayerTweaker { ~LineLayerTweaker() override = default; + void setGradientLineClip(double clip); + void setGradientLineClipColor(const mbgl::Color& color); + void execute(LayerGroupBase&, const PaintParameters&) override; #if MLN_RENDER_BACKEND_METAL @@ -49,6 +52,8 @@ class LineLayerTweaker : public LayerTweaker { private: template auto evaluate(const PaintParameters& parameters) const; + double line_clip_ = 0.0; + mbgl::Color line_clip_color = mbgl::Color(0.0, 0.0, 0.0, 0.0); #if MLN_RENDER_BACKEND_METAL template diff --git a/src/mbgl/renderer/layers/render_line_layer.cpp b/src/mbgl/renderer/layers/render_line_layer.cpp index e93bf879171e..bc0abeca01ee 100644 --- a/src/mbgl/renderer/layers/render_line_layer.cpp +++ b/src/mbgl/renderer/layers/render_line_layer.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include @@ -511,7 +513,9 @@ void RenderLineLayer::update(gfx::ShaderRegistry& shaders, LineOffset, LineWidth, LineFloorWidth, - LinePattern>( + LinePattern, + LineClip, + LineClipColor>( paintPropertyBinders, evaluated, propertiesAsUniforms, idLineColorVertexAttribute); if (!evaluated.get().from.empty()) { @@ -587,6 +591,22 @@ void RenderLineLayer::update(gfx::ShaderRegistry& shaders, } } + const auto& currLineClipProp = impl_cast(baseImpl).paint.get().value; + if (currLineClipProp.isConstant()) { + double currLineClipPropValue = currLineClipProp.asConstant(); + if (layerTweaker) { + LineLayerTweakerPtr lineLayerTweaker = std::static_pointer_cast(layerTweaker); + lineLayerTweaker->setGradientLineClip(currLineClipPropValue); + } + } + const auto& currLineClipColor = impl_cast(baseImpl).paint.get().value; + if (currLineClipColor.isConstant()) { + Color currLineClipColorValue = currLineClipColor.asConstant(); + if (layerTweaker) { + LineLayerTweakerPtr lineLayerTweaker = std::static_pointer_cast(layerTweaker); + lineLayerTweaker->setGradientLineClipColor(currLineClipColorValue); + } + } auto shader = lineGradientShaderGroup->getOrCreateShader( context, propertiesAsUniforms, posNormalAttribName); if (!shader) { @@ -603,8 +623,12 @@ void RenderLineLayer::update(gfx::ShaderRegistry& shaders, // create texture. to be reused for all the tiles of the layer colorRampTexture2D = context.createTexture2D(); colorRampTexture2D->setImage(colorRamp); + gfx::TextureFilterType filterType = impl_cast(baseImpl).gradientFilterType == + LineGradientFilterType::Linear + ? gfx::TextureFilterType::Linear + : gfx::TextureFilterType::Nearest; colorRampTexture2D->setSamplerConfiguration( - {gfx::TextureFilterType::Linear, gfx::TextureWrapType::Clamp, gfx::TextureWrapType::Clamp}); + {filterType, gfx::TextureWrapType::Clamp, gfx::TextureWrapType::Clamp}); } if (colorRampTexture2D) { diff --git a/src/mbgl/route/route.cpp b/src/mbgl/route/route.cpp new file mode 100644 index 000000000000..7b03c361ad48 --- /dev/null +++ b/src/mbgl/route/route.cpp @@ -0,0 +1,302 @@ +#include "mbgl/programs/attributes.hpp" +#include "mbgl/programs/uniforms.hpp" + +#include +#include +#include + +#include +#include + +namespace mbgl { + +namespace route { + +namespace { + +const double EPSILON = 1e-8; +const double HALF_EPSILON = EPSILON * 0.5; + +std::string tabs(uint32_t tabcount) { + std::string tabstr; + for (size_t i = 0; i < tabcount; ++i) { + tabstr += "\t"; + } + return tabstr; +}; + +[[maybe_unused]] std::string toString(const RouteSegmentOptions& rsopts, + const std::vector normalizedPositions, + uint32_t tabcount) { + std::stringstream ss; + + ss << tabs(tabcount) << "{" << std::endl; + ss << tabs(tabcount + 1) << "\"route_segment_options\" : {" << std::endl; + ss << tabs(tabcount + 2) << "\"color\" : [" << std::to_string(rsopts.color.r) << ", " + << std::to_string(rsopts.color.g) << ", " << std::to_string(rsopts.color.b) << ", " + << std::to_string(rsopts.color.a) << "]," << std::endl; + ss << tabs(tabcount + 2) << "\"priority\" : " << std::to_string(rsopts.priority) << "," << std::endl; + ss << tabs(tabcount + 2) << "\"geometry\" : [" << std::endl; + for (size_t i = 0; i < rsopts.geometry.size(); i++) { + mbgl::Point pt = rsopts.geometry[i]; + std::string terminating = i == rsopts.geometry.size() - 1 ? "" : ","; + ss << tabs(tabcount + 3) << "[" << std::to_string(pt.x) << ", " << std::to_string(pt.y) << "]" << terminating + << std::endl; + } + ss << tabs(tabcount + 2) << "]," << std::endl; // end of options + + ss << tabs(tabcount + 2) << "\"normalized_positions\" : ["; + for (size_t i = 0; i < normalizedPositions.size(); i++) { + std::string terminating = i == normalizedPositions.size() - 1 ? "]\n" : " ,"; + ss << std::to_string(normalizedPositions[i]) << terminating; + } + ss << tabs(tabcount + 1) << "}" << std::endl; + ss << tabs(tabcount) << "}"; + + return ss.str(); +} + +} // namespace + +Route::Route(const LineString& geometry, const RouteOptions& ropts) + : routeOptions_(ropts), + geometry_(geometry) { + assert((!std::isnan(geometry_[0].x) && !std::isnan(geometry_[0].y)) && "invalid geometry point"); + for (size_t i = 1; i < geometry_.size(); ++i) { + mbgl::Point a = geometry_[i]; + mbgl::Point b = geometry_[i - 1]; + assert((!std::isnan(a.x) && !std::isnan(a.y)) && "invalid geometry point"); + double dist = std::abs(mbgl::util::dist(a, b)); + segDistances_.push_back(dist); + totalDistance_ += dist; + } +} + +const RouteOptions& Route::getRouteOptions() const { + return routeOptions_; +} + +double Route::getTotalDistance() const { + return totalDistance_; +} + +bool Route::hasRouteSegments() const { + return !segments_.empty(); +} + +void Route::routeSegmentCreate(const RouteSegmentOptions& rsegopts) { + RouteSegment routeSeg(rsegopts, geometry_, segDistances_, totalDistance_); + segments_.insert(routeSeg); + // regenerate the gradients + segGradient_.clear(); +} + +mbgl::LineString Route::getGeometry() const { + return geometry_; +} + +std::map Route::getRouteColorStops(const mbgl::Color& routeColor) const { + std::map gradients; + gradients[0.0] = routeColor; + gradients[1.0] = routeColor; + + return gradients; +} + +std::vector Route::compactSegments() const { + std::vector compacted; + // insert the first range + SegmentRange sr; + const auto& firstSegment = segments_.begin(); + uint32_t firstSegmentNormalizedSize = firstSegment->getNormalizedPositions().size(); + double firstPos = firstSegment->getNormalizedPositions()[0]; + double lastPos = firstSegment->getNormalizedPositions()[firstSegmentNormalizedSize - 1]; + sr.range = {firstPos, lastPos}; + sr.color = segments_.begin()->getRouteSegmentOptions().color; + compacted.push_back(sr); + + for (auto iter = std::next(segments_.begin()); iter != segments_.end(); ++iter) { + if (iter->getNormalizedPositions().empty()) continue; + const std::vector& currPositions = iter->getNormalizedPositions(); + const RouteSegmentOptions& currOptions = iter->getRouteSegmentOptions(); + + const std::vector& prevPositions = std::prev(iter)->getNormalizedPositions(); + const RouteSegmentOptions& prevOptions = std::prev(iter)->getRouteSegmentOptions(); + + const auto& prevDist = prevPositions[prevPositions.size() - 1]; + const auto& currDist = currPositions[0]; + const auto& prevColor = prevOptions.color; + const auto& currColor = currOptions.color; + bool isIntersecting = prevDist >= currDist; + if (isIntersecting) { + if (prevColor == currColor) { + // merge the segments + compacted.rbegin()->range.second = currPositions[currPositions.size() - 1]; + continue; + + } else if (prevOptions.priority >= currOptions.priority) { + firstPos = prevPositions[prevPositions.size() - 1] + EPSILON; + lastPos = currPositions[currPositions.size() - 1]; + sr.range = {firstPos, lastPos}; + sr.color = currColor; + + } else if (prevOptions.priority < currOptions.priority) { + // modify the previous segment and leave some space. We want all segments to be disjoint. + compacted.rbegin()->range.second = currPositions[0] - EPSILON; + + // add the current segment + firstPos = currPositions[0]; + lastPos = currPositions[currPositions.size() - 1]; + sr.range = {firstPos, lastPos}; + sr.color = currColor; + } + } else { + firstPos = currPositions[0]; + lastPos = currPositions[currPositions.size() - 1]; + sr.range = {firstPos, lastPos}; + sr.color = currColor; + } + + compacted.push_back(sr); + } + + return compacted; +} + +std::map Route::getRouteSegmentColorStops(const mbgl::Color& routeColor) { + std::map colorStops; + if (segments_.empty()) { + return getRouteColorStops(routeColor); + } + + std::vector compacted = compactSegments(); + // Initialize the color ramp with the routeColor + colorStops[0.0] = routeColor; + + for (const auto& sr : compacted) { + double firstPos = sr.range.first; + double lastPos = sr.range.second; + + double pre_pos = firstPos - HALF_EPSILON < 0.0 ? 0.0 : firstPos - HALF_EPSILON; + double post_pos = lastPos + HALF_EPSILON > 1.0f ? 1.0f : lastPos + HALF_EPSILON; + + colorStops[pre_pos] = routeColor; + colorStops[firstPos] = sr.color; + colorStops[lastPos] = sr.color; + colorStops[post_pos] = routeColor; + } + + if (colorStops.rbegin()->first != 1.0) { + colorStops[1.0] = routeColor; + } + + return colorStops; +} + +void Route::routeSetProgress(const double t) { + progress_ = t; +} + +double Route::routeGetProgress() const { + return progress_; +} + +double Route::getProgressPercent(const Point& progressPoint) const { + if (geometry_.size() < 2) { + return 0.0; + } + + double minDistance = std::numeric_limits::max(); + size_t closestSegmentIndex = 0; + double closestPercentage = 0.0; + + // Iterate through each line segment in the polyline + for (size_t i = 0; i < geometry_.size() - 1; i++) { + const Point& p1 = geometry_[i]; + const Point& p2 = geometry_[i + 1]; + + // Vector from p1 to p2 + double segmentX = p2.x - p1.x; + double segmentY = p2.y - p1.y; + double segmentLength = segDistances_[i]; + + if (segmentLength == 0.0) continue; + + // Vector from p1 to progressPoint + double vectorX = progressPoint.x - p1.x; + double vectorY = progressPoint.y - p1.y; + + // Project progressPoint onto the line segment + double projection = (vectorX * segmentX + vectorY * segmentY) / segmentLength; + double percentage = std::max(0.0, std::min(1.0, projection / segmentLength)); + + // Calculate the closest point on the line segment + double closestX = p1.x + percentage * segmentX; + double closestY = p1.y + percentage * segmentY; + + // Calculate distance to the line segment + double distance = std::sqrt(std::pow(progressPoint.x - closestX, 2) + std::pow(progressPoint.y - closestY, 2)); + + // Update if this is the closest segment found + if (distance < minDistance) { + minDistance = distance; + closestSegmentIndex = i; + closestPercentage = percentage; + } + } + + // Calculate distance up to the projected point using stored segment distances + double distanceToPoint = 0.0; + for (size_t i = 0; i < closestSegmentIndex; i++) { + distanceToPoint += segDistances_[i]; + } + distanceToPoint += segDistances_[closestSegmentIndex] * closestPercentage; + + return totalDistance_ > 0.0 ? distanceToPoint / totalDistance_ : 0.0; +} + +uint32_t Route::getNumRouteSegments() const { + return static_cast(segments_.size()); +} + +std::vector Route::getRouteSegmentDistances() const { + return segDistances_; +} + +bool Route::routeSegmentsClear() { + segments_.clear(); + segGradient_.clear(); + + return true; +} + +std::string Route::segmentsToString(uint32_t tabcount) const { + std::stringstream ss; + ss << tabs(tabcount) << "[" << std::endl; + for (auto iter = segments_.begin(); iter != segments_.end(); ++iter) { + auto segment = *iter; + std::string terminatingCommaStr = std::next(iter) == segments_.end() ? "" : ","; + ss << toString(segment.getRouteSegmentOptions(), segment.getNormalizedPositions(), tabcount + 1) + << terminatingCommaStr << std::endl; + } + ss << tabs(tabcount) << "]"; + + return ss.str(); +} + +Route& Route::operator=(const Route& other) noexcept { + if (this == &other) { + return *this; + } + routeOptions_ = other.routeOptions_; + progress_ = other.progress_; + segDistances_ = other.segDistances_; + segments_ = other.segments_; + geometry_ = other.geometry_; + totalDistance_ = other.totalDistance_; + segGradient_ = other.segGradient_; + return *this; +} + +} // namespace route +} // namespace mbgl diff --git a/src/mbgl/route/route_manager.cpp b/src/mbgl/route/route_manager.cpp new file mode 100644 index 000000000000..530fa1b683dc --- /dev/null +++ b/src/mbgl/route/route_manager.cpp @@ -0,0 +1,774 @@ + +#include "mbgl/util/containers.hpp" +#include "mbgl/util/math.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // For formatting +#include + +namespace mbgl { +namespace route { + +const std::string RouteManager::BASE_ROUTE_LAYER = "base_route_layer_"; +const std::string RouteManager::ACTIVE_ROUTE_LAYER = "active_route_layer_"; +const std::string RouteManager::GEOJSON_BASE_ROUTE_SOURCE_ID = "base_route_geojson_source_"; +const std::string RouteManager::GEOJSON_ACTIVE_ROUTE_SOURCE_ID = "active_route_geojson_source_"; + +namespace { + +std::string tabs(uint32_t tabcount) { + std::string tabstr; + for (size_t i = 0; i < tabcount; ++i) { + tabstr += "\t"; + } + return tabstr; +} + +[[maybe_unused]] std::string embeddAPIcaptures(const std::vector& apiCaptures) { + std::stringstream ss; + ss << "{" << std::endl; + ss << tabs(1) << "\"apiCalls\":[" << std::endl; + for (size_t i = 0; i < apiCaptures.size(); ++i) { + ss << tabs(2) << apiCaptures[i]; + if (i == (apiCaptures.size() - 1)) { + ss << std::endl; + } else { + ss << "," << std::endl; + } + } + ss << tabs(1) << "]" << std::endl; + ss << "}" << std::endl; + + return ss.str(); +} + +[[maybe_unused]] std::string createAPIcapture(const std::string& fnname, + const std::unordered_map& args, + const std::string& resultType, + const std::string& resultValue, + const std::unordered_map& extraDataMap, + const std::string& extraData = "") { + std::stringstream tss; + // Format the time as a string + static uint32_t eventID = 0; + std::stringstream ss; + + ss << "{" << std::endl; + ss << tabs(2) << "\"event_id\" : " << std::to_string(eventID++) << "," << std::endl; + ss << tabs(2) << "\"api_name\" : \"" << fnname << "\"," << std::endl; + ss << tabs(2) << "\"parameters\" : {" << std::endl; + for (auto iter = args.begin(); iter != args.end(); ++iter) { + std::string terminatingStr = std::next(iter) == args.end() ? "" : ","; + ss << tabs(3) << "\"" << iter->first << "\" : " << iter->second << terminatingStr << std::endl; + } + ss << tabs(2) << "}," << std::endl; + ss << tabs(2) << "\"extra_data\" : " << "\"" << extraData << "\"," << std::endl; + ss << tabs(2) << "\"extra_data_map\" : " << "{" << std::endl; + for (auto iter = extraDataMap.begin(); iter != extraDataMap.end(); ++iter) { + std::string terminatingStr = std::next(iter) == extraDataMap.end() ? "" : ","; + ss << tabs(3) << "\"" << iter->first << "\" : " << iter->second << terminatingStr << std::endl; + } + ss << tabs(2) << "}," << std::endl; + ss << tabs(2) << "\"result\" : {" << std::endl; + ss << tabs(2) << "\"result_type\" : " << "\"" << resultType << "\"," << std::endl; + ss << tabs(2) << "\"result_value\" : " << "\"" << resultValue << "\"" << std::endl; + ss << tabs(2) << "}" << std::endl; + ss << tabs(1) << "}"; + return ss.str(); +} + +#define TRACE_ROUTE_CALL(apiCaptures, functionParamMap, resultType, resultValue, extraDataMap, extraData) \ + apiCaptures.push_back( \ + createAPIcapture(__FUNCTION__, functionParamMap, resultType, resultValue, extraDataMap, extraData)); + +std::string formatElapsedTime(long long value) { + std::stringstream ss; + ss.imbue(std::locale("")); // Use the user's default locale for number formatting + ss << std::fixed << value; + return ss.str(); +} + +[[maybe_unused]] std::string toString(bool onOff) { + return onOff ? "true" : "false"; +} + +[[maybe_unused]] std::string toString(const LineString& line, uint32_t tabcount) { + std::stringstream ss; + ss << tabs(tabcount) << "[" << std::endl; + for (size_t i = 0; i < line.size(); i++) { + std::string terminatingCommaStr = i == line.size() - 1 ? "" : ","; + ss << tabs(tabcount + 1) << "[" << std::to_string(line[i].x) << ", " << std::to_string(line[i].y) << "]" + << terminatingCommaStr << std::endl; + } + ss << tabs(tabcount) << "]"; + + return ss.str(); +} + +[[maybe_unused]] std::string toString(const std::map& mapdata) { + std::stringstream ss; + ss << "[" << std::endl; + for (auto iter = mapdata.begin(); iter != mapdata.end(); iter++) { + std::string terminatingStr = std::next(iter) == mapdata.end() ? "" : ", "; + ss << "{\"" << std::to_string(iter->first) << "\" : " << iter->second << " }" << terminatingStr << std::endl; + } + ss << "]"; + + return ss.str(); +} + +[[maybe_unused]] std::string toString(const RouteOptions& ropts, uint32_t tabcount) { + std::stringstream ss; + ss << tabs(tabcount) << "{" << std::endl; + ss << tabs(tabcount + 1) << "\"innerColor\": " << "[" << std::to_string(ropts.innerColor.r) << ", " + << std::to_string(ropts.innerColor.g) << ", " << std::to_string(ropts.innerColor.b) << ", " + << std::to_string(ropts.innerColor.a) << "]," << std::endl; + ss << tabs(tabcount + 1) << "\"outerColor\": " << "[" << std::to_string(ropts.outerColor.r) << ", " + << std::to_string(ropts.outerColor.g) << ", " << std::to_string(ropts.outerColor.b) << ", " + << std::to_string(ropts.outerColor.a) << "]," << std::endl; + ss << tabs(tabcount + 1) << "\"innerClipColor\": " << "[" << std::to_string(ropts.innerClipColor.r) << ", " + << std::to_string(ropts.innerClipColor.g) << ", " << std::to_string(ropts.innerClipColor.b) << ", " + << std::to_string(ropts.innerClipColor.a) << "]," << std::endl; + ss << tabs(tabcount + 1) << "\"outerClipColor\": " << "[" << std::to_string(ropts.outerClipColor.r) << ", " + << std::to_string(ropts.outerClipColor.g) << ", " << std::to_string(ropts.outerClipColor.b) << ", " + << std::to_string(ropts.outerClipColor.a) << "]," << std::endl; + ss << tabs(tabcount + 1) << "\"innerWidth\": " << "\"" << std::to_string(ropts.innerWidth) << "\"," << std::endl; + ss << tabs(tabcount + 1) << "\"outerWidth\": " << "\"" << std::to_string(ropts.outerWidth) << "\"," << std::endl; + ss << tabs(tabcount + 1) << "\"layerBefore\": " << "\"" << (ropts.layerBefore) << "\"," << std::endl; + if (!ropts.outerWidthZoomStops.empty()) { + ss << tabs(tabcount + 1) << "\"outerWidthZoomStops\": " << toString(ropts.outerWidthZoomStops) << ", " + << std::endl; + } + if (!ropts.innerWidthZoomStops.empty()) { + ss << tabs(tabcount + 1) << "\"innerWidthZoomStops\": " << toString(ropts.innerWidthZoomStops) << ", " + << std::endl; + } + ss << tabs(tabcount + 1) << "\"useDynamicWidths\": " << toString(ropts.useDynamicWidths) << std::endl; + ss << tabs(tabcount) << "}"; + return ss.str(); +} + +[[maybe_unused]] std::string toString(const std::map& gradient) { + std::stringstream ss; + ss << "[" << std::endl; + for (auto iter = gradient.begin(); iter != gradient.end(); iter++) { + std::string terminatingStr = std::next(iter) == gradient.end() ? "" : ", "; + ss << "{\"" << std::to_string(iter->first) << "\" : " << "[" << std::to_string(iter->second.r) << ", " + << std::to_string(iter->second.g) << ", " << std::to_string(iter->second.b) << ", " + << std::to_string(iter->second.a) << "] }" << terminatingStr << std::endl; + } + ss << "]"; + + return ss.str(); +} + +constexpr double PIXEL_DENSITY = 1.6; + +constexpr double getZoomStepDownValue() { + return PIXEL_DENSITY - 1.0; +} + +constexpr double PIXEL_DENSITY_FACTOR = 1.0 / PIXEL_DENSITY; +constexpr double ROUTE_LINE_ZOOM_LEVEL_4 = 4 - getZoomStepDownValue(); +constexpr double ROUTE_LINE_ZOOM_LEVEL_10 = 10 - getZoomStepDownValue(); +constexpr double ROUTE_LINE_ZOOM_LEVEL_18 = 18 - getZoomStepDownValue(); +constexpr double ROUTE_LINE_ZOOM_LEVEL_20 = 20 - getZoomStepDownValue(); + +constexpr double ROUTE_LINE_WEIGHT_6 = 6; +constexpr double ROUTE_LINE_WEIGHT_9 = 9; +constexpr double ROUTE_LINE_WEIGHT_16 = 16; +constexpr double ROUTE_LINE_WEIGHT_22 = 22; + +constexpr double ROUTE_LINE_CASING_MULTIPLIER = 1.6 * PIXEL_DENSITY_FACTOR; +constexpr double ROUTE_LINE_MULTIPLIER = 1.0 * PIXEL_DENSITY_FACTOR; + +std::map getDefaultRouteLineCasingWeights() { + return {{ROUTE_LINE_ZOOM_LEVEL_4, ROUTE_LINE_WEIGHT_6 * ROUTE_LINE_CASING_MULTIPLIER}, + {ROUTE_LINE_ZOOM_LEVEL_10, ROUTE_LINE_WEIGHT_9 * ROUTE_LINE_CASING_MULTIPLIER}, + {ROUTE_LINE_ZOOM_LEVEL_18, ROUTE_LINE_WEIGHT_16 * ROUTE_LINE_CASING_MULTIPLIER}, + {ROUTE_LINE_ZOOM_LEVEL_20, ROUTE_LINE_WEIGHT_22 * ROUTE_LINE_CASING_MULTIPLIER}}; +} + +std::map getDefaultRouteLineWeights() { + return {{ROUTE_LINE_ZOOM_LEVEL_4, ROUTE_LINE_WEIGHT_6 * ROUTE_LINE_MULTIPLIER}, + {ROUTE_LINE_ZOOM_LEVEL_10, ROUTE_LINE_WEIGHT_9 * ROUTE_LINE_MULTIPLIER}, + {ROUTE_LINE_ZOOM_LEVEL_18, ROUTE_LINE_WEIGHT_16 * ROUTE_LINE_MULTIPLIER}, + {ROUTE_LINE_ZOOM_LEVEL_20, ROUTE_LINE_WEIGHT_22 * ROUTE_LINE_MULTIPLIER}}; +} + +struct AvgIntervalStat { + std::chrono::steady_clock::time_point lastStartTime; + std::chrono::duration totalIntervalDuration{0.0}; + long long intervalCount = 0; + bool firstCall = true; +}; + +AvgIntervalStat routeCreateStat; +AvgIntervalStat routeSegmentCreateStat; + +} // namespace + +RouteManager::RouteManager() + : routeIDpool_(100) {} + +std::string RouteManager::dirtyTypeToString(const RouteManager::DirtyType& dt) const { + switch (dt) { + case DirtyType::dtRouteGeometry: + return "\"dtRouteGeometry\""; + case DirtyType::dtRouteProgress: + return "\"dtRouteProgress\""; + case DirtyType::dtRouteSegments: + return "\"dtRouteSegments\""; + } + + return ""; +} + +std::vector RouteManager::getAllRoutes() const { + std::vector routeIDs; + for (const auto& iter : routeMap_) { + routeIDs.push_back(iter.first); + } + + return routeIDs; +} + +int RouteManager::getTopMost(const std::vector& routeList) const { + assert(style_ != nullptr && "style not set"); + + if (style_ != nullptr) { + std::vector layers = style_->getLayers(); + + if (!layers.empty()) { + for (size_t i = layers.size() - 1; i >= 0; i--) { + const std::string currLayerName = layers[i]->getID(); + + for (size_t j = 0; j < routeList.size(); j++) { + const RouteID routeID = routeList[j]; + const std::string& layerName = getBaseRouteLayerName(routeID); + if (layerName == currLayerName) { + return j; + } + } + } + } + } + + return -1; +} + +std::string RouteManager::captureSnapshot() const { + std::stringstream ss; + ss << "{" << std::endl; + ss << tabs(1) << "\"num_routes\" : " << std::to_string(routeMap_.size()) << "," << std::endl; + ss << tabs(1) << "\"routes\": " << "[" << std::endl; + for (auto iter = routeMap_.begin(); iter != routeMap_.end(); ++iter) { + std::string terminatingStr = std::next(iter) == routeMap_.end() ? "" : ","; + ss << tabs(2) << "{" << std::endl; + ss << tabs(3) << "\"route_id\" : " << std::to_string(iter->first.id) << "," << std::endl; + ss << tabs(3) << "\"route\" : {" << std::endl; + ss << tabs(4) << "\"route_options\" : " << std::endl; + ss << toString(iter->second.getRouteOptions(), 5) << "," << std::endl; + ss << tabs(4) << "\"geometry\" : " << std::endl; + ss << toString(iter->second.getGeometry(), 4) << "," << std::endl; + ss << tabs(4) << "\"route_segments\" : " << std::endl; + ss << iter->second.segmentsToString(5) << std::endl; + ss << tabs(3) << "}" << std::endl; + ss << tabs(2) << "}" << terminatingStr << std::endl; + } + ss << tabs(1) << "]" << std::endl; + ss << "}" << std::endl; + return ss.str(); +} + +void RouteManager::setStyle(style::Style& style) { + if (style_ != nullptr && style_ != &style) { + // remove the old base, active layer and source for each route and add them to the new style + for (auto& routeIter : routeMap_) { + const auto& routeID = routeIter.first; + std::string baseLayerID = getBaseRouteLayerName(routeID); + std::string activeLayerID = getActiveRouteLayerName(routeID); + std::string baseGeoJSONsrcID = getBaseGeoJSONsourceName(routeID); + std::string activeGeoJSONsrcID = getActiveGeoJSONsourceName(routeID); + + std::unique_ptr baseLayer = style_->removeLayer(baseLayerID); + std::unique_ptr baseGeoJSONsrc = style_->removeSource(baseGeoJSONsrcID); + std::unique_ptr activeLayer = style_->removeLayer(activeLayerID); + std::unique_ptr activeGeoJSONsrc = style_->removeSource(activeGeoJSONsrcID); + + if (baseLayer) { + style.addLayer(std::move(baseLayer)); + } + if (baseGeoJSONsrc) { + style.addSource(std::move(baseGeoJSONsrc)); + } + if (activeLayer) { + style.addLayer(std::move(activeLayer)); + } + if (activeGeoJSONsrc) { + style.addSource(std::move(activeGeoJSONsrc)); + } + } + } + style_ = &style; +} + +bool RouteManager::hasStyle() const { + return style_ != nullptr; +} + +RouteID RouteManager::routeCreate(const LineString& geometry, const RouteOptions& ropts) { + RouteID rid; + bool success = routeIDpool_.createID((rid.id)); + if (success && rid.isValid()) { + Route route(geometry, ropts); + routeMap_[rid] = route; + stats_.numRoutes++; + dirtyRouteMap_[DirtyType::dtRouteGeometry].insert(rid); + + auto now = std::chrono::steady_clock::now(); + if (!routeCreateStat.firstCall) { + auto interval = now - routeCreateStat.lastStartTime; + routeCreateStat.totalIntervalDuration += interval; + routeCreateStat.intervalCount++; + if (routeCreateStat.intervalCount > 0) { + stats_.avgRouteCreationInterval = + std::chrono::duration(routeCreateStat.totalIntervalDuration).count() / + routeCreateStat.intervalCount; + } + + } else { + routeCreateStat.firstCall = false; + } + routeCreateStat.lastStartTime = now; + } + + return rid; +} + +bool RouteManager::routeSegmentCreate(const RouteID& routeID, const RouteSegmentOptions& routeSegOpts) { + assert(routeID.isValid() && "Invalid route ID"); + assert(routeMap_.find(routeID) != routeMap_.end() && "Route not found internally"); + if (routeID.isValid() && routeMap_.find(routeID) != routeMap_.end()) { + // route segments must have atleast 2 points + if (routeSegOpts.geometry.size() < 2) { + return false; + } + + routeMap_[routeID].routeSegmentCreate(routeSegOpts); + stats_.numRouteSegments++; + + validateAddToDirtyBin(routeID, DirtyType::dtRouteSegments); + + return true; + } + + return false; +} + +void RouteManager::validateAddToDirtyBin(const RouteID& routeID, const DirtyType& dirtyBin) { + // check if this route is in the list of dirty geometry routes. if so, then no need to set dirty segments, as it + // will be created during finalize + bool foundDirtyRoute = false; + if (dirtyRouteMap_.find(DirtyType::dtRouteGeometry) != dirtyRouteMap_.end()) { + const auto& dirtyRouteIDs = dirtyRouteMap_[DirtyType::dtRouteGeometry]; + if (dirtyRouteIDs.find(routeID) != dirtyRouteIDs.end()) { + foundDirtyRoute = true; + } + } + if (!foundDirtyRoute) { + dirtyRouteMap_[dirtyBin].insert(routeID); + } +} + +void RouteManager::routeClearSegments(const RouteID& routeID) { + assert(routeID.isValid() && "invalid route ID"); + if (routeID.isValid() && routeMap_.find(routeID) != routeMap_.end()) { + stats_.numRouteSegments -= routeMap_[routeID].getNumRouteSegments(); + if (routeMap_[routeID].hasRouteSegments()) { + routeMap_[routeID].routeSegmentsClear(); + validateAddToDirtyBin(routeID, DirtyType::dtRouteSegments); + } + + auto now = std::chrono::steady_clock::now(); + if (!routeSegmentCreateStat.firstCall) { + auto interval = now - routeSegmentCreateStat.lastStartTime; + routeSegmentCreateStat.totalIntervalDuration += interval; + routeSegmentCreateStat.intervalCount++; + if (routeSegmentCreateStat.intervalCount > 0) { + stats_.avgRouteSegmentCreationInterval = + std::chrono::duration(routeSegmentCreateStat.totalIntervalDuration).count() / + routeSegmentCreateStat.intervalCount; + } + + } else { + routeSegmentCreateStat.firstCall = false; + } + routeSegmentCreateStat.lastStartTime = now; + } +} + +bool RouteManager::routeDispose(const RouteID& routeID) { + assert(style_ != nullptr && "Style not set!"); + assert(routeID.isValid() && "Invalid route ID"); + assert(routeMap_.find(routeID) != routeMap_.end() && "Route not found internally"); + bool success = false; + if (routeID.isValid() && routeMap_.find(routeID) != routeMap_.end() && style_ != nullptr) { + std::string baseLayerName = getBaseRouteLayerName(routeID); + std::string activeLayerName = getActiveRouteLayerName(routeID); + std::string baseGeoJSONsrcName = getBaseGeoJSONsourceName(routeID); + std::string activeGeoJSONsrcName = getActiveGeoJSONsourceName(routeID); + + if (style_->removeLayer(baseLayerName) != nullptr) { + success = true; + } + if (style_->removeLayer(activeLayerName) != nullptr) { + success = true; + } + if (style_->removeSource(baseGeoJSONsrcName) != nullptr) { + success = true; + } + + if (style_->removeSource(activeGeoJSONsrcName) != nullptr) { + success = true; + } + + routeMap_.erase(routeID); + routeIDpool_.destroyID(routeID.id); + stats_.numRoutes--; + + return success; + } + + return success; +} + +bool RouteManager::hasRoutes() const { + return !routeMap_.empty(); +} + +bool RouteManager::routeSetProgress(const RouteID& routeID, const double progress) { + assert(style_ != nullptr && "Style not set!"); + assert(routeID.isValid() && "invalid route ID"); + double validProgress = std::clamp(progress, 0.0, 1.0); + bool success = false; + if (routeID.isValid() && routeMap_.find(routeID) != routeMap_.end()) { + routeMap_[routeID].routeSetProgress(validProgress); + + validateAddToDirtyBin(routeID, DirtyType::dtRouteProgress); + + success = true; + } + + return success; +} + +bool RouteManager::routeSetProgress(const RouteID& routeID, const mbgl::Point& progressPoint) { + assert(routeID.isValid() && "invalid route ID"); + bool success = false; + if (routeID.isValid() && routeMap_.find(routeID) != routeMap_.end()) { + double progressPercent = routeMap_.at(routeID).getProgressPercent(progressPoint); + routeMap_[routeID].routeSetProgress(progressPercent); + + validateAddToDirtyBin(routeID, DirtyType::dtRouteProgress); + success = true; + } + + return success; +} + +std::string RouteManager::getActiveRouteLayerName(const RouteID& routeID) const { + return ACTIVE_ROUTE_LAYER + std::to_string(routeID.id); +} + +std::string RouteManager::getBaseRouteLayerName(const RouteID& routeID) const { + return BASE_ROUTE_LAYER + std::to_string(routeID.id); +} + +std::string RouteManager::getActiveGeoJSONsourceName(const RouteID& routeID) const { + return GEOJSON_ACTIVE_ROUTE_SOURCE_ID + std::to_string(routeID.id); +} + +std::string RouteManager::getBaseGeoJSONsourceName(const RouteID& routeID) const { + return GEOJSON_BASE_ROUTE_SOURCE_ID + std::to_string(routeID.id); +} + +const std::string RouteManager::getStats() { + rapidjson::Document document; + document.SetObject(); + rapidjson::Document::AllocatorType& allocator = document.GetAllocator(); + + rapidjson::Value route_stats(rapidjson::kObjectType); + route_stats.AddMember("num_routes", stats_.numRoutes, allocator); + route_stats.AddMember("num_traffic_zones", stats_.numRouteSegments, allocator); + route_stats.AddMember("num_finalize_invocations", stats_.numFinalizedInvoked, allocator); + route_stats.AddMember("inconsistant_route_API_usages", stats_.inconsistentAPIusage, allocator); + route_stats.AddMember("avg_route_finalize_elapse_millis", stats_.finalizeMillis, allocator); + route_stats.AddMember("avg_route_create_interval_seconds", stats_.avgRouteCreationInterval, allocator); + route_stats.AddMember( + "avg_route_segment_create_interval_seconds", stats_.avgRouteSegmentCreationInterval, allocator); + + document.AddMember("route_stats", route_stats, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::PrettyWriter writer(buffer); + document.Accept(writer); + + return buffer.GetString(); +} + +void RouteManager::finalizeRoute(const RouteID& routeID, const DirtyType& dt) { + assert(routeID.isValid() && "invalid route ID"); + using namespace mbgl::style; + using namespace mbgl::style::expression; + + const auto& createDynamicWidthExpression = [](const std::map& zoomstops) { + ParsingContext pc; + ParseResult pr = dsl::zoom(); + std::unique_ptr zoomValueExp = std::move(pr.value()); + + Interpolator linearInterpolator = dsl::linear(); + + using namespace mbgl::style::expression; + std::map> stops; + + for (auto& zoomstopIter : zoomstops) { + stops[zoomstopIter.first] = dsl::literal(zoomstopIter.second); + } + + const auto& type = stops.begin()->second->getType(); + ParsingContext ctx; + ParseResult result = createInterpolate( + type, std::move(linearInterpolator), std::move(zoomValueExp), std::move(stops), ctx); + assert(result); + std::unique_ptr expression = std::move(*result); + PropertyExpression expressionFloat = PropertyExpression(std::move(expression)); + return expressionFloat; + }; + + const auto& createLayer = [&](const std::string& sourceID, + const std::string& layerID, + const Route& route, + const Color& color, + const Color& clipColor, + int width, + const std::map& zoomstops) { + if (style_->getSource(sourceID) != nullptr) { + return false; + } + mapbox::geojsonvt::feature_collection featureCollection; + const auto& geom = route.getGeometry(); + featureCollection.emplace_back(geom); + + GeoJSONOptions opts; + opts.lineMetrics = true; + std::unique_ptr geoJSONsrc = std::make_unique( + sourceID, mbgl::makeMutable(std::move(opts))); + geoJSONsrc->setGeoJSON(featureCollection); + + style_->addSource(std::move(geoJSONsrc)); + + // create the layers for each route + std::unique_ptr layer = std::make_unique(layerID, sourceID); + if (style_->getLayer(layerID) != nullptr) { + return false; + } + layer->setLineColor(color); + layer->setLineCap(LineCapType::Round); + layer->setLineJoin(LineJoinType::Round); + + layer->setGradientLineFilter(LineGradientFilterType::Nearest); + layer->setGradientLineClipColor(clipColor); + + if (zoomstops.empty()) { + layer->setLineWidth(width); + } else { + const PropertyExpression& lineWidthExpression = createDynamicWidthExpression(zoomstops); + layer->setLineWidth(lineWidthExpression); + } + + auto& routeOpts = route.getRouteOptions(); + if (routeOpts.layerBefore.empty()) { + style_->addLayer(std::move(layer)); + } else { + style_->addLayer(std::move(layer), routeOpts.layerBefore); + } + + return true; + }; + + const auto& createGradientExpression = [](const std::map& gradient) { + ParsingContext pc; + ParseResult pr = createCompoundExpression("line-progress", {}, pc); + std::unique_ptr lineprogressValueExp = std::move(pr.value()); + + Interpolator linearInterpolator = dsl::linear(); + + using namespace mbgl::style::expression; + std::map> stops; + for (auto& segGradientIter : gradient) { + stops[segGradientIter.first] = (dsl::literal(segGradientIter.second)); + } + + const auto& type = stops.begin()->second->getType(); + + ParsingContext ctx; + ParseResult result = createInterpolate( + type, std::move(linearInterpolator), std::move(lineprogressValueExp), std::move(stops), ctx); + assert(result); + std::unique_ptr expression = std::move(*result); + + return expression; + }; + + std::string captureExtraDataStr; + if (routeID.isValid() && routeMap_.find(routeID) != routeMap_.end()) { + bool updateRouteLayers = false; + bool updateGradients = false; + bool updateProgress = false; + switch (dt) { + case DirtyType::dtRouteGeometry: { + updateRouteLayers = true; + updateGradients = true; + updateProgress = true; + } break; + + case DirtyType::dtRouteProgress: { + updateProgress = true; + } break; + + case DirtyType::dtRouteSegments: { + updateGradients = true; + } break; + } + + std::string baseLayerName = getBaseRouteLayerName(routeID); + std::string baseGeoJSONSourceName = getBaseGeoJSONsourceName(routeID); + std::string activeLayerName = getActiveRouteLayerName(routeID); + std::string activeGeoJSONSourceName = getActiveGeoJSONsourceName(routeID); + auto& route = routeMap_.at(routeID); + const RouteOptions& routeOptions = route.getRouteOptions(); + + if (updateRouteLayers) { + // create layer for casing/base + std::map baseZoomStops; + if (routeOptions.useDynamicWidths) { + baseZoomStops = !routeOptions.outerWidthZoomStops.empty() ? routeOptions.outerWidthZoomStops + : getDefaultRouteLineCasingWeights(); + } + if (!createLayer(baseGeoJSONSourceName, + baseLayerName, + route, + routeOptions.outerColor, + routeOptions.outerClipColor, + routeOptions.outerWidth, + baseZoomStops)) { + stats_.inconsistentAPIusage = true; + mbgl::Log::Info(mbgl::Event::Style, "Trying to update a layer that is not created"); + } + + // create layer for active/blue + std::map activeLineWidthStops; + if (routeOptions.useDynamicWidths) { + activeLineWidthStops = !routeOptions.innerWidthZoomStops.empty() ? routeOptions.innerWidthZoomStops + : getDefaultRouteLineWeights(); + } + if (!createLayer(activeGeoJSONSourceName, + activeLayerName, + route, + routeOptions.innerColor, + routeOptions.innerClipColor, + routeOptions.innerWidth, + activeLineWidthStops)) { + stats_.inconsistentAPIusage = true; + mbgl::Log::Info(mbgl::Event::Style, "Trying to update a layer that is not created"); + } + } + + Layer* activeRouteLayer = style_->getLayer(activeLayerName); + LineLayer* activeRouteLineLayer = static_cast(activeRouteLayer); + if (!activeRouteLineLayer) { + stats_.inconsistentAPIusage = true; + mbgl::Log::Info(mbgl::Event::Style, "Trying to update a layer that is not created"); + } + Layer* baseRouteLayer = style_->getLayer(baseLayerName); + LineLayer* baseRouteLineLayer = static_cast(baseRouteLayer); + if (!baseRouteLineLayer) { + stats_.inconsistentAPIusage = true; + mbgl::Log::Info(mbgl::Event::Style, "Trying to update a layer that is not created"); + } + + // Create the gradient colors expressions and set on the active layer + std::unordered_map gradientDebugMap; + if (updateGradients) { + // create the gradient expression for active route. + std::map gradientMap = route.getRouteSegmentColorStops(routeOptions.innerColor); + + std::unique_ptr gradientExpression = createGradientExpression(gradientMap); + + ColorRampPropertyValue activeColorRampProp(std::move(gradientExpression)); + activeRouteLineLayer->setLineGradient(activeColorRampProp); + + // create the gradient expression for the base route + std::map baseLayerGradient = route.getRouteColorStops(routeOptions.outerColor); + std::unique_ptr baseLayerExpression = createGradientExpression(baseLayerGradient); + + ColorRampPropertyValue baseColorRampProp(std::move(baseLayerExpression)); + baseRouteLineLayer->setLineGradient(baseColorRampProp); + } + + if (updateProgress) { + double progress = route.routeGetProgress(); + activeRouteLineLayer->setGradientLineClip(progress); + baseRouteLineLayer->setGradientLineClip(progress); + } + } +} + +void RouteManager::finalize() { + using namespace mbgl::style; + using namespace mbgl::style::expression; + using namespace std::chrono; + + stats_.numFinalizedInvoked++; + auto startclock = high_resolution_clock::now(); + { + assert(style_ != nullptr); + if (style_ != nullptr) { + // create the layers and geojsonsource for base route + for (const auto& iter : dirtyRouteMap_) { + DirtyType dirtyType = iter.first; + for (const auto& routeID : iter.second) { + finalizeRoute(routeID, dirtyType); + } + } + dirtyRouteMap_.clear(); + } + } + auto stopclock = high_resolution_clock::now(); + stats_.finalizeMillis = formatElapsedTime(duration_cast(stopclock - startclock).count()); +} + +RouteManager::~RouteManager() {} + +} // namespace route +} // namespace mbgl diff --git a/src/mbgl/route/route_segment.cpp b/src/mbgl/route/route_segment.cpp new file mode 100644 index 000000000000..6c2e91350706 --- /dev/null +++ b/src/mbgl/route/route_segment.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include + +namespace mbgl { +namespace route { + +namespace { +// Function to check if point A is between points B and C +bool isPointBetween(const mbgl::Point& A, + const mbgl::Point& B, + const mbgl::Point& C, + bool quickDirty = false) { + // Check if the points are collinear (on the same line) + double crossProduct = (C.y - B.y) * (A.x - B.x) - (C.x - B.x) * (A.y - B.y); + + if (std::abs(crossProduct) > 1e-9) { // Using a small epsilon for floating-point comparison + return false; // Not collinear, so A cannot be between B and C + } + + // Check if A is within the bounding box of B and C. This is a simplified version. + // A more robust check would involve projecting A onto the line BC and checking + // if the projection lies within the segment BC. The bounding box check is sufficient + // for many common cases and is less computationally expensive. + if (quickDirty) { + double minX = std::min(B.x, C.x); + double maxX = std::max(B.x, C.x); + double minY = std::min(B.y, C.y); + double maxY = std::max(B.y, C.y); + + return (A.x >= minX && A.x <= maxX && A.y >= minY && A.y <= maxY); + } + + // More robust check (projection method - commented out for brevity, but recommended): + double dotProduct = (A.x - B.x) * (C.x - B.x) + (A.y - B.y) * (C.y - B.y); + double squaredLengthBC = (C.x - B.x) * (C.x - B.x) + (C.y - B.y) * (C.y - B.y); + + if (squaredLengthBC == 0) { // B and C are the same point + return (A.x == B.x && A.y == B.y); // A must be equal to B (and C) + } + + double t = dotProduct / squaredLengthBC; + + return (t >= 0 && t <= 1); +} +} // namespace + +RouteSegment::RouteSegment(const RouteSegmentOptions& routeSegOptions, + const LineString& routeGeometry, + const std::vector& routeGeomDistances, + double routeTotalDistance) + : options_(routeSegOptions) { + // calculate the normalized points and the expressions + double currDist = 0.0; + for (const auto& pt : routeSegOptions.geometry) { + bool ptIntersectionFound = false; + for (size_t i = 1; i < routeGeometry.size(); ++i) { + const mbgl::Point& pt1 = routeGeometry[i - 1]; + const mbgl::Point& pt2 = pt; + const mbgl::Point& pt3 = routeGeometry[i]; + + if (isPointBetween(pt2, pt1, pt3)) { + double partialDist = mbgl::util::dist(pt1, pt2); + currDist += partialDist; + ptIntersectionFound = true; + break; + } else { + currDist += routeGeomDistances[i - 1]; + } + } + + if (ptIntersectionFound) { + double normalizedDist = currDist / routeTotalDistance; + normalizedPositions_.push_back(normalizedDist); + } + + currDist = 0.0; + } +} + +std::vector RouteSegment::getNormalizedPositions() const { + return normalizedPositions_; +} + +RouteSegmentOptions RouteSegment::getRouteSegmentOptions() const { + return options_; +} + +RouteSegment::~RouteSegment() {} +} // namespace route +} // namespace mbgl diff --git a/src/mbgl/style/layers/line_layer.cpp b/src/mbgl/style/layers/line_layer.cpp index a18ba5776d77..a1c490024881 100644 --- a/src/mbgl/style/layers/line_layer.cpp +++ b/src/mbgl/style/layers/line_layer.cpp @@ -138,6 +138,19 @@ void LineLayer::setLineSortKey(const PropertyValue& value) { observer->onLayerChanged(*this); } +void LineLayer::setGradientLineFilter(const LineGradientFilterType& value) { + if (value == getGradientLineFilter()) return; + auto impl_ = mutableImpl(); + impl_->gradientFilterType = value; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); +} + +LineGradientFilterType LineLayer::getGradientLineFilter() const { + return impl().gradientFilterType; +} + + // Paint properties PropertyValue LineLayer::getDefaultLineBlur() { @@ -438,6 +451,37 @@ TransitionOptions LineLayer::getLineWidthTransition() const { return impl().paint.template get().options; } +void LineLayer::setGradientLineClip(const PropertyValue& value) { + if(value == getGradientLineClip()) { + return; + } + + auto impl_ = mutableImpl(); + impl_->paint.template get().value = value; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); +} + +const PropertyValue& LineLayer::getGradientLineClip() const { + return impl().paint.template get().value; +} + +void LineLayer::setGradientLineClipColor(const PropertyValue& value) { + if(value == getGradientLineClipColor()) { + return; + } + + auto impl_ = mutableImpl(); + impl_->paint.template get().value = value; + baseImpl = std::move(impl_); + observer->onLayerChanged(*this); + +} + +const PropertyValue& LineLayer::getGradientLineClipColor() const { + return impl().paint.template get().value; +} + using namespace conversion; namespace { diff --git a/src/mbgl/style/layers/line_layer_impl.hpp b/src/mbgl/style/layers/line_layer_impl.hpp index a5c7229fb2f9..62cb0cde2045 100644 --- a/src/mbgl/style/layers/line_layer_impl.hpp +++ b/src/mbgl/style/layers/line_layer_impl.hpp @@ -20,6 +20,7 @@ class LineLayer::Impl : public Layer::Impl { LineLayoutProperties::Unevaluated layout; LinePaintProperties::Transitionable paint; + LineGradientFilterType gradientFilterType = LineGradientFilterType::Linear; DECLARE_LAYER_TYPE_INFO; }; diff --git a/src/mbgl/style/layers/line_layer_properties.hpp b/src/mbgl/style/layers/line_layer_properties.hpp index 09615261ae18..238880ee59c9 100644 --- a/src/mbgl/style/layers/line_layer_properties.hpp +++ b/src/mbgl/style/layers/line_layer_properties.hpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace mbgl { namespace style { @@ -45,6 +46,14 @@ struct LineBlur : DataDrivenPaintProperty { + static float defaultValue() { return 0.f; } +}; + +struct LineClipColor : DataDrivenPaintProperty { + static Color defaultValue() { return Color(0.0, 0.0, 0.0, 0.0); } +}; + struct LineColor : DataDrivenPaintProperty { static Color defaultValue() { return Color::black(); } }; @@ -109,7 +118,9 @@ class LinePaintProperties : public Properties< LinePattern, LineTranslate, LineTranslateAnchor, - LineWidth + LineWidth, + LineClip, + LineClipColor > {}; class LineLayerProperties final : public LayerProperties {