Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d4f9c44
Read VROOM summary duration
hir-al-14 May 22, 2026
c94cd0a
read planned route start time
hir-al-14 May 22, 2026
7351d5c
use VROOM duration for weather timing
hir-al-14 May 22, 2026
25a35ab
added weather timing fields
hir-al-14 May 22, 2026
610f24a
matched OpenWeather hours to route start
hir-al-14 May 22, 2026
61e54f5
change logic to check weather after initial VROOM route solve
hir-al-14 May 22, 2026
458e91c
fix: avoid blocking weather reroutes
hir-al-14 May 25, 2026
3d57661
Fix weather reroute formatting
hir-al-14 May 25, 2026
e4767ea
Clean up driver assist review items
hir-al-14 May 25, 2026
652a45b
clarify weather recalculation options
hir-al-14 Jun 27, 2026
51f2d48
align driver route import error expectation
hir-al-14 Jun 27, 2026
dd20793
added traffic forecast confog
hir-al-14 May 22, 2026
5222a40
added traffic delay and threshold logic
hir-al-14 May 22, 2026
78851e4
added Google traffic response helpers
hir-al-14 May 22, 2026
1c8edfc
added google traffic route delay
hir-al-14 May 22, 2026
cb31613
feat: use traffic delay in route solve
hir-al-14 May 22, 2026
14d69f0
fix: traffic leg departure times
hir-al-14 May 22, 2026
498ea31
comment to clarify traffic leg timing test
hir-al-14 May 22, 2026
89879bc
fix: avoid blocking sync traffic forecasts
hir-al-14 May 26, 2026
2eca47b
Store traffic forecast options once
hir-al-14 May 26, 2026
b859fed
Fix traffic endpoint formatting
hir-al-14 May 26, 2026
84714f5
added explanation comments for ReadLegDeparture and #undef GetJob
hir-al-14 May 26, 2026
d3bd6a1
Cover relative traffic leg timing
hir-al-14 May 26, 2026
146ba6e
fix: pr check errors
hir-al-14 May 26, 2026
bcfdcdf
Fix traffic endpoint clang formatting
hir-al-14 May 26, 2026
d69889d
Split traffic delay by route leg timing
hir-al-14 May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

#include "deliveryoptimizer/api/optimize_request.hpp"

#include <chrono>
#include <cstddef>
#include <json/json.h>
#include <optional>
#include <string>
#include <vector>

namespace deliveryoptimizer::api {

Expand All @@ -17,6 +20,14 @@ struct WeatherForecastOptions {
std::string openweather_base_url;
};

struct TrafficForecastOptions {
bool enabled{false};
int reoptimize_threshold_seconds{300};
double reoptimize_threshold_percent{5.0};
std::string google_maps_api_key;
std::string google_maps_base_url;
};

struct OpenWeatherDelayEstimate {
bool available{false};
int delay_seconds_per_stop{0};
Expand All @@ -26,34 +37,108 @@ struct OpenWeatherDelayEstimate {
struct WeatherImpactEstimate {
int stop_count{0};
int baseline_duration_seconds{0};
int baseline_route_duration_seconds{0};
int delay_seconds_per_stop{0};
int weather_delay_seconds{0};
int weather_adjusted_duration_seconds{0};
int reoptimize_threshold_seconds{300};
bool should_reoptimize{false};
std::string source;
std::optional<std::chrono::sys_seconds> planned_start_time;
std::optional<std::chrono::sys_seconds> estimated_finish_time;
};

struct TrafficImpact {
int baseline_duration_seconds{0};
int traffic_delay_seconds{0};
int traffic_adjusted_duration_seconds{0};
int reoptimize_threshold_seconds{300};
bool should_reoptimize{false};
std::string source;
};

struct TrafficDelayEstimate {
bool available{false};
int delay_seconds{0};
std::string source;
};

struct TrafficLeg {
Coordinate origin;
Coordinate destination;
std::chrono::sys_seconds departure_time;
};

[[nodiscard]] WeatherForecastOptions ResolveWeatherForecastOptionsFromEnv();

[[nodiscard]] TrafficForecastOptions ResolveTrafficForecastOptionsFromEnv();

[[nodiscard]] bool IsOpenWeatherConfigured(const WeatherForecastOptions& options);

[[nodiscard]] int EstimateServiceSeconds(const OptimizeRequestInput& input);

[[nodiscard]] OpenWeatherDelayEstimate
FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coordinate& coordinate);
[[nodiscard]] OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate(
const WeatherForecastOptions& options, const Coordinate& coordinate,
std::optional<std::chrono::sys_seconds> route_start_time = std::nullopt,
std::optional<int> route_duration_seconds = std::nullopt);

[[nodiscard]] int
ReadOpenWeatherDelay(const Json::Value& body,
std::optional<std::chrono::sys_seconds> route_start_time = std::nullopt,
std::optional<int> route_duration_seconds = std::nullopt);

[[nodiscard]] bool IsGoogleMapsConfigured(const TrafficForecastOptions& options);

[[nodiscard]] std::string BuildTrafficPath(const Coordinate& origin, const Coordinate& destination,
std::chrono::sys_seconds departure_time,
const std::string& api_key);

[[nodiscard]] std::optional<int> ReadTrafficDelay(const Json::Value& body);

[[nodiscard]] TrafficDelayEstimate FetchTrafficDelay(const TrafficForecastOptions& options,
const TrafficLeg& leg);

[[nodiscard]] std::vector<TrafficLeg>
ReadTrafficLegs(const Json::Value& vroom_output,
std::optional<std::chrono::sys_seconds> route_start_time = std::nullopt);

[[nodiscard]] TrafficDelayEstimate
ReadRouteTraffic(const TrafficForecastOptions& options, const Json::Value& vroom_output,
std::optional<std::chrono::sys_seconds> route_start_time = std::nullopt);

[[nodiscard]] WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& options,
std::size_t stop_count,
int baseline_duration_seconds);

[[nodiscard]] TrafficImpact EstimateTrafficImpact(const TrafficForecastOptions& options,
int baseline_duration_seconds,
int traffic_delay_seconds, std::string source);

[[nodiscard]] WeatherImpactEstimate
EstimateRouteWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input,
int baseline_duration_seconds);

[[nodiscard]] std::optional<std::chrono::sys_seconds>
ReadRouteStartTime(const OptimizeRequestInput& input);

[[nodiscard]] std::optional<int> ReadVroomDuration(const Json::Value& vroom_output);

[[nodiscard]] WeatherImpactEstimate RecalculateWeatherImpact(const WeatherForecastOptions& options,
const OptimizeRequestInput& input,
const Json::Value& vroom_output);

[[nodiscard]] Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input,
const WeatherImpactEstimate& impact);

[[nodiscard]] Json::Value BuildTrafficAdjustedVroomInput(const OptimizeRequestInput& input,
const WeatherImpactEstimate& weather,
const TrafficImpact& traffic,
const Json::Value& vroom_output);

[[nodiscard]] Json::Value BuildWeatherForecastAnnotation(const WeatherForecastOptions& options,
const WeatherImpactEstimate& impact);

void AddTrafficForecast(Json::Value& forecast, const TrafficForecastOptions& options,
const TrafficImpact& impact);

} // namespace deliveryoptimizer::api
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class OptimizationJobRuntime {
std::shared_ptr<ObservabilityRegistry> observability_;
OptimizationJobRuntimeOptions options_;
WeatherForecastOptions weather_options_;
TrafficForecastOptions traffic_options_;
std::deque<WorkerState> worker_states_;
std::vector<std::jthread> workers_;
std::jthread heartbeat_thread_;
Expand Down
129 changes: 107 additions & 22 deletions app/api/src/endpoints/deliveries_optimize_endpoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
#include "deliveryoptimizer/api/vroom_runner.hpp"

#include <drogon/drogon.h>
#include <functional>
#include <json/json.h>
#include <memory>
#include <optional>
#include <string_view>
#include <thread>
#include <trantor/net/EventLoop.h>
#include <utility>

Expand Down Expand Up @@ -94,6 +96,70 @@ void DispatchResponse(
response_loop->queueInLoop([callback, response] { (*callback)(response); });
}

void FinishWithTraffic(
const std::shared_ptr<deliveryoptimizer::api::SolveCoordinator>& coordinator,
std::shared_ptr<deliveryoptimizer::api::OptimizeRequestInput> optimize_request,
const deliveryoptimizer::api::SolveRequestSize request_size,
deliveryoptimizer::api::TrafficForecastOptions traffic_options,
std::optional<Json::Value> forecast,
const deliveryoptimizer::api::WeatherImpactEstimate weather_impact,
std::function<void(const CompletedResponse&)> respond_with_completion,
deliveryoptimizer::api::CoordinatedSolveResult weather_result) {
if (!weather_result.output.has_value()) {
const deliveryoptimizer::api::SolveExecutionResult response_result =
deliveryoptimizer::api::BuildSolveExecutionResult(*optimize_request, weather_result,
forecast);
respond_with_completion(BuildSolveExecutionResponse(response_result));
return;
}

std::thread([coordinator, optimize_request = std::move(optimize_request), request_size,
traffic_options = std::move(traffic_options), forecast = std::move(forecast),
weather_impact, respond_with_completion = std::move(respond_with_completion),
weather_result = std::move(weather_result)]() mutable {
std::optional<Json::Value> final_forecast = forecast;
const Json::Value& route_output = *weather_result.output;

const deliveryoptimizer::api::TrafficDelayEstimate traffic_delay =
deliveryoptimizer::api::ReadRouteTraffic(
traffic_options, route_output,
deliveryoptimizer::api::ReadRouteStartTime(*optimize_request));
const deliveryoptimizer::api::TrafficImpact traffic =
deliveryoptimizer::api::EstimateTrafficImpact(
traffic_options, deliveryoptimizer::api::ReadVroomDuration(route_output).value_or(0),
traffic_delay.delay_seconds, traffic_delay.source);
if (final_forecast.has_value()) {
deliveryoptimizer::api::AddTrafficForecast(*final_forecast, traffic_options, traffic);
}

if (traffic.should_reoptimize) {
Json::Value route_output_copy = route_output;
const deliveryoptimizer::api::SolveAdmissionStatus traffic_rerun_status = coordinator->Submit(
request_size,
[optimize_request, weather_impact, traffic, route_output_copy] {
return deliveryoptimizer::api::BuildTrafficAdjustedVroomInput(
*optimize_request, weather_impact, traffic, route_output_copy);
},
[optimize_request, final_forecast, respond_with_completion](
const deliveryoptimizer::api::CoordinatedSolveResult& traffic_result) mutable {
const deliveryoptimizer::api::SolveExecutionResult response_result =
deliveryoptimizer::api::BuildSolveExecutionResult(*optimize_request, traffic_result,
final_forecast);
respond_with_completion(BuildSolveExecutionResponse(response_result));
});
if (traffic_rerun_status != deliveryoptimizer::api::SolveAdmissionStatus::kAccepted) {
respond_with_completion(BuildAdmissionRejectionResponse(traffic_rerun_status));
}
return;
}

const deliveryoptimizer::api::SolveExecutionResult response_result =
deliveryoptimizer::api::BuildSolveExecutionResult(*optimize_request, weather_result,
final_forecast);
respond_with_completion(BuildSolveExecutionResponse(response_result));
}).detach();
}

} // namespace

namespace deliveryoptimizer::api {
Expand All @@ -102,13 +168,14 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app,
const SolveAdmissionConfig& admission_config,
std::shared_ptr<ObservabilityRegistry> observability) {
const WeatherForecastOptions weather_options = ResolveWeatherForecastOptionsFromEnv();
auto coordinator = std::make_shared<SolveCoordinator>(
admission_config, std::make_shared<ProcessVroomRunner>(ResolveVroomRuntimeConfigFromEnv()),
SolveCoordinatorOptions{}, observability);
const TrafficForecastOptions traffic_options = ResolveTrafficForecastOptionsFromEnv();
auto runner = std::make_shared<ProcessVroomRunner>(ResolveVroomRuntimeConfigFromEnv());
auto coordinator = std::make_shared<SolveCoordinator>(admission_config, runner,
SolveCoordinatorOptions{}, observability);

app.registerHandler(
"/api/v1/deliveries/optimize",
[coordinator = std::move(coordinator), weather_options,
[coordinator = std::move(coordinator), weather_options, traffic_options,
observability = std::move(observability)](
const drogon::HttpRequestPtr& request,
std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
Expand Down Expand Up @@ -174,28 +241,46 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app,
.jobs = optimize_request_ptr->jobs.size(),
.vehicles = optimize_request_ptr->vehicles.size(),
};
auto weather_impact = std::make_shared<std::optional<WeatherImpactEstimate>>(std::nullopt);

const SolveAdmissionStatus admission_status = coordinator->Submit(
request_size,
[optimize_request_ptr, weather_options, weather_impact] {
const int baseline_seconds = EstimateServiceSeconds(*optimize_request_ptr);
const WeatherImpactEstimate impact = EstimateWeatherImpact(
weather_options, optimize_request_ptr->jobs.size(), baseline_seconds);
*weather_impact = impact;
return BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact);
},
[optimize_request_ptr, weather_options, weather_impact,
request_size, [optimize_request_ptr] { return BuildVroomInput(*optimize_request_ptr); },
[coordinator, optimize_request_ptr, request_size, weather_options, traffic_options,
respond_with_completion](const CoordinatedSolveResult& result) mutable {
std::optional<Json::Value> forecast;
if (result.output.has_value()) {
const WeatherImpactEstimate impact = weather_impact->value_or(
EstimateWeatherImpact(weather_options, optimize_request_ptr->jobs.size(),
EstimateServiceSeconds(*optimize_request_ptr)));
forecast = BuildWeatherForecastAnnotation(weather_options, impact);
if (!result.output.has_value()) {
respond_with_completion(BuildSolveExecutionResponse(
BuildSolveExecutionResult(*optimize_request_ptr, result, forecast)));
return;
}

WeatherForecastOptions sync_weather_options = weather_options;
// Clear the key so recalculation short-circuits OpenWeather; sync path must not
// block the event loop.
sync_weather_options.openweather_api_key.clear();
const WeatherImpactEstimate impact = RecalculateWeatherImpact(
sync_weather_options, *optimize_request_ptr, *result.output);
forecast = BuildWeatherForecastAnnotation(sync_weather_options, impact);

if (!impact.should_reoptimize) {
FinishWithTraffic(coordinator, optimize_request_ptr, request_size, traffic_options,
forecast, impact, respond_with_completion, result);
return;
}

const SolveAdmissionStatus rerun_status = coordinator->Submit(
request_size,
[optimize_request_ptr, impact] {
return BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact);
},
[coordinator, optimize_request_ptr, request_size, traffic_options, forecast,
impact,
respond_with_completion](const CoordinatedSolveResult& rerun_result) mutable {
FinishWithTraffic(coordinator, optimize_request_ptr, request_size,
traffic_options, forecast, impact, respond_with_completion,
rerun_result);
});
if (rerun_status != SolveAdmissionStatus::kAccepted) {
respond_with_completion(BuildAdmissionRejectionResponse(rerun_status));
}
respond_with_completion(BuildSolveExecutionResponse(
BuildSolveExecutionResult(*optimize_request_ptr, result, forecast)));
},
lifecycle);
if (admission_status != SolveAdmissionStatus::kAccepted) {
Expand Down
1 change: 1 addition & 0 deletions app/api/src/endpoints/optimization_jobs_endpoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <drogon/drogon.h>

// Windows SDK defines GetJob as a macro so it is undef before including jsoncpp.
#ifdef GetJob
#undef GetJob
#endif
Expand Down
Loading
Loading