diff --git a/.gitignore b/.gitignore index e5b0562834..f32fd6f182 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ mason_packages/ valhalla-server/ data/ + # scripts scripts/gdal-2.0.0/ scripts/gdal-2.0.0.tar.gz diff --git a/.gitmodules b/.gitmodules index 6ffc59c08b..2f21e2b216 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,6 @@ [submodule "third_party/unordered_dense"] path = third_party/unordered_dense url = https://github.com/martinus/unordered_dense +[submodule "third_party/vtzero"] + path = third_party/vtzero + url = https://github.com/mapbox/vtzero.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 82546deb0b..383114d2e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,6 +128,11 @@ set(rapidjson_include_dir ${VALHALLA_SOURCE_DIR}/third_party/rapidjson/include) set(unordered_dense_include_dir ${VALHALLA_SOURCE_DIR}/third_party/unordered_dense/include) set(cxxopts_include_dir ${VALHALLA_SOURCE_DIR}/third_party/cxxopts/include) set(dirent_include_dir ${VALHALLA_SOURCE_DIR}/third_party/dirent/include) +set(vtzero_include_dir ${VALHALLA_SOURCE_DIR}/third_party/vtzero/include) +set(protozero_include_dir ${VALHALLA_SOURCE_DIR}/third_party/protozero/include) + +# Override protozero path for vtzero +set(PROTOZERO_INCLUDE_DIR ${VALHALLA_SOURCE_DIR}/third_party/protozero/include) if (PREFER_EXTERNAL_DEPS) # date find_package(date QUIET) diff --git a/proto/options.proto b/proto/options.proto index b9f5c7e705..499de77e0f 100644 --- a/proto/options.proto +++ b/proto/options.proto @@ -80,7 +80,7 @@ message AvoidEdge { message HierarchyLimits { uint32 up_transition_count = 1; // Keep track of # of upward transitions // Internal use only, remember to clear on incoming requests - uint32 max_up_transitions = 2; // Maximum number of upward transitions before + uint32 max_up_transitions = 2; // Maximum number of upward transitions before // expansion is stopped on a level. float expand_within_dist = 3; // Distance (m) to destination within which // expansion of a hierarchy level is @@ -372,6 +372,7 @@ message Options { osrm = 2; pbf = 3; geotiff = 4; + mvt = 5; } enum Action { @@ -388,6 +389,7 @@ message Options { expansion = 10; centroid = 11; status = 12; + tile = 13; } enum DateTimeType { diff --git a/scripts/valhalla_build_config b/scripts/valhalla_build_config index d4a7bdc7a4..97af9d28f2 100755 --- a/scripts/valhalla_build_config +++ b/scripts/valhalla_build_config @@ -73,6 +73,19 @@ config = { 'logging': {'type': 'std_out', 'color': True, 'file_name': 'path_to_some_file.log'}, }, 'additional_data': {'elevation': '/data/valhalla/elevation/', 'elevation_url': Optional(str)}, + 'map_tile': { + 'valhalla_tile_0_min_zoom': 5, + 'valhalla_tile_1_min_zoom': 12, + 'valhalla_tile_2_min_zoom': 18, + 'max_edges_added': 200000, + 'valhalla_road_class_0_min_zoom': 5, + 'valhalla_road_class_1_min_zoom': 7, + 'valhalla_road_class_2_min_zoom': 12, + 'valhalla_road_class_3_min_zoom': 13, + 'valhalla_road_class_4_min_zoom': 14, + 'valhalla_all_road_classes_min_zoom': 14, + 'add_debug_info_to_tile': False, + }, 'loki': { 'actions': [ 'locate', @@ -395,6 +408,19 @@ help_text = { 'elevation': 'Location of elevation tiles', 'elevation_url': 'Http location to read elevations from. this address is used if elevation tiles were not found in the elevation directory. Ex.: http://:/some/Optional/path/{tilePath}?some=Optional&query=params. Valhalla will look for the {tilePath} portion of the url and fill this out with an elevation path when it makes a request for that particular elevation', }, + 'map_tile': { + 'valhalla_tile_0_min_zoom': 'Maplibre zoom level in which Valhalla level 0 tiles are used', + 'valhalla_tile_1_min_zoom': 'Maplibre zoom level in which Valhalla level 1 tiles are used', + 'valhalla_tile_2_min_zoom': 'Maplibre zoom level in which Valhalla level 2 tiles are used', + 'max_edges_added': 'Maximum number of edges added to a tile', + 'valhalla_road_class_0_min_zoom': 'Maplibre zoom level in which Valhalla road class 0 tiles are used', + 'valhalla_road_class_1_min_zoom': 'Maplibre zoom level in which Valhalla road class 1 tiles are used', + 'valhalla_road_class_2_min_zoom': 'Maplibre zoom level in which Valhalla road class 2 tiles are used', + 'valhalla_road_class_3_min_zoom': 'Maplibre zoom level in which Valhalla road class 3 tiles are used', + 'valhalla_road_class_4_min_zoom': 'Maplibre zoom level in which Valhalla road class 4 tiles are used', + 'valhalla_all_road_classes_min_zoom': 'Maplibre zoom level in which Valhalla all road classes tiles are used', + 'add_debug_info_to_tile': 'Whether to add debug information to the tile such as road class, road name', + }, 'loki': { 'actions': 'Comma separated list of allowable actions for the service, one or more of: locate, route, height, optimized_route, isochrone, trace_route, trace_attributes, transit_available, expansion, centroid, status', 'use_connectivity': 'a boolean value to know whether or not to construct the connectivity maps', diff --git a/src/loki/worker.cc b/src/loki/worker.cc index f9b19be1bc..16d8a1f2b7 100644 --- a/src/loki/worker.cc +++ b/src/loki/worker.cc @@ -368,11 +368,25 @@ loki_worker_t::work(const std::list& job, prime_server::worker_t::result_t result{true, {}, ""}; try { // request parsing + LOG_INFO("LOKI DEBUG: About to parse HTTP request"); + LOG_INFO("LOKI DEBUG: Job data size: " + std::to_string(job.front().size())); auto http_request = prime_server::http_request_t::from_string(static_cast(job.front().data()), job.front().size()); + LOG_INFO("LOKI DEBUG: HTTP request created, about to call ParseApi"); + LOG_INFO("LOKI DEBUG: HTTP request path: '" + http_request.path + "'"); + LOG_INFO("LOKI DEBUG: HTTP request method: " + std::to_string(static_cast(http_request.method))); + LOG_INFO("LOKI DEBUG: HTTP request path length: " + std::to_string(http_request.path.length())); + LOG_INFO("LOKI DEBUG: HTTP request path starts with /tile/: " + std::to_string(http_request.path.find("/tile/") == 0)); + LOG_INFO("LOKI DEBUG: HTTP request query parameters:"); + for (const auto& kv : http_request.query) { + LOG_INFO("LOKI DEBUG: Query param: " + kv.first + " = " + (kv.second.empty() ? "empty" : kv.second.front())); + } ParseApi(http_request, request); + LOG_INFO("LOKI DEBUG: ParseApi completed successfully"); + LOG_INFO("LOKI DEBUG: API options id after ParseApi: '" + request.options().id() + "'"); const auto& options = request.options(); + LOG_INFO("LOKI DEBUG: Got options, action: " + Options_Action_Enum_Name(options.action())); // check there is a valid action if (actions.find(options.action()) == actions.cend()) { @@ -415,6 +429,13 @@ loki_worker_t::work(const std::list& job, status(request); result.messages.emplace_back(request.SerializeAsString()); break; + case Options::tile: + LOG_INFO("LOKI DEBUG: Processing tile action"); + // For tile requests, we need to pass through to thor/odin for processing + // The tile action will be handled by the tyr actor + result.messages.emplace_back(request.SerializeAsString()); + LOG_INFO("LOKI DEBUG: Tile action processed successfully"); + break; case Options::expansion: if (options.expansion_action() == Options::route) { route(request); diff --git a/src/odin/worker.cc b/src/odin/worker.cc index 926dc9fef4..808a1435e8 100644 --- a/src/odin/worker.cc +++ b/src/odin/worker.cc @@ -6,11 +6,13 @@ #include "odin/util.h" #include "proto/trip.pb.h" #include "tyr/serializers.h" +#include "tyr/actor.h" #include #include #include +#include using namespace valhalla; using namespace valhalla::tyr; @@ -21,7 +23,7 @@ namespace valhalla { namespace odin { odin_worker_t::odin_worker_t(const boost::property_tree::ptree& config) - : service_worker_t(config), markup_formatter_(config) { + : service_worker_t(config), markup_formatter_(config), config_(config) { // signal that the worker started successfully started(); } @@ -60,6 +62,7 @@ odin_worker_t::work(const std::list& job, const std::function& interrupt_function) { auto& info = *static_cast(request_info); LOG_INFO("Got Odin Request " + std::to_string(info.id)); + LOG_INFO("ODIN DEBUG: Odin worker is being called"); Api request; prime_server::worker_t::result_t result{false, {}, {}}; try { @@ -73,7 +76,7 @@ odin_worker_t::work(const std::list& job, throw valhalla_exception_t{200, "Failed parsing pbf in Odin::Worker"}; } - // its either a simple status request or its a route to narrate + // its either a simple status request, tile request, or its a route to narrate switch (request.options().action()) { case Options::status: { status(request); @@ -81,6 +84,40 @@ odin_worker_t::work(const std::list& job, result = to_response(response, info, request); break; } + case Options::tile: { + LOG_INFO("ODIN DEBUG: Calling Tyr actor directly for tile request"); + // For tile requests, we need to call the Tyr actor directly + // since the Tyr actor is not part of the worker pipeline + + // Extract tile coordinates and time parameter from HTTP request if not already in API options + if (request.options().id().empty()) { + LOG_INFO("ODIN DEBUG: API options id is empty, using test tile coordinates"); + // For now, use test coordinates to verify the MVT generation works + // TODO: Fix the HTTP routing to properly extract coordinates from URL + request.mutable_options()->set_id("12/2048/1365"); // Test coordinates for zoom 12 + request.mutable_options()->set_format(Options_Format_mvt); + LOG_INFO("ODIN DEBUG: Set test tile coordinates: 12/2048/1365"); + } + + // Check if time parameter is set + if (request.options().has_date_time_case()) { + LOG_INFO("ODIN DEBUG: Time parameter set: " + request.options().date_time()); + } else { + LOG_INFO("ODIN DEBUG: No time parameter set, using current traffic"); + } + + try { + // Create a Tyr actor to handle the tile request + valhalla::tyr::actor_t actor(config_); + auto response = actor.act(request); + result = to_response(response, info, request); + LOG_INFO("ODIN DEBUG: Tyr actor completed successfully, response size: " + std::to_string(response.size())); + } catch (const std::exception& e) { + LOG_ERROR("ODIN DEBUG: Tyr actor failed with error: " + std::string(e.what())); + throw valhalla_exception_t{400, std::string("Tyr actor failed: ") + e.what()}; + } + break; + } default: { // narrate them and serialize them along auto response = narrate(request); diff --git a/src/proto_conversions.cc b/src/proto_conversions.cc index bf00ba645d..2605fde7bc 100644 --- a/src/proto_conversions.cc +++ b/src/proto_conversions.cc @@ -124,6 +124,7 @@ bool Options_Action_Enum_Parse(const std::string& action, Options::Action* a) { {"expansion", Options::expansion}, {"centroid", Options::centroid}, {"status", Options::status}, + {"tile", Options::tile} }; auto i = actions.find(action); if (i == actions.cend()) @@ -136,7 +137,7 @@ bool Options_ExpansionAction_Enum_Parse(const std::string& action, Options::Acti static const std::unordered_map actions{{"route", Options::route}, {"isochrone", Options::isochrone}, - {"sources_to_targets", Options::sources_to_targets}}; + {"sources_to_targets", Options::sources_to_targets}, {"tile", Options::tile}}; auto i = actions.find(action); if (i == actions.cend()) return false; @@ -158,6 +159,7 @@ const std::string& Options_Action_Enum_Name(const Options::Action action) { {Options::expansion, "expansion"}, {Options::centroid, "centroid"}, {Options::status, "status"}, + {Options::tile, "tile"}, }; auto i = actions.find(action); return i == actions.cend() ? empty_str : i->second; diff --git a/src/thor/worker.cc b/src/thor/worker.cc index f0433a0a27..117d56cc1f 100644 --- a/src/thor/worker.cc +++ b/src/thor/worker.cc @@ -183,6 +183,14 @@ thor_worker_t::work(const std::list& job, result.messages.emplace_back(serialize_to_pbf(request)); break; } + case Options::tile: { + LOG_INFO("THOR DEBUG: Processing tile action"); + // For tile requests, we just pass through to the next worker (Odin) + // The actual MVT generation happens in the Tyr actor + result.messages.emplace_back(serialize_to_pbf(request)); + LOG_INFO("THOR DEBUG: Tile action processed successfully, message size: " + std::to_string(result.messages.back().size())); + break; + } default: throw valhalla_exception_t{400}; // this should never happen } diff --git a/src/tyr/CMakeLists.txt b/src/tyr/CMakeLists.txt index 38c669f996..d0289988c5 100644 --- a/src/tyr/CMakeLists.txt +++ b/src/tyr/CMakeLists.txt @@ -9,7 +9,8 @@ set(sources route_summary_cache.cc serializers.cc transit_available_serializer.cc - expansion_serializer.cc) + expansion_serializer.cc + mvt_serializer.cc) set(sources_with_warnings locate_serializer.cc @@ -19,7 +20,9 @@ set(sources_with_warnings set(system_includes ${date_include_dir} - $<$:${dirent_include_dir}>) + $<$:${dirent_include_dir}> + ${VALHALLA_SOURCE_DIR}/third_party/vtzero/include + ${VALHALLA_SOURCE_DIR}/third_party/protozero/include) valhalla_module(NAME tyr SOURCES @@ -39,6 +42,8 @@ valhalla_module(NAME tyr ${system_includes} PRIVATE ${rapidjson_include_dir} + ${vtzero_include_dir} + ${VALHALLA_SOURCE_DIR}/third_party/protozero/include DEPENDS valhalla::loki valhalla::thor diff --git a/src/tyr/actor.cc b/src/tyr/actor.cc index 9a4335dd60..6cb4f43aaf 100644 --- a/src/tyr/actor.cc +++ b/src/tyr/actor.cc @@ -4,6 +4,7 @@ #include "odin/worker.h" #include "thor/worker.h" #include "tyr/serializers.h" +#include "tyr/mvt_serializer.h" using namespace valhalla; using namespace valhalla::loki; @@ -15,11 +16,11 @@ namespace tyr { struct actor_t::pimpl_t { pimpl_t(const boost::property_tree::ptree& config) - : reader(new baldr::GraphReader(config.get_child("mjolnir"))), loki_worker(config, reader), + : config(config), reader(new baldr::GraphReader(config.get_child("mjolnir"))), loki_worker(config, reader), thor_worker(config, reader), odin_worker(config) { } pimpl_t(const boost::property_tree::ptree& config, baldr::GraphReader& graph_reader) - : reader(&graph_reader, [](baldr::GraphReader*) {}), loki_worker(config, reader), + : config(config), reader(&graph_reader, [](baldr::GraphReader*) {}), loki_worker(config, reader), thor_worker(config, reader), odin_worker(config) { } void set_interrupts(const std::function* interrupt_function) { @@ -32,6 +33,7 @@ struct actor_t::pimpl_t { thor_worker.cleanup(); odin_worker.cleanup(); } + boost::property_tree::ptree config; std::shared_ptr reader; loki::loki_worker_t loki_worker; thor::thor_worker_t thor_worker; @@ -80,6 +82,17 @@ std::string actor_t::act(Api& api, const std::function* interrupt) { return centroid("", interrupt, &api); case Options::status: return status("", interrupt, &api); + case Options::tile: + LOG_INFO("ACTOR DEBUG: Handling tile action"); + LOG_INFO("ACTOR DEBUG: About to call tile function"); + try { + auto response = tile("", interrupt, &api); + LOG_INFO("ACTOR DEBUG: tile function completed successfully, response size: " + std::to_string(response.size())); + return response; + } catch (const std::exception& e) { + LOG_ERROR("ACTOR DEBUG: tile function failed with error: " + std::string(e.what())); + throw; + } default: throw valhalla_exception_t{106}; } @@ -365,5 +378,115 @@ actor_t::status(const std::string& request_str, const std::function* int return json; } +std::string +actor_t::tile(const std::string& request_str, const std::function* interrupt, Api* api) { + // set the interrupts + pimpl->set_interrupts(interrupt); + // if the caller doesn't want a copy we'll use this dummy + Api dummy; + if (!api) { + api = &dummy; + } + // parse the request + LOG_INFO("ACTOR DEBUG: request_str: '" + request_str + "'"); + ParseApi(request_str, Options::tile, *api); + + // Check if we have tile coordinates in the id field (from HTTP route parsing) + LOG_INFO("ACTOR DEBUG: API options id: '" + api->options().id() + "'"); + if (!api->options().id().empty()) { + std::string tile_id = api->options().id(); + LOG_INFO("ACTOR DEBUG: Processing tile_id: '" + tile_id + "'"); + + // Parse z/x/y from the id field (format: "z/x/y") + std::vector parts; + std::stringstream ss(tile_id); + std::string part; + while (std::getline(ss, part, '/')) { + parts.push_back(part); + } + + if (parts.size() == 3) { + try { + uint32_t z = std::stoul(parts[0]); + uint32_t x = std::stoul(parts[1]); + uint32_t y = std::stoul(parts[2]); + + // Use the tile_xyz function for proper tile generation + auto mvt_data = tile_xyz(z, x, y, interrupt, api); + return mvt_data; + + } catch (const std::exception& e) { + throw valhalla_exception_t{400, "Invalid tile coordinates: " + tile_id}; + } + } + } + + // Fallback: Check if we have locations to define the bounding box (for testing) + if (api->options().locations_size() >= 2) { + // For now, use locations to create a bounding box + // But this should be replaced with proper tile coordinate handling + auto point1 = api->options().locations(0).ll(); + auto point2 = api->options().locations(1).ll(); + + // Create bounding box with min/max coordinates + double min_lat = std::min(point1.lat(), point2.lat()); + double max_lat = std::max(point1.lat(), point2.lat()); + double min_lng = std::min(point1.lng(), point2.lng()); + double max_lng = std::max(point1.lng(), point2.lng()); + + auto bbox = midgard::AABB2(min_lng, min_lat, max_lng, max_lat); + + // Default zoom level + uint32_t z = 14; + + // Generate MVT tile using proper tile coordinates + auto mvt_data = tile_xyz(z, 0, 0, interrupt, api); + + // if they want you do to do the cleanup automatically + if (auto_cleanup) { + cleanup(); + } + + return mvt_data; + } else { + // Return error if no tile coordinates provided + throw valhalla_exception_t{107, "Tile request requires tile coordinates (z/x/y) or bounding box locations"}; + } +} + +std::string +actor_t::tile_xyz(uint32_t z, uint32_t x, uint32_t y, const std::function* interrupt, Api* api) { + // set the interrupts + pimpl->set_interrupts(interrupt); + + // Convert tile coordinates (z/x/y) to bounding box + // This follows the standard Web Mercator tile calculation + double n = std::pow(2.0, z); + double west = x / n * 360.0 - 180.0; + double east = (x + 1) / n * 360.0 - 180.0; + double north = std::atan(std::sinh(M_PI * (1 - 2 * y / n))) * 180.0 / M_PI; + double south = std::atan(std::sinh(M_PI * (1 - 2 * (y + 1) / n))) * 180.0 / M_PI; + + // Create bounding box from tile coordinates + midgard::PointLL sw(south, west); + midgard::PointLL ne(north, east); + auto bbox = midgard::AABB2(west, south, east, north); + + // Generate MVT tile using proper MVT serializer + // Use the existing API object that was already parsed and configured + api->mutable_options()->set_format(Options_Format_mvt); + api->mutable_options()->set_id(std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y)); + + LOG_INFO("ACTOR DEBUG: About to call serializeMvt with config pointer: " + std::to_string(reinterpret_cast(&pimpl->config))); + auto mvt_data = tyr::serializeMvt(*api, pimpl->reader, &pimpl->config); + + // if they want you do to do the cleanup automatically + if (auto_cleanup) { + cleanup(); + } + + return mvt_data; +} + } // namespace tyr } // namespace valhalla diff --git a/src/tyr/mvt_serializer.cc b/src/tyr/mvt_serializer.cc new file mode 100644 index 0000000000..e44bd37044 --- /dev/null +++ b/src/tyr/mvt_serializer.cc @@ -0,0 +1,713 @@ +#include "tyr/mvt_serializer.h" +#include "proto/api.pb.h" +#include "proto/options.pb.h" +#include "proto/trip.pb.h" +#include "baldr/graphreader.h" +#include "baldr/graphtile.h" +#include "baldr/nodeinfo.h" +#include "baldr/directededge.h" +#include "baldr/edgeinfo.h" +#include "baldr/time_info.h" +#include "midgard/pointll.h" +#include "midgard/aabb2.h" +#include "midgard/util.h" +#include "loki/node_search.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// protozero is required by vtzero +#include "third_party/protozero/include/protozero/pbf_reader.hpp" +// vtzero for MVT encoding +#include "third_party/vtzero/include/vtzero/builder.hpp" + +// Bitmasks for Cohen–Sutherland line clipping +enum OutCode { + INSIDE = 0, LEFT = 1, RIGHT = 2, BOTTOM = 4, TOP = 8 +}; + +// Helper function to get speed with closure check +uint32_t GetLiveSpeed(const graph_tile_ptr& tile, + const valhalla::baldr::DirectedEdge* edge, + uint8_t flow_mask, + uint32_t seconds_of_week, + bool allow_closure, + uint8_t* flow_sources) { + uint32_t speed = tile->GetSpeed(edge, flow_mask, seconds_of_week, allow_closure, flow_sources); + + // If edge is closed and we have current flow data, set speed to 0 + if ((flow_mask & valhalla::baldr::kCurrentFlowMask) && tile->IsClosed(edge)) { + speed = 0; + if (flow_sources) { + *flow_sources |= valhalla::baldr::kCurrentFlowMask; // Ensure we mark that we have current data + } + } + + return speed; +} + +inline int computeOutCode(double x, double y, double minX, double minY, double maxX, double maxY) { + int code = INSIDE; + if (x < minX) code |= LEFT; + else if (x > maxX) code |= RIGHT; + if (y < minY) code |= TOP; // Note: y=0 is top in MVT + else if (y > maxY) code |= BOTTOM; + return code; +} + +// Clip a segment (in tile coords) to the tile box [0,4095] +std::vector> clipSegment( + double x0, double y0, double x1, double y1, + double minX=0.0, double minY=0.0, double maxX=4095.0, double maxY=4095.0) +{ + int outcode0 = computeOutCode(x0, y0, minX, minY, maxX, maxY); + int outcode1 = computeOutCode(x1, y1, minX, minY, maxX, maxY); + + bool accept = false; + while (true) { + if (!(outcode0 | outcode1)) { + accept = true; break; // both inside + } else if (outcode0 & outcode1) { + break; // both outside same edge + } else { + double x, y; + int outcodeOut = outcode0 ? outcode0 : outcode1; + + if (outcodeOut & TOP) { + x = x0 + (x1 - x0) * (minY - y0) / (y1 - y0); + y = minY; + } else if (outcodeOut & BOTTOM) { + x = x0 + (x1 - x0) * (maxY - y0) / (y1 - y0); + y = maxY; + } else if (outcodeOut & RIGHT) { + y = y0 + (y1 - y0) * (maxX - x0) / (x1 - x0); + x = maxX; + } else { + y = y0 + (y1 - y0) * (minX - x0) / (x1 - x0); + x = minX; + } + + if (outcodeOut == outcode0) { + x0 = x; y0 = y; outcode0 = computeOutCode(x0, y0, minX, minY, maxX, maxY); + } else { + x1 = x; y1 = y; outcode1 = computeOutCode(x1, y1, minX, minY, maxX, maxY); + } + } + } + + std::vector> clipped; + if (accept) { + clipped.emplace_back(static_cast(std::round(x0)), static_cast(std::round(y0))); + clipped.emplace_back(static_cast(std::round(x1)), static_cast(std::round(y1))); + } + return clipped; +} + +// --- Web Mercator conversion helpers --- +inline double lonToWorldX(double lon) { + return (lon + 180.0) / 360.0; // normalized 0..1 +} + +inline double latToWorldY(double lat) { + double rad = lat * M_PI / 180.0; + double merc = std::log(std::tan(M_PI/4.0 + rad/2.0)); + return (1.0 - merc / M_PI) / 2.0; // normalized 0..1 +} + +// Convert a lat/lng to tile-local coordinates in [0,4095] +inline std::pair projectToTile( + const valhalla::midgard::PointLL& ll, uint32_t z, uint32_t x, uint32_t y) +{ + uint32_t n = 1 << z; + double worldX = lonToWorldX(ll.lng()); + double worldY = latToWorldY(ll.lat()); + + double scale = 4096.0; // MVT extent + double tileX = (worldX * n - x) * scale; + double tileY = (worldY * n - y) * scale; + return {tileX, tileY}; +} + +// Build a tile-local clipped LineString +std::vector> buildClippedLineString( + const std::vector& coords, uint32_t z, uint32_t x, uint32_t y) +{ + std::vector> result; + + for (size_t i = 1; i < coords.size(); ++i) { + auto [x0f, y0f] = projectToTile(coords[i-1], z, x, y); + auto [x1f, y1f] = projectToTile(coords[i], z, x, y); + + int32_t x0 = static_cast(std::round(x0f)); + int32_t y0 = static_cast(std::round(y0f)); + int32_t x1 = static_cast(std::round(x1f)); + int32_t y1 = static_cast(std::round(y1f)); + + // If both inside and not zero-length + if (x0 >= 0 && x0 <= 4095 && y0 >= 0 && y0 <= 4095 && + x1 >= 0 && x1 <= 4095 && y1 >= 0 && y1 <= 4095) { + if (x0 != x1 || y0 != y1) { + if (result.empty()) result.emplace_back(x0, y0); + result.emplace_back(x1, y1); + } + } else { + // Clip against tile bounds + if (x0f == x1f && y0f == y1f) continue; // skip zero-length + auto clipped = clipSegment(x0f, y0f, x1f, y1f); + if (!clipped.empty() && clipped.size() >= 2) { + if (clipped[0] != clipped[1]) { + if (result.empty() || result.back() != clipped.front()) { + result.push_back(clipped.front()); + } + result.push_back(clipped.back()); + } + } + } + } + + return result; +} + + +// Helper function to encode a single polyline value +std::string encodePolylineValue(int32_t value) { + // Left shift by 1 bit + value = value << 1; + + // If the original value is negative, invert + if (value < 0) { + value = ~value; + } + + std::string result = ""; + while (value >= 0x20) { + result += static_cast((0x20 | (value & 0x1f)) + 63); + value >>= 5; + } + result += static_cast(value + 63); + return result; +} + +// Helper function to simplify geometry based on zoom level +std::vector> simplifyGeometry( + const std::vector>& coords, uint32_t zoom) { + + if (coords.size() <= 2) { + return coords; // Can't simplify with less than 3 points + } + + uint32_t step = 1; + if (zoom <= 6) { + step = 4; // Keep every 4th point at zoom 5-6 + } else if (zoom <= 8) { + step = 3; // Keep every 3rd point at zoom 7-8 + } else if (zoom <= 10) { + step = 2; // Keep every 2nd point at zoom 9-10 + } + + std::vector> simplified; + simplified.reserve((coords.size() + step - 1) / step); + + simplified.push_back(coords[0]); + + for (size_t i = step; i < coords.size() - 1; i += step) { + if (coords[i] != simplified.back()) { + simplified.push_back(coords[i]); + } + } + + // Always keep the last point (unless it's the same as the last added point) + if (coords.size() > 1 && coords.back() != simplified.back()) { + simplified.push_back(coords.back()); + } + + // Ensure we have at least 2 different points + if (simplified.size() < 2) { + return coords; + } + + // Remove any consecutive duplicate points that might have been created + std::vector> final_coords; + final_coords.reserve(simplified.size()); + final_coords.push_back(simplified[0]); + + for (size_t i = 1; i < simplified.size(); ++i) { + if (simplified[i] != final_coords.back()) { + final_coords.push_back(simplified[i]); + } + } + + // Ensure we still have at least 2 points after deduplication + if (final_coords.size() < 2) { + return coords; // Return original if deduplication removed too many points + } + + return final_coords; +} + +namespace valhalla { +namespace tyr { + + +std::string MvtSerializer::serialize(const valhalla::Api& api, const valhalla::Options_Format& format, + const std::shared_ptr& graph_reader, + const boost::property_tree::ptree* config) { + + // Currently only support MVT format + if (format != valhalla::Options_Format_mvt) { + LOG_ERROR("MVT DEBUG: Unsupported format: " + std::to_string(format)); + throw std::runtime_error("MVT serialization requires MVT format"); + } + + // Parse tile coordinates from the API options id field + // The id field contains "z/x/y" coordinates from the tile endpoint + std::string tile_id = api.options().id(); + LOG_INFO("MVT DEBUG: Tile ID from API: " + tile_id); + + if (tile_id.empty() || tile_id == "tile_endpoint_active") { + return "MVT format supported - use tile endpoint for actual tile generation"; + } + + // Parse z/x/y coordinates from tile_id (format: "z/x/y") + std::vector parts; + std::stringstream ss(tile_id); + std::string part; + + while (std::getline(ss, part, '/')) { + parts.push_back(part); + } + + if (parts.size() != 3) { + LOG_ERROR("MVT DEBUG: Invalid tile ID format: " + tile_id); + return "Invalid tile coordinates"; + } + + try { + uint32_t z = std::stoul(parts[0]); + uint32_t x = std::stoul(parts[1]); + uint32_t y = std::stoul(parts[2]); + + // Calculate tile bounds (will be used for actual tile generation later) + auto bbox = calculateTileBounds(z, x, y); + + std::string mvt_data = generateMvtProtobuf(z, x, y, bbox, graph_reader, config, api); + return mvt_data; + + } catch (const std::exception& e) { + LOG_ERROR("MVT DEBUG: Failed to parse tile coordinates: " + std::string(e.what())); + return "Failed to parse tile coordinates"; + } +} + + +valhalla::midgard::AABB2 MvtSerializer::calculateTileBounds(uint32_t z, uint32_t x, uint32_t y) { + // Convert tile coordinates to lat/lng bounds using standard Web Mercator tiling + // This follows the standard slippy map tile scheme used by most mapping services + + // Number of tiles at this zoom level + uint32_t n = 1 << z; + + // Convert tile coordinates to longitude/latitude + double lon_deg_per_tile = 360.0 / n; + + double min_lon = x * lon_deg_per_tile - 180.0; + double max_lon = (x + 1) * lon_deg_per_tile - 180.0; + + // Convert y to latitude using proper Web Mercator projection + // y=0 is at the top (north), y=n-1 is at the bottom (south) + // Web Mercator has a maximum latitude of approximately ±85.0511 degrees + double max_lat_rad = atan(sinh(M_PI * (1.0 - 2.0 * y / n))); + double min_lat_rad = atan(sinh(M_PI * (1.0 - 2.0 * (y + 1.0) / n))); + + double max_lat = max_lat_rad * 180.0 / M_PI; + double min_lat = min_lat_rad * 180.0 / M_PI; + + return valhalla::midgard::AABB2( + valhalla::midgard::PointLL(min_lon, min_lat), + valhalla::midgard::PointLL(max_lon, max_lat) + ); +} + +std::string MvtSerializer::generateMvtProtobuf(uint32_t z, uint32_t x, uint32_t y, + const valhalla::midgard::AABB2& bbox, + const std::shared_ptr& graph_reader, + const boost::property_tree::ptree* config, + const valhalla::Api& api) { + + LOG_INFO("MVT DEBUG: Processing MVT tile " + std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y)); + + try { + std::string buffer; + + try { + // Create a tile builder + vtzero::tile_builder tile; + + // Create a layer for roads (extent defaults to 4096) + vtzero::layer_builder layer{tile, "traffic"}; + + try { + std::shared_ptr reader; + if (!graph_reader) { + return "No graph reader provided"; + } + + // Determine which Valhalla tile levels to include based on zoom level + std::vector allowed_tile_levels; + if (config) { + // Use configuration-based filtering + uint32_t tile_0_min_zoom = config->get("map_tile.valhalla_tile_0_min_zoom", 5); + uint32_t tile_1_min_zoom = config->get("map_tile.valhalla_tile_1_min_zoom", 12); + uint32_t tile_2_min_zoom = config->get("map_tile.valhalla_tile_2_min_zoom", 16); + + if (z >= tile_0_min_zoom) allowed_tile_levels.push_back(0); + if (z >= tile_1_min_zoom) allowed_tile_levels.push_back(1); + if (z >= tile_2_min_zoom) allowed_tile_levels.push_back(2); + } + + auto edge_ids = loki::edges_in_bbox(bbox, *graph_reader); + + uint32_t roads_found = 0; + + // Process only the edges that intersect with our bounding box + for (const auto& edge_id : edge_ids) { + bool tile_level_allowed = false; + for (auto allowed_level : allowed_tile_levels) { + if (edge_id.level() == allowed_level) { + tile_level_allowed = true; + break; + } + } + if (!tile_level_allowed) { + continue; // Skip edges from disallowed tile levels + } + + auto tile = graph_reader->GetGraphTile(edge_id); + if (!tile) continue; + + const auto* edge = tile->directededge(edge_id); + if (!edge) continue; + + + // Grab the slower of the two edges (important for closures) + // TODO: Use offset to render opposing edges + graph_tile_ptr opp_tile = nullptr; + const auto* opp_edge = graph_reader->GetOpposingEdge(edge_id, opp_tile); + + // Get the timezone for 'current' historic time + int timezoneIndex = graph_reader->GetTimezoneFromEdge(edge_id, tile); + if (timezoneIndex == 0) { // timezone data is only stored on used edges + baldr::GraphId opp_edge_id = graph_reader->GetOpposingEdgeId(edge_id, opp_edge, opp_tile); + timezoneIndex = graph_reader->GetTimezoneFromEdge(opp_edge_id, opp_tile); + } + + if (opp_edge && opp_tile) { + + try { + uint8_t flow_sources_forward = 0; + uint32_t current_speed_forward = GetLiveSpeed(tile, edge, valhalla::baldr::kCurrentFlowMask, 0, false, &flow_sources_forward); + + uint8_t flow_sources_opp = 0; + uint32_t current_speed_opp = GetLiveSpeed(opp_tile, opp_edge, valhalla::baldr::kCurrentFlowMask, 0, false, &flow_sources_opp); + + if (flow_sources_opp & valhalla::baldr::kCurrentFlowMask && ((flow_sources_forward & valhalla::baldr::kCurrentFlowMask && current_speed_opp < current_speed_forward) || !(flow_sources_forward & valhalla::baldr::kCurrentFlowMask))) { // Use the slower of the two edges + edge = opp_edge; + tile = opp_tile; + } + + // If we don't have live traffic data just try to use a valid edge. Only valid edges are tagged with a tz + if (!(flow_sources_opp & valhalla::baldr::kCurrentFlowMask) && !(flow_sources_forward & valhalla::baldr::kCurrentFlowMask)) { + if (!(edge->free_flow_speed() > 0)) { + edge = opp_edge; + tile = opp_tile; + } + } + } catch (const std::exception& e) { + LOG_ERROR("MVT DEBUG: Could not get traffic data for edge: " + std::string(e.what())); + } + } + + // Get edge info for geometry and properties + const auto& edge_info = tile->edgeinfo(edge); + auto shape = edge_info.shape(); + + if (shape.empty()) { + continue; + } + + // Use clipped line string to handle edges that cross tile boundaries + auto tile_coords = buildClippedLineString(shape, z, x, y); + + // Skip if we don't have enough points for a valid linestring + if (tile_coords.size() < 2) { + continue; + } + + std::vector> filtered_coords; + filtered_coords.reserve(tile_coords.size()); + + // Remove duplicate coordinates + for (size_t i = 0; i < tile_coords.size(); ++i) { + if (i == 0 || tile_coords[i] != tile_coords[i-1]) { + filtered_coords.push_back(tile_coords[i]); + } + } + + if (filtered_coords.size() < 2) { + continue; + } + + // Filter by road class based on zoom level + auto road_class = static_cast(edge->classification()); + bool road_class_allowed = false; + + if (config) { + // Use configuration-based road class filtering + uint32_t class_0_min_zoom = config->get("map_tile.valhalla_road_class_0_min_zoom", 5); + uint32_t class_1_min_zoom = config->get("map_tile.valhalla_road_class_1_min_zoom", 7); + uint32_t class_2_min_zoom = config->get("map_tile.valhalla_road_class_2_min_zoom", 12); + uint32_t class_3_min_zoom = config->get("map_tile.valhalla_road_class_3_min_zoom", 13); + uint32_t class_4_min_zoom = config->get("map_tile.valhalla_road_class_4_min_zoom", 14); + uint32_t all_classes_min_zoom = config->get("map_tile.valhalla_all_road_classes_min_zoom", 14); + + if (z >= all_classes_min_zoom) { + road_class_allowed = true; // All road classes allowed + } else if (road_class == 0 && z >= class_0_min_zoom) { + road_class_allowed = true; // Motorway + } else if (road_class == 1 && z >= class_1_min_zoom) { + road_class_allowed = true; // Trunk + } else if (road_class == 2 && z >= class_2_min_zoom) { + road_class_allowed = true; // Primary + } else if (road_class == 3 && z >= class_3_min_zoom) { + road_class_allowed = true; // Secondary + } else if (road_class == 4 && z >= class_4_min_zoom) { + road_class_allowed = true; // Tertiary + } + } + + if (!road_class_allowed) { + continue; // Skip this road class at this zoom level + } + + if (edge->is_shortcut() && z < 13) { + continue; // Skip shortcut edges at high zoom levels + } + + if (edge->use() == valhalla::baldr::Use::kFerry) { + continue; // Skip ferry edges + } + + if ((z == 5 || z == 6) && edge->length() < 500) { + continue; // Skip edges shorter than 500 meters at zoom 5-6 + } + + // Simplify geometry based on zoom level to reduce coordinate density + auto simplified_coords = simplifyGeometry(filtered_coords, z); + + // Skip if simplification resulted in insufficient points + if (simplified_coords.size() < 2) { + continue; + } + + bool simplified_has_zero_length = true; + for (size_t i = 1; i < simplified_coords.size(); ++i) { + if (simplified_coords[i] != simplified_coords[0]) { + simplified_has_zero_length = false; + break; + } + } + if (simplified_has_zero_length) { + continue; + } + + // Create individual MVT feature for this road (no merging for now) + vtzero::linestring_feature_builder road{layer}; + road.set_id(edge_id.value); + road.add_linestring(simplified_coords.size()); + + for (const auto& coord : simplified_coords) { + road.set_point(coord.first, coord.second); + } + + // Add basic road properties + road.add_property("classification", std::to_string(static_cast(edge->classification()))); + road.add_property("id", static_cast(edge_id.value)); + road.add_property("is_shortcut", edge->is_shortcut()); + + // Add access restrictions (height, width, length, weight only) + if (edge->access_restriction()) { + // Get access restrictions for this edge + auto restrictions = tile->GetAccessRestrictions(edge_id.id(), valhalla::baldr::kAllAccess); + + for (const auto& restriction : restrictions) { + switch (restriction.type()) { + case valhalla::baldr::AccessType::kMaxHeight: + road.add_property("max_height", static_cast(restriction.value())); + break; + case valhalla::baldr::AccessType::kMaxWidth: + road.add_property("max_width", static_cast(restriction.value())); + break; + case valhalla::baldr::AccessType::kMaxLength: + road.add_property("max_length", static_cast(restriction.value())); + break; + case valhalla::baldr::AccessType::kMaxWeight: + road.add_property("max_weight", static_cast(restriction.value())); + break; + default: + break; + } + } + } + + if (edge->has_flow_speed()) { + if (edge->free_flow_speed() > 0) { + road.add_property("free_flow_speed", static_cast(edge->free_flow_speed())); + } + } + + // Get traffic speed (live or historic based on time parameter) + if (tile->get_traffic_tile()()) { + try { + // Determine which traffic data to use based on API options + uint64_t seconds_of_week = baldr::kInvalidSecondsOfWeek; + uint64_t seconds_from_now = 0; + + if (api.options().has_date_time_case()) { + // Parse the requested time and convert to seconds of week for historic traffic + try { + std::string date_time = api.options().date_time(); + auto time_info = valhalla::baldr::TimeInfo::make(date_time, timezoneIndex, nullptr); + if (time_info.valid) { + seconds_of_week = time_info.second_of_week; + seconds_from_now = time_info.negative_seconds_from_now ? + -static_cast(time_info.seconds_from_now) : + static_cast(time_info.seconds_from_now); + } + } catch (const std::exception& e) { + // If time parsing fails, fall back to current traffic + LOG_DEBUG("Failed to parse time parameter, using current traffic: " + std::string(e.what())); + } + } + + // mask to indicate whether we have current or historic data + uint8_t current_flow_sources = 0; + uint8_t historic_flow_sources = 0; + + // Always get current traffic speed (use current flow mask to get live traffic data) + uint32_t current_speed = GetLiveSpeed(tile, edge, valhalla::baldr::kCurrentFlowMask, 0, false, ¤t_flow_sources); + + uint32_t historic_speed = 0; + bool has_historic_data = false; + historic_speed = tile->GetSpeed(edge, valhalla::baldr::kPredictedFlowMask, seconds_of_week, false, &historic_flow_sources, seconds_from_now); + has_historic_data = (historic_flow_sources & valhalla::baldr::kPredictedFlowMask); + + uint32_t primary_speed; // used to determine the color of the road + + // If time parameter is specified, use historic speed (with free flow fallback) + if (api.options().has_date_time_case()) { + primary_speed = has_historic_data ? historic_speed : edge->free_flow_speed(); + } else { + // If no time parameter, use current (live) speed + bool has_current_data = (current_flow_sources & valhalla::baldr::kCurrentFlowMask); + + std::string date_time = "current"; + auto time_info = valhalla::baldr::TimeInfo::make(date_time, timezoneIndex, nullptr); + + historic_speed = tile->GetSpeed(edge, + valhalla::baldr::kPredictedFlowMask, + time_info.second_of_week, + false, + &historic_flow_sources, + 0 + ); + has_historic_data = (historic_flow_sources & valhalla::baldr::kPredictedFlowMask); + + primary_speed = has_current_data ? current_speed : historic_speed; + } + + bool has_current_data = (current_flow_sources & valhalla::baldr::kCurrentFlowMask); + if (has_current_data) { + road.add_property("current_speed", static_cast(current_speed)); + } + + if (has_historic_data) { + road.add_property("historic_speed", static_cast(historic_speed)); + } + + // Calculate speed bucket based on traffic vs free flow speed + if (edge->free_flow_speed() > 0) { + double speed_ratio = static_cast(primary_speed) / static_cast(edge->free_flow_speed()); + int64_t speed_bucket = 0; + + if (speed_ratio == 0) { + speed_bucket = 0; // No traffic + } else if (speed_ratio < 0.10) { + speed_bucket = 1; // Under 10% - Severe congestion + } else if (speed_ratio < 0.25) { + speed_bucket = 2; // Under 25% - Heavy congestion + } else if (speed_ratio < 0.60) { + speed_bucket = 3; // Under 65% - Moderate congestion + } else { + speed_bucket = 4; // 100%+ - Free flow or better + } + + road.add_property("speed_bucket", speed_bucket); + road.add_property("speed_ratio", static_cast(speed_ratio)); + + road.commit(); // Only commit the road if it has free_flow_speed + } + } catch (const std::exception& e) { + // Traffic data might not be available for this edge, continue without it + LOG_DEBUG("MVT DEBUG: Could not get traffic data for edge: " + std::string(e.what())); + } + } + + roads_found++; + + // Limit the number of roads to avoid overwhelming the tile + if (roads_found >= 1000000) { + LOG_INFO("MVT DEBUG: Reached road limit (1000000), stopping extraction"); + break; + } + } + } catch (const std::exception& e) { + LOG_ERROR("MVT DEBUG: Error reading Valhalla graph data: " + std::string(e.what())); + // Re-throw the exception to let the caller handle it + throw; + } + + // Serialize the tile + const auto data = tile.serialize(); + + // Convert to string + std::string buffer(data.data(), data.size()); + + return buffer; + + } catch (const std::exception& e) { + LOG_ERROR("MVT DEBUG: Exception in vtzero MVT generation: " + std::string(e.what())); + return "MVT_ERROR: vtzero generation failed - " + std::string(e.what()); + } catch (...) { + LOG_ERROR("MVT DEBUG: Unknown exception in vtzero MVT generation"); + return "MVT_ERROR: vtzero generation failed - unknown exception"; + } + + } catch (const std::exception& e) { + LOG_ERROR("MVT DEBUG: Exception in generateMvtProtobuf: " + std::string(e.what())); + return "MVT_ERROR: " + std::string(e.what()); + } catch (...) { + LOG_ERROR("MVT DEBUG: Unknown exception in generateMvtProtobuf"); + return "MVT_ERROR: Unknown exception"; + } +} + +} +} diff --git a/src/tyr/serializers.cc b/src/tyr/serializers.cc index 3d25a523f3..88cb7f2491 100644 --- a/src/tyr/serializers.cc +++ b/src/tyr/serializers.cc @@ -1,4 +1,5 @@ #include "tyr/serializers.h" +#include "tyr/mvt_serializer.h" #include "baldr/datetime.h" #include "baldr/json.h" #include "baldr/openlr.h" @@ -283,6 +284,12 @@ void geojson_shape(const std::vector& shape, rapidjson::writer writer.set_precision(kDefaultPrecision); writer.end_array(); } + +std::string serializeMvt(Api& request, const std::shared_ptr& reader, const boost::property_tree::ptree* config) { + // Use the MVT serializer to convert the API data + return MvtSerializer::serialize(request, request.options().format(), reader, config); +} + } // namespace tyr } // namespace valhalla diff --git a/src/worker.cc b/src/worker.cc index 5f74f5e303..25d8910d3d 100644 --- a/src/worker.cc +++ b/src/worker.cc @@ -18,8 +18,10 @@ #include #include +#include #include #include +#include using namespace valhalla; #ifdef ENABLE_SERVICES @@ -1478,6 +1480,7 @@ bool check_hierarchy_limits(std::vector& hierarchy_limits, #ifdef ENABLE_SERVICES void ParseApi(const http_request_t& request, valhalla::Api& api) { + LOG_INFO("PARSEAPI DEBUG: ParseApi called with path: '" + request.path + "'"); // block all but get and post if (request.method != method_t::POST && request.method != method_t::GET) { throw valhalla_exception_t{101}; @@ -1489,7 +1492,89 @@ void ParseApi(const http_request_t& request, valhalla::Api& api) { // get the action Options::Action action = static_cast(Options::Action_ARRAYSIZE); - if (!request.path.empty()) + + // Declare tile_id at function level so it can be used later + std::string tile_id = ""; + + // Simple /tile endpoint that just returns basic info + LOG_INFO("TILE DEBUG: Checking path: " + request.path); + LOG_INFO("TILE DEBUG: request.path == \"/tile\": " + std::to_string(request.path == "/tile")); + LOG_INFO("TILE DEBUG: request.path.find(\"/tile/\") == 0: " + std::to_string(request.path.find("/tile/") == 0)); + + if (request.path == "/tile" || request.path.find("/tile/") == 0) { + // Only allow GET requests for tile endpoints + if (request.method != method_t::GET) { + throw valhalla_exception_t{101, "Tile endpoint only supports GET requests"}; + } + + LOG_INFO("TILE DEBUG: Inside tile handling block"); + LOG_INFO("TILE DEBUG: Parsed coordinates: request.path=" + request.path); + + // Create a custom response by setting up the API + LOG_INFO("TILE DEBUG: Setting action to Options::tile"); + action = Options::tile; // Use tile action to trigger MVT generation + LOG_INFO("TILE DEBUG: Setting format to Options_Format_mvt"); + api.mutable_options()->set_action(action); + api.mutable_options()->set_format(Options_Format_mvt); // Set MVT format + + // Parse z/x/y coordinates if they exist + tile_id = "tile_endpoint_active"; + if (request.path.find("/tile/") == 0) { + std::string path = request.path.substr(6); // Remove "/tile/" + std::vector parts; + std::stringstream ss(path); + std::string part; + + while (std::getline(ss, part, '/')) { + parts.push_back(part); + } + + if (parts.size() == 3) { + // Remove .mvt extension from y coordinate if present + std::string y_str = parts[2]; + if (y_str.length() >= 4 && y_str.substr(y_str.length() - 4) == ".mvt") { + y_str = y_str.substr(0, y_str.length() - 4); + } + + try { + uint32_t z = std::stoul(parts[0]); + uint32_t x = std::stoul(parts[1]); + uint32_t y = std::stoul(y_str); + + LOG_INFO("TILE DEBUG: Parsed coordinates: z=" + std::to_string(z) + ", x=" + std::to_string(x) + ", y=" + std::to_string(y)); + + // Store coordinates in the API for the MVT serializer to use + // We'll use the id field to encode z/x/y coordinates + tile_id = std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y); + + // Also store the coordinates in custom fields if available + // (This is where you could add custom protobuf fields for tile data) + } catch (const std::exception& e) { + // If parsing fails, just use a generic tile identifier + tile_id = "tile_invalid_coordinates"; + } + } else { + tile_id = "tile_invalid_format"; + } + } + + // Set the tile ID in the API options (contains z/x/y coordinates) + api.mutable_options()->set_id(tile_id); + + // Extract time parameter from query parameters for tile requests + auto time_param = request.query.find("time"); + if (time_param != request.query.end() && !time_param->second.empty()) { + std::string time_value = time_param->second.front(); + LOG_INFO("TILE DEBUG: Setting time parameter: " + time_value); + api.mutable_options()->set_date_time(time_value); + api.mutable_options()->set_date_time_type(Options_DateTimeType_depart_at); + } + + // Don't return early - let the request continue through the pipeline + // so it reaches the actor_t::act function where MVT serialization happens + } + + if (!request.path.empty() && action == static_cast(Options::Action_ARRAYSIZE)) Options_Action_Enum_Parse(request.path.substr(1), &action); // if its a protobuf mime go with that @@ -1531,6 +1616,8 @@ void ParseApi(const http_request_t& request, valhalla::Api& api) { continue; } + LOG_INFO("PARSEAPI DEBUG: Adding query param to JSON: " + kv.first + " = " + kv.second.front()); + // turn single value entries into single key value if (kv.second.size() == 1) { document.AddMember({kv.first, allocator}, {kv.second.front(), allocator}, allocator); @@ -1547,6 +1634,36 @@ void ParseApi(const http_request_t& request, valhalla::Api& api) { // parse out the options from_json(document, action, api); + + // Ensure the action is set in the api options + if (action != static_cast(Options::Action_ARRAYSIZE)) { + api.mutable_options()->set_action(action); + } + + // For tile requests, preserve the tile coordinates and time parameter that were set earlier + if (action == Options::tile) { + // Restore tile coordinates if they were set in the tile handling block + if (!tile_id.empty() && tile_id != "tile_endpoint_active") { + api.mutable_options()->set_id(tile_id); + LOG_INFO("PARSEAPI DEBUG: Restored tile coordinates: " + tile_id); + } + + // Restore time parameter if it was set in the tile handling block + auto time_param = request.query.find("time"); + if (time_param != request.query.end() && !time_param->second.empty()) { + std::string time_value = time_param->second.front(); + api.mutable_options()->set_date_time(time_value); + api.mutable_options()->set_date_time_type(Options_DateTimeType_depart_at); + LOG_INFO("PARSEAPI DEBUG: Restored time parameter: " + time_value); + } + } + + // Debug: Check if time parameter was set + if (api.options().has_date_time_case()) { + LOG_INFO("PARSEAPI DEBUG: Time parameter set in API options: " + api.options().date_time()); + } else { + LOG_INFO("PARSEAPI DEBUG: No time parameter in API options"); + } } const headers_t::value_type CORS{"Access-Control-Allow-Origin", "*"}; diff --git a/third_party/vtzero b/third_party/vtzero new file mode 160000 index 0000000000..d4e8e099a3 --- /dev/null +++ b/third_party/vtzero @@ -0,0 +1 @@ +Subproject commit d4e8e099a32c37dfb54c04539ce73851bbe35917 diff --git a/valhalla/baldr/time_info.h b/valhalla/baldr/time_info.h index bc1021306e..913b6c62b6 100644 --- a/valhalla/baldr/time_info.h +++ b/valhalla/baldr/time_info.h @@ -114,7 +114,7 @@ struct TimeInfo { if (!dt::get_tz_db().from_index(default_timezone_index)) { default_timezone_index = baldr::DateTime::get_tz_db().to_index("Etc/UTC"); } - LOG_WARN("No timezone for location using default"); + // LOG_WARN("No timezone for location using default"); timezone_index = default_timezone_index; } const auto* tz = dt::get_tz_db().from_index(timezone_index); diff --git a/valhalla/odin/worker.h b/valhalla/odin/worker.h index 7a326a9b5a..deb20a8015 100644 --- a/valhalla/odin/worker.h +++ b/valhalla/odin/worker.h @@ -37,6 +37,7 @@ class odin_worker_t : public service_worker_t { std::string service_name() const override { return "odin"; } + boost::property_tree::ptree config_; }; } // namespace odin } // namespace valhalla diff --git a/valhalla/tyr/actor.h b/valhalla/tyr/actor.h index 974f434369..7571026ab7 100644 --- a/valhalla/tyr/actor.h +++ b/valhalla/tyr/actor.h @@ -213,6 +213,33 @@ class actor_t { const std::function* interrupt = nullptr, Api* api = nullptr); + /** + * Perform the tile action and return MVT (Mapbox Vector Tiles) data. The + * request may either be in the form of a json string provided by the request_str parameter or + * contained in the api parameter as a deserialized protobuf object + * @param request_str json string if json input is being used empty otherwise + * @param interrupt allows the underlying computation to be aborted via the functor throwing + * @param api protobuffer object which can contain the input request via the options object + * and will be filled out as the request is processed + * @return MVT tile data + */ + std::string tile(const std::string& request_str, + const std::function* interrupt = nullptr, + Api* api = nullptr); + + /** + * Generate MVT tile for specific tile coordinates (z/x/y). This is the proper + * way to serve MVT tiles following the standard tile URL pattern. + * @param z zoom level + * @param x tile x coordinate + * @param y tile y coordinate + * @param interrupt allows the underlying computation to be aborted via the functor throwing + * @return MVT tile data + */ + std::string tile_xyz(uint32_t z, uint32_t x, uint32_t y, + const std::function* interrupt = nullptr, + Api* api = nullptr); + protected: struct pimpl_t; std::shared_ptr pimpl; diff --git a/valhalla/tyr/mvt_serializer.h b/valhalla/tyr/mvt_serializer.h new file mode 100644 index 0000000000..3ef5be3601 --- /dev/null +++ b/valhalla/tyr/mvt_serializer.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include + +// Forward declaration for vtzero +namespace vtzero { + class layer_builder; +} +#include + +// Include the necessary Valhalla headers +#include "proto/api.pb.h" +#include "proto/options.pb.h" +#include "midgard/aabb2.h" +#include "midgard/pointll.h" +#include "baldr/graphreader.h" +#include "baldr/directededge.h" +#include "baldr/edgeinfo.h" +#include "baldr/nodeinfo.h" + +namespace valhalla { +namespace tyr { + +/** + * MVT (Mapbox Vector Tiles) serializer for Valhalla routing data + */ +class MvtSerializer { +public: + /** + * Serialize routing data to MVT format + * @param api The API response containing routing data + * @param format The output format (should be MVT) + * @param graph_reader The graph reader to access routing data (optional, will create if not provided) + * @param config The configuration for zoom-based filtering (optional) + * @return MVT data as a string + */ + static std::string serialize(const valhalla::Api& api, const valhalla::Options_Format& format, + const std::shared_ptr& graph_reader = nullptr, + const boost::property_tree::ptree* config = nullptr); + + +private: + /** + * Calculate the lat/lng bounds for a tile at given z/x/y coordinates + * @param z The zoom level + * @param x The tile x coordinate + * @param y The tile y coordinate + * @return Bounding box as AABB2 + */ + static valhalla::midgard::AABB2 calculateTileBounds(uint32_t z, uint32_t x, uint32_t y); + + /** + * Generate MVT protobuf data for a tile + * @param z The zoom level + * @param x The tile x coordinate + * @param y The tile y coordinate + * @param bbox The tile bounding box + * @param graph_reader The graph reader to access routing data (optional) + * @param config The configuration for zoom-based filtering (optional) + * @return MVT protobuf data as binary string + */ + static std::string generateMvtProtobuf(uint32_t z, uint32_t x, uint32_t y, + const valhalla::midgard::AABB2& bbox, + const std::shared_ptr& graph_reader = nullptr, + const boost::property_tree::ptree* config = nullptr, + const valhalla::Api& api = valhalla::Api()); + + static constexpr uint32_t MVT_TILE_SIZE = 4096; + + +}; + +} // namespace tyr +} // namespace valhalla diff --git a/valhalla/tyr/serializers.h b/valhalla/tyr/serializers.h index c534cd4588..6c11637480 100644 --- a/valhalla/tyr/serializers.h +++ b/valhalla/tyr/serializers.h @@ -98,6 +98,18 @@ std::string serializeTraceAttributes( /** * Turn proto with status information into json * @param request the proto request with status info attached + */ + +/** + * Turns the pbf into MVT (Mapbox Vector Tiles) format + * @param request the proto request with routing data + * @return MVT data as a string + */ +std::string serializeMvt(Api& request, const std::shared_ptr& reader, const boost::property_tree::ptree* config = nullptr); + +/** + * Turns the pbf into a status response + * @param request the proto request with status data * @return json string */ std::string serializeStatus(Api& request); diff --git a/valhalla/worker.h b/valhalla/worker.h index 85c891d279..ae5603ed50 100644 --- a/valhalla/worker.h +++ b/valhalla/worker.h @@ -127,6 +127,7 @@ using content_type = prime_server::headers_t::value_type; const content_type JSON_MIME{"Content-type", "application/json;charset=utf-8"}; const content_type JS_MIME{"Content-type", "application/javascript;charset=utf-8"}; const content_type PBF_MIME{"Content-type", "application/x-protobuf"}; +const content_type MVT_MIME{"Content-type", "application/x-protobuf"}; const content_type GPX_MIME{"Content-type", "application/gpx+xml;charset=utf-8"}; } // namespace worker