diff --git a/.pkg b/.pkg index 50071b88..7e6a724c 100644 --- a/.pkg +++ b/.pkg @@ -9,7 +9,7 @@ [geo] url=git@github.com:motis-project/geo.git branch=master - commit=0a14addf42e91b267906a156c9c2564935c03eaf + commit=cee17208dba98f5f3ddaf78d0bfff9c083557ba3 [utl] url=git@github.com:motis-project/utl.git branch=master diff --git a/.pkg.lock b/.pkg.lock index 16c65745..f7c27c29 100644 --- a/.pkg.lock +++ b/.pkg.lock @@ -1,24 +1,24 @@ -139770128757021168 -cista 48210e6925658163952e458a4d13629ffdb0cea3 +5786421993668347507 +cista fabfc0cbcf07343a2e0c2def3009555b22f0ae46 PEGTL 1c1aa6e650e4d26f10fa398f148ec0cdc5f0808d res b759b93316afeb529b6cb5b2548b24c41e382fb0 date ce88cc33b5551f66655614eeebb7c5b7189025fb googletest 7b64fca6ea0833628d6f86255a81424365f7cc0c fmt dc10f83be70ac2873d5f8d1ce317596f1fd318a2 -utl 77aac494c45d2b070e65fe712abc34ac74a91d0f +utl 8bfa7fc4edc23f716173bdfbcab8294fcc31e457 oh d21c30f40e52a83d6dc09bcffd0067598b5ec069 -zlib ee0742244d93c4237154ae16c3db42b5f284b442 -boost 60cae66449fa3c9599b2b7d3d5d44c65301ed3a3 +zlib-ng 68ab3e2d80253ec5dc3c83691d9ff70477b32cd3 +boost 73549ebca677fe6214202a1ab580362b4f80e653 doctest 70e8f76437b76dd5e9c0a2eb9b907df190ab71a0 -geo 0a14addf42e91b267906a156c9c2564935c03eaf +geo cee17208dba98f5f3ddaf78d0bfff9c083557ba3 miniz 1edbdece9d71dc65c6ff405572ee37cbdcef7af4 libressl 24acd9e710fbe842e863572da9d738715fbc74b8 -curl 3358dac21192864ef2ba47c88704d3b8d8203804 +curl 39c8a51e8ee0ab7ea712886df79c068405a2e008 json 410c74782230daaa15054d6ee0975c0607091cb3 opentelemetry-proto 1624689398a3226c45994d70cb544a1e781dc032 abseil-cpp ba5240842d352b4b67a32092453a2fe5fe53a62e -protobuf d8136b9c6a62db6ce09900ecdeb82bb793096cbd -opentelemetry-cpp ec4aef6b17b697052edef5417825ad71947b2ed1 +protobuf df2dd518c68b882c9dce5346393f8c388108e733 +opentelemetry-cpp 60770dc9dc63e3543fc87d605b2e88fd53d7a414 pugixml 60175e80e2f5e97e027ac78f7e14c5acc009ce50 unordered_dense 77e91016354e6d8cba24a86c5abb807de2534c02 wyhash 1e012b57fc2227a9e583a57e2eacb3da99816d99 diff --git a/include/nigiri/common/interval.h b/include/nigiri/common/interval.h index 8df73e8a..3dc455b9 100644 --- a/include/nigiri/common/interval.h +++ b/include/nigiri/common/interval.h @@ -47,8 +47,8 @@ struct interval { t_ -= x; return *this; } - iterator operator+(difference_type const x) const { return *this += x; } - iterator operator-(difference_type const x) const { return *this -= x; } + iterator operator+(difference_type const x) const { return {t_ + x}; } + iterator operator-(difference_type const x) const { return {t_ - x}; } friend difference_type operator-(iterator const& a, iterator const& b) { return static_cast(cista::to_idx(a.t_) - cista::to_idx(b.t_)); diff --git a/include/nigiri/loader/gtfs/shape.h b/include/nigiri/loader/gtfs/shape.h index 6b91d6e5..0befbcae 100644 --- a/include/nigiri/loader/gtfs/shape.h +++ b/include/nigiri/loader/gtfs/shape.h @@ -10,10 +10,20 @@ struct shapes_storage; namespace nigiri::loader::gtfs { +using relative_shape_idx_t = + cista::strong; + struct shape_loader_state { + shape_idx_t get_shape_idx(relative_shape_idx_t const i) const { + return shape_idx_t{to_idx(i) + index_offset_}; + } + relative_shape_idx_t get_relative_idx(shape_idx_t const i) const { + return relative_shape_idx_t{to_idx(i) - index_offset_}; + } + hash_map id_map_{}; - std::vector> distances_{}; - shape_idx_t index_offset_; + vector_map> distances_{}; + shape_idx_t::value_t index_offset_; }; shape_loader_state parse_shapes(std::string_view const, shapes_storage&); diff --git a/include/nigiri/loader/gtfs/shape_prepare.h b/include/nigiri/loader/gtfs/shape_prepare.h index 980980e2..8f363828 100644 --- a/include/nigiri/loader/gtfs/shape_prepare.h +++ b/include/nigiri/loader/gtfs/shape_prepare.h @@ -1,18 +1,20 @@ #pragma once -#include "nigiri/loader/gtfs/shape.h" #include "nigiri/loader/gtfs/trip.h" +#include "nigiri/types.h" namespace nigiri { +struct shape_loader_state; struct shapes_storage; struct timetable; } // namespace nigiri namespace nigiri::loader::gtfs { -void calculate_shape_offsets(timetable const&, - shapes_storage&, - vector_map const&, - shape_loader_state const&); +void calculate_shape_offsets_and_bboxes( + timetable const&, + shapes_storage&, + shape_loader_state const&, + vector_map const&); } // namespace nigiri::loader::gtfs diff --git a/include/nigiri/shapes_storage.h b/include/nigiri/shapes_storage.h index 46f5df40..c9050175 100644 --- a/include/nigiri/shapes_storage.h +++ b/include/nigiri/shapes_storage.h @@ -1,10 +1,12 @@ #pragma once #include +#include #include #include "cista/containers/pair.h" +#include "geo/box.h" #include "geo/latlng.h" #include "nigiri/types.h" @@ -23,6 +25,9 @@ struct shapes_storage { shape_offset_idx_t add_offsets(std::vector const&); void add_trip_shape_offsets( trip_idx_t, cista::pair const&); + geo::box get_bounding_box(route_idx_t) const; + std::optional get_bounding_box(route_idx_t, + std::size_t segment) const; cista::mmap::protection mode_; std::filesystem::path p_; @@ -31,6 +36,8 @@ struct shapes_storage { mm_vecvec offsets_; mm_vec_map> trip_offset_indices_; + mm_vec_map route_bboxes_; + mm_vecvec route_segment_bboxes_; }; } // namespace nigiri \ No newline at end of file diff --git a/src/loader/gtfs/load_timetable.cc b/src/loader/gtfs/load_timetable.cc index 42c1339e..2bb0c81c 100644 --- a/src/loader/gtfs/load_timetable.cc +++ b/src/loader/gtfs/load_timetable.cc @@ -261,9 +261,6 @@ 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{}; @@ -367,6 +364,11 @@ void load_timetable(loader_config const& config, progress_tracker->increment(); } + if (shapes_data != nullptr) { + calculate_shape_offsets_and_bboxes(tt, *shapes_data, shape_states, + trip_data.data_); + } + // Build location_routes map for (auto l = tt.location_routes_.size(); l != tt.n_locations(); ++l) { tt.location_routes_.emplace_back(location_routes[location_idx_t{l}]); diff --git a/src/loader/gtfs/shape.cc b/src/loader/gtfs/shape.cc index a2791e53..b1d4d016 100644 --- a/src/loader/gtfs/shape.cc +++ b/src/loader/gtfs/shape.cc @@ -26,12 +26,11 @@ shape_loader_state parse_shapes(std::string_view const data, utl::csv_col distance_; }; - auto const index_offset = static_cast(shapes.size()); auto states = shape_loader_state{ - .index_offset_ = index_offset, + .index_offset_ = static_cast(shapes.size()), }; auto lookup = cached_lookup(states.id_map_); - auto seq = std::vector>{}; + auto seq = vector_map>{}; auto const progress_tracker = utl::get_active_progress_tracker(); progress_tracker->status("Parse Shapes") @@ -49,7 +48,7 @@ shape_loader_state parse_shapes(std::string_view const data, }); auto polyline = shapes[shape_idx]; polyline.push_back(geo::latlng{*entry.lat_, *entry.lon_}); - auto const state_idx = to_idx(shape_idx - index_offset); + auto const state_idx = states.get_relative_idx(shape_idx); seq[state_idx].push_back(*entry.seq_); auto& distances = states.distances_[state_idx]; if (distances.empty()) { @@ -65,11 +64,13 @@ shape_loader_state parse_shapes(std::string_view const data, }); auto polyline = std::vector(); - auto shape_idx = states.index_offset_; - for (auto i = 0U; i != states.distances_.size(); ++i) { + for (auto const i : + interval{relative_shape_idx_t{0U}, + relative_shape_idx_t{states.distances_.size()}}) { if (utl::is_sorted(seq[i], std::less<>{})) { continue; } + auto const shape_idx = states.get_shape_idx(i); polyline.resize(shapes[shape_idx].size()); for (auto j = 0U; j != shapes[shape_idx].size(); ++j) { @@ -79,7 +80,6 @@ shape_loader_state parse_shapes(std::string_view const data, std::tie(seq[i], states.distances_[i], polyline) = utl::sort_by(seq[i], states.distances_[i], polyline); std::copy(begin(polyline), end(polyline), begin(shapes[shape_idx])); - ++shape_idx; } return states; diff --git a/src/loader/gtfs/shape_prepare.cc b/src/loader/gtfs/shape_prepare.cc index a89df1c7..4b51a464 100644 --- a/src/loader/gtfs/shape_prepare.cc +++ b/src/loader/gtfs/shape_prepare.cc @@ -5,16 +5,25 @@ #include #include +#include "cista/strong.h" + +#include "geo/box.h" #include "geo/latlng.h" #include "geo/polyline.h" #include "utl/enumerate.h" -#include "utl/get_or_create.h" +#include "utl/helpers/algorithm.h" +#include "utl/insert_sorted.h" +#include "utl/pairwise.h" +#include "utl/parallel_for.h" #include "utl/progress_tracker.h" +#include "nigiri/loader/gtfs/shape.h" +#include "nigiri/loader/gtfs/trip.h" +#include "nigiri/rt/frun.h" #include "nigiri/shapes_storage.h" #include "nigiri/stop.h" -#include "nigiri/types.h" +#include "nigiri/timetable.h" namespace nigiri::loader::gtfs { @@ -67,71 +76,240 @@ std::vector get_offsets_by_dist_traveled( auto offsets = std::vector{}; offsets.reserve(dist_traveled_stops_times.size()); auto remaining_shape_begin = begin(dist_traveled_shape_edges); + // Ensure final offset maps to valid shape point + auto const shape_end = + begin(dist_traveled_shape_edges) + + (end(dist_traveled_shape_edges) - begin(dist_traveled_shape_edges)) - 1; for (auto const& distance : dist_traveled_stops_times) { - remaining_shape_begin = std::lower_bound( - remaining_shape_begin, end(dist_traveled_shape_edges), distance); + remaining_shape_begin = + std::lower_bound(remaining_shape_begin, shape_end, 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& a, - std::pair const& b) noexcept { - return (a.first == b.first) && (*a.second == *b.second); - }; - auto shape_offsets_cache = - hash_map, shape_offset_idx_t, - decltype(key_hash), decltype(key_compare)>{}; +struct stop_seq_dist { + bool operator<(stop_seq_dist const& o) const { + return *stop_seq_ < *o.stop_seq_; + } + bool operator==(stop_seq_dist const& o) const { + return *stop_seq_ == *o.stop_seq_; + } + bool operator!=(stop_seq_dist const& o) const { + return *stop_seq_ != *o.stop_seq_; + } + + stop_seq_t const* stop_seq_; + std::vector const* dist_traveled_; + struct result { + shape_offset_idx_t shape_offset_idx_{shape_offset_idx_t::invalid()}; + std::vector offsets_; + geo::box trip_bbox_; + std::vector segment_bboxes_; + } result_{}; +}; + +using task = std::vector; + +vector_map create_offset_tasks( + vector_map const& trips, + shape_loader_state const& states) { + auto tasks = vector_map{}; + tasks.resize(static_cast(states.id_map_.size())); + for (auto const& trip : trips) { + if (trip.shape_idx_ == shape_idx_t::invalid()) { + continue; + } + utl::insert_sorted( + tasks[states.get_relative_idx(trip.shape_idx_)], + {&trip.stop_seq_, + trip.distance_traveled_.empty() ? nullptr : &trip.distance_traveled_}); + } + return tasks; +} + +void process_task(timetable const& tt, + shapes_storage const& shapes_data, + shape_loader_state const& shape_states, + relative_shape_idx_t const i, + task& t) { + auto const shape = shapes_data.get_shape(shape_states.get_shape_idx(i)); + auto const& shape_distances = shape_states.distances_[i]; + for (auto& x : t) { + auto& r = x.result_; + auto const& [stop_seq, distances] = std::tie(x.stop_seq_, x.dist_traveled_); + + // Calculate offsets + r.offsets_ = (shape.size() < stop_seq->size()) + ? std::vector{} + : (!shape_distances.empty() && distances != nullptr) + ? get_offsets_by_dist_traveled(*distances, shape_distances) + : get_offsets_by_stops(tt, shape, *stop_seq); + + // Calculate bounding boxes + if (r.offsets_.empty()) { + for (auto const s : *stop_seq) { + r.trip_bbox_.extend(tt.locations_.coordinates_[stop{s}.location_idx()]); + } + } else { + r.segment_bboxes_.resize(r.offsets_.size() - 1); + auto is_trivial = true; + for (auto const [segment_idx, segment] : + utl::enumerate(utl::pairwise(r.offsets_))) { + auto const& [from, to] = segment; + for (auto const point : + shape.subspan(cista::to_idx(from), cista::to_idx(to - from + 1))) { + r.trip_bbox_.extend(point); + r.segment_bboxes_[segment_idx].extend(point); + } + auto const from_l = + tt.locations_ + .coordinates_[stop{(*stop_seq)[segment_idx]}.location_idx()]; + auto const to_l = + tt.locations_.coordinates_[stop{(*stop_seq)[segment_idx + 1]} + .location_idx()]; + auto const stop_bbox = geo::make_box({from_l, to_l}); + if (!stop_bbox.contains(r.segment_bboxes_[segment_idx])) { + is_trivial = false; + } + } + if (is_trivial) { + r.segment_bboxes_.clear(); + } + } + } +} + +void assign_shape_offsets(shapes_storage& shapes_data, + vector_map const& trips, + vector_map& tasks, + shape_loader_state const& states) { + for (auto& task : tasks) { + for (auto& x : task) { + auto& r = x.result_; + if (!r.offsets_.empty()) { + r.shape_offset_idx_ = shapes_data.add_offsets(std::move(r.offsets_)); + } + } + } for (auto const& trip : trips) { - progress_tracker->increment(); auto const trip_idx = trip.trip_idx_; auto const shape_idx = trip.shape_idx_; + if (shape_idx == shape_idx_t::invalid()) { + shapes_data.add_trip_shape_offsets( + trip_idx, cista::pair{shape_idx, shape_offset_idx_t::invalid()}); + } else { + auto const task = tasks[states.get_relative_idx(shape_idx)]; + auto const x = std::ranges::lower_bound( + task, trip.stop_seq_, + [&](stop_seq_t const& a, stop_seq_t const& b) { return a < b; }, + [](stop_seq_dist const& s) { return *s.stop_seq_; }); + if (x != end(task) && + x->result_.shape_offset_idx_ != shape_offset_idx_t::invalid()) { + shapes_data.add_trip_shape_offsets( + trip_idx, cista::pair{shape_idx, x->result_.shape_offset_idx_}); + } else { + shapes_data.add_trip_shape_offsets( + trip_idx, + cista::pair{shape_idx_t::invalid(), shape_offset_idx_t::invalid()}); + } + } + } +} - 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_[to_idx(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); - if (shape.size() < trip.stop_seq_.size()) { - return shape_offset_idx_t::invalid(); // >= 1 shape/point required - } - auto const offsets = get_offsets_by_stops(tt, shape, trip.stop_seq_); - return shapes_data.add_offsets(offsets); +void assign_bounding_boxes(timetable const& tt, + shapes_storage& shapes_data, + vector_map const& tasks, + shape_loader_state const& shape_states) { + auto const new_routes = + interval{static_cast(shapes_data.route_bboxes_.size()), + static_cast(tt.route_transport_ranges_.size())}; + for (auto const r : new_routes) { + auto const seq = tt.route_location_seq_[r]; + assert(seq.size() > 0U); + auto bounding_box = geo::box{}; + auto segment_bboxes = std::vector{}; + segment_bboxes.resize(seq.size() - 1); + auto const stop_indices = + interval{stop_idx_t{0U}, static_cast(seq.size())}; + + // Create basic bounding boxes, span by stops + for (auto const [i, s] : utl::enumerate(stop_indices)) { + auto const pos = tt.locations_.coordinates_[stop{seq[s]}.location_idx()]; + bounding_box.extend(pos); + if (i > 0U) { + segment_bboxes[i - 1U].extend(pos); + } + if (i < segment_bboxes.size()) { + segment_bboxes[i].extend(pos); + } + } + + auto is_trivial = true; + + for (auto const transport_idx : tt.route_transport_ranges_[r]) { + auto const frun = rt::frun{tt, nullptr, + rt::run{.t_ = transport{transport_idx}, + .stop_range_ = stop_indices, + .rt_ = rt_transport_idx_t::invalid()}}; + frun.for_each_trip([&](trip_idx_t const trip_idx, + interval const absolute_range) { + auto const [shape_idx, offset_idx] = + shapes_data.trip_offset_indices_[trip_idx]; + if (shape_idx == shape_idx_t::invalid() || + offset_idx == shape_offset_idx_t::invalid()) { + return; + } + auto const& task = tasks[shape_states.get_relative_idx(shape_idx)]; + auto const it = utl::find_if(task, [&](stop_seq_dist const& s) { + return s.result_.shape_offset_idx_ == offset_idx; }); - shapes_data.add_trip_shape_offsets( - trip_idx, cista::pair{shape_offset_idx == shape_offset_idx_t::invalid() - ? shape_idx_t::invalid() - : shape_idx, - shape_offset_idx}); + if (it == end(task)) { + return; + } + + auto const& res = it->result_; + bounding_box.extend(res.trip_bbox_); + auto const& bboxes = res.segment_bboxes_; + if (!bboxes.empty()) { + for (auto const [i, bbox] : utl::enumerate(bboxes)) { + segment_bboxes[i + cista::to_idx(absolute_range.from_)].extend( + bbox); + } + is_trivial = false; + } + }); + } + + if (is_trivial) { + segment_bboxes.clear(); + } + + shapes_data.route_bboxes_.emplace_back(std::move(bounding_box)); + shapes_data.route_segment_bboxes_.emplace_back(std::move(segment_bboxes)); } } +void calculate_shape_offsets_and_bboxes( + timetable const& tt, + shapes_storage& shapes_data, + shape_loader_state const& shape_states, + vector_map const& trips) { + auto const progress_tracker = utl::get_active_progress_tracker(); + progress_tracker->status("Creating trip offsets") + .out_bounds(98.F, 99.F) + .in_high(shape_states.id_map_.size()); + auto tasks = create_offset_tasks(trips, shape_states); + utl::parallel_for_run(tasks.size(), [&](std::size_t const i) { + auto const s = relative_shape_idx_t{i}; + process_task(tt, shapes_data, shape_states, s, tasks[s]); + }); + + progress_tracker->status("Writing offsets and bounding boxes") + .out_bounds(99.F, 100.F); + assign_shape_offsets(shapes_data, trips, tasks, shape_states); + assign_bounding_boxes(tt, shapes_data, tasks, shape_states); +} + } // namespace nigiri::loader::gtfs \ No newline at end of file diff --git a/src/shapes_storage.cc b/src/shapes_storage.cc index 3d72ce97..969262b2 100644 --- a/src/shapes_storage.cc +++ b/src/shapes_storage.cc @@ -2,8 +2,12 @@ #include +#include "cista/strong.h" + #include "fmt/core.h" +#include "utl/verify.h" + #include "nigiri/types.h" namespace fs = std::filesystem; @@ -23,7 +27,11 @@ shapes_storage::shapes_storage(std::filesystem::path path, mm("platform_ref_index.bin")}}, offsets_{mm_vec{mm("shape_offsets_data.bin")}, mm_vec{mm("shape_offsets_idx.bin")}}, - trip_offset_indices_{mm("shape_trip_offsets.bin")} {} + trip_offset_indices_{mm("shape_trip_offsets.bin")}, + route_bboxes_{mm("shape_route_bboxes.bin")}, + route_segment_bboxes_{ + mm_vec{mm("shape_route_segment_bboxes_data.bin")}, + mm_vec{mm("shape_route_segment_bboxes_idx.bin")}} {} cista::mmap shapes_storage::mm(char const* file) { return cista::mmap{(p_ / file).generic_string().c_str(), mode_}; @@ -86,4 +94,20 @@ void shapes_storage::add_trip_shape_offsets( trip_offset_indices_.emplace_back(offset_idx); } +geo::box shapes_storage::get_bounding_box(route_idx_t const route_idx) const { + utl::verify(cista::to_idx(route_idx) < route_bboxes_.size(), + "Route index {} is out of bounds", route_idx); + return route_bboxes_[route_idx]; +} + +std::optional shapes_storage::get_bounding_box( + nigiri::route_idx_t const route_idx, std::size_t const segment) const { + + utl::verify(cista::to_idx(route_idx) < route_segment_bboxes_.size(), + "Route index {} is out of bounds", route_idx); + auto const& bboxes = route_segment_bboxes_[route_idx]; + return segment < bboxes.size() ? bboxes[segment] + : std::optional{std::nullopt}; +} + } // namespace nigiri \ No newline at end of file diff --git a/test/rt/frun_shape_test.cc b/test/rt/frun_shape_test.cc index 96c9af37..fca96b92 100644 --- a/test/rt/frun_shape_test.cc +++ b/test/rt/frun_shape_test.cc @@ -50,6 +50,7 @@ N,N,3.0,3.0 N1,N1,3.5,3.0 O,O,4.0,4.0 Q,Q,0.0,0.0 +Q+,Q+,-1.0,-1.0 S,S,4.0,1.0 T,T,5.0,1.0 U,U,6.0,2.0 @@ -64,23 +65,32 @@ 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 +ROUTE_2,AGENCY_1,Route 2,,3 +ROUTE_3,AGENCY_1,Route 3,,3 +ROUTE_4,AGENCY_1,Route 4,,3 +ROUTE_5,AGENCY_1,Route 5,,3 +ROUTE_6,AGENCY_1,Route 6,,3 +ROUTE_7,AGENCY_1,Route 7,,3 +ROUTE_8,AGENCY_1,Route 8,,3 +ROUTE_9,AGENCY_1,Route 9,,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, -ROUTE_1,SERVICE_1,TRIP_13,E,BLOCK_8,SHAPE_9, +ROUTE_2,SERVICE_1,TRIP_2,E,BLOCK_2,SHAPE_2, +ROUTE_2,SERVICE_1,TRIP_3,E,BLOCK_2,SHAPE_3, +ROUTE_2,SERVICE_1,TRIP_4,E,BLOCK_2,SHAPE_4, +ROUTE_3,SERVICE_1,TRIP_5,E,BLOCK_3,SHAPE_5, +ROUTE_4,SERVICE_1,TRIP_5+,E,BLOCK_5+,SHAPE_5, +ROUTE_5,SERVICE_1,TRIP_6,E,BLOCK_4,, +ROUTE_5,SERVICE_1,TRIP_6+,E,BLOCK_4,SHAPE_QQ+, +ROUTE_6,SERVICE_1,TRIP_7,E,BLOCK_5,SHAPE_2, +ROUTE_6,SERVICE_1,TRIP_8,E,BLOCK_5,, +ROUTE_6,SERVICE_1,TRIP_9,E,BLOCK_5,, +ROUTE_6,SERVICE_1,TRIP_10,E,BLOCK_5,SHAPE_6, +ROUTE_7,SERVICE_1,TRIP_11,E,BLOCK_11,SHAPE_11, +ROUTE_8,SERVICE_1,TRIP_12,E,BLOCK_12,SHAPE_12, +ROUTE_9,SERVICE_1,TRIP_13,E,BLOCK_13,SHAPE_13, # shapes.txt "shape_id","shape_pt_lat","shape_pt_lon","shape_pt_sequence","shape_dist_traveled" @@ -119,27 +129,30 @@ 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_QQ+,0.0,0.0,1, +SHAPE_QQ+,-0.4,-0.6,2, +SHAPE_QQ+,-1.0,-1.0,2, 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 -SHAPE_9,1.0,1.0,0, -SHAPE_9,2.5,2.5,1, -SHAPE_9,3.625,3.625,2, -SHAPE_9,4.0,4.0,3, -SHAPE_9,4.0,4.0,4, +SHAPE_11,1.0,1.0,0,0.0 +SHAPE_11,1.5,1.5,1,0.7 +SHAPE_11,2.0,2.0,2,1.4 +SHAPE_11,2.5,2.5,3,2.1 +SHAPE_11,3.0,3.0,4,2.9 +SHAPE_11,3.5,3.5,5,3.5 +SHAPE_11,4.0,4.0,6,4.2 +SHAPE_12,1.0,1.0,0,0.0 +SHAPE_12,1.5,1.5,1,0.0 +SHAPE_12,2.0,2.0,2,2.0 +SHAPE_12,3.0,3.0,3,4.0 +SHAPE_13,1.0,1.0,0, +SHAPE_13,2.5,2.5,1, +SHAPE_13,3.625,3.625,2, +SHAPE_13,4.0,4.0,3, +SHAPE_13,4.0,4.0,4, # stop_times.txt trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type,shape_dist_traveled @@ -167,6 +180,8 @@ 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_6+,11:00:00,11:00:00,Q,1,0,0, +TRIP_6+,12:00:00,12: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, @@ -326,7 +341,7 @@ TEST( }), leg_shape); } - // sub trip of a merged trip + // sub trip of a block trip { // Create run transit_realtime::TripDescriptor td; @@ -442,6 +457,20 @@ TEST( leg_shape); } } + // Testing bounding boxes + { + // Full route A -> F -> G -> H -> I -> J -> K (Index 1) + EXPECT_EQ((geo::make_box({{1.0, 0.5}, {5.0, 3.0}})), + shapes_data.get_bounding_box(route_idx_t{1U})); + // Shape segment not contained in simple box + // H -> I + { + auto const segment_box = + shapes_data.get_bounding_box(route_idx_t{1}, 4); + ASSERT_TRUE(segment_box.has_value()); + EXPECT_EQ((geo::make_box({{3.0, 2.5}, {4.0, 3.0}})), *segment_box); + } + } } // Multiple trips, some with and some without shape { @@ -663,6 +692,22 @@ TEST( }), leg_shape); } + // N -> O + // For O: shape < stop_times => Could be out of bounds + { + 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.5F}, + {4.0F, 4.0F}, + }), + leg_shape); + } } // Trip with multiple leading 0.0 distances { @@ -733,6 +778,36 @@ TEST( leg_shape); } } + // Bounding boxes for route containing sequential trips + { + // Trip with bounding boxes defined by stops if followed by trip where + // bounding boxes are extended by shape + // Route 6 + { + auto const r = route_idx_t{5U}; + // Ensure the correct route is used + EXPECT_EQ((geo::make_box({{1.0, 0.5}, {7.0, 3.0}})), + shapes_data.get_bounding_box(r)); + // On 4th trip + auto const extended_bbox = shapes_data.get_bounding_box(r, 6); + ASSERT_TRUE(extended_bbox.has_value()); + EXPECT_EQ((geo::make_box({{6.5, 2.0}, {7.0, 3.0}})), *extended_bbox); + // On 3rd trip + auto const test_bbox = shapes_data.get_bounding_box(r, 5); + ASSERT_TRUE(extended_bbox.has_value()); + EXPECT_EQ((geo::make_box({{6.0, 2.0}, {7.0, 3.0}})), *test_bbox); + } + // Do not insert bounding boxes if sequential trip has no segment bounding + // boxes + { + auto const r = route_idx_t{4U}; + // Ensure the correct route is used + EXPECT_EQ((geo::make_box({{-1.0, -1.0}, {1.0, 1.0}})), + shapes_data.get_bounding_box(r)); + // On 1st trip + ASSERT_FALSE(shapes_data.get_bounding_box(r, 0).has_value()); + } + } } } // namespace \ No newline at end of file diff --git a/test/shape_test.cc b/test/shape_test.cc index 01bec8c3..24076f72 100644 --- a/test/shape_test.cc +++ b/test/shape_test.cc @@ -1,5 +1,6 @@ #include "gtest/gtest.h" +#include "geo/box.h" #include "geo/polyline.h" #include "nigiri/loader/gtfs/load_timetable.h" @@ -73,6 +74,7 @@ Last,5.5,2.5,2 Last,5.5,3.0,3 Last,6.0,3.0,5 Last,5.0,2.0,8 +Last,4.0,1.9,11 Last,4.0,2.0,13 # stop_times.txt @@ -123,8 +125,8 @@ TEST(shape, single_trip_with_shape) { 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}, + {4.0, 5.0}, {5.5, 2.5}, {5.5, 3.0}, {6.0, 3.0}, + {5.0, 2.0}, {4.0, 1.9}, {4.0, 2.0}, }; EXPECT_EQ(expected_shape, shape_by_trip_idx); EXPECT_EQ(expected_shape, shape_by_shape_idx); @@ -155,4 +157,31 @@ TEST(shape, single_trip_with_shape) { EXPECT_TRUE(shape_by_invalid_trip_idx.empty()); EXPECT_TRUE(shape_by_invalid_shape_idx.empty()); } + + // Testing bounding boxes + { + // Full shape in bounding box included + EXPECT_EQ((geo::make_box({{0.0, 2.0}, {1.0, 4.0}})), + shapes_data.get_bounding_box(route_idx_t{0U})); + // Shape in bounding box included + EXPECT_EQ((geo::make_box({{4.0, 1.9}, {6.0, 5.0}})), + shapes_data.get_bounding_box(route_idx_t{2U})); + // Bounding boxes for segments + // Bounding box extended by shape + { + auto const extended_by_shape = + shapes_data.get_bounding_box(route_idx_t{2}, 3); + ASSERT_TRUE(extended_by_shape.has_value()); + EXPECT_EQ((geo::make_box({{4.0, 1.9}, {5.0, 2.0}})), *extended_by_shape); + + auto const before_last_extend = + shapes_data.get_bounding_box(route_idx_t{2}, 2); + ASSERT_TRUE(before_last_extend.has_value()); + EXPECT_EQ((geo::make_box({{5.0, 2.0}, {6.0, 3.0}})), *before_last_extend); + } + // Shape contained in bounding box + { + EXPECT_FALSE(shapes_data.get_bounding_box(route_idx_t{4}, 0).has_value()); + } + } }