diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b54b246b..be3c88db8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,7 +191,8 @@ jobs: buildcache-${{ matrix.config.preset }}- - name: Dependencies Cache - uses: actions/cache@v4 + uses: actions/cache/restore@v4 + id: restore-deps with: path: ${{ github.workspace }}/deps key: deps-${{ hashFiles('.pkg') }} @@ -220,6 +221,13 @@ jobs: path: ${{ github.workspace }}/.buildcache key: ${{ steps.restore-buildcache.outputs.cache-primary-key }} + - name: Save deps cache + if: always() + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/deps + key: ${{ steps.restore-deps.outputs.cache-primary-key }} + # ==== DISTRIBUTION ==== - name: Create Distribution run: | diff --git a/.pkg b/.pkg index e830a2ed1..a80b8b674 100644 --- a/.pkg +++ b/.pkg @@ -1,7 +1,7 @@ [nigiri] url=git@github.com:motis-project/nigiri.git branch=master - commit=ad5f236e0180b00b44a25b4e873fe32a03ae5ea3 + commit=87d3f42582f685efa719ccb08dc0c853673d8ac4 [cista] url=git@github.com:felixguendling/cista.git branch=master @@ -9,7 +9,7 @@ [osr] url=git@github.com:motis-project/osr.git branch=master - commit=650e05c5dc59598f84a5e9bae2db75d5cc3433b8 + commit=02c4efb5749086fc74fc736b2d092fdd77829b86 [utl] url=git@github.com:motis-project/utl.git branch=master @@ -25,7 +25,7 @@ [net] url=git@github.com:motis-project/net.git branch=master - commit=6a457f5eaa077078fcac4153c5a178657737eee8 + commit=43423bb08c636c9dd91e4d58b5b557c6d2f25efe [openapi-cpp] url=git@github.com:triptix-tech/openapi-cpp.git branch=master @@ -45,7 +45,7 @@ [boost] url=git@github.com:motis-project/boost.git branch=boost-1.86.0 - commit=930f38eb0365ceb7853273e03da4d9e7787abfb9 + commit=4a9aca6cb8af75be6e58f28c09cc7e39f61e6173 [tg] url=git@github.com:triptix-tech/tg.git branch=main diff --git a/.pkg.lock b/.pkg.lock index 4bac6da57..10daf351b 100644 --- a/.pkg.lock +++ b/.pkg.lock @@ -1,7 +1,7 @@ -16722610972019258869 +9396686199344891021 cista 6362f3ad8c3133a0abf64e5d8c9ea3e21f531ee8 zlib-ng 68ab3e2d80253ec5dc3c83691d9ff70477b32cd3 -boost 930f38eb0365ceb7853273e03da4d9e7787abfb9 +boost 4a9aca6cb8af75be6e58f28c09cc7e39f61e6173 googletest 7b64fca6ea0833628d6f86255a81424365f7cc0c lz4 c4765545ebb14b0a56c663e21923166923f8280e mimalloc e2f4fe647e8aff4603a7d5119b8639fd1a47c8a6 @@ -13,7 +13,7 @@ res b759b93316afeb529b6cb5b2548b24c41e382fb0 date ce88cc33b5551f66655614eeebb7c5b7189025fb yaml-cpp 1d8ca1f35eb3a9c9142462b28282a848e5d29a91 openapi-cpp 688d45bd96addb26eaccc5d264761030e5ef43f9 -net 6a457f5eaa077078fcac4153c5a178657737eee8 +net 43423bb08c636c9dd91e4d58b5b557c6d2f25efe PEGTL 1c1aa6e650e4d26f10fa398f148ec0cdc5f0808d oh d21c30f40e52a83d6dc09bcffd0067598b5ec069 doctest 70e8f76437b76dd5e9c0a2eb9b907df190ab71a0 @@ -28,7 +28,7 @@ opentelemetry-cpp 60770dc9dc63e3543fc87d605b2e88fd53d7a414 pugixml 60175e80e2f5e97e027ac78f7e14c5acc009ce50 unordered_dense b33b037377ca966bbdd9cccc3417e46e88f83bfb wyhash 1e012b57fc2227a9e583a57e2eacb3da99816d99 -nigiri ad5f236e0180b00b44a25b4e873fe32a03ae5ea3 +nigiri 87d3f42582f685efa719ccb08dc0c853673d8ac4 conf f9bf4bd83bf55a2170725707e526cbacc45dcc66 expat 636c9861e8e7c119f3626d1e6c260603ab624516 libosmium 6e6d6b3081cc8bdf25dda89730e25c36eb995516 @@ -44,7 +44,7 @@ sol2 40c7cbc7c5cfed1e8c7f1bbe6fcbe23d7a67fc75 variant 5aa73631dc969087c77433a5cdef246303051f69 tiles ab6c4b13544570f893c2d64434c613d8fd7d2ceb rtree.c 6ed73a7dc4f1184f2b5b2acd8ac1c2b28a273057 -osr 650e05c5dc59598f84a5e9bae2db75d5cc3433b8 +osr 02c4efb5749086fc74fc736b2d092fdd77829b86 reflect-cpp c54fe66de4650b60c23aadd4a06d9db4ffeda22f FTXUI dd6a5d371fd7a3e2937bb579955003c54b727233 tg 20c0f298b8ce58de29a790290f44dca7c4ecc364 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3046f1813..fa41ba16a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(motis) +project(motis LANGUAGES C CXX ASM) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) @@ -81,6 +81,7 @@ if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") -Wno-shadow-uncaptured-local -Wno-documentation-deprecated-sync -Wno-float-equal + -Wno-deprecated-declarations -Werror) elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") set(motis-compile-options -Wall -Wextra -Werror -Wno-unknown-pragmas) @@ -113,6 +114,7 @@ target_link_libraries(motislib osr adr boost-json + Boost::fiber motis-api reflectcpp web-server diff --git a/CMakePresets.json b/CMakePresets.json index 7c4895690..0a93c9193 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -12,6 +12,8 @@ "generator": "Ninja", "binaryDir": "${sourceDir}/build/macos-x86_64-release", "cacheVariables": { + "BOOST_CONTEXT_ABI": "sysv", + "BOOST_CONTEXT_ARCHITECTURE": "x86_64", "CMAKE_OSX_ARCHITECTURES": "x86_64", "CMAKE_CXX_FLAGS": "-stdlib=libc++", "CMAKE_BUILD_TYPE": "Release" diff --git a/include/motis/config.h b/include/motis/config.h index a5a6ef11b..bbf6438e1 100644 --- a/include/motis/config.h +++ b/include/motis/config.h @@ -28,6 +28,7 @@ struct config { bool requires_rt_timetable_updates() const; bool has_gbfs_feeds() const; + bool has_odm() const; bool operator==(config const&) const = default; @@ -120,6 +121,7 @@ struct config { std::optional proxy_{}; }; std::optional gbfs_{}; + std::optional odm_{}; bool street_routing_{false}; bool osr_footpath_{false}; diff --git a/include/motis/constants.h b/include/motis/constants.h index 72f8a34a3..9f54215eb 100644 --- a/include/motis/constants.h +++ b/include/motis/constants.h @@ -1,7 +1,5 @@ #pragma once -#include "cista/serialization.h" - namespace motis { // search radius for neighbors to route to [seconds] @@ -23,7 +21,4 @@ constexpr auto const kTransferTimeMultiplier = 1.5F; // are updated on elevator status changes [meters] constexpr auto const kElevatorUpdateRadius = 1000.; -// first nigiri::transport_mode_id_t used for GBFS -constexpr auto const kGbfsTransportModeIdOffset = 10; - } // namespace motis diff --git a/include/motis/data.h b/include/motis/data.h index 4ef25d081..246223cec 100644 --- a/include/motis/data.h +++ b/include/motis/data.h @@ -64,9 +64,9 @@ struct data { auto cista_members() { // !!! Remember to add all new members !!! - return std::tie(t_, r_, tc_, w_, pl_, l_, tt_, tags_, location_rtee_, - elevator_nodes_, shapes_, railviz_static_, matches_, rt_, - gbfs_); + return std::tie(config_, t_, r_, tc_, w_, pl_, l_, tt_, tags_, + location_rtee_, elevator_nodes_, shapes_, railviz_static_, + matches_, rt_, gbfs_); } std::filesystem::path path_; diff --git a/include/motis/endpoints/routing.h b/include/motis/endpoints/routing.h index 7f5175e5c..3d9c3438b 100644 --- a/include/motis/endpoints/routing.h +++ b/include/motis/endpoints/routing.h @@ -4,18 +4,48 @@ #include #include +#include "boost/thread/tss.hpp" + #include "osr/location.h" #include "osr/types.h" #include "nigiri/routing/clasz_mask.h" +#include "nigiri/routing/raptor/raptor_state.h" +#include "nigiri/routing/raptor_search.h" #include "motis-api/motis-api.h" #include "motis/elevators/elevators.h" #include "motis/fwd.h" #include "motis/match_platforms.h" +#include "motis/place.h" namespace motis::ep { +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern boost::thread_specific_ptr search_state; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern boost::thread_specific_ptr raptor_state; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern boost::thread_specific_ptr> blocked; + +using stats_map_t = std::map; + +bool is_intermodal(place_t const&); + +nigiri::routing::location_match_mode get_match_mode(place_t const&); + +std::vector station_start(nigiri::location_idx_t); + +std::vector get_via_stops( + nigiri::timetable const&, + tag_lookup const&, + std::optional> const& vias, + std::vector const& times); + +void remove_slower_than_fastest_direct(nigiri::routing::query&); + struct routing { api::plan_response operator()(boost::urls::url_view const&) const; @@ -53,6 +83,7 @@ struct routing { bool wheelchair, std::chrono::seconds max) const; + config const& config_; osr::ways const* w_; osr::lookup const* l_; osr::platforms const* pl_; diff --git a/include/motis/http_req.h b/include/motis/http_req.h index 511e15c90..cdd17b1f0 100644 --- a/include/motis/http_req.h +++ b/include/motis/http_req.h @@ -19,6 +19,12 @@ boost::asio::awaitable http_GET( std::map const& headers, std::chrono::seconds timeout); +boost::asio::awaitable http_POST( + boost::urls::url, + std::map const& headers, + std::string const& body, + std::chrono::seconds timeout); + std::string get_http_body(http_response const&); } // namespace motis diff --git a/include/motis/odm/meta_router.h b/include/motis/odm/meta_router.h new file mode 100644 index 000000000..a4ef2e009 --- /dev/null +++ b/include/motis/odm/meta_router.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include + +#include "osr/location.h" + +#include "nigiri/types.h" + +#include "motis-api/motis-api.h" + +#include "motis/endpoints/routing.h" +#include "motis/fwd.h" +#include "motis/gbfs/routing_data.h" +#include "motis/place.h" + +namespace nigiri { +struct timetable; +struct rt_timetable; +} // namespace nigiri + +namespace nigiri::routing { +struct query; +struct journey; +} // namespace nigiri::routing + +namespace motis::odm { + +struct meta_router { + meta_router(ep::routing const&, + api::plan_params const&, + std::vector const& pre_transit_modes, + std::vector const& post_transit_modes, + std::vector const& direct_modes, + std::variant const& from, + std::variant const& to, + api::Place const& from_p, + api::Place const& to_p, + nigiri::routing::query const& start_time, + std::vector& direct, + nigiri::duration_t fastest_direct_, + bool odm_pre_transit, + bool odm_post_transit, + bool odm_direct); + + api::plan_response run(); + +private: + void init_prima(nigiri::interval const& from_intvl, + nigiri::interval const& to_intvl); + + ep::routing const& r_; + api::plan_params const& query_; + std::vector const& pre_transit_modes_; + std::vector const& post_transit_modes_; + std::vector const& direct_modes_; + std::variant const& from_; + std::variant const& to_; + api::Place const& from_place_; + api::Place const& to_place_; + nigiri::routing::query const& start_time_; + std::vector& direct_; + nigiri::duration_t fastest_direct_; + bool odm_pre_transit_; + bool odm_post_transit_; + bool odm_direct_; + + nigiri::timetable const* tt_; + std::shared_ptr const rt_; + nigiri::rt_timetable const* rtt_; + motis::elevators const* e_; + gbfs::gbfs_routing_data gbfs_rd_; + std::variant const& start_; + std::variant const& dest_; + std::vector start_modes_; + std::vector dest_modes_; + + std::optional> const& + start_form_factors_; + std::optional> const& + dest_form_factors_; + std::optional> const& + start_propulsion_types_; + std::optional> const& + dest_propulsion_types_; + std::optional> const& start_rental_providers_; + std::optional> const& dest_rental_providers_; +}; + +} // namespace motis::odm \ No newline at end of file diff --git a/include/motis/odm/mixer.h b/include/motis/odm/mixer.h new file mode 100644 index 000000000..874df0109 --- /dev/null +++ b/include/motis/odm/mixer.h @@ -0,0 +1,35 @@ +#pragma once + +#include "nigiri/routing/journey.h" +#include "nigiri/routing/pareto_set.h" +#include "nigiri/types.h" + +#include "motis-api/motis-api.h" + +namespace motis::odm { + +struct cost_threshold { + std::int32_t threshold_; + std::int32_t cost_; +}; + +std::int32_t tally(std::int32_t, std::vector const&); + +struct mixer { + void mix(nigiri::pareto_set const& pt_journeys, + std::vector& odm_journeys) const; + std::int32_t transfer_cost(nigiri::routing::journey const&) const; + void cost_domination( + nigiri::pareto_set const& pt_journeys, + std::vector& odm_journeys) const; + void productivity_domination( + std::vector& odm_journeys) const; + + double alpha_; + double beta_; + std::vector walk_cost_; + std::vector taxi_cost_; + std::vector transfer_cost_; +}; + +} // namespace motis::odm \ No newline at end of file diff --git a/include/motis/odm/odm.h b/include/motis/odm/odm.h new file mode 100644 index 000000000..cb8f15edd --- /dev/null +++ b/include/motis/odm/odm.h @@ -0,0 +1,18 @@ +#pragma once + +#include "nigiri/routing/journey.h" +#include "nigiri/types.h" + +#include "motis-api/motis-api.h" + +namespace motis::odm { + +constexpr auto const kODMTransferBuffer = nigiri::duration_t{5}; +constexpr auto const kWalk = + static_cast(api::ModeEnum::WALK); + +enum which_mile { kFirstMile, kLastMile }; + +bool is_odm_leg(nigiri::routing::journey::leg const&); + +} // namespace motis::odm \ No newline at end of file diff --git a/include/motis/odm/prima.h b/include/motis/odm/prima.h new file mode 100644 index 000000000..1094d1dd9 --- /dev/null +++ b/include/motis/odm/prima.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include "geo/latlng.h" + +#include "nigiri/routing/journey.h" +#include "nigiri/routing/start_times.h" + +#include "motis-api/motis-api.h" + +namespace motis::ep { +struct routing; +} // namespace motis::ep + +namespace motis::odm { + +struct direct_ride { + nigiri::unixtime_t dep_; + nigiri::unixtime_t arr_; +}; + +struct capacities { + std::uint8_t wheelchairs_; + std::uint8_t bikes_; + std::uint8_t passengers_; + std::uint8_t luggage_; +}; + +struct prima { + void init(api::Place const& from, + api::Place const& to, + api::plan_params const& query); + std::string get_prima_request(nigiri::timetable const&) const; + std::size_t n_events() const; + bool blacklist_update(std::string_view json); + bool whitelist_update(std::string_view json); + void adjust_to_whitelisting(); + + geo::latlng from_; + geo::latlng to_; + nigiri::event_type fixed_; + capacities cap_; + + std::vector from_rides_{}; + std::vector to_rides_{}; + std::vector direct_rides_{}; + + std::vector prev_from_rides_{}; + std::vector prev_to_rides_{}; + std::vector prev_direct_rides_{}; + + std::vector odm_journeys_{}; +}; + +} // namespace motis::odm \ No newline at end of file diff --git a/include/motis/odm/query_factory.h b/include/motis/odm/query_factory.h new file mode 100644 index 000000000..314132214 --- /dev/null +++ b/include/motis/odm/query_factory.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include "nigiri/routing/query.h" + +namespace motis::odm { + +struct query_factory { + nigiri::routing::query walk_walk() const; + nigiri::routing::query walk_short() const; + nigiri::routing::query walk_long() const; + nigiri::routing::query short_walk() const; + nigiri::routing::query long_walk() const; + nigiri::routing::query short_short() const; + nigiri::routing::query short_long() const; + nigiri::routing::query long_short() const; + nigiri::routing::query long_long() const; + + // invariants + nigiri::routing::query base_query_; + + // offsets + std::vector start_walk_; + std::vector dest_walk_; + nigiri::hash_map> + td_start_walk_; + nigiri::hash_map> + td_dest_walk_; + nigiri::hash_map> + odm_start_short_; + nigiri::hash_map> + odm_start_long_; + nigiri::hash_map> + odm_dest_short_; + nigiri::hash_map> + odm_dest_long_; + +private: + nigiri::routing::query make( + std::vector const& start, + nigiri::hash_map> const& td_start, + std::vector const& dest, + nigiri::hash_map> const& td_dest) + const; +}; + +} // namespace motis::odm \ No newline at end of file diff --git a/include/motis/scheduler/runner.h b/include/motis/scheduler/runner.h new file mode 100644 index 000000000..b6fa9f8a0 --- /dev/null +++ b/include/motis/scheduler/runner.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "boost/fiber/algo/work_stealing.hpp" +#include "boost/fiber/buffered_channel.hpp" +#include "boost/fiber/operations.hpp" + +#include "net/web_server/query_router.h" + +#include "motis/scheduler/scheduler_algo.h" + +namespace motis { + +struct runner { + runner(std::size_t const n_threads, std::size_t const buffer_size) + : n_threads_{n_threads}, ch_{buffer_size} {} + + auto run_fn() { + return [&]() { + boost::fibers::use_scheduling_algorithm(n_threads_); + auto t = net::fiber_exec::task_t{}; + while (ch_.pop(t) != boost::fibers::channel_op_status::closed) { + t(); + } + }; + } + + std::size_t n_threads_; + net::fiber_exec::channel_t ch_; +}; + +} // namespace motis \ No newline at end of file diff --git a/include/motis/scheduler/scheduler_algo.h b/include/motis/scheduler/scheduler_algo.h new file mode 100644 index 000000000..a94dbc905 --- /dev/null +++ b/include/motis/scheduler/scheduler_algo.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include + +#include "boost/context/detail/prefetch.hpp" +#include "boost/fiber/algo/algorithm.hpp" +#include "boost/fiber/detail/context_spinlock_queue.hpp" +#include "boost/fiber/detail/thread_barrier.hpp" +#include "boost/fiber/properties.hpp" +#include "boost/fiber/scheduler.hpp" +#include "boost/intrusive_ptr.hpp" + +namespace motis { + +struct fiber_props : public boost::fibers::fiber_properties { + fiber_props(boost::fibers::context*); + ~fiber_props() override; + + // In order to keep request latency low, finishing already started requests + // has to be prioritized over new requests. Otherwise, the server only starts + // new requests and never finishes anything. + enum class type : std::uint8_t { + kHighPrio, // follow-up work scheduled by work + kLowPrio // initial work scheduled by I/O (web request / batch query) + } type_{type::kHighPrio}; +}; + +struct work_stealing + : public boost::fibers::algo::algorithm_with_properties { + static std::atomic counter_; + static std::vector > schedulers_; + + static void init_(std::uint32_t, + std::vector >&); + + work_stealing(std::uint32_t, bool = true); + + work_stealing(work_stealing const&) = delete; + work_stealing(work_stealing&&) = delete; + + work_stealing& operator=(work_stealing const&) = delete; + work_stealing& operator=(work_stealing&&) = delete; + + void awakened(boost::fibers::context*, fiber_props&) noexcept override; + + boost::fibers::context* pick_next() noexcept override; + + virtual boost::fibers::context* steal() noexcept { return rqueue_.steal(); } + + bool has_ready_fibers() const noexcept override { return !rqueue_.empty(); } + + void suspend_until( + std::chrono::steady_clock::time_point const&) noexcept override; + + void notify() noexcept override; + + std::uint32_t id_; + std::uint32_t thread_count_; + boost::fibers::detail::context_spinlock_queue rqueue_{}; + boost::fibers::detail::context_spinlock_queue high_prio_rqueue_{}; + std::mutex mtx_{}; + std::condition_variable cnd_{}; + bool flag_{false}; + bool suspend_; +}; + +} // namespace motis \ No newline at end of file diff --git a/include/motis/transport_mode_ids.h b/include/motis/transport_mode_ids.h new file mode 100644 index 000000000..73193ee20 --- /dev/null +++ b/include/motis/transport_mode_ids.h @@ -0,0 +1,14 @@ +#pragma once + +#include "osr/routing/profile.h" + +#include "nigiri/types.h" + +namespace motis { + +constexpr auto const kOdmTransportModeId = + static_cast(osr::kNumProfiles); +constexpr auto const kGbfsTransportModeIdOffset = + static_cast(osr::kNumProfiles + 1U); + +} // namespace motis \ No newline at end of file diff --git a/openapi.yaml b/openapi.yaml index 31e018756..3fdd296c6 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1137,6 +1137,7 @@ components: - `RENTAL` Experimental. Expect unannounced breaking changes (without version bumps). - `CAR` - `CAR_PARKING` + - `ODM` # Transit modes @@ -1162,6 +1163,7 @@ components: - RENTAL - CAR - CAR_PARKING + - ODM # === Transit === - TRANSIT - TRAM @@ -1500,6 +1502,42 @@ components: returnConstraint: $ref: '#/components/schemas/RentalReturnConstraint' + ODMType: + type: string + enum: + - TAXI + - RIDE_SHARING + + ODM: + description: Vehicle with driver, e.g., taxi + type: object + required: + - systemId + properties: + systemId: + type: string + description: ODM system ID + systemName: + type: string + description: ODM system name + url: + type: string + description: URL of the ODM system + companyName: + type: string + description: Name of company that offers the service + odmUriAndroid: + type: string + description: ODM URI for Android (deep link to the specific station or vehicle) + odmUriIOS: + type: string + description: ODM URI for iOS (deep link to the specific station or vehicle) + odmUriWeb: + type: string + description: ODM URI for web (deep link to the specific station or vehicle) + odmType: + $ref: '#/components/schemas/ODMType' + Leg: type: object required: @@ -1603,6 +1641,8 @@ components: $ref: '#/components/schemas/StepInstruction' rental: $ref: '#/components/schemas/Rental' + odm: + $ref: '#/components/schemas/ODM' Itinerary: type: object diff --git a/src/config.cc b/src/config.cc index 443de5b37..4d6821d66 100644 --- a/src/config.cc +++ b/src/config.cc @@ -110,6 +110,8 @@ void config::verify() const { "TIMETABLE"); utl::verify(!has_gbfs_feeds() || street_routing_, "feature GBFS requires feature STREET_ROUTING"); + utl::verify(!has_odm() || (street_routing_ && timetable_), + "feature ODM requires feature STREET_ROUTING"); if (timetable_) { for (auto const& [_, d] : timetable_->datasets_) { @@ -166,6 +168,8 @@ bool config::has_gbfs_feeds() const { return gbfs_.has_value() && !gbfs_->feeds_.empty(); } +bool config::has_odm() const { return odm_.has_value(); } + } // namespace motis // ==================== diff --git a/src/endpoints/routing.cc b/src/endpoints/routing.cc index 58a590751..8c5f3608a 100644 --- a/src/endpoints/routing.cc +++ b/src/endpoints/routing.cc @@ -27,6 +27,7 @@ #include "motis/journey_to_response.h" #include "motis/max_distance.h" #include "motis/mode_to_profile.h" +#include "motis/odm/meta_router.h" #include "motis/parse_location.h" #include "motis/street_routing.h" #include "motis/tag_lookup.h" @@ -46,13 +47,13 @@ using td_offsets_t = n::hash_map>; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static boost::thread_specific_ptr search_state; +boost::thread_specific_ptr search_state; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static boost::thread_specific_ptr raptor_state; +boost::thread_specific_ptr raptor_state; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static boost::thread_specific_ptr> blocked; +boost::thread_specific_ptr> blocked; place_t get_place(n::timetable const* tt, tag_lookup const* tags, @@ -90,6 +91,10 @@ td_offsets_t routing::get_td_offsets(elevators const& e, auto ret = hash_map>{}; for (auto const m : modes) { + if (m == api::ModeEnum::ODM) { + continue; + } + auto const profile = to_profile(m, wheelchair); if (profile != osr::search_profile::kWheelchair) { @@ -440,7 +445,7 @@ api::plan_response routing::operator()(boost::urls::url_view const& url) const { auto const [start_time, t] = get_start_time(query); UTL_START_TIMING(direct); - auto const [direct, fastest_direct] = + auto [direct, fastest_direct] = t.has_value() && !direct_modes.empty() && w_ && l_ ? route_direct(e, gbfs_rd, from_p, to_p, direct_modes, query.directRentalFormFactors_, @@ -456,6 +461,35 @@ api::plan_response routing::operator()(boost::urls::url_view const& url) const { utl::verify(tt_ != nullptr && tags_ != nullptr, "mode=TRANSIT requires timetable to be loaded"); + auto const with_odm_pre_transit = + utl::find(pre_transit_modes, api::ModeEnum::ODM) != + end(pre_transit_modes); + auto const with_odm_post_transit = + utl::find(post_transit_modes, api::ModeEnum::ODM) != + end(post_transit_modes); + auto const with_odm_direct = + utl::find(direct_modes, api::ModeEnum::ODM) != end(direct_modes); + + if (with_odm_pre_transit || with_odm_post_transit || with_odm_direct) { + utl::verify(config_.has_odm(), "ODM not configured"); + return odm::meta_router{*this, + query, + pre_transit_modes, + post_transit_modes, + direct_modes, + from, + to, + from_p, + to_p, + start_time, + direct, + fastest_direct, + with_odm_pre_transit, + with_odm_post_transit, + with_odm_direct} + .run(); + } + UTL_START_TIMING(query_preparation); auto q = n::routing::query{ .start_time_ = start_time.start_time_, diff --git a/src/gbfs/routing_data.cc b/src/gbfs/routing_data.cc index 6dedfcf78..e8cf4a9e3 100644 --- a/src/gbfs/routing_data.cc +++ b/src/gbfs/routing_data.cc @@ -11,6 +11,7 @@ #include "motis/constants.h" #include "motis/gbfs/data.h" #include "motis/gbfs/osr_mapping.h" +#include "motis/transport_mode_ids.h" namespace motis::gbfs { diff --git a/src/http_req.cc b/src/http_req.cc index 3ac1c5ff0..a123b63cd 100644 --- a/src/http_req.cc +++ b/src/http_req.cc @@ -27,13 +27,16 @@ namespace ssl = asio::ssl; constexpr auto const kBodySizeLimit = 128U * 1024U * 1024U; // 128 M template -asio::awaitable req(Stream&&, - boost::urls::url const&, - std::map const&); +asio::awaitable req( + Stream&&, + boost::urls::url const&, + std::map const&, + std::optional const& body = std::nullopt); asio::awaitable req_no_tls( boost::urls::url const& url, std::map const& headers, + std::optional const& body, std::chrono::seconds const timeout) { auto executor = co_await asio::this_coro::executor; auto resolver = asio::ip::tcp::resolver{executor}; @@ -45,12 +48,13 @@ asio::awaitable req_no_tls( stream.expires_after(timeout); co_await stream.async_connect(results); - co_return co_await req(std::move(stream), url, headers); + co_return co_await req(std::move(stream), url, headers, body); } asio::awaitable req_tls( boost::urls::url const& url, std::map const& headers, + std::optional const& body, std::chrono::seconds const timeout) { auto ssl_ctx = ssl::context{ssl::context::tlsv12_client}; ssl_ctx.set_default_verify_paths(); @@ -77,16 +81,17 @@ asio::awaitable req_tls( url.host(), url.has_port() ? url.port() : "443"); co_await beast::get_lowest_layer(stream).async_connect(results); co_await stream.async_handshake(ssl::stream_base::client); - co_return co_await req(std::move(stream), url, headers); + co_return co_await req(std::move(stream), url, headers, body); } template asio::awaitable req( Stream&& stream, boost::urls::url const& url, - std::map const& headers) { - auto req = http::request{http::verb::get, - url.encoded_target(), 11}; + std::map const& headers, + std::optional const& body) { + auto req = http::request{ + body ? http::verb::post : http::verb::get, url.encoded_target(), 11}; req.set(http::field::host, url.host()); req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); req.set(http::field::accept_encoding, "gzip"); @@ -94,6 +99,11 @@ asio::awaitable req( req.set(k, v); } + if (body) { + req.body() = *body; + req.prepare_payload(); + } + co_await http::async_write(stream, req); auto p = http::response_parser{}; @@ -118,11 +128,37 @@ asio::awaitable> http_GET( while (n_redirects < 3U) { auto const res = co_await (next_url.scheme_id() == boost::urls::scheme::https - ? req_tls(next_url, headers, timeout) - : req_no_tls(next_url, headers, timeout)); + ? req_tls(next_url, headers, std::nullopt, timeout) + : req_no_tls(next_url, headers, std::nullopt, timeout)); + auto const code = res.base().result_int(); + if (code >= 300 && code < 400) { + next_url = boost::urls::url{res.base()["Location"]}; + ++n_redirects; + continue; + } else { + co_return res; + } + } + throw utl::fail(R"(too many redirects: "{}", latest="{}")", + fmt::streamed(url), fmt::streamed(next_url)); +} + +asio::awaitable> http_POST( + boost::urls::url url, + std::map const& headers, + std::string const& body, + std::chrono::seconds timeout) { + auto n_redirects = 0U; + auto next_url = url; + while (n_redirects < 3U) { + auto const res = + co_await (next_url.scheme_id() == boost::urls::scheme::https + ? req_tls(next_url, headers, body, timeout) + : req_no_tls(next_url, headers, body, timeout)); auto const code = res.base().result_int(); if (code >= 300 && code < 400) { next_url = boost::urls::url{res.base()["Location"]}; + ++n_redirects; continue; } else { co_return res; diff --git a/src/journey_to_response.cc b/src/journey_to_response.cc index 821d81cc4..9aa3cbea4 100644 --- a/src/journey_to_response.cc +++ b/src/journey_to_response.cc @@ -24,6 +24,7 @@ #include "motis/tag_lookup.h" #include "motis/timetable/clasz_to_mode.h" #include "motis/timetable/time_conv.h" +#include "motis/transport_mode_ids.h" namespace n = nigiri; @@ -179,6 +180,8 @@ api::Itinerary journey_to_response(osr::ways const* w, *w, *l, gbfs_rd, e, from, to, x.transport_mode_id_ >= kGbfsTransportModeIdOffset ? api::ModeEnum::RENTAL + : x.transport_mode_id_ == kOdmTransportModeId + ? api::ModeEnum::ODM : to_mode(osr::search_profile{ static_cast(x.transport_mode_id_)}), wheelchair, j_leg.dep_time_, j_leg.arr_time_, diff --git a/src/mode_to_profile.cc b/src/mode_to_profile.cc index 3d2936e1b..971d66b63 100644 --- a/src/mode_to_profile.cc +++ b/src/mode_to_profile.cc @@ -20,6 +20,7 @@ osr::search_profile to_profile(api::ModeEnum const m, bool const wheelchair) { return wheelchair ? osr::search_profile::kWheelchair : osr::search_profile::kFoot; case api::ModeEnum::BIKE: return osr::search_profile::kBike; + case api::ModeEnum::ODM: [[fallthrough]]; case api::ModeEnum::CAR: return osr::search_profile::kCar; case api::ModeEnum::CAR_PARKING: return wheelchair ? osr::search_profile::kCarParkingWheelchair diff --git a/src/odm/meta_router.cc b/src/odm/meta_router.cc new file mode 100644 index 000000000..ca4adad27 --- /dev/null +++ b/src/odm/meta_router.cc @@ -0,0 +1,657 @@ +#if defined(_MSC_VER) +// needs to be the first to include WinSock.h +#include "boost/asio.hpp" +#endif + +#include "motis/odm/meta_router.h" + +#include + +#include "fmt/std.h" + +#include "boost/asio/co_spawn.hpp" +#include "boost/asio/detached.hpp" +#include "boost/asio/io_context.hpp" +#include "boost/fiber/future.hpp" +#include "boost/fiber/future/packaged_task.hpp" +#include "boost/thread/tss.hpp" + +#include "utl/erase_duplicates.h" + +#include "nigiri/routing/journey.h" +#include "nigiri/routing/limits.h" +#include "nigiri/routing/query.h" +#include "nigiri/routing/raptor_search.h" +#include "nigiri/routing/start_times.h" +#include "nigiri/types.h" + +#include "osr/routing/route.h" + +#include "motis-api/motis-api.h" +#include "motis/constants.h" +#include "motis/elevators/elevators.h" +#include "motis/endpoints/routing.h" +#include "motis/gbfs/routing_data.h" +#include "motis/http_req.h" +#include "motis/journey_to_response.h" +#include "motis/odm/mixer.h" +#include "motis/odm/odm.h" +#include "motis/odm/prima.h" +#include "motis/odm/query_factory.h" +#include "motis/place.h" +#include "motis/street_routing.h" +#include "motis/timetable/modes_to_clasz_mask.h" +#include "motis/timetable/time_conv.h" +#include "motis/transport_mode_ids.h" + +namespace motis::odm { + +namespace n = nigiri; +using namespace std::chrono_literals; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static boost::thread_specific_ptr p; + +constexpr auto const kMinODMOffsetLength = n::duration_t{3}; +constexpr auto const kBlacklistPath = "/api/blacklist"; +constexpr auto const kWhitelistPath = "/api/whitelist"; +static auto const kReqHeaders = std::map{ + {"Content-Type", "application/json"}, {"Accept", "application/json"}}; + +constexpr auto const kInfinityDuration = + n::duration_t{std::numeric_limits::max()}; + +static auto const kOdmMixer = mixer{.alpha_ = 1.5, + .beta_ = 0.39, + .walk_cost_ = {{0, 1}, {15, 10}}, + .taxi_cost_ = {{0, 35}, {1, 12}}, + .transfer_cost_ = {{0, 10}}}; + +using td_offsets_t = + n::hash_map>; + +void print_time(auto const& start, std::string_view name) { + fmt::println("{}: {}", name, + std::chrono::duration_cast( + std::chrono::steady_clock::now() - start)); +} + +meta_router::meta_router(ep::routing const& r, + api::plan_params const& query, + std::vector const& pre_transit_modes, + std::vector const& post_transit_modes, + std::vector const& direct_modes, + std::variant const& from, + std::variant const& to, + api::Place const& from_p, + api::Place const& to_p, + nigiri::routing::query const& start_time, + std::vector& direct, + nigiri::duration_t const fastest_direct, + bool const odm_pre_transit, + bool const odm_post_transit, + bool const odm_direct) + : r_{r}, + query_{query}, + pre_transit_modes_{pre_transit_modes}, + post_transit_modes_{post_transit_modes}, + direct_modes_{direct_modes}, + from_{from}, + to_{to}, + from_place_{from_p}, + to_place_{to_p}, + start_time_{start_time}, + direct_{direct}, + fastest_direct_{fastest_direct}, + odm_pre_transit_{odm_pre_transit}, + odm_post_transit_{odm_post_transit}, + odm_direct_{odm_direct}, + tt_{r_.tt_}, + rt_{r.rt_}, + rtt_{rt_->rtt_.get()}, + e_{rt_->e_.get()}, + gbfs_rd_{r.w_, r.l_, r.gbfs_}, + start_{query_.arriveBy_ ? to_ : from_}, + dest_{query_.arriveBy_ ? from_ : to_}, + start_modes_{query_.arriveBy_ ? post_transit_modes_ : pre_transit_modes_}, + dest_modes_{query_.arriveBy_ ? pre_transit_modes_ : post_transit_modes_}, + start_form_factors_{query_.arriveBy_ + ? query_.postTransitRentalFormFactors_ + : query_.preTransitRentalFormFactors_}, + dest_form_factors_{query_.arriveBy_ + ? query_.preTransitRentalFormFactors_ + : query_.postTransitRentalFormFactors_}, + start_propulsion_types_{query_.arriveBy_ + ? query_.postTransitRentalPropulsionTypes_ + : query_.preTransitRentalPropulsionTypes_}, + dest_propulsion_types_{query_.arriveBy_ + ? query_.postTransitRentalPropulsionTypes_ + : query_.preTransitRentalPropulsionTypes_}, + start_rental_providers_{query_.arriveBy_ + ? query_.postTransitRentalProviders_ + : query_.preTransitRentalProviders_}, + dest_rental_providers_{query_.arriveBy_ + ? query_.preTransitRentalProviders_ + : query_.postTransitRentalProviders_} { + if (ep::blocked.get() == nullptr && r.w_ != nullptr) { + ep::blocked.reset(new osr::bitvec{r.w_->n_nodes()}); + } +} + +n::interval get_dest_intvl( + n::direction dir, n::interval const& start_intvl) { + return dir == n::direction::kForward + ? n::interval{start_intvl.from_, + start_intvl.to_ + 12h} + : n::interval{start_intvl.from_ - 12h, + start_intvl.to_}; +} + +n::duration_t init_direct(std::vector& direct_rides, + ep::routing const& r, + elevators const* e, + gbfs::gbfs_routing_data& gbfs, + api::Place const& from_p, + api::Place const& to_p, + n::interval const intvl, + api::plan_params const& query) { + auto [direct, duration] = r.route_direct( + e, gbfs, from_p, to_p, {api::ModeEnum::CAR}, std::nullopt, std::nullopt, + std::nullopt, intvl.from_, + query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR, + std::chrono::seconds{query.maxDirectTime_}); + direct_rides.clear(); + if (query.arriveBy_) { + for (auto arr = + std::chrono::floor(intvl.to_ - duration) + + duration; + intvl.contains(arr); arr -= 1h) { + direct_rides.push_back({.dep_ = arr - duration, .arr_ = arr}); + } + } else { + for (auto dep = std::chrono::ceil(intvl.from_); + intvl.contains(dep); dep += 1h) { + direct_rides.push_back({.dep_ = dep, .arr_ = dep + duration}); + } + } + return duration; +} + +void init_pt(std::vector& rides, + ep::routing const& r, + osr::location const& l, + osr::direction dir, + api::plan_params const& query, + gbfs::gbfs_routing_data& gbfs_rd, + n::timetable const& tt, + n::rt_timetable const* rtt, + n::interval const& intvl, + n::routing::query const& start_time, + n::routing::location_match_mode location_match_mode, + std::chrono::seconds const max) { + auto offsets = r.get_offsets( + l, dir, {api::ModeEnum::ODM}, std::nullopt, std::nullopt, std::nullopt, + query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR, max, + query.maxMatchingDistance_, gbfs_rd); + + std::erase_if( + offsets, [](auto const& o) { return o.duration_ < kMinODMOffsetLength; }); + + for (auto& o : offsets) { + o.duration_ += kODMTransferBuffer; + } + + rides.clear(); + rides.reserve(offsets.size() * 2); + + n::routing::get_starts( + dir == osr::direction::kForward ? n::direction::kForward + : n::direction::kBackward, + tt, rtt, intvl, offsets, {}, n::routing::kMaxTravelTime, + location_match_mode, false, rides, true, start_time.prf_idx_, + start_time.transfer_time_settings_); +} + +void meta_router::init_prima(n::interval const& from_intvl, + n::interval const& to_intvl) { + if (p.get() == nullptr) { + p.reset(new prima{}); + } + + p->init(from_place_, to_place_, query_); + + auto direct_duration = std::optional>{}; + if (odm_direct_ && r_.w_ && r_.l_) { + direct_duration = init_direct(p->direct_rides_, r_, e_, gbfs_rd_, + from_place_, to_place_, to_intvl, query_); + } + + if (odm_pre_transit_ && holds_alternative(from_)) { + init_pt(p->from_rides_, r_, std::get(from_), + osr::direction::kForward, query_, gbfs_rd_, *tt_, rtt_, from_intvl, + start_time_, + query_.arriveBy_ ? start_time_.dest_match_mode_ + : start_time_.start_match_mode_, + std::chrono::seconds{direct_duration + ? std::min(direct_duration->count(), + query_.maxPreTransitTime_) + : query_.maxPreTransitTime_}); + std::erase(start_modes_, api::ModeEnum::ODM); + } + + if (odm_post_transit_ && holds_alternative(to_)) { + init_pt(p->to_rides_, r_, std::get(to_), + osr::direction::kBackward, query_, gbfs_rd_, *tt_, rtt_, to_intvl, + start_time_, + query_.arriveBy_ ? start_time_.start_match_mode_ + : start_time_.dest_match_mode_, + std::chrono::seconds{direct_duration + ? std::min(direct_duration->count(), + query_.maxPostTransitTime_) + : query_.maxPostTransitTime_}); + std::erase(dest_modes_, api::ModeEnum::ODM); + } +} + +bool ride_comp(n::routing::start const& a, n::routing::start const& b) { + return std::tie(a.stop_, a.time_at_start_, a.time_at_stop_) < + std::tie(b.stop_, b.time_at_start_, b.time_at_stop_); +} + +auto ride_time_halves(std::vector& rides) { + utl::sort(rides, [](auto const& a, auto const& b) { + auto const ride_time = [](auto const& ride) { + return std::chrono::abs(ride.time_at_stop_ - ride.time_at_start_); + }; + return ride_time(a) < ride_time(b); + }); + auto const half = rides.size() / 2; + auto lo = rides | std::views::take(half); + auto hi = rides | std::views::drop(half); + std::ranges::sort(lo, ride_comp); + std::ranges::sort(hi, ride_comp); + return std::pair{lo, hi}; +} + +auto get_td_offsets(auto const& rides) { + auto td_offsets = td_offsets_t{}; + utl::equal_ranges_linear( + rides, [](auto const& a, auto const& b) { return a.stop_ == b.stop_; }, + [&](auto&& from_it, auto&& to_it) { + td_offsets.emplace(from_it->stop_, + std::vector{}); + for (auto const& r : n::it_range{from_it, to_it}) { + auto const dep = std::min(r.time_at_stop_, r.time_at_start_); + auto const dur = std::chrono::abs(r.time_at_stop_ - r.time_at_start_); + if (td_offsets.at(from_it->stop_).size() > 1) { + auto last = rbegin(td_offsets.at(from_it->stop_)); + auto const second_last = std::next(last); + if (dep == + std::clamp(dep, second_last->valid_from_, last->valid_from_)) { + // increase validity interval of last offset + last->valid_from_ = dep + dur; + continue; + } + } + // add new offset + td_offsets.at(from_it->stop_) + .push_back({.valid_from_ = dep, + .duration_ = dur, + .transport_mode_id_ = kOdmTransportModeId}); + td_offsets.at(from_it->stop_) + .push_back({.valid_from_ = dep + dur, + .duration_ = n::footpath::kMaxDuration, + .transport_mode_id_ = kOdmTransportModeId}); + } + }); + return td_offsets; +} + +auto collect_odm_journeys(auto& futures) { + p->odm_journeys_.clear(); + for (auto& f : futures | std::views::drop(1)) { + for (auto& j : f.get().journeys_) { + p->odm_journeys_.emplace_back(std::move(j)); + } + } + fmt::println("[whitelisting] collected {} ODM-PT journeys", + p->odm_journeys_.size()); +} + +void extract_rides() { + p->from_rides_.clear(); + p->to_rides_.clear(); + for (auto const& j : p->odm_journeys_) { + if (!j.legs_.empty()) { + if (is_odm_leg(j.legs_.front())) { + p->from_rides_.push_back({.time_at_start_ = j.legs_.front().dep_time_, + .time_at_stop_ = j.legs_.front().arr_time_, + .stop_ = j.legs_.front().to_}); + } + } + if (j.legs_.size() > 1) { + if (is_odm_leg(j.legs_.back())) { + p->to_rides_.push_back({.time_at_start_ = j.legs_.back().arr_time_, + .time_at_stop_ = j.legs_.back().dep_time_, + .stop_ = j.legs_.back().from_}); + } + } + } + + utl::erase_duplicates(p->from_rides_, ride_comp, std::equal_to<>{}); + utl::erase_duplicates(p->to_rides_, ride_comp, std::equal_to<>{}); +} + +void add_direct() { + for (auto const& d : p->direct_rides_) { + p->odm_journeys_.push_back(n::routing::journey{ + .legs_ = + {{n::direction::kForward, + get_special_station(n::special_station::kStart), + get_special_station(n::special_station::kEnd), d.dep_, d.arr_, + n::routing::offset{get_special_station(n::special_station::kEnd), + std::chrono::abs(d.arr_ - d.dep_), + kOdmTransportModeId}}}, + .start_time_ = d.dep_, + .dest_time_ = d.arr_, + .dest_ = get_special_station(n::special_station::kEnd), + .transfers_ = 0U}); + } + fmt::println("[whitelisting] added {} direct rides", p->direct_rides_.size()); +} + +struct routing_result { + explicit routing_result( + n::routing::routing_result rr) + : journeys_{*rr.journeys_}, + interval_{rr.interval_}, + search_stats_{rr.search_stats_}, + algo_stats_{rr.algo_stats_} {} + + n::pareto_set journeys_; + n::interval interval_; + n::routing::search_stats search_stats_; + n::routing::raptor_stats algo_stats_; +}; + +api::plan_response meta_router::run() { + // init + auto const init_start = std::chrono::steady_clock::now(); + utl::verify(r_.tt_ != nullptr && r_.tags_ != nullptr, + "mode=TRANSIT requires timetable to be loaded"); + auto stats = motis::ep::stats_map_t{}; + auto const start_intvl = std::visit( + utl::overloaded{[](n::interval const i) { return i; }, + [](n::unixtime_t const t) { + return n::interval{t, t}; + }}, + start_time_.start_time_); + auto const dest_intvl = get_dest_intvl( + query_.arriveBy_ ? n::direction::kBackward : n::direction::kForward, + start_intvl); + auto const& from_intvl = query_.arriveBy_ ? dest_intvl : start_intvl; + auto const& to_intvl = query_.arriveBy_ ? start_intvl : dest_intvl; + init_prima(from_intvl, to_intvl); + print_time(init_start, "[init]"); + + // blacklisting + auto blacklist_response = std::optional{}; + auto ioc = boost::asio::io_context{}; + auto const bl_start = std::chrono::steady_clock::now(); + try { + fmt::println("[blacklisting] request for {} events", p->n_events()); + boost::asio::co_spawn( + ioc, + [&]() -> boost::asio::awaitable { + auto const prima_msg = co_await http_POST( + boost::urls::url{*r_.config_.odm_ + kBlacklistPath}, kReqHeaders, + p->get_prima_request(*tt_), 10s); + blacklist_response = get_http_body(prima_msg); + }, + boost::asio::detached); + ioc.run(); + } catch (std::exception const& e) { + fmt::println("[blacklisting] networking failed: {}", e.what()); + blacklist_response = std::nullopt; + } + print_time(bl_start, "[blacklisting]"); + + // prepare queries + auto const prep_queries_start = std::chrono::steady_clock::now(); + auto const blacklisted = + blacklist_response && p->blacklist_update(*blacklist_response); + auto const [from_rides_short, from_rides_long] = + ride_time_halves(p->from_rides_); + auto const [to_rides_short, to_rides_long] = ride_time_halves(p->to_rides_); + + auto q = n::routing::query{ + .start_time_ = start_time_.start_time_, + .start_match_mode_ = motis::ep::get_match_mode(start_), + .dest_match_mode_ = motis::ep::get_match_mode(dest_), + .use_start_footpaths_ = !motis::ep::is_intermodal(start_), + .max_transfers_ = static_cast( + query_.maxTransfers_.has_value() ? *query_.maxTransfers_ + : n::routing::kMaxTransfers), + .max_travel_time_ = query_.maxTravelTime_ + .and_then([](std::int64_t const dur) { + return std::optional{n::duration_t{dur}}; + }) + .value_or(kInfinityDuration), + .min_connection_count_ = static_cast(query_.numItineraries_), + .extend_interval_earlier_ = start_time_.extend_interval_earlier_, + .extend_interval_later_ = start_time_.extend_interval_later_, + .prf_idx_ = static_cast( + query_.useRoutedTransfers_ + ? (query_.pedestrianProfile_ == + api::PedestrianProfileEnum::WHEELCHAIR + ? 2U + : 1U) + : 0U), + .allowed_claszes_ = to_clasz_mask(query_.transitModes_), + .require_bike_transport_ = query_.requireBikeTransport_, + .transfer_time_settings_ = + n::routing::transfer_time_settings{ + .default_ = (query_.minTransferTime_ == 0 && + query_.additionalTransferTime_ == 0 && + query_.transferTimeFactor_ == 1.0), + .min_transfer_time_ = n::duration_t{query_.minTransferTime_}, + .additional_time_ = n::duration_t{query_.additionalTransferTime_}, + .factor_ = static_cast(query_.transferTimeFactor_)}, + .via_stops_ = motis::ep::get_via_stops(*tt_, *r_.tags_, query_.via_, + query_.viaMinimumStay_), + .fastest_direct_ = fastest_direct_ == kInfinityDuration + ? std::nullopt + : std::optional{fastest_direct_}}; + + auto const qf = query_factory{ + .base_query_ = std::move(q), + .start_walk_ = std::visit( + utl::overloaded{[&](tt_location const l) { + return motis::ep::station_start(l.l_); + }, + [&](osr::location const& pos) { + auto const dir = query_.arriveBy_ + ? osr::direction::kBackward + : osr::direction::kForward; + return r_.get_offsets( + pos, dir, start_modes_, start_form_factors_, + start_propulsion_types_, + start_rental_providers_, + query_.pedestrianProfile_ == + api::PedestrianProfileEnum::WHEELCHAIR, + std::chrono::seconds{query_.maxPreTransitTime_}, + query_.maxMatchingDistance_, gbfs_rd_); + }}, + start_), + .dest_walk_ = std::visit( + utl::overloaded{ + [&](tt_location const l) { + return motis::ep::station_start(l.l_); + }, + [&](osr::location const& pos) { + auto const dir = query_.arriveBy_ ? osr::direction::kForward + : osr::direction::kBackward; + return r_.get_offsets( + pos, dir, dest_modes_, dest_form_factors_, + dest_propulsion_types_, dest_rental_providers_, + query_.pedestrianProfile_ == + api::PedestrianProfileEnum::WHEELCHAIR, + std::chrono::seconds{query_.maxPostTransitTime_}, + query_.maxMatchingDistance_, gbfs_rd_); + }}, + dest_), + .td_start_walk_ = + e_ != nullptr + ? std::visit( + utl::overloaded{ + [&](tt_location) { return td_offsets_t{}; }, + [&](osr::location const& pos) { + auto const dir = query_.arriveBy_ + ? osr::direction::kBackward + : osr::direction::kForward; + return r_.get_td_offsets( + *e_, pos, dir, start_modes_, + query_.pedestrianProfile_ == + api::PedestrianProfileEnum::WHEELCHAIR, + std::chrono::seconds{query_.maxPreTransitTime_}); + }}, + start_) + : td_offsets_t{}, + .td_dest_walk_ = + e_ != nullptr + ? std::visit( + utl::overloaded{ + [&](tt_location) { return td_offsets_t{}; }, + [&](osr::location const& pos) { + auto const dir = query_.arriveBy_ + ? osr::direction::kForward + : osr::direction::kBackward; + return r_.get_td_offsets( + *e_, pos, dir, dest_modes_, + query_.pedestrianProfile_ == + api::PedestrianProfileEnum::WHEELCHAIR, + std::chrono::seconds{query_.maxPostTransitTime_}); + }}, + dest_) + : td_offsets_t{}, + .odm_start_short_ = query_.arriveBy_ ? get_td_offsets(to_rides_short) + : get_td_offsets(from_rides_short), + .odm_start_long_ = query_.arriveBy_ ? get_td_offsets(to_rides_long) + : get_td_offsets(from_rides_long), + .odm_dest_short_ = query_.arriveBy_ ? get_td_offsets(from_rides_short) + : get_td_offsets(to_rides_short), + .odm_dest_long_ = query_.arriveBy_ ? get_td_offsets(from_rides_long) + : get_td_offsets(to_rides_long)}; + + auto const make_task = [&](n::routing::query q) { + return boost::fibers::packaged_task{ + [&, q = std::move(q)]() mutable { + if (ep::search_state.get() == nullptr) { + ep::search_state.reset(new n::routing::search_state{}); + } + if (ep::raptor_state.get() == nullptr) { + ep::raptor_state.reset(new n::routing::raptor_state{}); + } + + return routing_result{raptor_search( + *tt_, rtt_, *ep::search_state, *ep::raptor_state, std::move(q), + query_.arriveBy_ ? n::direction::kBackward + : n::direction::kForward, + query_.timeout_.has_value() + ? std::optional{*query_.timeout_} + : std::nullopt)}; + }}; + }; + + auto tasks = std::vector>{}; + tasks.emplace_back(make_task(qf.walk_walk())); + if (blacklisted) { + tasks.emplace_back(make_task(qf.walk_short())); + tasks.emplace_back(make_task(qf.walk_long())); + tasks.emplace_back(make_task(qf.short_walk())); + tasks.emplace_back(make_task(qf.long_walk())); + tasks.emplace_back(make_task(qf.short_short())); + tasks.emplace_back(make_task(qf.short_long())); + tasks.emplace_back(make_task(qf.long_short())); + tasks.emplace_back(make_task(qf.long_long())); + } else { + fmt::println("[blacklisting] failed, omitting ODM routing"); + } + print_time(prep_queries_start, "[prepare queries]"); + + // routing + auto const routing_start = std::chrono::steady_clock::now(); + auto futures = utl::to_vec(tasks, [](auto& t) { return t.get_future(); }); + for (auto& task : tasks) { + boost::fibers::fiber(std::move(task)).detach(); + } + for (auto const& f : futures) { + f.wait(); + } + auto const pt_result = futures.front().get(); + print_time(routing_start, "[routing]"); + + // whitelisting + auto whitelist_response = std::optional{}; + auto ioc2 = boost::asio::io_context{}; + if (blacklisted) { + auto const wl_start = std::chrono::steady_clock::now(); + collect_odm_journeys(futures); + extract_rides(); + try { + fmt::println("[whitelisting] request for {} events", p->n_events()); + boost::asio::co_spawn( + ioc2, + [&]() -> boost::asio::awaitable { + auto const prima_msg = co_await http_POST( + boost::urls::url{*r_.config_.odm_ + kWhitelistPath}, + kReqHeaders, p->get_prima_request(*tt_), 10s); + whitelist_response = get_http_body(prima_msg); + }, + boost::asio::detached); + ioc2.run(); + } catch (std::exception const& e) { + fmt::println("[whitelisting] networking failed: {}", e.what()); + whitelist_response = std::nullopt; + } + print_time(wl_start, "[whitelisting]"); + } + + // mixing + auto const mixing_start = std::chrono::steady_clock::now(); + auto const whitelisted = + whitelist_response && p->whitelist_update(*whitelist_response); + if (whitelisted) { + p->adjust_to_whitelisting(); + add_direct(); + fmt::println("[whitelisting] success"); + } else { + p->odm_journeys_.clear(); + fmt::println("[whitelisting] failed, discarding ODM journeys"); + } + fmt::println("[mixing] {} PT journeys and {} ODM journeys", + pt_result.journeys_.size(), p->odm_journeys_.size()); + kOdmMixer.mix(pt_result.journeys_, p->odm_journeys_); + print_time(mixing_start, "[mixing]"); + + return {.from_ = from_place_, + .to_ = to_place_, + .direct_ = std::move(direct_), + .itineraries_ = utl::to_vec( + p->odm_journeys_, + [&, cache = street_routing_cache_t{}](auto&& j) mutable { + return journey_to_response( + r_.w_, r_.l_, r_.pl_, *tt_, *r_.tags_, e_, rtt_, + r_.matches_, r_.shapes_, gbfs_rd_, + query_.pedestrianProfile_ == + api::PedestrianProfileEnum::WHEELCHAIR, + j, start_, dest_, cache, *ep::blocked, + query_.detailedTransfers_); + }), + .previousPageCursor_ = + fmt::format("EARLIER|{}", to_seconds(pt_result.interval_.from_)), + .nextPageCursor_ = + fmt::format("LATER|{}", to_seconds(pt_result.interval_.to_))}; +} + +} // namespace motis::odm \ No newline at end of file diff --git a/src/odm/mixer.cc b/src/odm/mixer.cc new file mode 100644 index 000000000..97061e551 --- /dev/null +++ b/src/odm/mixer.cc @@ -0,0 +1,152 @@ +#include "motis/odm/mixer.h" + +#include "utl/overloaded.h" + +#include "nigiri/special_stations.h" + +#include "motis/odm/odm.h" +#include "motis/transport_mode_ids.h" + +namespace motis::odm { + +namespace n = nigiri; + +std::int32_t tally(std::int32_t const x, + std::vector const& ct) { + auto acc = std::int32_t{0}; + for (auto i = 0U; i < ct.size() && ct[i].threshold_ < x; ++i) { + auto const valid_until = i + 1U == ct.size() + ? std::numeric_limits::max() + : ct[i + 1U].threshold_; + acc += (std::min(x, valid_until) - ct[i].threshold_) * ct[i].cost_; + } + return acc; +} + +std::int32_t mixer::transfer_cost(n::routing::journey const& j) const { + return tally(j.transfers_, transfer_cost_); +} + +std::int32_t distance(n::routing::journey const& a, + n::routing::journey const& b) { + auto const overtakes = [](auto const& x, auto const& y) { + return x.departure_time() > y.departure_time() && + x.arrival_time() < y.arrival_time(); + }; + + return overtakes(a, b) || overtakes(b, a) + ? 0 + : std::min( + std::chrono::abs(a.departure_time() - b.departure_time()), + std::chrono::abs(a.arrival_time() - b.arrival_time())) + .count(); +} + +void mixer::cost_domination( + n::pareto_set const& pt_journeys, + std::vector& odm_journeys) const { + + auto const leg_cost = [&](auto const& leg) { + return std::visit( + utl::overloaded{[](n::routing::journey::run_enter_exit const&) { + return std::int32_t{0}; + }, + [&](n::footpath const& fp) { + return tally(fp.duration().count(), walk_cost_); + }, + [&](n::routing::offset const& o) { + if (o.transport_mode_id_ == kOdmTransportModeId) { + return tally(o.duration().count(), taxi_cost_); + } else if (o.transport_mode_id_ == kWalk) { + return tally(o.duration().count(), walk_cost_); + } + utl::verify( + o.transport_mode_id_ == kOdmTransportModeId || + o.transport_mode_id_ == kWalk, + "unknown transport mode"); + return std::int32_t{0}; + }}, + leg.uses_); + }; + + auto const pt_time = [](auto const& j) { + auto const leg_duration = [](auto const& l) { + return std::visit( + utl::overloaded{ + [](n::routing::journey::run_enter_exit const& ree + [[maybe_unused]]) { return n::duration_t{0}; }, + [](n::footpath const& fp) { return fp.duration(); }, + [](n::routing::offset const& o) { return o.duration(); }}, + l.uses_); + }; + return j.travel_time() - leg_duration(j.legs_.front()) - + ((j.legs_.size() > 1) ? leg_duration(j.legs_.back()) + : n::duration_t{0}); + }; + + auto const cost = [&](auto const& j) { + return (leg_cost(j.legs_.front()) + + (j.legs_.size() > 1 ? leg_cost(j.legs_.back()) : 0) + + pt_time(j).count() + transfer_cost(j)); + }; + + auto const is_dominated = [&](auto const& odm_journey) { + auto const dominates = [&](auto const& pt_journey) { + auto const alpha_term = + alpha_ * + (static_cast(pt_journey.travel_time().count()) / + static_cast(odm_journey.travel_time().count())) * + distance(pt_journey, odm_journey); + return cost(pt_journey) + alpha_term < cost(odm_journey); + }; + + return utl::any_of(pt_journeys, dominates); + }; + + std::erase_if(odm_journeys, is_dominated); +} + +void mixer::productivity_domination( + std::vector& odm_journeys) const { + auto const cost = [&](auto const& j) -> double { + return j.travel_time().count() + transfer_cost(j); + }; + + auto const taxi_time = [](n::routing::journey const& j) -> double { + return (is_odm_leg(j.legs_.front()) + ? std::get(j.legs_.front().uses_) + .duration() + .count() + : 0) + + ((j.legs_.size() > 1 && is_odm_leg(j.legs_.back())) + ? std::get(j.legs_.back().uses_) + .duration() + .count() + : 0); + }; + + auto const is_dominated = [&](auto const& b) { + auto const dominates_b = [&](auto const& a) { + auto const prod_a = cost(b) / taxi_time(a); + auto const prod_b = (cost(a) + beta_ * distance(a, b)) / taxi_time(b); + return prod_a > prod_b; + }; + return utl::any_of(odm_journeys, dominates_b); + }; + + std::erase_if(odm_journeys, is_dominated); +} + +void mixer::mix(n::pareto_set const& pt_journeys, + std::vector& odm_journeys) const { + cost_domination(pt_journeys, odm_journeys); + productivity_domination(odm_journeys); + for (auto const& j : pt_journeys) { + odm_journeys.emplace_back(j); + } + utl::sort(odm_journeys, [](auto const& a, auto const& b) { + return a.departure_time() < b.departure_time(); + }); +} + +} // namespace motis::odm \ No newline at end of file diff --git a/src/odm/odm.cc b/src/odm/odm.cc new file mode 100644 index 000000000..6e155e339 --- /dev/null +++ b/src/odm/odm.cc @@ -0,0 +1,17 @@ +#include "motis/odm/odm.h" + +#include "nigiri/routing/journey.h" + +#include "motis/transport_mode_ids.h" + +namespace motis::odm { + +namespace n = nigiri; + +bool is_odm_leg(nigiri::routing::journey::leg const& l) { + return std::holds_alternative(l.uses_) && + std::get(l.uses_).transport_mode_id_ == + kOdmTransportModeId; +} + +} // namespace motis::odm \ No newline at end of file diff --git a/src/odm/prima.cc b/src/odm/prima.cc new file mode 100644 index 000000000..0918e359c --- /dev/null +++ b/src/odm/prima.cc @@ -0,0 +1,288 @@ +#include "motis/odm/prima.h" + +#include + +#include "boost/json.hpp" + +#include "utl/erase_if.h" +#include "utl/pipes.h" +#include "utl/zip.h" + +#include "nigiri/common/parse_time.h" +#include "nigiri/timetable.h" + +#include "motis/odm/odm.h" + +namespace motis::odm { + +namespace n = nigiri; +namespace json = boost::json; + +static constexpr auto const kInfeasible = + std::numeric_limits::min(); + +void prima::init(api::Place const& from, + api::Place const& to, + api::plan_params const& query) { + from_ = geo::latlng{from.lat_, from.lon_}; + to_ = geo::latlng{to.lat_, to.lon_}; + fixed_ = query.arriveBy_ ? n::event_type::kArr : n::event_type::kDep; + cap_ = { + .wheelchairs_ = static_cast( + query.pedestrianProfile_ == api::PedestrianProfileEnum::WHEELCHAIR + ? 1 + : 0), + .bikes_ = static_cast(query.requireBikeTransport_ ? 1 : 0), + .passengers_ = 1U, + .luggage_ = 0U}; +} + +std::int64_t to_millis(n::unixtime_t const t) { + return std::chrono::duration_cast( + t.time_since_epoch()) + .count(); +} + +n::unixtime_t to_unix(std::int64_t const t) { + return n::unixtime_t{ + std::chrono::duration_cast(std::chrono::milliseconds{t})}; +} + +json::array to_json(std::vector const& v, + n::timetable const& tt, + which_mile const wm) { + auto a = json::array{}; + utl::equal_ranges_linear( + v, + [](n::routing::start const& a, n::routing::start const& b) { + return a.stop_ == b.stop_; + }, + [&](auto&& from_it, auto&& to_it) { + auto const& pos = tt.locations_.coordinates_[from_it->stop_]; + a.emplace_back(json::value{ + {"lat", pos.lat_}, + {"lng", pos.lng_}, + {"times", + utl::all(from_it, to_it) | + utl::transform([&](n::routing::start const& s) { + return wm == which_mile::kFirstMile + ? to_millis(s.time_at_stop_ - kODMTransferBuffer) + : to_millis(s.time_at_stop_ + kODMTransferBuffer); + }) | + utl::emplace_back_to()}}); + }); + return a; +} + +json::array to_json(std::vector const& v, + n::event_type const fixed) { + return utl::all(v) // + | utl::transform([&](direct_ride const& r) { + return to_millis(fixed == n::event_type::kDep ? r.dep_ : r.arr_); + }) // + | utl::emplace_back_to(); +} + +json::value to_json(capacities const& c) { + return {{"wheelchairs", c.wheelchairs_}, + {"bikes", c.bikes_}, + {"passengers", c.passengers_}, + {"luggage", c.luggage_}}; +} + +json::value to_json(prima const& p, n::timetable const& tt) { + return {{"start", {{"lat", p.from_.lat_}, {"lng", p.from_.lng_}}}, + {"target", {{"lat", p.to_.lat_}, {"lng", p.to_.lng_}}}, + {"startBusStops", to_json(p.from_rides_, tt, kFirstMile)}, + {"targetBusStops", to_json(p.to_rides_, tt, kLastMile)}, + {"directTimes", to_json(p.direct_rides_, p.fixed_)}, + {"startFixed", p.fixed_ == n::event_type::kDep}, + {"capacities", to_json(p.cap_)}}; +} + +std::string prima::get_prima_request(n::timetable const& tt) const { + return json::serialize(to_json(*this, tt)); +} + +std::size_t prima::n_events() const { + return from_rides_.size() + to_rides_.size() + direct_rides_.size(); +} + +bool prima::blacklist_update(std::string_view json) { + auto const update_pt_rides = + [](std::vector& rides, + std::vector& prev_rides, + json::array const& update) { + std::swap(rides, prev_rides); + rides.clear(); + auto prev_it = std::begin(prev_rides); + for (auto const& stop : update) { + for (auto const& feasible : stop.as_array()) { + if (feasible.as_bool()) { + rides.emplace_back(*prev_it); + } + ++prev_it; + if (prev_it == end(prev_rides)) { + return; + } + } + } + }; + + auto const update_direct_rides = [](std::vector& rides, + std::vector& prev_rides, + json::array const& update) { + std::swap(rides, prev_rides); + rides.clear(); + for (auto const [prev, feasible] : utl::zip(prev_rides, update)) { + if (feasible.as_bool()) { + rides.emplace_back(prev); + } + } + }; + + try { + auto const o = json::parse(json).as_object(); + update_pt_rides(from_rides_, prev_from_rides_, o.at("start").as_array()); + update_pt_rides(to_rides_, prev_to_rides_, o.at("target").as_array()); + update_direct_rides(direct_rides_, prev_direct_rides_, + o.at("direct").as_array()); + } catch (std::exception const& e) { + std::cout << e.what() << "\nInvalid blacklist response: " << json << "\n"; + return false; + } + return true; +} + +void update_pt_rides(std::vector& rides, + std::vector& prev_rides, + json::array const& update, + which_mile const wm) { + std::swap(rides, prev_rides); + rides.clear(); + auto prev_it = std::begin(prev_rides); + for (auto const& stop : update) { + for (auto const& event : stop.as_array()) { + if (event.is_null()) { + rides.push_back({.time_at_start_ = kInfeasible, + .time_at_stop_ = kInfeasible, + .stop_ = prev_it->stop_}); + } else { + auto const time_at_coord_str = + wm == kFirstMile + ? to_unix(event.as_object().at("pickupTime").as_int64()) + : to_unix(event.as_object().at("dropoffTime").as_int64()); + auto const time_at_stop_str = + wm == kFirstMile + ? to_unix(event.as_object().at("dropoffTime").as_int64()) + : to_unix(event.as_object().at("pickupTime").as_int64()); + rides.push_back({.time_at_start_ = time_at_coord_str, + .time_at_stop_ = time_at_stop_str, + .stop_ = prev_it->stop_}); + } + ++prev_it; + if (prev_it == end(prev_rides)) { + return; + } + } + } +} + +void update_direct_rides(std::vector& rides, + json::array const& update) { + rides.clear(); + for (auto const& ride : update) { + if (!ride.is_null()) { + rides.push_back({to_unix(ride.as_object().at("pickupTime").as_int64()), + to_unix(ride.as_object().at("dropoffTime").as_int64())}); + } + } +} + +bool prima::whitelist_update(std::string_view json) { + try { + auto const o = json::parse(json).as_object(); + update_pt_rides(from_rides_, prev_from_rides_, o.at("start").as_array(), + kFirstMile); + update_pt_rides(to_rides_, prev_to_rides_, o.at("target").as_array(), + kLastMile); + update_direct_rides(direct_rides_, o.at("direct").as_array()); + } catch (std::exception const& e) { + std::cout << e.what() << "\nInvalid whitelist response: " << json << "\n"; + return false; + } + return true; +} + +void prima::adjust_to_whitelisting() { + for (auto const [from_ride, prev_from_ride] : + utl::zip(from_rides_, prev_from_rides_)) { + + auto const uses_prev_from = + [&, prev_from = prev_from_ride /* hack for MacOS - fixed with 16 */]( + nigiri::routing::journey const& j) { + return j.legs_.size() > 1 && + j.legs_.front().dep_time_ == prev_from.time_at_start_ && + j.legs_.front().arr_time_ == prev_from.time_at_stop_ && + j.legs_.front().to_ == prev_from.stop_ && + is_odm_leg(j.legs_.front()); + }; + + if (from_ride.time_at_start_ == kInfeasible) { + utl::erase_if(odm_journeys_, uses_prev_from); + } else { + for (auto& j : odm_journeys_) { + if (uses_prev_from(j)) { + auto const l = begin(j.legs_); + l->dep_time_ = from_ride.time_at_start_; + l->arr_time_ = from_ride.time_at_stop_; + std::get(l->uses_).duration_ = + l->arr_time_ - l->dep_time_; + j.start_time_ = l->dep_time_; + // fill gap (transfer/waiting) with footpath + j.legs_.emplace( + std::next(l), + n::routing::journey::leg{ + n::direction::kForward, l->to_, l->to_, l->arr_time_, + std::next(l)->dep_time_, + n::footpath{l->to_, std::next(l)->dep_time_ - l->arr_time_}}); + } + } + } + } + + for (auto const [to_ride, prev_to_ride] : + utl::zip(to_rides_, prev_to_rides_)) { + + auto const uses_prev_to = [&, prev = prev_to_ride](auto const& j) { + return j.legs_.size() > 1 && + j.legs_.back().dep_time_ == prev.time_at_stop_ && + j.legs_.back().arr_time_ == prev.time_at_start_ && + j.legs_.back().from_ == prev.stop_ && is_odm_leg(j.legs_.back()); + }; + + if (to_ride.time_at_start_ == kInfeasible) { + utl::erase_if(odm_journeys_, uses_prev_to); + } else { + for (auto& j : odm_journeys_) { + if (uses_prev_to(j)) { + auto const l = std::prev(end(j.legs_)); + l->dep_time_ = to_ride.time_at_stop_; + l->arr_time_ = to_ride.time_at_start_; + std::get(l->uses_).duration_ = + l->arr_time_ - l->dep_time_; + j.dest_time_ = l->arr_time_; + // fill gap (transfer/waiting) with footpath + j.legs_.emplace( + l, n::routing::journey::leg{ + n::direction::kForward, l->from_, l->from_, + std::prev(l)->arr_time_, l->dep_time_, + n::footpath{l->from_, + l->dep_time_ - std::prev(l)->arr_time_}}); + } + } + } + } +} + +} // namespace motis::odm \ No newline at end of file diff --git a/src/odm/query_factory.cc b/src/odm/query_factory.cc new file mode 100644 index 000000000..194383a45 --- /dev/null +++ b/src/odm/query_factory.cc @@ -0,0 +1,61 @@ +#include "motis/odm/query_factory.h" + +#include "motis/endpoints/routing.h" + +namespace motis::odm { + +namespace n = nigiri; + +n::routing::query query_factory::make( + std::vector const& start, + n::hash_map> const& + td_start, + std::vector const& dest, + n::hash_map> const& + td_dest) const { + auto q = base_query_; + q.start_ = start; + q.destination_ = dest; + q.td_start_ = td_start; + q.td_dest_ = td_dest; + motis::ep::remove_slower_than_fastest_direct(q); + return q; +} + +n::routing::query query_factory::walk_walk() const { + return make(start_walk_, td_start_walk_, dest_walk_, td_dest_walk_); +} + +n::routing::query query_factory::walk_short() const { + return make(start_walk_, td_start_walk_, {}, odm_dest_short_); +} + +n::routing::query query_factory::walk_long() const { + return make(start_walk_, td_start_walk_, {}, odm_dest_long_); +} + +n::routing::query query_factory::short_walk() const { + return make({}, odm_start_short_, dest_walk_, td_dest_walk_); +} + +n::routing::query query_factory::long_walk() const { + return make({}, odm_start_long_, dest_walk_, td_dest_walk_); +} + +n::routing::query query_factory::short_short() const { + return make({}, odm_start_short_, {}, odm_dest_short_); +} + +n::routing::query query_factory::short_long() const { + return make({}, odm_start_short_, {}, odm_dest_long_); +} + +n::routing::query query_factory::long_short() const { + return make({}, odm_start_long_, {}, odm_dest_short_); +} + +n::routing::query query_factory::long_long() const { + return make({}, odm_start_long_, {}, odm_dest_long_); +} + +} // namespace motis::odm \ No newline at end of file diff --git a/src/scheduler/scheduler_algo.cc b/src/scheduler/scheduler_algo.cc new file mode 100644 index 000000000..2e5b1b464 --- /dev/null +++ b/src/scheduler/scheduler_algo.cc @@ -0,0 +1,113 @@ +#include "motis/scheduler/scheduler_algo.h" + +#include "boost/context/detail/prefetch.hpp" +#include "boost/fiber/context.hpp" +#include "boost/fiber/detail/context_spinlock_queue.hpp" +#include "boost/fiber/properties.hpp" +#include "boost/fiber/scheduler.hpp" +#include "boost/fiber/type.hpp" + +namespace bf = boost::fibers; + +namespace motis { + +fiber_props::fiber_props(bf::context* ctx) : fiber_properties{ctx} {} +fiber_props::~fiber_props() = default; + +std::atomic work_stealing::counter_{0}; +std::vector > work_stealing::schedulers_{}; + +void work_stealing::init_( + std::uint32_t thread_count, + std::vector >& schedulers) { + std::vector >{thread_count, nullptr}.swap( + schedulers); +} + +work_stealing::work_stealing(std::uint32_t thread_count, bool suspend) + : id_{counter_++}, thread_count_{thread_count}, suspend_{suspend} { + static boost::fibers::detail::thread_barrier b{thread_count}; + // initialize the array of schedulers + static std::once_flag flag; + std::call_once(flag, &work_stealing::init_, thread_count_, + std::ref(schedulers_)); + // register pointer of this scheduler + schedulers_[id_] = this; + b.wait(); +} + +void work_stealing::awakened(bf::context* ctx, fiber_props& props) noexcept { + if (!ctx->is_context(bf::type::pinned_context)) { + ctx->detach(); + } + if (props.type_ == fiber_props::type::kHighPrio) { + props.type_ = fiber_props::type::kLowPrio; + high_prio_rqueue_.push(ctx); + } else { + rqueue_.push(ctx); + } +} + +bf::context* work_stealing::pick_next() noexcept { + bf::context* victim = nullptr; + if (victim = high_prio_rqueue_.pop(); nullptr != victim) { + boost::context::detail::prefetch_range(victim, sizeof(bf::context)); + if (!victim->is_context(bf::type::pinned_context)) { + bf::context::active()->attach(victim); + } + } else if (victim = rqueue_.pop(); nullptr != victim) { + boost::context::detail::prefetch_range(victim, sizeof(bf::context)); + if (!victim->is_context(bf::type::pinned_context)) { + bf::context::active()->attach(victim); + } + } else if (thread_count_ > 1U) { + std::uint32_t id = 0; + std::size_t count = 0, size = schedulers_.size(); + static thread_local std::minstd_rand generator{std::random_device{}()}; + std::uniform_int_distribution distribution{ + 0, static_cast(thread_count_ - 1)}; + do { + do { + ++count; + // random selection of one logical cpu + // that belongs to the local NUMA node + id = distribution(generator); + // prevent stealing from own scheduler + } while (id == id_); + // steal context from other scheduler + victim = schedulers_[id]->steal(); + } while (nullptr == victim && count < size); + if (nullptr != victim) { + boost::context::detail::prefetch_range(victim, sizeof(bf::context)); + BOOST_ASSERT(!victim->is_context(bf::type::pinned_context)); + bf::context::active()->attach(victim); + } + } + return victim; +} + +void work_stealing::suspend_until( + std::chrono::steady_clock::time_point const& time_point) noexcept { + if (suspend_) { + if ((std::chrono::steady_clock::time_point::max)() == time_point) { + std::unique_lock lk{mtx_}; + cnd_.wait(lk, [this]() { return flag_; }); + flag_ = false; + } else { + std::unique_lock lk{mtx_}; + cnd_.wait_until(lk, time_point, [this]() { return flag_; }); + flag_ = false; + } + } +} + +void work_stealing::notify() noexcept { + if (suspend_) { + std::unique_lock lk{mtx_}; + flag_ = true; + lk.unlock(); + cnd_.notify_all(); + } +} + +} // namespace motis \ No newline at end of file diff --git a/src/server.cc b/src/server.cc index 507646bde..644e1b5fa 100644 --- a/src/server.cc +++ b/src/server.cc @@ -32,6 +32,8 @@ #include "motis/endpoints/update_elevator.h" #include "motis/gbfs/update.h" #include "motis/rt_update.h" +#include "motis/scheduler/runner.h" +#include "motis/scheduler/scheduler_algo.h" namespace fs = std::filesystem; namespace asio = boost::asio; @@ -53,10 +55,12 @@ void POST(auto&& r, std::string target, From& from) { } int server(data d, config const& c) { + auto const server_config = c.server_.value_or(config::server{}); + auto ioc = asio::io_context{}; - auto workers = asio::io_context{}; auto s = net::web_server{ioc}; - auto qr = net::query_router{net::asio_exec(ioc, workers)}; + auto r = runner{server_config.n_threads_, 1024U}; + auto qr = net::query_router{net::fiber_exec{ioc, r.ch_}}; POST(qr, "/api/matches", d); POST(qr, "/api/elevators", d); @@ -81,7 +85,6 @@ int server(data d, config const& c) { qr.route("GET", "/tiles/", ep::tiles{*d.tiles_}); } - auto const server_config = c.server_.value_or(config::server{}); qr.serve_files(server_config.web_folder_); qr.enable_cors(); s.set_timeout(std::chrono::minutes{5}); @@ -101,7 +104,7 @@ int server(data d, config const& c) { if (c.requires_rt_timetable_updates()) { rt_update_ioc = std::make_unique(); rt_update_thread = std::make_unique([&]() { - utl::set_current_thread_name("rt update"); + utl::set_current_thread_name("motis rt update"); run_rt_update(*rt_update_ioc, c, *d.tt_, *d.tags_, d.rt_); rt_update_ioc->run(); }); @@ -112,22 +115,21 @@ int server(data d, config const& c) { if (d.w_ && d.l_ && c.has_gbfs_feeds()) { gbfs_update_ioc = std::make_unique(); gbfs_update_thread = std::make_unique([&]() { - utl::set_current_thread_name("gbfs update"); + utl::set_current_thread_name("motis gbfs update"); gbfs::run_gbfs_update(*gbfs_update_ioc, c, *d.w_, *d.l_, d.gbfs_); gbfs_update_ioc->run(); }); } - auto const work_guard = asio::make_work_guard(workers); - auto threads = std::vector( - static_cast(std::max(1U, server_config.n_threads_))); + auto threads = std::vector{server_config.n_threads_}; for (auto [i, t] : utl::enumerate(threads)) { - t = std::thread(net::run(workers)); - utl::set_thread_name(t, fmt::format("worker {}", i)); + t = std::thread{r.run_fn()}; + utl::set_thread_name(t, fmt::format("motis worker {}", i)); } auto const stop = net::stop_handler(ioc, [&]() { fmt::println("shutdown"); + r.ch_.close(); s.stop(); ioc.stop(); @@ -143,7 +145,6 @@ int server(data d, config const& c) { server_config.host_, server_config.port_, server_config.port_); net::run(ioc)(); - workers.stop(); for (auto& t : threads) { t.join(); } diff --git a/src/street_routing.cc b/src/street_routing.cc index 0ef43670c..91561033e 100644 --- a/src/street_routing.cc +++ b/src/street_routing.cc @@ -344,7 +344,9 @@ api::Itinerary route(osr::ways const& w, } auto& leg = itinerary.legs_.emplace_back(api::Leg{ - .mode_ = is_rental ? api::ModeEnum::RENTAL : to_mode(lb->mode_), + .mode_ = is_rental ? api::ModeEnum::RENTAL + : mode == api::ModeEnum::ODM ? mode + : to_mode(lb->mode_), .from_ = pred_place, .to_ = next_place, .duration_ = std::chrono::duration_cast( diff --git a/test/odm_test.cc b/test/odm_test.cc new file mode 100644 index 000000000..89663ba04 --- /dev/null +++ b/test/odm_test.cc @@ -0,0 +1,335 @@ +#include "gtest/gtest.h" + +#include "nigiri/loader/dir.h" +#include "nigiri/loader/gtfs/load_timetable.h" +#include "nigiri/loader/init_finish.h" +#include "nigiri/common/parse_time.h" +#include "nigiri/routing/journey.h" +#include "nigiri/routing/pareto_set.h" +#include "nigiri/special_stations.h" + +#include "motis/odm/mixer.h" +#include "motis/odm/odm.h" +#include "motis/odm/prima.h" +#include "motis/transport_mode_ids.h" + +namespace n = nigiri; +using namespace motis::odm; +using namespace std::chrono_literals; +using namespace date; + +static auto const kOdmMixer = mixer{.alpha_ = 1.5, + .beta_ = 0.1, + .walk_cost_ = {{0, 1}, {15, 10}}, + .taxi_cost_ = {{0, 35}, {1, 12}}, + .transfer_cost_ = {{0, 10}}}; + +TEST(odm, tally) { + auto const ct = std::vector{{0, 30}, {1, 1}, {10, 2}}; + EXPECT_EQ(0, tally(0, ct)); + EXPECT_EQ(30, tally(1, ct)); + EXPECT_EQ(43, tally(12, ct)); +} + +n::routing::journey direct_taxi(n::unixtime_t const dep, + n::unixtime_t const arr) { + return {.legs_ = {n::routing::journey::leg{ + n::direction::kForward, + get_special_station(n::special_station::kStart), + get_special_station(n::special_station::kEnd), dep, arr, + n::routing::offset{get_special_station(n::special_station::kEnd), + arr - dep, motis::kOdmTransportModeId}}}, + .start_time_ = dep, + .dest_time_ = arr, + .dest_ = get_special_station(n::special_station::kEnd), + .transfers_ = 0U}; +} + +TEST(odm, pt_taxi_no_direct) { + auto pt = n::routing::journey{ + .legs_ = {n::routing::journey::leg{ + n::direction::kForward, + get_special_station(n::special_station::kStart), + n::location_idx_t{23U}, n::unixtime_t{10h + 17min}, + n::unixtime_t{10h + 47min}, + n::routing::offset{n::location_idx_t{23U}, 30min, kWalk}}}, + .start_time_ = n::unixtime_t{10h + 17min}, + .dest_time_ = n::unixtime_t{11h}, + .dest_ = get_special_station(n::special_station::kEnd), + .transfers_ = 0U}; + + auto pt_journeys = n::pareto_set{}; + pt_journeys.add(n::routing::journey{pt}); + + auto pt_taxi = n::routing::journey{ + .legs_ = {n::routing::journey::leg{ + n::direction::kForward, + get_special_station(n::special_station::kStart), + n::location_idx_t{23U}, n::unixtime_t{10h + 43min}, + n::unixtime_t{10h + 47min}, + n::routing::offset{n::location_idx_t{23U}, 4min, + motis::kOdmTransportModeId}}}, + .start_time_ = n::unixtime_t{10h + 43min}, + .dest_time_ = n::unixtime_t{11h}, + .dest_ = get_special_station(n::special_station::kEnd), + .transfers_ = 0U}; + + auto odm_journeys = std::vector{ + pt_taxi, + direct_taxi(n::unixtime_t{10h + 17min}, n::unixtime_t{10h + 27min}), + direct_taxi(n::unixtime_t{10h + 43min}, n::unixtime_t{10h + 53min}), + direct_taxi(n::unixtime_t{10h + 50min}, n::unixtime_t{11h + 00min}), + direct_taxi(n::unixtime_t{10h + 00min}, n::unixtime_t{10h + 10min}), + direct_taxi(n::unixtime_t{11h + 00min}, n::unixtime_t{10h + 10min})}; + + kOdmMixer.mix(pt_journeys, odm_journeys); + + ASSERT_EQ(odm_journeys.size(), 2U); + EXPECT_NE(utl::find(odm_journeys, pt), end(odm_journeys)); + EXPECT_NE(utl::find(odm_journeys, pt_taxi), end(odm_journeys)); +} + +TEST(odm, taxi_saves_transfers) { + auto pt = n::routing::journey{ + .legs_ = {n::routing::journey::leg{ + n::direction::kForward, + get_special_station(n::special_station::kStart), + n::location_idx_t{23U}, n::unixtime_t{10h}, + n::unixtime_t{10h + 5min}, + n::routing::offset{n::location_idx_t{23U}, 5min, kWalk}}, + n::routing::journey::leg{ + n::direction::kForward, n::location_idx_t{42U}, + get_special_station(n::special_station::kEnd), + n::unixtime_t{10h + 55min}, n::unixtime_t{11h}, + n::routing::offset{n::location_idx_t{42U}, 5min, kWalk}}}, + .start_time_ = n::unixtime_t{10h}, + .dest_time_ = n::unixtime_t{11h}, + .dest_ = get_special_station(n::special_station::kEnd), + .transfers_ = 4U}; + + auto pt_journeys = n::pareto_set{}; + pt_journeys.add(n::routing::journey{pt}); + + auto odm_journeys = std::vector{ + {.legs_ = {{n::direction::kForward, + get_special_station(n::special_station::kStart), + n::location_idx_t{24U}, n::unixtime_t{10h + 14min}, + n::unixtime_t{10h + 20min}, + n::routing::offset{n::location_idx_t{24U}, 6min, + motis::kOdmTransportModeId}}, + {n::direction::kForward, n::location_idx_t{42U}, + get_special_station(n::special_station::kEnd), + n::unixtime_t{10h + 55min}, n::unixtime_t{11h}, + n::routing::offset{n::location_idx_t{42U}, 5min, kWalk}}}, + .start_time_ = n::unixtime_t{10h + 14min}, + .dest_time_ = n::unixtime_t{11h}, + .dest_ = get_special_station(n::special_station::kEnd), + .transfers_ = 2U}, + {.legs_ = {{n::direction::kForward, + get_special_station(n::special_station::kStart), + n::location_idx_t{25U}, n::unixtime_t{10h + 20min}, + n::unixtime_t{10h + 30min}, + n::routing::offset{n::location_idx_t{25U}, 10min, + motis::kOdmTransportModeId}}, + {n::direction::kForward, n::location_idx_t{42U}, + get_special_station(n::special_station::kEnd), + n::unixtime_t{10h + 55min}, n::unixtime_t{11h}, + n::routing::offset{n::location_idx_t{42U}, 5min, kWalk}}}, + .start_time_ = n::unixtime_t{10h + 20min}, + .dest_time_ = n::unixtime_t{11h}, + .dest_ = get_special_station(n::special_station::kEnd), + .transfers_ = 1U}, + {.legs_ = {{n::direction::kForward, + get_special_station(n::special_station::kStart), + n::location_idx_t{26U}, n::unixtime_t{10h + 30min}, + n::unixtime_t{10h + 45min}, + n::routing::offset{n::location_idx_t{26U}, 15min, + motis::kOdmTransportModeId}}, + {n::direction::kForward, n::location_idx_t{42U}, + get_special_station(n::special_station::kEnd), + n::unixtime_t{10h + 55min}, n::unixtime_t{11h}, + n::routing::offset{n::location_idx_t{42U}, 5min, kWalk}}}, + .start_time_ = n::unixtime_t{10h + 30min}, + .dest_time_ = n::unixtime_t{11h}, + .dest_ = get_special_station(n::special_station::kEnd), + .transfers_ = 0U}}; + + kOdmMixer.mix(pt_journeys, odm_journeys); + + ASSERT_EQ(odm_journeys.size(), 1U); + EXPECT_NE(utl::find(odm_journeys, pt), end(odm_journeys)); +} + +n::loader::mem_dir tt_files() { + return n::loader::mem_dir::read(R"__( +"( +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +A,A,A,0.1,0.1,,,, +B,B,B,0.2,0.2,,,, +C,C,C,0.3,0.3,,,, +D,D,D,0.4,0.4,,,, +)__"); +} + +constexpr auto const kExpectedInitial = + R"({"start":{"lat":0E0,"lng":0E0},"target":{"lat":1E0,"lng":1E0},"startBusStops":[{"lat":1E-1,"lng":1E-1,"times":[39300000,42900000]},{"lat":2E-1,"lng":2E-1,"times":[42900000]}],"targetBusStops":[{"lat":3.0000000000000004E-1,"lng":3.0000000000000004E-1,"times":[47100000]},{"lat":4E-1,"lng":4E-1,"times":[50700000]}],"directTimes":[36000000,39600000],"startFixed":true,"capacities":{"wheelchairs":1,"bikes":0,"passengers":1,"luggage":0}})"; + +constexpr auto const invalid_response = R"({"message":"Internal Error"})"; + +constexpr auto const blacklisting_response = R"( +{ + "start": [[true,false],[true]], + "target": [[true],[false]], + "direct": [false,true] +} +)"; + +constexpr auto const blacklisted = + R"({"start":{"lat":0E0,"lng":0E0},"target":{"lat":1E0,"lng":1E0},"startBusStops":[{"lat":1E-1,"lng":1E-1,"times":[39300000]},{"lat":2E-1,"lng":2E-1,"times":[42900000]}],"targetBusStops":[{"lat":3.0000000000000004E-1,"lng":3.0000000000000004E-1,"times":[47100000]}],"directTimes":[39600000],"startFixed":true,"capacities":{"wheelchairs":1,"bikes":0,"passengers":1,"luggage":0}})"; + +// 1970-01-01T09:57:00Z, 1970-01-01T10:55:00Z +// 1970-01-01T14:07:00Z, 1970-01-01T14:46:00Z +// 1970-01-01T11:30:00Z, 1970-01-01T12:30:00Z +constexpr auto const whitelisting_response = R"( +{ + "start": [[{"pickupTime": 35820000, "dropoffTime": 39300000}],[null]], + "target": [[{"pickupTime": 50820000, "dropoffTime": 53160000}]], + "direct": [{"pickupTime": 41400000,"dropoffTime": 45000000}] +} +)"; + +constexpr auto const adjusted_to_whitelisting = R"( +[1970-01-01 09:57, 1970-01-01 12:00] +TRANSFERS: 0 + FROM: (START, START) [1970-01-01 09:57] + TO: (END, END) [1970-01-01 12:00] +leg 0: (START, START) [1970-01-01 09:57] -> (A, A) [1970-01-01 10:55] + MUMO (id=7, duration=58) +leg 1: (A, A) [1970-01-01 10:55] -> (A, A) [1970-01-01 11:00] + FOOTPATH (duration=5) +leg 2: (A, A) [1970-01-01 11:00] -> (END, END) [1970-01-01 12:00] + MUMO (id=0, duration=60) + +[1970-01-01 09:57, 1970-01-01 14:46] +TRANSFERS: 0 + FROM: (START, START) [1970-01-01 09:57] + TO: (END, END) [1970-01-01 14:46] +leg 0: (START, START) [1970-01-01 09:57] -> (A, A) [1970-01-01 10:55] + MUMO (id=7, duration=58) +leg 1: (A, A) [1970-01-01 10:55] -> (A, A) [1970-01-01 11:00] + FOOTPATH (duration=5) +leg 2: (A, A) [1970-01-01 11:00] -> (C, C) [1970-01-01 13:00] + FOOTPATH (duration=120) +leg 3: (C, C) [1970-01-01 13:00] -> (C, C) [1970-01-01 14:07] + FOOTPATH (duration=67) +leg 4: (C, C) [1970-01-01 14:07] -> (END, END) [1970-01-01 14:46] + MUMO (id=7, duration=39) + +)"; + +TEST(odm, prima_update) { + using namespace nigiri; + using namespace nigiri::loader; + using namespace nigiri::loader::gtfs; + + timetable tt; + tt.date_range_ = {date::sys_days{2017_y / January / 1}, + date::sys_days{2017_y / January / 2}}; + register_special_stations(tt); + auto const src = source_idx_t{0}; + gtfs::load_timetable({}, src, tt_files(), tt); + finalize(tt); + + auto const get_loc_idx = [&](auto&& s) { + return tt.locations_.location_id_to_idx_.at({.id_ = s, .src_ = src}); + }; + + auto p = prima{ + .from_ = {0.0, 0.0}, + .to_ = {1.0, 1.0}, + .fixed_ = n::event_type::kDep, + .cap_ = {.wheelchairs_ = 1, .bikes_ = 0, .passengers_ = 1, .luggage_ = 0}, + .from_rides_ = {{.time_at_start_ = n::unixtime_t{10h}, + .time_at_stop_ = n::unixtime_t{11h}, + .stop_ = get_loc_idx("A")}, + {.time_at_start_ = n::unixtime_t{11h}, + .time_at_stop_ = n::unixtime_t{12h}, + .stop_ = get_loc_idx("A")}, + {.time_at_start_ = n::unixtime_t{11h}, + .time_at_stop_ = n::unixtime_t{12h}, + .stop_ = get_loc_idx("B")}}, + .to_rides_ = {{.time_at_start_ = n::unixtime_t{14h}, + .time_at_stop_ = n::unixtime_t{13h}, + .stop_ = get_loc_idx("C")}, + {.time_at_start_ = n::unixtime_t{15h}, + .time_at_stop_ = n::unixtime_t{14h}, + .stop_ = get_loc_idx("D")}}, + .direct_rides_ = { + {.dep_ = n::unixtime_t{10h}, .arr_ = n::unixtime_t{11h}}, + {.dep_ = n::unixtime_t{11h}, .arr_ = n::unixtime_t{12h}}}}; + + EXPECT_EQ(kExpectedInitial, p.get_prima_request(tt)); + EXPECT_FALSE(p.blacklist_update(invalid_response)); + EXPECT_TRUE(p.blacklist_update(blacklisting_response)); + EXPECT_EQ(blacklisted, p.get_prima_request(tt)); + EXPECT_FALSE(p.whitelist_update(invalid_response)); + EXPECT_TRUE(p.whitelist_update(whitelisting_response)); + + p.odm_journeys_.push_back( + {.legs_ = {{n::direction::kForward, + get_special_station(special_station::kStart), + get_loc_idx("A"), n::unixtime_t{10h}, n::unixtime_t{11h}, + n::routing::offset{get_loc_idx("A"), 1h, + motis::kOdmTransportModeId}}, + {n::direction::kForward, get_loc_idx("A"), + get_special_station(special_station::kEnd), + n::unixtime_t{11h}, n::unixtime_t{12h}, + n::routing::offset{get_loc_idx("A"), 1h, kWalk}}}, + .start_time_ = n::unixtime_t{10h}, + .dest_time_ = n::unixtime_t{12h}, + .dest_ = get_special_station(special_station::kEnd)}); + + p.odm_journeys_.push_back( + {.legs_ = {{n::direction::kForward, + get_special_station(special_station::kStart), + get_loc_idx("B"), n::unixtime_t{11h}, n::unixtime_t{12h}, + n::routing::offset{get_loc_idx("B"), 1h, + motis::kOdmTransportModeId}}, + {n::direction::kForward, get_loc_idx("B"), + get_special_station(special_station::kEnd), + n::unixtime_t{12h}, n::unixtime_t{13h}, + n::routing::offset{get_loc_idx("B"), 1h, kWalk}}}, + .start_time_ = n::unixtime_t{11h}, + .dest_time_ = n::unixtime_t{13h}, + .dest_ = get_special_station(special_station::kEnd)}); + + p.odm_journeys_.push_back( + {.legs_ = {{n::direction::kForward, + get_special_station(special_station::kStart), + get_loc_idx("A"), n::unixtime_t{10h}, n::unixtime_t{11h}, + n::routing::offset{get_loc_idx("A"), 1h, + motis::kOdmTransportModeId}}, + {n::direction::kForward, get_loc_idx("A"), get_loc_idx("C"), + n::unixtime_t{11h}, n::unixtime_t{13h}, + n::footpath{get_loc_idx("C"), 2h}}, + {n::direction::kForward, get_loc_idx("C"), + get_special_station(special_station::kEnd), + n::unixtime_t{13h}, n::unixtime_t{14h}, + n::routing::offset{get_loc_idx("C"), 1h, + motis::kOdmTransportModeId}}}, + .start_time_ = n::unixtime_t{10h}, + .dest_time_ = n::unixtime_t{14h}, + .dest_ = get_special_station(special_station::kEnd)}); + + p.adjust_to_whitelisting(); + + auto ss = std::stringstream{}; + ss << "\n"; + for (auto const& j : p.odm_journeys_) { + j.print(ss, tt, nullptr); + ss << "\n"; + } + + EXPECT_EQ(adjusted_to_whitelisting, ss.str()); +} diff --git a/ui/src/app.html b/ui/src/app.html index d09e778db..703c90a71 100644 --- a/ui/src/app.html +++ b/ui/src/app.html @@ -101,6 +101,23 @@ d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z" /> + + + + + + + + + diff --git a/ui/src/lib/getModeName.ts b/ui/src/lib/getModeName.ts index 44b5dcbb9..f45a2aeb5 100644 --- a/ui/src/lib/getModeName.ts +++ b/ui/src/lib/getModeName.ts @@ -29,6 +29,8 @@ export const getModeName = (l: Leg) => { case 'CAR': case 'CAR_PARKING': return t.car; + case 'ODM': + return t.taxi; default: return `${l.mode}`; } diff --git a/ui/src/lib/i18n/de.ts b/ui/src/lib/i18n/de.ts index 12e4f01e5..8d58644c2 100644 --- a/ui/src/lib/i18n/de.ts +++ b/ui/src/lib/i18n/de.ts @@ -9,6 +9,7 @@ const translations: Translations = { scooterStanding: 'Stehroller', scooterSeated: 'Sitzroller', car: 'Auto', + taxi: 'Taxi', moped: 'Moped', from: 'Von', to: 'Nach', diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index 9d50650ea..9e21aa72a 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -9,6 +9,7 @@ const translations: Translations = { scooterStanding: 'Standing kick scooter', scooterSeated: 'Seated kick scooter', car: 'Car', + taxi: 'Taxi', moped: 'Moped', from: 'From', to: 'To', diff --git a/ui/src/lib/i18n/fr.ts b/ui/src/lib/i18n/fr.ts index 27124c09d..b40660b29 100644 --- a/ui/src/lib/i18n/fr.ts +++ b/ui/src/lib/i18n/fr.ts @@ -8,6 +8,7 @@ const translations: Translations = { scooterStanding: 'Trottinette', scooterSeated: 'Trottinette avec siège', car: 'Voiture', + taxi: 'Taxi', moped: 'Mobylette', from: 'De', to: 'À', diff --git a/ui/src/lib/i18n/pl.ts b/ui/src/lib/i18n/pl.ts index fc52016a8..98a5d2492 100644 --- a/ui/src/lib/i18n/pl.ts +++ b/ui/src/lib/i18n/pl.ts @@ -9,6 +9,7 @@ const translations: Translations = { scooterStanding: 'Hulajnoga stojąca', scooterSeated: 'Hulajnoga z siedziskiem', car: 'Samochód', + taxi: 'Taksówka', moped: 'Skuter', from: 'Z', to: 'Do', diff --git a/ui/src/lib/i18n/translation.ts b/ui/src/lib/i18n/translation.ts index d2e220249..945965864 100644 --- a/ui/src/lib/i18n/translation.ts +++ b/ui/src/lib/i18n/translation.ts @@ -13,6 +13,7 @@ export type Translations = { scooterStanding: string; scooterSeated: string; car: string; + taxi: string; moped: string; from: string; to: string; diff --git a/ui/src/lib/modeStyle.ts b/ui/src/lib/modeStyle.ts index e2807211b..e68da12a2 100644 --- a/ui/src/lib/modeStyle.ts +++ b/ui/src/lib/modeStyle.ts @@ -47,6 +47,9 @@ export const getModeStyle = (l: LegLike): [string, string, string] => { case 'CAR_PARKING': return ['car', '#333333', 'white']; + case 'ODM': + return ['taxi', '#fdb813', 'white']; + case 'TRANSIT': case 'BUS': return ['bus', '#ff9800', 'white']; diff --git a/ui/src/lib/openapi/schemas.gen.ts b/ui/src/lib/openapi/schemas.gen.ts index b5991e02d..375209779 100644 --- a/ui/src/lib/openapi/schemas.gen.ts +++ b/ui/src/lib/openapi/schemas.gen.ts @@ -127,6 +127,7 @@ export const ModeSchema = { - \`RENTAL\` Experimental. Expect unannounced breaking changes (without version bumps). - \`CAR\` - \`CAR_PARKING\` + - \`ODM\` # Transit modes @@ -146,7 +147,7 @@ export const ModeSchema = { - \`REGIONAL_RAIL\`: regional train `, type: 'string', - enum: ['WALK', 'BIKE', 'RENTAL', 'CAR', 'CAR_PARKING', 'TRANSIT', 'TRAM', 'SUBWAY', 'FERRY', 'AIRPLANE', 'METRO', 'BUS', 'COACH', 'RAIL', 'HIGHSPEED_RAIL', 'LONG_DISTANCE', 'NIGHT_RAIL', 'REGIONAL_FAST_RAIL', 'REGIONAL_RAIL', 'OTHER'] + enum: ['WALK', 'BIKE', 'RENTAL', 'CAR', 'CAR_PARKING', 'ODM', 'TRANSIT', 'TRAM', 'SUBWAY', 'FERRY', 'AIRPLANE', 'METRO', 'BUS', 'COACH', 'RAIL', 'HIGHSPEED_RAIL', 'LONG_DISTANCE', 'NIGHT_RAIL', 'REGIONAL_FAST_RAIL', 'REGIONAL_RAIL', 'OTHER'] } as const; export const VertexTypeSchema = { @@ -476,6 +477,50 @@ export const RentalSchema = { } } as const; +export const ODMTypeSchema = { + type: 'string', + enum: ['TAXI', 'RIDE_SHARING'] +} as const; + +export const ODMSchema = { + description: 'Vehicle with driver, e.g., taxi', + type: 'object', + required: ['systemId'], + properties: { + systemId: { + type: 'string', + description: 'ODM system ID' + }, + systemName: { + type: 'string', + description: 'ODM system name' + }, + url: { + type: 'string', + description: 'URL of the ODM system' + }, + companyName: { + type: 'string', + description: 'Name of company that offers the service' + }, + odmUriAndroid: { + type: 'string', + description: 'ODM URI for Android (deep link to the specific station or vehicle)' + }, + odmUriIOS: { + type: 'string', + description: 'ODM URI for iOS (deep link to the specific station or vehicle)' + }, + odmUriWeb: { + type: 'string', + description: 'ODM URI for web (deep link to the specific station or vehicle)' + }, + odmType: { + '$ref': '#/components/schemas/ODMType' + } + } +} as const; + export const LegSchema = { type: 'object', required: ['mode', 'startTime', 'endTime', 'scheduledStartTime', 'scheduledEndTime', 'realTime', 'duration', 'from', 'to', 'legGeometry'], @@ -595,6 +640,9 @@ used for walking, biking and driving. }, rental: { '$ref': '#/components/schemas/Rental' + }, + odm: { + '$ref': '#/components/schemas/ODM' } } } as const; diff --git a/ui/src/lib/openapi/types.gen.ts b/ui/src/lib/openapi/types.gen.ts index 5e501da8a..94715f1da 100644 --- a/ui/src/lib/openapi/types.gen.ts +++ b/ui/src/lib/openapi/types.gen.ts @@ -116,6 +116,7 @@ export type PedestrianProfile = 'FOOT' | 'WHEELCHAIR'; * - `RENTAL` Experimental. Expect unannounced breaking changes (without version bumps). * - `CAR` * - `CAR_PARKING` + * - `ODM` * * # Transit modes * @@ -135,7 +136,7 @@ export type PedestrianProfile = 'FOOT' | 'WHEELCHAIR'; * - `REGIONAL_RAIL`: regional train * */ -export type Mode = 'WALK' | 'BIKE' | 'RENTAL' | 'CAR' | 'CAR_PARKING' | 'TRANSIT' | 'TRAM' | 'SUBWAY' | 'FERRY' | 'AIRPLANE' | 'METRO' | 'BUS' | 'COACH' | 'RAIL' | 'HIGHSPEED_RAIL' | 'LONG_DISTANCE' | 'NIGHT_RAIL' | 'REGIONAL_FAST_RAIL' | 'REGIONAL_RAIL' | 'OTHER'; +export type Mode = 'WALK' | 'BIKE' | 'RENTAL' | 'CAR' | 'CAR_PARKING' | 'ODM' | 'TRANSIT' | 'TRAM' | 'SUBWAY' | 'FERRY' | 'AIRPLANE' | 'METRO' | 'BUS' | 'COACH' | 'RAIL' | 'HIGHSPEED_RAIL' | 'LONG_DISTANCE' | 'NIGHT_RAIL' | 'REGIONAL_FAST_RAIL' | 'REGIONAL_RAIL' | 'OTHER'; /** * - `NORMAL` - latitude / longitude coordinate or address @@ -386,6 +387,43 @@ export type Rental = { returnConstraint?: RentalReturnConstraint; }; +export type ODMType = 'TAXI' | 'RIDE_SHARING'; + +/** + * Vehicle with driver, e.g., taxi + */ +export type ODM = { + /** + * ODM system ID + */ + systemId: string; + /** + * ODM system name + */ + systemName?: string; + /** + * URL of the ODM system + */ + url?: string; + /** + * Name of company that offers the service + */ + companyName?: string; + /** + * ODM URI for Android (deep link to the specific station or vehicle) + */ + odmUriAndroid?: string; + /** + * ODM URI for iOS (deep link to the specific station or vehicle) + */ + odmUriIOS?: string; + /** + * ODM URI for web (deep link to the specific station or vehicle) + */ + odmUriWeb?: string; + odmType?: ODMType; +}; + export type Leg = { /** * Transport mode for this leg @@ -469,6 +507,7 @@ export type Leg = { */ steps?: Array; rental?: Rental; + odm?: ODM; }; export type Itinerary = {