From 164480a8ca850a2411d5aa7d547013d65a9ce543 Mon Sep 17 00:00:00 2001 From: Michael Kutzner <174690291+MichaelKutzner@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:37:07 +0200 Subject: [PATCH] Calculate stop to subshape mapping during import (#136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP: Move subshape request into `nigiri` * WIP: Remove `trip_idx_t` from function signature * WIP: Explore wrong shape for merged trips * WIP: Fix missing end segment * Fix trip index on merged trips * Add check for stop order on same section * WIP: Create struct to handle shapes data * WIP: Swap to new shapes storage struct * WIP: Format code * WIP: Remove temporary cache * WIP: Remove mutable cache * WIP: Fix array variant * WIP: Parametrize test * Add missing header * WIP: Calculate shape offsets per stop during load * WIP: Use cache to improve import duration This also adds files missing in last commit * Add test for duplicated shape offsets * Cleanup code * Fix build errors * Fix formatting * Update progress bar * Remove `constexpr` specifier * Ensure mapping uses correct indices * Make `shapes_storage` optional Using a `shapes_storage` instance will now always calculate and store shapes and shape offsets. * WIP: Avoid creating an additional vector * Add test for shared shapes * Cleanup test input * Move initializer used in `if` * Simplify cache offset calculation * Remove aliases used only once * Use lambda expressions for offsets calculation cache This will remove warnings about possibly unused functions in defined function objects. * Add vector to store duplicated shape offsets * WIP: Store trip to shape mapping in shapes_storage * Remove 'trip_shape_indices_' from 'timetable' * Revert accidentally applied formatting * Fix code style * Fix typo * fixup! Revert accidentally applied formatting * Fix missing header * Use uniform formatting library * Simplify equals and hash functions * Fix progess bar update * Duplicate alias defined in osr * Delete dead code * WIP: Add prototype supporting block trips * Fix assertions * Update tests for changes function This will also prepare the data set for multiple tests using a common timetable. * Handle trips without shape * WIP: Add support for runs containing a trip subset * WIP: Handle offsets of merged sub trips * WIP: Add support for runs covering multiple trips * Delete debug output * WIP: Add support for shapes covering two trips Notice that the connection stop will be processed twice for now. * WIP: Fix duplicated connection stop * WIP: Add support for many trips * Fix offsets * Support merged trips with and without shapes * Simplify some code * Fix includes * Remove previous implementation * Simplify shape offset calculation * Use apropiate loop statement * Remove not required namespace * Improve test description * Reduce duplicated code * Fix missing `const` * Fix formatting * WIP: Prepare shape offset calculate by distance * Simplify property access * Fix test data * Remove stop deduplication for merged trips Stops connecting multiple trips in a journey leg will no longer be merged. This will slightly improve code readability without having a notible effect for most data sets. Furthermore, it will support GTFS data sets that may use different coordinates for stops connecting multiple trips. * Calculate offsets based on distance traveled * Add test for different traveled distances * Fix formatting * Fix out of bounds error * Remove lambda function * Swap checks for performance improvement * Fix assertion Fix offset, as 'stop_range_' is inclusive while 'range' is exclusive * Prefer algorithms provided by 'utl' * Use better variable name * Fix missing namespace 'std' * Replace unicode arrows * Use explicit loop to replace 'std::for_each' * Move expected values into assertions * Remove not needed assertion * Fix missing type conversion for interval shifts * Use operations defined for interval * Use enumeration * Use unsigned integers for initialization This is only applied for data types that are based on unsigned integers * Use base type for offset calculation * Revert "Fix assertion" This reverts commit 99da3ed352dfe954d63b9af6f0643f1dd423c1d3. * Fix variable name * Use 'interval::end()' implementation Notice that using `end(interval)` will attempt to use `frun::end()` instead. * wip * Fix code * Add tests for mixed shape trips * WIP: Simplify shape processing * Cleanup code * Fix behavior for single stops * Format code * Replace call to 'std::views::pairwise' * Remove not supported 'std::views::pairwise' * Use offset cache for shapes with distance traveled * Remove calls to 'std::make_pair' * Change temporary data structure This will use a vector map to avoid an additional hash map. * Simplify inserts * Store median distance traveled This allows minor errors within shape distance traveled * Fix formatting * Reduce memory usage * Remove not required headers * Rename variables to match intended usage * Simplify check for valid distances traveled * Fix offset error for multiple data sets * Fix included headers * Use trailing return type * Improve variable names * Remove no longer used function * Remove no longer relevant information from test * WIP: Change code to operate on segments * Remove duplicated shape points * Move duplicated code into shared lambda with state * Add tests for interval intersection * Remove not needed variable * Use lvalue reference instead of rvalue reference * Remove misleading alias * Use default capture * Update names * Fix naming * Store invalid starting point into constant * Replace comment with assert * Use alias * Replace 'index' with 'idx' * Remove not needed 'inline' * Add brief explanation for stored median * WIP: Setup tests for missing distances * Avoid zero vectors for 'shape_dist_traveled' * Move length check to the end * Improve memory usage for empty shape_dist_traveled As values must increase, only leading `0.0`s are allowed. * Update test to use multiple leading `0.0`s * Simplify check for valid distances * Reduce memory usage when no shapes are used * Mark unused variable * Fix data type * Delete obsolete compare function * Simplify shape offset calculation Assume visual errors will not be noticed by end users. Therefore some optimization can be skipped. * Update cista dependency * Remove unused include --------- Co-authored-by: Felix Gündling --- .pkg | 2 +- .pkg.lock | 7 +- exe/import.cc | 5 +- include/nigiri/common/interval.h | 13 +- include/nigiri/common/sort_by.h | 3 + include/nigiri/loader/gtfs/load_timetable.h | 5 +- include/nigiri/loader/gtfs/loader.h | 3 +- include/nigiri/loader/gtfs/shape.h | 9 +- include/nigiri/loader/gtfs/shape_prepare.h | 15 + include/nigiri/loader/gtfs/stop_time.h | 3 +- include/nigiri/loader/gtfs/trip.h | 4 +- include/nigiri/loader/hrd/loader.h | 3 +- include/nigiri/loader/load.h | 3 +- include/nigiri/loader/loader_interface.h | 3 +- include/nigiri/rt/frun.h | 11 + include/nigiri/shape.h | 32 +- include/nigiri/timetable.h | 3 - include/nigiri/types.h | 7 +- src/abi.cc | 1 + src/loader/gtfs/load_timetable.cc | 35 +- src/loader/gtfs/loader.cc | 4 +- src/loader/gtfs/shape.cc | 33 +- src/loader/gtfs/shape_prepare.cc | 128 ++++ src/loader/gtfs/stop_time.cc | 19 +- src/loader/gtfs/trip.cc | 3 +- src/loader/hrd/loader.cc | 2 +- src/loader/load.cc | 2 +- src/rt/frun.cc | 67 ++ src/shape.cc | 93 ++- test/interval_test.cc | 27 +- test/loader/gtfs/shape_test.cc | 30 +- test/loader/gtfs/stop_time_test.cc | 17 +- test/loader/gtfs/test_data.cc | 24 +- test/loader/gtfs/trip_test.cc | 2 +- test/rt/frun_shape_test.cc | 686 ++++++++++++++++++++ test/rt/rt_block_id_test.cc | 71 ++ test/shape_test.cc | 40 +- 37 files changed, 1284 insertions(+), 131 deletions(-) create mode 100644 include/nigiri/loader/gtfs/shape_prepare.h create mode 100644 src/loader/gtfs/shape_prepare.cc create mode 100644 test/rt/frun_shape_test.cc diff --git a/.pkg b/.pkg index dd9f70e4..d897517c 100644 --- a/.pkg +++ b/.pkg @@ -5,7 +5,7 @@ [cista] url=git@github.com:felixguendling/cista.git branch=master - commit=f52a62c4d83377acd398227ab4fcd6c946bdbd70 + commit=f1358310262c347a8b4a533e5dd6184ec97ba637 [geo] url=git@github.com:motis-project/geo.git branch=master diff --git a/.pkg.lock b/.pkg.lock index b4c9f6c6..e226f583 100644 --- a/.pkg.lock +++ b/.pkg.lock @@ -1,8 +1,8 @@ -8342248202595136248 +14235113949304861054 cista f52a62c4d83377acd398227ab4fcd6c946bdbd70 PEGTL 1c1aa6e650e4d26f10fa398f148ec0cdc5f0808d -res 7d97784ba785ce8a2677ea77164040fde484fb04 -date d84b23ca2432e17f3f04a3e0cc96b096b99c39a2 +res b759b93316afeb529b6cb5b2548b24c41e382fb0 +date ce88cc33b5551f66655614eeebb7c5b7189025fb googletest 7b64fca6ea0833628d6f86255a81424365f7cc0c fmt dc10f83be70ac2873d5f8d1ce317596f1fd318a2 utl 77aac494c45d2b070e65fe712abc34ac74a91d0f @@ -19,5 +19,6 @@ opentelemetry-proto 1624689398a3226c45994d70cb544a1e781dc032 abseil-cpp ba5240842d352b4b67a32092453a2fe5fe53a62e protobuf d8136b9c6a62db6ce09900ecdeb82bb793096cbd opentelemetry-cpp ec4aef6b17b697052edef5417825ad71947b2ed1 +pugixml 60175e80e2f5e97e027ac78f7e14c5acc009ce50 unordered_dense 77e91016354e6d8cba24a86c5abb807de2534c02 wyhash 1e012b57fc2227a9e583a57e2eacb3da99816d99 diff --git a/exe/import.cc b/exe/import.cc index b92b3b2f..55a677b7 100644 --- a/exe/import.cc +++ b/exe/import.cc @@ -106,10 +106,9 @@ int main(int ac, char** av) { assistance = std::make_unique(read_assistance(f.view())); } - auto shapes = std::unique_ptr(); + auto shapes = std::unique_ptr{}; if (vm.contains("shapes")) { - shapes = - std::make_unique(create_shapes_storage(out_shapes)); + shapes = std::make_unique(out_shapes); } auto const start = parse_date(start_date); diff --git a/include/nigiri/common/interval.h b/include/nigiri/common/interval.h index e1f63271..8df73e8a 100644 --- a/include/nigiri/common/interval.h +++ b/include/nigiri/common/interval.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -56,12 +57,12 @@ struct interval { }; template - interval operator+(X const& x) const { + interval operator>>(X const& x) const { return {static_cast(from_ + x), static_cast(to_ + x)}; } template - interval operator-(X const& x) const { + interval operator<<(X const& x) const { return {static_cast(from_ - x), static_cast(to_ - x)}; } @@ -79,6 +80,14 @@ struct interval { return from_ < o.to_ && to_ > o.from_; } + interval intersect(interval const& o) const { + if (overlaps(o)) { + return {std::max(from_, o.from_), std::min(to_, o.to_)}; + } else { + return {}; + } + } + iterator begin() const { return {from_}; } iterator end() const { return {to_}; } friend iterator begin(interval const& r) { return r.begin(); } diff --git a/include/nigiri/common/sort_by.h b/include/nigiri/common/sort_by.h index 604542c2..f88c6abf 100644 --- a/include/nigiri/common/sort_by.h +++ b/include/nigiri/common/sort_by.h @@ -10,6 +10,9 @@ template void apply_permutation(std::vector const& permutation, T const& orig, T& vec) { + if (orig.empty()) { + return; + } for (auto i = 0U; i != permutation.size(); ++i) { vec[i] = orig[permutation[i]]; } diff --git a/include/nigiri/loader/gtfs/load_timetable.h b/include/nigiri/loader/gtfs/load_timetable.h index 6c620d48..dd8c0fc0 100644 --- a/include/nigiri/loader/gtfs/load_timetable.h +++ b/include/nigiri/loader/gtfs/load_timetable.h @@ -2,6 +2,7 @@ #include "nigiri/loader/dir.h" #include "nigiri/loader/loader_interface.h" +#include "nigiri/shape.h" #include "nigiri/types.h" namespace nigiri { @@ -23,7 +24,7 @@ void load_timetable(loader_config const&, dir const&, timetable&, assistance_times* = nullptr, - shapes_storage_t* = nullptr); + shapes_storage* = nullptr); void load_timetable(loader_config const&, source_idx_t, @@ -31,6 +32,6 @@ void load_timetable(loader_config const&, timetable&, hash_map&, assistance_times* = nullptr, - shapes_storage_t* = nullptr); + shapes_storage* = nullptr); } // namespace nigiri::loader::gtfs \ No newline at end of file diff --git a/include/nigiri/loader/gtfs/loader.h b/include/nigiri/loader/gtfs/loader.h index a65d9d11..c676f063 100644 --- a/include/nigiri/loader/gtfs/loader.h +++ b/include/nigiri/loader/gtfs/loader.h @@ -1,6 +1,7 @@ #pragma once #include "nigiri/loader/loader_interface.h" +#include "nigiri/shape.h" namespace nigiri::loader::gtfs { @@ -12,7 +13,7 @@ struct gtfs_loader : public loader_interface { timetable&, hash_map&, assistance_times*, - shapes_storage_t*) const override; + shapes_storage*) const override; cista::hash_t hash(dir const&) const override; std::string_view name() const override; }; diff --git a/include/nigiri/loader/gtfs/shape.h b/include/nigiri/loader/gtfs/shape.h index 8c20d488..e25eb8e2 100644 --- a/include/nigiri/loader/gtfs/shape.h +++ b/include/nigiri/loader/gtfs/shape.h @@ -2,6 +2,7 @@ #include +#include "nigiri/shape.h" #include "nigiri/types.h" namespace nigiri::loader::gtfs { @@ -11,8 +12,12 @@ struct shape_state { std::size_t last_seq_{}; }; -using shape_id_map_t = hash_map; +struct shape_loader_state { + hash_map id_map_{}; + vecvec distances_{}; + shape_idx_t index_offset_; +}; -shape_id_map_t parse_shapes(std::string_view const, shapes_storage_t&); +shape_loader_state parse_shapes(std::string_view const, shapes_storage&); } // namespace nigiri::loader::gtfs \ No newline at end of file diff --git a/include/nigiri/loader/gtfs/shape_prepare.h b/include/nigiri/loader/gtfs/shape_prepare.h new file mode 100644 index 00000000..cab62590 --- /dev/null +++ b/include/nigiri/loader/gtfs/shape_prepare.h @@ -0,0 +1,15 @@ +#pragma once + +#include "nigiri/loader/gtfs/shape.h" +#include "nigiri/loader/gtfs/trip.h" +#include "nigiri/shape.h" +#include "nigiri/timetable.h" + +namespace nigiri::loader::gtfs { + +void calculate_shape_offsets(timetable const&, + shapes_storage&, + vector_map const&, + shape_loader_state const&); + +} // namespace nigiri::loader::gtfs diff --git a/include/nigiri/loader/gtfs/stop_time.h b/include/nigiri/loader/gtfs/stop_time.h index 0082d61c..ea62441e 100644 --- a/include/nigiri/loader/gtfs/stop_time.h +++ b/include/nigiri/loader/gtfs/stop_time.h @@ -10,6 +10,7 @@ namespace nigiri::loader::gtfs { void read_stop_times(timetable&, trip_data&, locations_map const&, - std::string_view file_content); + std::string_view file_content, + bool); } // namespace nigiri::loader::gtfs diff --git a/include/nigiri/loader/gtfs/trip.h b/include/nigiri/loader/gtfs/trip.h index 792f53bf..1468bd5a 100644 --- a/include/nigiri/loader/gtfs/trip.h +++ b/include/nigiri/loader/gtfs/trip.h @@ -15,6 +15,7 @@ #include "nigiri/loader/gtfs/shape.h" #include "nigiri/loader/gtfs/stop.h" #include "nigiri/timetable.h" +#include "nigiri/types.h" namespace nigiri::loader::gtfs { @@ -98,6 +99,7 @@ struct trip { std::vector seq_numbers_; std::vector event_times_; std::vector stop_headsigns_; + std::vector distance_traveled_; std::optional> frequency_; bool requires_interpolation_{false}; @@ -126,7 +128,7 @@ trip_data read_trips( timetable&, route_map_t const&, traffic_days_t const&, - shape_id_map_t const&, + shape_loader_state const&, std::string_view file_content, std::array const& bikes_allowed_default); diff --git a/include/nigiri/loader/hrd/loader.h b/include/nigiri/loader/hrd/loader.h index 7df1e95c..2b34987f 100644 --- a/include/nigiri/loader/hrd/loader.h +++ b/include/nigiri/loader/hrd/loader.h @@ -2,6 +2,7 @@ #include "nigiri/loader/hrd/parser_config.h" #include "nigiri/loader/loader_interface.h" +#include "nigiri/shape.h" namespace nigiri::loader::hrd { @@ -14,7 +15,7 @@ struct hrd_loader : public loader_interface { timetable& tt, hash_map&, assistance_times*, - shapes_storage_t*) const override; + shapes_storage*) const override; cista::hash_t hash(dir const&) const override; nigiri::loader::hrd::config config_; }; diff --git a/include/nigiri/loader/load.h b/include/nigiri/loader/load.h index eebaebcd..c4dc6eeb 100644 --- a/include/nigiri/loader/load.h +++ b/include/nigiri/loader/load.h @@ -7,6 +7,7 @@ #include "nigiri/loader/build_footpaths.h" #include "nigiri/common/interval.h" +#include "nigiri/shape.h" #include "nigiri/timetable.h" #include "nigiri/types.h" @@ -19,7 +20,7 @@ timetable load(std::vector> const&, finalize_options const&, interval const&, assistance_times* = nullptr, - shapes_storage_t* = nullptr, + shapes_storage* = nullptr, bool ignore = false); } // namespace nigiri::loader \ No newline at end of file diff --git a/include/nigiri/loader/loader_interface.h b/include/nigiri/loader/loader_interface.h index cdc4fa8f..1a053dd0 100644 --- a/include/nigiri/loader/loader_interface.h +++ b/include/nigiri/loader/loader_interface.h @@ -5,6 +5,7 @@ #include "nigiri/loader/assistance.h" #include "nigiri/loader/dir.h" +#include "nigiri/shape.h" #include "nigiri/types.h" namespace nigiri { @@ -28,7 +29,7 @@ struct loader_interface { timetable&, hash_map&, assistance_times*, - shapes_storage_t*) const = 0; + shapes_storage*) const = 0; virtual cista::hash_t hash(dir const&) const = 0; virtual std::string_view name() const = 0; }; diff --git a/include/nigiri/rt/frun.h b/include/nigiri/rt/frun.h index dd604582..01a709d5 100644 --- a/include/nigiri/rt/frun.h +++ b/include/nigiri/rt/frun.h @@ -1,10 +1,16 @@ #pragma once +#include #include +#include "geo/latlng.h" + +#include "nigiri/common/interval.h" #include "nigiri/location.h" #include "nigiri/rt/run.h" +#include "nigiri/shape.h" #include "nigiri/stop.h" +#include "nigiri/types.h" namespace nigiri { struct rt_timetable; @@ -136,6 +142,11 @@ struct frun : public run { trip_idx_t trip_idx() const; clasz get_clasz() const noexcept; + void for_each_shape_point( + shapes_storage const*, + interval const&, + std::function const&) const; + void print(std::ostream&, interval); friend std::ostream& operator<<(std::ostream&, frun const&); diff --git a/include/nigiri/shape.h b/include/nigiri/shape.h index 9ab163a6..f69b3394 100644 --- a/include/nigiri/shape.h +++ b/include/nigiri/shape.h @@ -3,24 +3,30 @@ #include #include +#include "cista/containers/pair.h" + #include "geo/latlng.h" #include "nigiri/types.h" namespace nigiri { -struct timetable; -} - -namespace nigiri { - -shapes_storage_t create_shapes_storage( - std::filesystem::path const&, - cista::mmap::protection = cista::mmap::protection::WRITE); - -std::span get_shape(timetable const&, - shapes_storage_t const&, - trip_idx_t); -std::span get_shape(shapes_storage_t const&, shape_idx_t); +struct shapes_storage { + explicit shapes_storage( + std::filesystem::path const&, + cista::mmap::protection = cista::mmap::protection::WRITE); + std::span get_shape(shape_idx_t) const; + std::span get_shape(trip_idx_t) const; + std::span get_shape(trip_idx_t, + interval const&) const; + shape_offset_idx_t add_offsets(std::vector const&); + void add_trip_shape_offsets( + trip_idx_t, cista::pair const&); + + mm_vecvec data_; + mm_vecvec offsets_; + mm_vec_map> + trip_offset_indices_; +}; } // namespace nigiri \ No newline at end of file diff --git a/include/nigiri/timetable.h b/include/nigiri/timetable.h index 2a276170..5e12b673 100644 --- a/include/nigiri/timetable.h +++ b/include/nigiri/timetable.h @@ -386,9 +386,6 @@ struct timetable { // Trip index -> all transports with a stop interval paged_vecvec trip_transport_ranges_; - // Trip index -> shape per trip - vector_map trip_shape_indices_; - // Transport -> stop sequence numbers (relevant for GTFS-RT stop matching) // Compaction: // - empty = zero-based sequence 0,1,2,... diff --git a/include/nigiri/types.h b/include/nigiri/types.h index b5ac4a3e..28ede22b 100644 --- a/include/nigiri/types.h +++ b/include/nigiri/types.h @@ -112,6 +112,9 @@ using optional = cista::optional; template using nvec = cista::raw::nvec; +template +using mm_vec_map = cista::basic_mmap_vec; + template using mm_vec = cista::basic_mmap_vec; @@ -134,6 +137,9 @@ using route_idx_t = cista::strong; using section_idx_t = cista::strong; using section_db_idx_t = cista::strong; using shape_idx_t = cista::strong; +using shape_offset_t = cista::strong; +using shape_offset_idx_t = + cista::strong; using trip_idx_t = cista::strong; using trip_id_idx_t = cista::strong; using transport_idx_t = cista::strong; @@ -171,7 +177,6 @@ using attribute_combination_idx_t = cista::strong; using provider_idx_t = cista::strong; -using shapes_storage_t = mm_vecvec; using transport_range_t = pair>; struct trip_debug { diff --git a/src/abi.cc b/src/abi.cc index 45125963..b636dd64 100644 --- a/src/abi.cc +++ b/src/abi.cc @@ -20,6 +20,7 @@ #include "nigiri/rt/create_rt_timetable.h" #include "nigiri/rt/gtfsrt_update.h" #include "nigiri/rt/rt_timetable.h" +#include "nigiri/shape.h" #include "nigiri/timetable.h" #include "nigiri/types.h" diff --git a/src/loader/gtfs/load_timetable.cc b/src/loader/gtfs/load_timetable.cc index daa0b751..42c1339e 100644 --- a/src/loader/gtfs/load_timetable.cc +++ b/src/loader/gtfs/load_timetable.cc @@ -24,6 +24,7 @@ #include "nigiri/loader/gtfs/route_key.h" #include "nigiri/loader/gtfs/services.h" #include "nigiri/loader/gtfs/shape.h" +#include "nigiri/loader/gtfs/shape_prepare.h" #include "nigiri/loader/gtfs/stop.h" #include "nigiri/loader/gtfs/stop_seq_number_encoding.h" #include "nigiri/loader/gtfs/stop_time.h" @@ -84,10 +85,10 @@ void load_timetable(loader_config const& config, dir const& d, timetable& tt, assistance_times* assistance, - shapes_storage_t* shapes) { + shapes_storage* shapes_data) { auto local_bitfield_indices = hash_map{}; load_timetable(config, src, d, tt, local_bitfield_indices, assistance, - shapes); + shapes_data); } void load_timetable(loader_config const& config, @@ -96,7 +97,7 @@ void load_timetable(loader_config const& config, timetable& tt, hash_map& bitfield_indices, assistance_times* assistance, - shapes_storage_t* shapes) { + shapes_storage* shapes_data) { nigiri::scoped_timer const global_timer{"gtfs parser"}; auto const load = [&](std::string_view file_name) -> file { @@ -115,24 +116,26 @@ void load_timetable(loader_config const& config, auto const dates = read_calendar_date(load(kCalendarDatesFile).data()); auto const service = merge_traffic_days(tt.internal_interval_days(), calendar, dates); - auto const shape_indices = - (shapes != nullptr) ? parse_shapes(load(kShapesFile).data(), *shapes) - : shape_id_map_t{}; + auto const shape_states = + (shapes_data != nullptr) + ? parse_shapes(load(kShapesFile).data(), *shapes_data) + : shape_loader_state{}; auto trip_data = - read_trips(tt, routes, service, shape_indices, load(kTripsFile).data(), + read_trips(tt, routes, service, shape_states, load(kTripsFile).data(), config.bikes_allowed_default_); read_frequencies(trip_data, load(kFrequenciesFile).data()); - read_stop_times(tt, trip_data, stops, load(kStopTimesFile).data()); + read_stop_times(tt, trip_data, stops, load(kStopTimesFile).data(), + shapes_data != nullptr); { auto const timer = scoped_timer{"loader.gtfs.trips.sort"}; for (auto& t : trip_data.data_) { if (t.requires_sorting_) { t.stop_headsigns_.resize(t.seq_numbers_.size()); - std::tie(t.seq_numbers_, t.stop_seq_, t.event_times_, - t.stop_headsigns_) = + std::tie(t.seq_numbers_, t.stop_seq_, t.event_times_, t.stop_headsigns_, + t.distance_traveled_) = sort_by(t.seq_numbers_, t.stop_seq_, t.event_times_, - t.stop_headsigns_); + t.stop_headsigns_, t.distance_traveled_); } } } @@ -203,7 +206,7 @@ void load_timetable(loader_config const& config, { progress_tracker->status("Expand Trips") - .out_bounds(70.F, 85.F) + .out_bounds(68.F, 83.F) .in_high(trip_data.data_.size()); auto const timer = scoped_timer{"loader.gtfs.trips.expand"}; @@ -218,7 +221,7 @@ void load_timetable(loader_config const& config, { progress_tracker->status("Stay Seated") - .out_bounds(85.F, 87.F) + .out_bounds(83.F, 85.F) .in_high(route_services.size()); auto const timer = scoped_timer{"loader.gtfs.trips.block_id"}; @@ -231,7 +234,7 @@ void load_timetable(loader_config const& config, { progress_tracker->status("Write Trips") - .out_bounds(87.F, 100.F) + .out_bounds(85.F, 98.F) .in_high(route_services.size()); auto const is_train_number = [](auto const& s) { @@ -258,6 +261,9 @@ void load_timetable(loader_config const& config, {source_file_idx, trp.from_line_, trp.to_line_}, train_nr, stop_seq_numbers); } + if (shapes_data != nullptr) { + calculate_shape_offsets(tt, *shapes_data, trip_data.data_, shape_states); + } auto const timer = scoped_timer{"loader.gtfs.routes.build"}; auto const attributes = std::basic_string{}; @@ -370,7 +376,6 @@ void load_timetable(loader_config const& config, // Build transport ranges. for (auto const& t : trip_data.data_) { tt.trip_transport_ranges_.emplace_back(t.transport_ranges_); - tt.trip_shape_indices_.push_back(t.shape_idx_); } } } diff --git a/src/loader/gtfs/loader.cc b/src/loader/gtfs/loader.cc index 4c374ab2..4b0dac17 100644 --- a/src/loader/gtfs/loader.cc +++ b/src/loader/gtfs/loader.cc @@ -15,9 +15,9 @@ void gtfs_loader::load( timetable& tt, hash_map& global_bitfield_indices, assistance_times* assistance, - shapes_storage_t* shapes) const { + shapes_storage* shapes_data) const { return nigiri::loader::gtfs::load_timetable( - c, src, d, tt, global_bitfield_indices, assistance, shapes); + c, src, d, tt, global_bitfield_indices, assistance, shapes_data); } cista::hash_t gtfs_loader::hash(dir const& d) const { diff --git a/src/loader/gtfs/shape.cc b/src/loader/gtfs/shape.cc index ec637807..f54613de 100644 --- a/src/loader/gtfs/shape.cc +++ b/src/loader/gtfs/shape.cc @@ -1,5 +1,7 @@ #include "nigiri/loader/gtfs/shape.h" +#include "geo/latlng.h" + #include "utl/parser/buf_reader.h" #include "utl/parser/csv_range.h" #include "utl/parser/line_range.h" @@ -11,17 +13,22 @@ namespace nigiri::loader::gtfs { -shape_id_map_t parse_shapes(std::string_view const data, - shapes_storage_t& shapes) { +shape_loader_state parse_shapes(std::string_view const data, + shapes_storage& shapes_data) { + auto& shapes = shapes_data.data_; struct shape_entry { utl::csv_col id_; utl::csv_col lat_; utl::csv_col lon_; utl::csv_col seq_; + utl::csv_col distance_; }; - auto states = shape_id_map_t{}; - auto lookup = cached_lookup(states); + auto const index_offset = static_cast(shapes.size()); + auto states = shape_loader_state{ + .index_offset_ = index_offset, + }; + auto lookup = cached_lookup(states.id_map_); auto const progress_tracker = utl::get_active_progress_tracker(); progress_tracker->status("Parse Shapes") @@ -30,21 +37,31 @@ shape_id_map_t parse_shapes(std::string_view const data, utl::line_range{utl::make_buf_reader(data, progress_tracker->update_fn())} // | utl::csv() // | utl::for_each([&](shape_entry const entry) { - auto& state = lookup(entry.id_.val().view(), [&] { + auto& state = lookup(entry.id_->view(), [&] { auto const index = static_cast(shapes.size()); shapes.add_back_sized(0U); + states.distances_.add_back_sized(0U); return shape_state{index, 0U}; }); - auto const seq = entry.seq_.val(); + auto const seq = *entry.seq_; auto bucket = shapes[state.index_]; if (!bucket.empty() && state.last_seq_ >= seq) { log(log_lvl::error, "loader.gtfs.shape", "Non monotonic sequence for shape_id '{}': Sequence number {} " "followed by {}", - entry.id_.val().to_str(), state.last_seq_, seq); + entry.id_->to_str(), state.last_seq_, seq); } - bucket.push_back(geo::latlng{entry.lat_.val(), entry.lon_.val()}); + bucket.push_back(geo::latlng{*entry.lat_, *entry.lon_}); state.last_seq_ = seq; + auto distances = states.distances_[state.index_ - index_offset]; + if (distances.empty()) { + if (*entry.distance_ != 0.0) { + distances.grow(bucket.size()); + distances.back() = *entry.distance_; + } + } else { + distances.push_back(*entry.distance_); + } }); return states; } diff --git a/src/loader/gtfs/shape_prepare.cc b/src/loader/gtfs/shape_prepare.cc new file mode 100644 index 00000000..05b76b09 --- /dev/null +++ b/src/loader/gtfs/shape_prepare.cc @@ -0,0 +1,128 @@ +#include "nigiri/loader/gtfs/shape_prepare.h" + +#include +#include +#include +#include + +#include "geo/latlng.h" +#include "geo/polyline.h" + +#include "utl/enumerate.h" +#include "utl/get_or_create.h" +#include "utl/progress_tracker.h" + +#include "nigiri/stop.h" +#include "nigiri/types.h" + +namespace nigiri::loader::gtfs { + +std::size_t get_closest(geo::latlng const& pos, + std::span shape) { + if (shape.size() < 2U) { + return 0U; + } + auto const best = geo::distance_to_polyline(pos, shape); + auto const from = shape[best.segment_idx_]; + auto const to = shape[best.segment_idx_ + 1]; + return geo::distance(pos, from) <= geo::distance(pos, to) + ? best.segment_idx_ + : best.segment_idx_ + 1; +} + +std::vector get_offsets_by_stops( + timetable const& tt, + std::span shape, + stop_seq_t const& stop_seq) { + if (shape.empty()) { + return {}; + } + + auto offsets = std::vector(stop_seq.size()); + auto remaining_start = cista::base_t{0U}; + + for (auto const [i, s] : utl::enumerate(stop_seq)) { + if (i == 0U) { + offsets[0] = shape_offset_t{0U}; + } else if (i == stop_seq.size() - 1U) { + offsets[i] = shape_offset_t{shape.size() - 1U}; + } else { + auto const pos = tt.locations_.coordinates_[stop{s}.location_idx()]; + remaining_start += get_closest(pos, shape.subspan(remaining_start)); + offsets[i] = shape_offset_t{remaining_start}; + } + } + + return offsets; +} + +template + requires std::ranges::range && + std::is_same_v, double> +std::vector get_offsets_by_dist_traveled( + std::vector const& dist_traveled_stops_times, + DoubleRange const& dist_traveled_shape_edges) { + auto offsets = std::vector{}; + offsets.reserve(dist_traveled_stops_times.size()); + auto remaining_shape_begin = begin(dist_traveled_shape_edges); + for (auto const& distance : dist_traveled_stops_times) { + remaining_shape_begin = std::lower_bound( + remaining_shape_begin, end(dist_traveled_shape_edges), distance); + offsets.push_back(shape_offset_t{remaining_shape_begin - + begin(dist_traveled_shape_edges)}); + } + return offsets; +} + +void calculate_shape_offsets(timetable const& tt, + shapes_storage& shapes_data, + vector_map const& trips, + shape_loader_state const& shape_states) { + auto const progress_tracker = utl::get_active_progress_tracker(); + progress_tracker->status("Calculating shape offsets") + .out_bounds(98.F, 100.F) + .in_high(trips.size()); + + auto const key_hash = + [](std::pair const& pair) noexcept { + auto h = cista::BASE_HASH; + h = cista::hash_combine(h, cista::hashing{}(pair.first)); + h = cista::hash_combine(h, cista::hashing{}(*pair.second)); + return h; + }; + auto const key_compare = + [](std::pair const& lhs, + std::pair const& rhs) noexcept { + return (lhs.first == rhs.first) && (*lhs.second == *rhs.second); + }; + auto shape_offsets_cache = + hash_map, shape_offset_idx_t, + decltype(key_hash), decltype(key_compare)>{}; + for (auto const& trip : trips) { + progress_tracker->increment(); + auto const trip_idx = trip.trip_idx_; + auto const shape_idx = trip.shape_idx_; + + auto const shape_offset_idx = utl::get_or_create( + shape_offsets_cache, std::pair{shape_idx, &trip.stop_seq_}, [&]() { + if (shape_idx == shape_idx_t::invalid() || + trip.stop_seq_.size() < 2U) { + return shape_offset_idx_t::invalid(); + } + auto const& shape_distances = + shape_states.distances_[shape_idx - shape_states.index_offset_]; + if (!shape_distances.empty() && !trip.distance_traveled_.empty()) { + auto const offsets = get_offsets_by_dist_traveled( + trip.distance_traveled_, shape_distances); + return shapes_data.add_offsets(offsets); + } + auto const shape = shapes_data.get_shape(shape_idx); + auto const offsets = get_offsets_by_stops(tt, shape, trip.stop_seq_); + return shapes_data.add_offsets(offsets); + }); + shapes_data.add_trip_shape_offsets( + trip_idx, cista::pair{shape_idx, shape_offset_idx}); + } +} + +} // namespace nigiri::loader::gtfs \ No newline at end of file diff --git a/src/loader/gtfs/stop_time.cc b/src/loader/gtfs/stop_time.cc index d5701b14..c2c2a83f 100644 --- a/src/loader/gtfs/stop_time.cc +++ b/src/loader/gtfs/stop_time.cc @@ -21,10 +21,23 @@ namespace nigiri::loader::gtfs { +void add_distance(auto& trip_data, double const distance) { + auto& distances = trip_data.distance_traveled_; + if (distances.empty()) { + if (distance != 0.0) { + distances.resize(trip_data.seq_numbers_.size()); + distances.back() = distance; + } + } else { + distances.emplace_back(distance); + } +} + void read_stop_times(timetable& tt, trip_data& trips, locations_map const& stops, - std::string_view file_content) { + std::string_view file_content, + bool const store_distances) { struct csv_stop_time { utl::csv_col trip_id_; utl::csv_col arrival_time_; @@ -34,6 +47,7 @@ void read_stop_times(timetable& tt, utl::csv_col stop_headsign_; utl::csv_col pickup_type_; utl::csv_col drop_off_type_; + utl::csv_col distance_; }; auto const timer = scoped_timer{"read stop times"}; @@ -91,6 +105,9 @@ void read_stop_times(timetable& tt, .value()); t->event_times_.emplace_back( stop_events{.arr_ = arrival_time, .dep_ = departure_time}); + if (store_distances) { + add_distance(*t, *s.distance_); + } if (!s.stop_headsign_->empty()) { t->stop_headsigns_.resize(t->seq_numbers_.size(), diff --git a/src/loader/gtfs/trip.cc b/src/loader/gtfs/trip.cc index 78f6e43c..d9b2d0a0 100644 --- a/src/loader/gtfs/trip.cc +++ b/src/loader/gtfs/trip.cc @@ -257,7 +257,7 @@ trip_data read_trips( timetable& tt, route_map_t const& routes, traffic_days_t const& services, - shape_id_map_t const& shapes, + shape_loader_state const& shape_states, std::string_view file_content, std::array const& bikes_allowed_default) { struct csv_trip { @@ -270,6 +270,7 @@ trip_data read_trips( utl::csv_col shape_id_; utl::csv_col bikes_allowed_; }; + auto const& shapes = shape_states.id_map_; nigiri::scoped_timer const timer{"read trips"}; diff --git a/src/loader/hrd/loader.cc b/src/loader/hrd/loader.cc index e03351b3..dac1be34 100644 --- a/src/loader/hrd/loader.cc +++ b/src/loader/hrd/loader.cc @@ -17,7 +17,7 @@ void hrd_loader::load( timetable& tt, hash_map& global_bitfield_indices, assistance_times*, - shapes_storage_t*) const { + shapes_storage*) const { return nigiri::loader::hrd::load_timetable(src, config_, d, tt, global_bitfield_indices); } diff --git a/src/loader/load.cc b/src/loader/load.cc index 47e7d79a..5c9b4e4e 100644 --- a/src/loader/load.cc +++ b/src/loader/load.cc @@ -26,7 +26,7 @@ timetable load(std::vector> const& paths, finalize_options const& finalize_opt, interval const& date_range, assistance_times* a, - shapes_storage_t* shapes, + shapes_storage* shapes, bool ignore) { auto const loaders = get_loaders(); diff --git a/src/rt/frun.cc b/src/rt/frun.cc index e9b5f5a9..d5d9db27 100644 --- a/src/rt/frun.cc +++ b/src/rt/frun.cc @@ -1,5 +1,12 @@ #include "nigiri/rt/frun.h" +#include +#include +#include + +#include "utl/overloaded.h" +#include "utl/verify.h" + #include "nigiri/lookup/get_transport_stop_tz.h" #include "nigiri/rt/rt_timetable.h" #include "nigiri/timetable.h" @@ -368,6 +375,66 @@ clasz frun::get_clasz() const noexcept { } } +void frun::for_each_shape_point( + shapes_storage const* shapes_data, + interval const& range, + std::function const& callback) const { + utl::verify(range.size() >= 2, "Range must contain at least 2 stops. Is {}", + range.size()); + assert(stop_range_.from_ + range.to_ <= stop_range_.to_); + auto const absolute_stop_range = range >> stop_range_.from_; + auto const get_subshape = [&](interval absolute_range, + trip_idx_t const trip_idx, + stop_idx_t const absolute_trip_offset) + -> std::variant, interval> { + if (shapes_data != nullptr) { + auto const shape = shapes_data->get_shape( + trip_idx, absolute_range << absolute_trip_offset); + if (!shape.empty()) { + return shape; + } + } + return absolute_range << stop_range_.from_; + }; + constexpr auto kInvalidLatLng = geo::latlng{200, 200}; + auto consume_pos = [&, last_pos = + kInvalidLatLng](geo::latlng const& pos) mutable { + if (pos != last_pos) { + callback(pos); + } + last_pos = pos; + }; + // Range over all trips using absolute 'trip_details.offset_range_' + auto curr_trip_idx = trip_idx_t::invalid(); + auto absolute_trip_start = stop_idx_t{0U}; + for (auto const [from, to] : + utl::pairwise(interval{stop_idx_t{0U}, stop_range_.to_})) { + auto const trip_idx = + (*this)[static_cast(from - stop_range_.from_)] // + .get_trip_idx(event_type::kDep); + if (trip_idx != curr_trip_idx) { + curr_trip_idx = trip_idx; + absolute_trip_start = from; + } + auto const common_stops = + interval{from, static_cast(to + 1)}.intersect( + absolute_stop_range); + if (common_stops.size() > 1) { + std::visit(utl::overloaded{[&](std::span shape) { + for (auto const& pos : shape) { + consume_pos(pos); + } + }, + [&](interval relative_range) { + for (auto const stop_idx : relative_range) { + consume_pos((*this)[stop_idx].pos()); + } + }}, + get_subshape(common_stops, trip_idx, absolute_trip_start)); + } + } +} + trip_id frun::id() const noexcept { if (is_scheduled()) { auto const trip_idx = diff --git a/src/shape.cc b/src/shape.cc index 7cbb67eb..b0285d5b 100644 --- a/src/shape.cc +++ b/src/shape.cc @@ -1,38 +1,95 @@ #include "nigiri/shape.h" +#include + #include "fmt/core.h" -#include "nigiri/timetable.h" +#include "nigiri/types.h" namespace nigiri { -shapes_storage_t create_shapes_storage(std::filesystem::path const& path, - cista::mmap::protection const mode) { +template +cista::basic_mmap_vec create_storage_vector( + std::string_view const path, cista::mmap::protection const mode) { + return cista::basic_mmap_vec{cista::mmap{path.data(), mode}}; +} + +template +mm_vecvec create_storage(std::filesystem::path const& path, + std::string_view prefix, + cista::mmap::protection const mode) { return { - cista::basic_mmap_vec{cista::mmap{ - fmt::format("{}_data.bin", path.generic_string()).c_str(), mode}}, - cista::basic_mmap_vec, std::uint64_t>{ - cista::mmap{fmt::format("{}_idx.bin", path.generic_string()).c_str(), - mode}}}; + create_storage_vector( + fmt::format("{}_{}_data.bin", path.generic_string(), prefix), mode), + create_storage_vector>( + fmt::format("{}_{}_idx.bin", path.generic_string(), prefix), mode)}; } -std::span get_shape(timetable const& tt, - shapes_storage_t const& shapes, - trip_idx_t const trip_idx) { +std::pair, shape_offset_idx_t> get_shape( + shapes_storage const& storage, trip_idx_t const trip_idx) { if (trip_idx == trip_idx_t::invalid() || - trip_idx >= tt.trip_shape_indices_.size()) { + trip_idx >= storage.trip_offset_indices_.size()) { + return {}; + } + auto const [shape_idx, offset_idx] = storage.trip_offset_indices_[trip_idx]; + assert((shape_idx == shape_idx_t::invalid()) == + (offset_idx == shape_offset_idx_t::invalid())); + if (offset_idx == shape_offset_idx_t::invalid()) { return {}; } - return get_shape(shapes, tt.trip_shape_indices_[trip_idx]); + return std::pair{storage.get_shape(shape_idx), offset_idx}; } -std::span get_shape(shapes_storage_t const& shapes, - shape_idx_t const shape_idx) { - if (shape_idx == shape_idx_t::invalid() || shape_idx >= shapes.size()) { +shapes_storage::shapes_storage(std::filesystem::path const& path, + cista::mmap::protection const mode) + : data_{create_storage(path, "points", mode)}, + offsets_{create_storage( + path, "offsets", mode)}, + trip_offset_indices_{ + create_storage_vector, + trip_idx_t>( + fmt::format("{}_offset_indices.bin", path.generic_string()), + mode)} {} + +std::span shapes_storage::get_shape( + shape_idx_t const shape_idx) const { + if (shape_idx == shape_idx_t::invalid() || shape_idx > data_.size()) { return {}; } - auto const bucket = shapes[shape_idx]; - return {begin(bucket), end(bucket)}; + auto const shape = data_[shape_idx]; + return {begin(shape), end(shape)}; +} + +std::span shapes_storage::get_shape( + trip_idx_t const trip_idx) const { + auto const [shape, _] = nigiri::get_shape(*this, trip_idx); + return shape; +} + +std::span shapes_storage::get_shape( + trip_idx_t const trip_idx, interval const& range) const { + auto const [shape, offset_idx] = nigiri::get_shape(*this, trip_idx); + if (shape.empty()) { + return shape; + } + auto const offsets = offsets_[offset_idx]; + auto const from = static_cast(offsets[range.from_]); + auto const to = static_cast(offsets[range.to_ - 1]); + return shape.subspan(from, to - from + 1); +} + +shape_offset_idx_t shapes_storage::add_offsets( + std::vector const& offsets) { + auto const index = shape_offset_idx_t{offsets_.size()}; + offsets_.emplace_back(offsets); + return index; +} + +void shapes_storage::add_trip_shape_offsets( + [[maybe_unused]] trip_idx_t const trip_idx, + cista::pair const& offset_idx) { + assert(trip_idx == trip_offset_indices_.size()); + trip_offset_indices_.emplace_back(offset_idx); } } // namespace nigiri \ No newline at end of file diff --git a/test/interval_test.cc b/test/interval_test.cc index b4c06d76..bd6d8af9 100644 --- a/test/interval_test.cc +++ b/test/interval_test.cc @@ -28,13 +28,28 @@ TEST(interval, clamp) { TEST(interval, shift) { auto const i = interval{stop_idx_t{3}, stop_idx_t{14}}; - // operator+() - EXPECT_EQ((interval{stop_idx_t{25}, stop_idx_t{36}}), i + 22); - // operator-() - EXPECT_EQ((interval{stop_idx_t{1}, stop_idx_t{12}}), i - 2); + // operator>>() + EXPECT_EQ((interval{stop_idx_t{25}, stop_idx_t{36}}), i >> 22); + // operator<<() + EXPECT_EQ((interval{stop_idx_t{1}, stop_idx_t{12}}), i << 2); // Unsigned underflow EXPECT_EQ((interval{static_cast(-stop_idx_t{12}), static_cast(-stop_idx_t{1})}), - i - 15); - EXPECT_EQ((interval{stop_idx_t{65500}, stop_idx_t{65511}}), i - 39); + i >> -15); + EXPECT_EQ((interval{stop_idx_t{65500}, stop_idx_t{65511}}), i << 39); +} + +TEST(interval, intersection) { + auto const i = interval{100, 200}; + + // 'i' is superset + EXPECT_EQ((interval{125, 175}), i.intersect(interval{125, 175})); + // 'i' is subset + EXPECT_EQ(i, i.intersect(interval{75, 225})); + // Disjunct intervals + EXPECT_EQ((interval{0, 0}), i.intersect(interval{25, 75})); + // Other cases + EXPECT_EQ((interval{135, 200}), i.intersect(interval{135, 250})); + EXPECT_EQ((interval{100, 149}), i.intersect(interval{0, 149})); + EXPECT_EQ(i, i.intersect(i)); } diff --git a/test/loader/gtfs/shape_test.cc b/test/loader/gtfs/shape_test.cc index 36469357..d235da5b 100644 --- a/test/loader/gtfs/shape_test.cc +++ b/test/loader/gtfs/shape_test.cc @@ -20,8 +20,8 @@ namespace fs = std::filesystem; using namespace nigiri; using namespace nigiri::loader::gtfs; -shapes_storage_t create_tmp_shapes_storage(char const* path) { - return create_shapes_storage(fs::temp_directory_path() / path); +shapes_storage create_tmp_shapes_storage(char const* path) { + return shapes_storage{fs::temp_directory_path() / path}; } TEST(gtfs, shape_get_existing_shape_points) { @@ -38,8 +38,9 @@ TEST(gtfs, shape_get_existing_shape_points) { 3105,50.581956,6.379866,11 )"; - auto shape_data = create_tmp_shapes_storage("shape-test-builder"); - auto const shapes = parse_shapes(kShapesData, shape_data); + auto shapes_data = create_tmp_shapes_storage("shape-test-builder"); + auto const shape_states = parse_shapes(kShapesData, shapes_data); + auto const& shapes = shape_states.id_map_; EXPECT_EQ(end(shapes), shapes.find("1")); @@ -47,7 +48,7 @@ TEST(gtfs, shape_get_existing_shape_points) { {51.543652, 7.217830}, {51.478609, 7.223275}, }), - get_shape(shape_data, shapes.at("243").index_)); + shapes_data.get_shape(shapes.at("243").index_)); EXPECT_EQ((geo::polyline{ {50.553822, 6.356876}, @@ -58,7 +59,7 @@ TEST(gtfs, shape_get_existing_shape_points) { {50.578249, 6.383394}, {50.581956, 6.379866}, }), - get_shape(shape_data, shapes.at("3105").index_)); + shapes_data.get_shape(shapes.at("3105").index_)); } TEST(gtfs, shape_not_ascending_sequence) { @@ -72,13 +73,14 @@ TEST(gtfs, shape_not_ascending_sequence) { auto const buffer_guard = utl::make_finally([&]() { std::clog.rdbuf(backup); }); - auto shape_data = + auto shapes_data = create_tmp_shapes_storage("shape-test-not-ascending-sequence"); - auto const shapes = parse_shapes(kShapesData, shape_data); + auto const shape_states = parse_shapes(kShapesData, shapes_data); + auto const& shapes = shape_states.id_map_; std::clog.flush(); EXPECT_EQ((geo::polyline{{50.636512, 6.473487}, {50.636259, 6.473668}}), - get_shape(shape_data, shapes.at("1").index_)); + shapes_data.get_shape(shapes.at("1").index_)); EXPECT_TRUE(buffer.str().contains( "Non monotonic sequence for shape_id '1': Sequence number 1 " "followed by 0")); @@ -101,8 +103,9 @@ TEST(gtfs, shape_shuffled_rows) { 235,51.543652,7.217830,1 )"; - auto shape_data = create_tmp_shapes_storage("shape-test-shuffled-rows"); - auto const shapes = parse_shapes(kShapesData, shape_data); + auto shapes_data = create_tmp_shapes_storage("shape-test-shuffled-rows"); + auto const shape_states = parse_shapes(kShapesData, shapes_data); + auto const& shapes = shape_states.id_map_; auto const shape_points = std::initializer_list>{ @@ -138,7 +141,7 @@ TEST(gtfs, shape_shuffled_rows) { }}, }; for (auto [id, polyline] : shape_points) { - EXPECT_EQ(polyline, get_shape(shape_data, shapes.at(id).index_)); + EXPECT_EQ(polyline, shapes_data.get_shape(shapes.at(id).index_)); } } @@ -156,7 +159,8 @@ TEST(gtfs, shape_delay_insert_no_ascending_sequence) { auto shape_data = create_tmp_shapes_storage("shape-test-not-ascending-sequence"); - auto const shapes = parse_shapes(kShapesData, shape_data); + auto const shape_states = parse_shapes(kShapesData, shape_data); + auto const& shapes = shape_states.id_map_; std::clog.flush(); EXPECT_NE(shapes.find("1"), end(shapes)); diff --git a/test/loader/gtfs/stop_time_test.cc b/test/loader/gtfs/stop_time_test.cc index 54df3989..216de6f4 100644 --- a/test/loader/gtfs/stop_time_test.cc +++ b/test/loader/gtfs/stop_time_test.cc @@ -1,7 +1,6 @@ #include "gtest/gtest.h" #include "nigiri/loader/gtfs/files.h" -#include "nigiri/loader/gtfs/shape.h" #include "nigiri/loader/gtfs/stop_time.h" #include "nigiri/loader/loader_interface.h" @@ -39,14 +38,16 @@ TEST(gtfs, read_stop_times_example_data) { files.get_file(kStopFile).data(), files.get_file(kTransfersFile).data(), 0U); - read_stop_times(tt, trip_data, stops, files.get_file(kStopTimesFile).data()); + read_stop_times(tt, trip_data, stops, files.get_file(kStopTimesFile).data(), + true); for (auto& t : trip_data.data_) { if (t.requires_sorting_) { t.stop_headsigns_.resize(t.seq_numbers_.size()); - std::tie(t.seq_numbers_, t.stop_seq_, t.event_times_, t.stop_headsigns_) = + std::tie(t.seq_numbers_, t.stop_seq_, t.event_times_, t.stop_headsigns_, + t.distance_traveled_) = sort_by(t.seq_numbers_, t.stop_seq_, t.event_times_, - t.stop_headsigns_); + t.stop_headsigns_, t.distance_traveled_); } } @@ -100,6 +101,14 @@ TEST(gtfs, read_stop_times_example_data) { EXPECT_TRUE(stp.out_allowed()); EXPECT_TRUE(stp.in_allowed()); + // Check distances are stored iff at least 1 entry is != 0.0 + EXPECT_EQ((std::vector{0.0, 3.14, 5.0, 0.0, 0.0}), + trip_data.data_[awe1_it->second].distance_traveled_); + // Check distances are not stored if column is 0.0 + auto awd1_it = trip_data.trips_.find("AWD1"); + ASSERT_NE(end(trip_data.trips_), awd1_it); + EXPECT_TRUE(trip_data.data_[awd1_it->second].distance_traveled_.empty()); + read_frequencies(trip_data, files.get_file(kFrequenciesFile).data()); } diff --git a/test/loader/gtfs/test_data.cc b/test/loader/gtfs/test_data.cc index f7e1fd03..5cbdddd9 100644 --- a/test/loader/gtfs/test_data.cc +++ b/test/loader/gtfs/test_data.cc @@ -69,18 +69,18 @@ AWE1,20:30:00,28:00:00,420 )"}; constexpr auto const example_stop_times_content = - R"(trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type -AWD1,6:45,6:45,S6,6,0,0 -AWD1,,,S5,5,0,0 -AWD1,,,S4,4,0,0 -AWD1,6:20,6:20,S3,3,0,0 -AWD1,,,S2,2,0,0 -AWD1,6:10,6:10,S1,1,0,0 -AWE1,6:45,6:45,S6,5,0,0 -AWE1,,,S5,4,0,0 -AWE1,6:20,6:30,S3,3,0,0 -AWE1,,,S2,2,1,3 -AWE1,6:10,6:10,S1,1,0,0 + R"(trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type,shape_dist_traveled, +AWD1,6:45,6:45,S6,6,0,0,, +AWD1,,,S5,5,0,0,, +AWD1,,,S4,4,0,0,0.0, +AWD1,6:20,6:20,S3,3,0,0,0.0, +AWD1,,,S2,2,0,0,, +AWD1,6:10,6:10,S1,1,0,0,, +AWE1,6:45,6:45,S6,5,0,0,, +AWE1,,,S5,4,0,0,, +AWE1,6:20,6:30,S3,3,0,0,5.0, +AWE1,,,S2,2,1,3,3.14, +AWE1,6:10,6:10,S1,1,0,0,, )"; loader::mem_dir example_files() { diff --git a/test/loader/gtfs/trip_test.cc b/test/loader/gtfs/trip_test.cc index 21f2820e..92ec457a 100644 --- a/test/loader/gtfs/trip_test.cc +++ b/test/loader/gtfs/trip_test.cc @@ -17,7 +17,7 @@ using namespace nigiri; using namespace nigiri::loader; // linked from gtfs/shape_test.cc -shapes_storage_t create_tmp_shapes_storage(char const*); +shapes_storage create_tmp_shapes_storage(char const*); namespace nigiri::loader::gtfs { diff --git a/test/rt/frun_shape_test.cc b/test/rt/frun_shape_test.cc new file mode 100644 index 00000000..7fdf49b0 --- /dev/null +++ b/test/rt/frun_shape_test.cc @@ -0,0 +1,686 @@ +#include "gtest/gtest.h" + +#include +#include +#include +#include + +#include "geo/latlng.h" +#include "geo/polyline.h" + +#include "nigiri/loader/dir.h" +#include "nigiri/loader/gtfs/load_timetable.h" +#include "nigiri/loader/init_finish.h" +#include "nigiri/rt/create_rt_timetable.h" +#include "nigiri/rt/frun.h" +#include "nigiri/rt/gtfsrt_resolve_run.h" +#include "nigiri/rt/run.h" +#include "nigiri/shape.h" +#include "nigiri/timetable.h" + +#include "../raptor_search.h" + +using namespace date; +using namespace nigiri; +using namespace nigiri::loader; +using namespace nigiri::loader::gtfs; +using std::operator""sv; + +// linked from gtfs/shape_test.cc +shapes_storage create_tmp_shapes_storage(char const*); + +namespace { + +constexpr auto kSchedule = R"( +# agency.txt +agency_name,agency_url,agency_timezone,agency_lang,agency_phone,agency_id +test,https://test.com,Europe/Berlin,DE,0800123456,AGENCY_1 + +# stops.txt +stop_id,stop_name,stop_lat,stop_lon +A,A,1.0,1.0 +B,B,1.0,2.0 +C,C,1.0,3.0 +D,D,1.0,4.0 +F,F,2.0,1.0 +G,G,3.0,1.0 +H,H,3.0,2.0 +I,I,3.0,3.0 +J,J,4.0,3.0 +K,K,5.0,3.0 +M,M,2.0,2.0 +N,N,3.0,3.0 +N1,N1,3.5,3.0 +O,O,4.0,4.0 +Q,Q,0.0,0.0 +S,S,4.0,1.0 +T,T,5.0,1.0 +U,U,6.0,2.0 +V,V,7.0,3.0 +W,W,7.0,2.0 +X,X,7.0,1.0 + +# calendar_dates.txt +service_id,date,exception_type +SERVICE_1,20240101,1 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_type +ROUTE_1,AGENCY_1,Route 1,,3 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id,shape_id, +ROUTE_1,SERVICE_1,TRIP_1,E,BLOCK_1,SHAPE_1, +ROUTE_1,SERVICE_1,TRIP_2,E,BLOCK_2,SHAPE_2, +ROUTE_1,SERVICE_1,TRIP_3,E,BLOCK_2,SHAPE_3, +ROUTE_1,SERVICE_1,TRIP_4,E,BLOCK_2,SHAPE_4, +ROUTE_1,SERVICE_1,TRIP_5,E,BLOCK_3,SHAPE_5, +ROUTE_1,SERVICE_1,TRIP_5+,E,BLOCK_5+,SHAPE_5, +ROUTE_1,SERVICE_1,TRIP_6,E,BLOCK_4,, +ROUTE_1,SERVICE_1,TRIP_7,E,BLOCK_5,SHAPE_2, +ROUTE_1,SERVICE_1,TRIP_8,E,BLOCK_5,, +ROUTE_1,SERVICE_1,TRIP_9,E,BLOCK_5,, +ROUTE_1,SERVICE_1,TRIP_10,E,BLOCK_5,SHAPE_6, +ROUTE_1,SERVICE_1,TRIP_11,E,BLOCK_6,SHAPE_7, +ROUTE_1,SERVICE_1,TRIP_12,E,BLOCK_7,SHAPE_8, + +# shapes.txt +"shape_id","shape_pt_lat","shape_pt_lon","shape_pt_sequence","shape_dist_traveled" +SHAPE_1,1.0,1.0,0, +SHAPE_1,0.5,1.5,1, +SHAPE_1,1.0,2.0,2, +SHAPE_1,0.5,2.5,3, +SHAPE_1,1.0,3.0,4, +SHAPE_1,0.5,3.5,5, +SHAPE_1,1.0,4.0,6, +SHAPE_2,1.0,1.0,0, +SHAPE_2,1.5,0.5,1, +SHAPE_2,2.0,1.0,2, +SHAPE_2,2.5,0.5,3, +SHAPE_2,3.0,1.0,4, +SHAPE_3,3.0,1.0,0, +SHAPE_3,3.5,1.5,1, +SHAPE_3,3.0,2.0,2, +SHAPE_3,3.5,2.5,3, +SHAPE_3,3.0,3.0,4, +SHAPE_4,3.0,3.0,0, +SHAPE_4,3.5,2.5,1, +SHAPE_4,4.0,3.0,2, +SHAPE_4,4.5,2.5,3, +SHAPE_4,5.0,3.0,4, +SHAPE_5,1.0,1.0,0,0.00 +SHAPE_5,2.0,1.5,1,1.12 +SHAPE_5,2.0,2.0,2,1.62 +SHAPE_5,2.0,2.5,3,2.12 +SHAPE_5,1.5,2.0,4,2.83 +SHAPE_5,2.0,2.0,5,3.33 +SHAPE_5,3.0,2.0,6,4.33 +SHAPE_5,3.0,3.0,7,5.33 +SHAPE_5,3.0,3.5,8,5.83 +SHAPE_5,2.5,3.0,9,6.53 +SHAPE_5,3.0,3.0,10,7.03 +SHAPE_5,3.5,3.0,11,7.53 +SHAPE_5,4.0,4.0,11,8.71 +SHAPE_6,7.0,3.0,1, +SHAPE_6,6.5,2.5,2, +SHAPE_6,7.0,2.0,3, +SHAPE_6,6.5,1.5,4, +SHAPE_6,7.0,1.0,5, +SHAPE_7,1.0,1.0,0,0.0 +SHAPE_7,1.5,1.5,1,0.7 +SHAPE_7,2.0,2.0,2,1.4 +SHAPE_7,2.5,2.5,3,2.1 +SHAPE_7,3.0,3.0,4,2.9 +SHAPE_7,3.5,3.5,5,3.5 +SHAPE_7,4.0,4.0,6,4.2 +SHAPE_8,1.0,1.0,0,0.0 +SHAPE_8,1.5,1.5,1,0.0 +SHAPE_8,2.0,2.0,2,2.0 +SHAPE_8,3.0,3.0,3,4.0 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type,shape_dist_traveled +TRIP_1,10:00:00,10:00:00,A,1,0,0, +TRIP_1,11:00:00,11:00:00,B,2,0,0, +TRIP_1,12:00:00,12:00:00,C,3,0,0, +TRIP_1,13:00:00,13:00:00,D,4,0,0, +TRIP_2,10:00:00,10:00:00,A,1,0,0, +TRIP_2,11:00:00,11:00:00,F,2,0,0, +TRIP_2,12:00:00,12:00:00,G,3,0,0, +TRIP_3,12:00:00,12:00:00,G,3,0,0, +TRIP_3,13:00:00,13:00:00,H,4,0,0, +TRIP_3,14:00:00,14:00:00,I,5,0,0, +TRIP_4,14:00:00,14:00:00,I,5,0,0, +TRIP_4,15:00:00,15:00:00,J,6,0,0, +TRIP_4,16:00:00,16:00:00,K,7,0,0, +TRIP_5,10:00:00,10:00:00,A,1,0,0,0.00 +TRIP_5,11:00:00,11:00:00,M,2,0,0,1.62 +TRIP_5,12:00:00,12:00:00,N,3,0,0,7.03 +TRIP_5,13:00:00,13:00:00,O,4,0,0,8.71 +TRIP_5+,10:00:00,10:00:00,A,1,0,0, +TRIP_5+,11:00:00,11:00:00,M,2,0,0, +TRIP_5+,12:00:00,12:00:00,N,3,0,0, +TRIP_5+,12:30:00,12:30:00,N1,4,0,0, +TRIP_5+,13:00:00,13:00:00,O,5,0,0, +TRIP_6,10:00:00,10:00:00,A,1,0,0, +TRIP_6,11:00:00,11:00:00,Q,2,0,0, +TRIP_7,10:00:00,10:00:00,A,1,0,0, +TRIP_7,11:00:00,11:00:00,F,2,0,0, +TRIP_7,12:00:00,12:00:00,G,3,0,0, +TRIP_8,12:00:00,12:00:00,G,3,0,0, +TRIP_8,13:00:00,13:00:00,S,4,0,0, +TRIP_8,14:00:00,14:00:00,T,5,0,0, +TRIP_9,14:00:00,14:00:00,T,0,0,0, +TRIP_9,15:00:00,15:00:00,U,1,0,0, +TRIP_9,16:00:00,16:00:00,V,2,0,0, +TRIP_10,17:00:00,17:00:00,V,1,0,0, +TRIP_10,18:00:00,18:00:00,W,2,0,0, +TRIP_10,19:00:00,19:00:00,X,3,0,0, +TRIP_11,10:00:00,10:00:00,A,1,0,0,0.00 +TRIP_11,11:00:00,11:00:00,M,2,0,0,1.41 +TRIP_11,12:00:00,12:00:00,N,3,0,0,2.83 +TRIP_11,13:00:00,13:00:00,O,4,0,0,4.24 +TRIP_12,10:00:00,10:00:00,A,1,0,0,0.0 +TRIP_12,11:00:00,11:00:00,M,2,0,0,2.0 +TRIP_12,12:00:00,12:00:00,N,3,0,0,4.0 + +)"sv; + +TEST( + rt, + frun_for_each_shape_point_when_shapes_are_provided_then_process_all_subshapes) { + auto const schedule = mem_dir::read(kSchedule); + auto shapes_data = create_tmp_shapes_storage("rfun-for-each-shape-point"); + + // Load static timetable. + timetable tt; + tt.date_range_ = {date::sys_days{2024_y / January / 1}, + date::sys_days{2024_y / January / 2}}; + load_timetable({}, source_idx_t{0U}, schedule, tt, nullptr, &shapes_data); + finalize(tt); + + // Create empty RT timetable. + auto rtt = rt::create_rt_timetable(tt, date::sys_days{2024_y / January / 1}); + + auto leg_shape = std::vector{}; + auto const plot_point = [&leg_shape](geo::latlng const& point) { + leg_shape.push_back(point); + }; + + // TRIP_1 + { + // Create run + transit_realtime::TripDescriptor td; + td.set_trip_id("TRIP_1"); + auto const [r, t] = rt::gtfsrt_resolve_run( + date::sys_days{2024_y / January / 1}, tt, rtt, source_idx_t{0U}, td); + ASSERT_TRUE(r.valid()); + // Create full run + auto const full_run = rt::frun{tt, &rtt, r}; + + // Full trip + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{0U}, stop_idx_t{3U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {1.0F, 1.0F}, + {0.5F, 1.5F}, + {1.0F, 2.0F}, + {0.5F, 2.5F}, + {1.0F, 3.0F}, + {0.5F, 3.5F}, + {1.0F, 4.0F}, + }), + leg_shape); + } + // Single leg + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{1U}, stop_idx_t{2U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {1.0F, 2.0F}, + {0.5F, 2.5F}, + {1.0F, 3.0F}, + }), + leg_shape); + } + // Single stop + { + EXPECT_THROW( + { + try { + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{0U}, stop_idx_t{0U + 1U}}, + plot_point); + } catch (std::runtime_error& e) { + EXPECT_STREQ("Range must contain at least 2 stops. Is 1", + e.what()); + throw e; + } + }, + std::runtime_error); + } + } + // TRIP_6 (trip without shape) + { + // Create run + transit_realtime::TripDescriptor td; + td.set_trip_id("TRIP_6"); + auto const [r, t] = rt::gtfsrt_resolve_run( + date::sys_days{2024_y / January / 1}, tt, rtt, source_idx_t{0}, td); + ASSERT_TRUE(r.valid()); + // Create full run + auto const full_run = rt::frun{tt, &rtt, r}; + + leg_shape.clear(); + full_run.for_each_shape_point(&shapes_data, + interval{stop_idx_t{0U}, stop_idx_t{1U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {1.0F, 1.0F}, + {0.0F, 0.0F}, + }), + leg_shape); + } + // frun containing a sub trip + { + // Create run + transit_realtime::TripDescriptor td; + td.set_trip_id("TRIP_1"); + auto const [r, t] = rt::gtfsrt_resolve_run( + date::sys_days{2024_y / January / 1}, tt, rtt, source_idx_t{0}, td); + ASSERT_TRUE(r.valid()); + // Create sub run containing single trip leg + auto const r_modified = + rt::run{r.t_, interval{stop_idx_t{2U}, stop_idx_t{4U}}, r.rt_}; + // Create full run + auto const full_run = rt::frun{tt, &rtt, r_modified}; + + leg_shape.clear(); + full_run.for_each_shape_point(&shapes_data, + interval{stop_idx_t{0U}, stop_idx_t{1U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {1.0F, 3.0F}, + {0.5F, 3.5F}, + {1.0F, 4.0F}, + }), + leg_shape); + } + // sub trip of a merged trip + { + // Create run + transit_realtime::TripDescriptor td; + td.set_trip_id("TRIP_3"); + auto const [r, t] = rt::gtfsrt_resolve_run( + date::sys_days{2024_y / January / 1}, tt, rtt, source_idx_t{0}, td); + ASSERT_TRUE(r.valid()); + // Create sub run containing single trip leg + auto const r_modified = + rt::run{r.t_, interval{stop_idx_t{3U}, stop_idx_t{5U}}, r.rt_}; + // Create full run + auto const full_run = rt::frun{tt, &rtt, r_modified}; + + // H -> I + leg_shape.clear(); + full_run.for_each_shape_point(&shapes_data, + interval{stop_idx_t{0U}, stop_idx_t{1U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {3.0F, 2.0F}, + {3.5F, 2.5F}, + {3.0F, 3.0F}, + }), + leg_shape); + } + // Full run covering multiple trips + { + auto const results = nigiri::test::raptor_search( + tt, &rtt, "F", "J", + unixtime_t{sys_days{2024_y / January / 1}} + 10_hours); + ASSERT_EQ(1, results.size()); + ASSERT_EQ(1, results.begin()->legs_.size()); + auto const& leg = results.begin()->legs_[0]; + ASSERT_TRUE( + std::holds_alternative( + leg.uses_)); + auto const& run_ee = + std::get(leg.uses_); + auto const full_run = rt::frun(tt, &rtt, run_ee.r_); + + // Shape for a single trip + { + // A -> F -> G + { + leg_shape.clear(); + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{0U}, stop_idx_t{2U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {1.0F, 1.0F}, + {1.5F, 0.5F}, + {2.0F, 1.0F}, + {2.5F, 0.5F}, + {3.0F, 1.0F}, + }), + leg_shape); + } + // G -> H -> I + { + leg_shape.clear(); + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{2U}, stop_idx_t{4U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {3.0F, 1.0F}, + {3.5F, 1.5F}, + {3.0F, 2.0F}, + {3.5F, 2.5F}, + {3.0F, 3.0F}, + }), + leg_shape); + } + } + // Joined shape for continuous trips + { + // H -> I -> J + { + leg_shape.clear(); + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{3U}, stop_idx_t{5U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {3.0F, 2.0F}, + {3.5F, 2.5F}, + {3.0F, 3.0F}, + {3.5F, 2.5F}, + {4.0F, 3.0F}, + }), + leg_shape); + } + // F -> G -> H -> I -> J + { + leg_shape.clear(); + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{1U}, stop_idx_t{5U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {2.0F, 1.0F}, + {2.5F, 0.5F}, + {3.0F, 1.0F}, + {3.5F, 1.5F}, + {3.0F, 2.0F}, + {3.5F, 2.5F}, + {3.0F, 3.0F}, + {3.5F, 2.5F}, + {4.0F, 3.0F}, + }), + leg_shape); + } + } + } + // Multiple trips, some with and some without shape + { + auto const results = nigiri::test::raptor_search( + tt, &rtt, "F", "X", + unixtime_t{sys_days{2024_y / January / 1}} + 10_hours); + ASSERT_EQ(1, results.size()); + ASSERT_EQ(1, results.begin()->legs_.size()); + auto const& leg = results.begin()->legs_[0]; + ASSERT_TRUE( + std::holds_alternative( + leg.uses_)); + auto const& run_ee = + std::get(leg.uses_); + auto const full_run = rt::frun(tt, &rtt, run_ee.r_); + + // F -> G -> S -> T -> U -> V -> W -> X + // Shape -> No shape -> No shape -> Shape + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{1U}, stop_idx_t{8U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {2.0F, 1.0F}, + {2.5F, 0.5F}, + {3.0F, 1.0F}, + {4.0F, 1.0F}, + {5.0F, 1.0F}, + {6.0F, 2.0F}, + {7.0F, 3.0F}, + {6.5F, 2.5F}, + {7.0F, 2.0F}, + {6.5F, 1.5F}, + {7.0F, 1.0F}, + }), + leg_shape); + } + // F -> G -> S + // Shape -> No shape + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{1U}, stop_idx_t{3U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {2.0F, 1.0F}, + {2.5F, 0.5F}, + {3.0F, 1.0F}, + {4.0F, 1.0F}, + }), + leg_shape); + } + // U -> V -> W + // No shape -> Shape + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{5U}, stop_idx_t{7U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {6.0F, 2.0F}, + {7.0F, 3.0F}, + {6.5F, 2.5F}, + {7.0F, 2.0F}, + }), + leg_shape); + } + } + // Trip with distance traveled available + { + // Create run + transit_realtime::TripDescriptor td; + td.set_trip_id("TRIP_5"); + auto const [r, t] = rt::gtfsrt_resolve_run( + date::sys_days{2024_y / January / 1}, tt, rtt, source_idx_t{0}, td); + ASSERT_TRUE(r.valid()); + // Create full run + auto const full_run = rt::frun{tt, &rtt, r}; + + // Full trip + { + leg_shape.clear(); + + full_run.for_each_shape_point(&shapes_data, full_run.stop_range_, + plot_point); + + EXPECT_EQ((geo::polyline{ + {1.0F, 1.0F}, + {2.0F, 1.5F}, + {2.0F, 2.0F}, + {2.0F, 2.5F}, + {1.5F, 2.0F}, + {2.0F, 2.0F}, + {3.0F, 2.0F}, + {3.0F, 3.0F}, + {3.0F, 3.5F}, + {2.5F, 3.0F}, + {3.0F, 3.0F}, + {3.5F, 3.0F}, + {4.0F, 4.0F}, + }), + leg_shape); + } + // First leg, no loop + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{0U}, stop_idx_t{1U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {1.0F, 1.0F}, + {2.0F, 1.5F}, + {2.0F, 2.0F}, + }), + leg_shape); + } + // Last leg, no loop + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{2U}, stop_idx_t{3U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {3.0F, 3.0F}, + {3.5F, 3.0F}, + {4.0F, 4.0F}, + }), + leg_shape); + } + // Loop on start and end + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{1U}, stop_idx_t{2U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {2.0F, 2.0F}, + {2.0F, 2.5F}, + {1.5F, 2.0F}, + {2.0F, 2.0F}, + {3.0F, 2.0F}, + {3.0F, 3.0F}, + {3.0F, 3.5F}, + {2.5F, 3.0F}, + {3.0F, 3.0F}, + }), + leg_shape); + } + } + // Distance traveled available for shape but not on trip + { + // Create run + transit_realtime::TripDescriptor td; + td.set_trip_id("TRIP_5+"); + auto const [r, t] = rt::gtfsrt_resolve_run( + date::sys_days{2024_y / January / 1}, tt, rtt, source_idx_t{0}, td); + ASSERT_TRUE(r.valid()); + // Create full run + auto const full_run = rt::frun{tt, &rtt, r}; + + // Loop on start and end + // Match first stop each loop + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{1U}, stop_idx_t{2U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {2.0F, 2.0F}, + {2.0F, 2.5F}, + {1.5F, 2.0F}, + {2.0F, 2.0F}, + {3.0F, 2.0F}, + {3.0F, 3.0F}, + }), + leg_shape); + } + } + // Trip with not exactly matching distances traveled + { + // Create run + transit_realtime::TripDescriptor td; + td.set_trip_id("TRIP_11"); + auto const [r, t] = rt::gtfsrt_resolve_run( + date::sys_days{2024_y / January / 1}, tt, rtt, source_idx_t{0}, td); + ASSERT_TRUE(r.valid()); + // Create full run + auto const full_run = rt::frun{tt, &rtt, r}; + + // M -> N + // For M: shape < stop_times => shape + // For N: stop_times < shape => shape + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{1U}, stop_idx_t{2U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {2.5F, 2.5F}, + {3.0F, 3.0F}, + }), + leg_shape); + } + } + // Trip with multiple leading 0.0 distances + { + // Create run + transit_realtime::TripDescriptor td; + td.set_trip_id("TRIP_12"); + auto const [r, t] = rt::gtfsrt_resolve_run( + date::sys_days{2024_y / January / 1}, tt, rtt, source_idx_t{0}, td); + ASSERT_TRUE(r.valid()); + // Create full run + auto const full_run = rt::frun{tt, &rtt, r}; + + // A -> M + { + leg_shape.clear(); + + full_run.for_each_shape_point( + &shapes_data, interval{stop_idx_t{0U}, stop_idx_t{1U + 1U}}, + plot_point); + + EXPECT_EQ((geo::polyline{ + {1.0F, 1.0F}, + {1.5F, 1.5F}, + {2.0F, 2.0F}, + }), + leg_shape); + } + } +} + +} // namespace \ No newline at end of file diff --git a/test/rt/rt_block_id_test.cc b/test/rt/rt_block_id_test.cc index c460699a..9e7d905d 100644 --- a/test/rt/rt_block_id_test.cc +++ b/test/rt/rt_block_id_test.cc @@ -1,9 +1,16 @@ #include "gtest/gtest.h" +#include +#include + +#include "geo/latlng.h" +#include "geo/polyline.h" + #include "nigiri/loader/gtfs/files.h" #include "nigiri/loader/gtfs/load_timetable.h" #include "nigiri/loader/hrd/load_timetable.h" #include "nigiri/loader/init_finish.h" +#include "nigiri/routing/journey.h" #include "nigiri/rt/create_rt_timetable.h" #include "nigiri/rt/frun.h" #include "nigiri/rt/gtfsrt_resolve_run.h" @@ -11,6 +18,7 @@ #include "nigiri/rt/rt_timetable.h" #include "../loader/hrd/hrd_timetable.h" +#include "../raptor_search.h" #include "./util.h" @@ -21,6 +29,7 @@ using namespace nigiri::loader::gtfs; using namespace std::chrono_literals; using namespace std::string_view_literals; using namespace nigiri::test; +using nigiri::test::raptor_search; namespace { @@ -130,4 +139,66 @@ TEST(rt, rt_block_id_test) { << rt::frun{tt, &rtt, r2} << "\n" << rt::frun{tt, &rtt, r3} << "\n"; EXPECT_EQ(expected, ss.str()); + + // Get shape for journey leg containing multiple trips + { + auto const results = raptor_search( + tt, &rtt, "B", "E", + interval{unixtime_t{sys_days{2019_y / May / 2}} + 0_hours, + unixtime_t{sys_days{2019_y / May / 2}} + 1_hours}); + ASSERT_EQ(1, results.size()); + ASSERT_EQ(1, results.begin()->legs_.size()); + auto const& leg = results.begin()->legs_[0]; + ASSERT_TRUE( + std::holds_alternative( + leg.uses_)); + auto const& run_ee = + std::get(leg.uses_); + auto const fr = rt::frun(tt, &rtt, run_ee.r_); + auto leg_shape = std::vector{}; + + // Full journey leg + { + leg_shape.clear(); + + fr.for_each_shape_point(nullptr, run_ee.stop_range_, + [&leg_shape](geo::latlng const& point) { + leg_shape.push_back(point); + }); + + EXPECT_EQ((geo::polyline{ + {2.0F, 3.0F}, {4.0F, 5.0F}, {6.0F, 7.0F}, {8.0F, 9.0F}}), + leg_shape); + } + // Single leg + { + leg_shape.clear(); + + fr.for_each_shape_point(nullptr, + interval{stop_idx_t{2}, stop_idx_t{3 + 1}}, + [&leg_shape](geo::latlng const& point) { + leg_shape.push_back(point); + }); + + EXPECT_EQ((geo::polyline{{4.0F, 5.0F}, {6.0F, 7.0F}}), leg_shape); + } + // Single stop + { + EXPECT_THROW( + { + try { + fr.for_each_shape_point( + nullptr, interval{stop_idx_t{1}, stop_idx_t{1 + 1}}, + [&leg_shape](geo::latlng const& point) { + leg_shape.push_back(point); + }); + } catch (std::runtime_error& e) { + EXPECT_STREQ("Range must contain at least 2 stops. Is 1", + e.what()); + throw e; + } + }, + std::runtime_error); + } + } } \ No newline at end of file diff --git a/test/shape_test.cc b/test/shape_test.cc index 1228fafb..6bc1ed42 100644 --- a/test/shape_test.cc +++ b/test/shape_test.cc @@ -16,7 +16,7 @@ using namespace date; using namespace std::string_view_literals; // linked from gtfs/shape_test.cc -shapes_storage_t create_tmp_shapes_storage(char const*); +shapes_storage create_tmp_shapes_storage(char const*); namespace { @@ -113,32 +113,48 @@ TEST(shape, single_trip_with_shape) { date::sys_days{2024_y / March / 2}}; loader::register_special_stations(tt); auto local_bitfield_indices = hash_map{}; - auto shape_data = create_tmp_shapes_storage("shape-route-trip-with-shape"); + auto shapes_data = create_tmp_shapes_storage("shape-route-trip-with-shape"); loader::gtfs::load_timetable({}, source_idx_t{1}, loader::mem_dir::read(kWithShapes), tt, - local_bitfield_indices, nullptr, &shape_data); + local_bitfield_indices, nullptr, &shapes_data); loader::finalize(tt); // Testing shape 'Last', used by 'Trip 3' (index == 2) { - auto const shape_by_trip_index = get_shape(tt, shape_data, trip_idx_t{2}); - auto const shape_by_shape_index = get_shape(shape_data, shape_idx_t{3}); + auto const shape_by_trip_idx = shapes_data.get_shape(trip_idx_t{2}); + auto const shape_by_shape_idx = shapes_data.get_shape(shape_idx_t{3}); auto const expected_shape = geo::polyline{ {4.0f, 5.0f}, {5.5f, 2.5f}, {5.5f, 3.0f}, {6.0f, 3.0f}, {5.0f, 2.0f}, {4.0f, 2.0f}, }; - EXPECT_EQ(expected_shape, shape_by_trip_index); - EXPECT_EQ(expected_shape, shape_by_shape_index); + EXPECT_EQ(expected_shape, shape_by_trip_idx); + EXPECT_EQ(expected_shape, shape_by_shape_idx); } // Testing trip without shape, i.e. 'Trip 4' (index == 3) { - auto const shape_by_trip_index = get_shape(tt, shape_data, trip_idx_t{3}); - auto const shape_by_shape_index = - get_shape(shape_data, shape_idx_t::invalid()); + auto const shape_by_trip_idx = shapes_data.get_shape(trip_idx_t{3}); + auto const shape_by_shape_idx = + shapes_data.get_shape(shape_idx_t::invalid()); - EXPECT_TRUE(shape_by_trip_index.empty()); - EXPECT_TRUE(shape_by_shape_index.empty()); + EXPECT_TRUE(shape_by_trip_idx.empty()); + EXPECT_TRUE(shape_by_shape_idx.empty()); + } + + // Testing out of bounds + { + auto const shape_by_huge_trip_idx = shapes_data.get_shape(trip_idx_t{999}); + auto const shape_by_huge_shape_idx = + shapes_data.get_shape(shape_idx_t{999}); + auto const shape_by_invalid_trip_idx = + shapes_data.get_shape(trip_idx_t::invalid()); + auto const shape_by_invalid_shape_idx = + shapes_data.get_shape(shape_idx_t::invalid()); + + EXPECT_TRUE(shape_by_huge_trip_idx.empty()); + EXPECT_TRUE(shape_by_huge_shape_idx.empty()); + EXPECT_TRUE(shape_by_invalid_trip_idx.empty()); + EXPECT_TRUE(shape_by_invalid_shape_idx.empty()); } }