Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View file

@ -0,0 +1,28 @@
project(world_feed)
set(SRC
color_picker.cpp
color_picker.hpp
date_time_helpers.cpp
date_time_helpers.hpp
feed_helpers.cpp
feed_helpers.hpp
subway_converter.cpp
subway_converter.hpp
world_feed.cpp
world_feed.hpp
)
omim_add_library(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
generator # routing::transit::DeserializeFromJson
transit
drape_frontend # df::ToDrapeColor, df::LoadTransitColors
opening_hours
)
omim_add_tool_subdirectory(gtfs_converter)
omim_add_test_subdirectory(world_feed_tests)
omim_add_test_subdirectory(world_feed_integration_tests)

View file

@ -0,0 +1,88 @@
#include "transit/world_feed/color_picker.hpp"
#include "drape_frontend/apply_feature_functors.hpp"
#include "drape/color.hpp"
#include "base/string_utils.hpp"
#include <limits>
#include <tuple>
namespace
{
std::tuple<double, double, double> GetColors(dp::Color const & color)
{
return {color.GetRedF(), color.GetGreenF(), color.GetBlueF()};
}
double GetDistance(dp::Color const & color1, dp::Color const & color2)
{
auto [r1, g1, b1] = GetColors(color1);
auto [r2, g2, b2] = GetColors(color2);
// We use the cmetric (Color metric) for calculating the distance between two colors.
// https://en.wikipedia.org/wiki/Color_difference
// It reflects human perception of closest match for a specific colour. The formula weights RGB
// values to better fit eye perception and performs well at proper determinations of colors
// contributions, brightness of these colors, and degree to which human vision has less tolerance
// for these colors.
double const redMean = (r1 + r2) / 2.0;
double const redDelta = r1 - r2;
double const greenDelta = g1 - g2;
double const blueDelta = b1 - b2;
return (2.0 + redMean / 256.0) * redDelta * redDelta + 4 * greenDelta * greenDelta +
(2.0 + (255.0 - redMean) / 256.0) * blueDelta * blueDelta;
}
} // namespace
namespace transit
{
ColorPicker::ColorPicker()
{
df::LoadTransitColors();
// We need only colors for route polylines, not for text. So we skip items like
// 'transit_text_navy' and work only with items like 'transit_navy'.
for (auto const & [name, color] : df::GetTransitClearColors())
if (name.find(df::kTransitTextPrefix) == std::string::npos)
m_drapeClearColors.emplace(name, color);
}
std::string ColorPicker::GetNearestColor(std::string const & rgb)
{
static std::string const kDefaultColor = "default";
if (rgb.empty())
return kDefaultColor;
auto [it, inserted] = m_colorsToNames.emplace(rgb, kDefaultColor);
if (!inserted)
return it->second;
std::string nearestColor = kDefaultColor;
unsigned int intColor;
// We do not need to add to the cache invalid color, so we just return.
if (!strings::to_uint(rgb, intColor, 16))
return nearestColor;
dp::Color const color = df::ToDrapeColor(static_cast<uint32_t>(intColor));
double minDist = std::numeric_limits<double>::max();
for (auto const & [name, transitColor] : m_drapeClearColors)
{
if (double const dist = GetDistance(color, transitColor); dist < minDist)
{
minDist = dist;
nearestColor = name;
}
}
if (nearestColor.find(df::kTransitColorPrefix + df::kTransitLinePrefix) == 0)
nearestColor = nearestColor.substr(df::kTransitColorPrefix.size() + df::kTransitLinePrefix.size());
it->second = nearestColor;
return nearestColor;
}
} // namespace transit

View file

@ -0,0 +1,22 @@
#pragma once
#include "drape_frontend/color_constants.hpp"
#include <map>
#include <string>
#include <unordered_map>
namespace transit
{
class ColorPicker
{
public:
ColorPicker();
// Picks the closest match for the |rgb| color from our transit palette.
std::string GetNearestColor(std::string const & rgb);
private:
std::unordered_map<std::string, std::string> m_colorsToNames;
std::map<std::string, dp::Color> m_drapeClearColors;
};
} // namespace transit

View file

@ -0,0 +1,249 @@
#include "transit/world_feed/date_time_helpers.hpp"
#include "base/assert.hpp"
#include "base/logging.hpp"
#include "base/stl_helpers.hpp"
#include <cstdint>
#include <iosfwd>
#include <tuple>
#include <utility>
#include <boost/date_time/gregorian/gregorian.hpp>
namespace transit
{
osmoh::Time GetTimeOsmoh(gtfs::Time const & gtfsTime)
{
uint16_t hh;
uint16_t mm;
std::tie(hh, mm, std::ignore) = gtfsTime.get_hh_mm_ss();
return osmoh::Time(osmoh::Time::THours(hh) + osmoh::Time::TMinutes(mm));
}
osmoh::RuleSequence GetRuleSequenceOsmoh(gtfs::Time const & start, gtfs::Time const & end)
{
osmoh::RuleSequence ruleSeq;
ruleSeq.SetModifier(osmoh::RuleSequence::Modifier::Open);
osmoh::Timespan range(GetTimeOsmoh(start), GetTimeOsmoh(end));
ruleSeq.SetTimes({range});
return ruleSeq;
}
osmoh::MonthdayRange GetMonthdayRangeFromDates(gtfs::Date const & start, gtfs::Date const & end)
{
osmoh::MonthdayRange range;
SetOpeningHoursRange(range, start, true /* isStart */);
SetOpeningHoursRange(range, end, false /* isStart */);
return range;
}
struct AccumExceptionDates
{
public:
using GregorianInterval = std::pair<boost::gregorian::date, boost::gregorian::date>;
using GtfsInterval = std::pair<gtfs::Date, gtfs::Date>;
void InitIntervals(boost::gregorian::date const & gregorianDate, gtfs::Date const & gtfsDate);
void AddRange();
bool IsInited() const;
GregorianInterval m_GregorianInterval;
GtfsInterval m_GtfsInterval;
osmoh::TMonthdayRanges m_ranges;
private:
bool m_inited = false;
};
void AccumExceptionDates::InitIntervals(boost::gregorian::date const & gregorianDate, gtfs::Date const & gtfsDate)
{
m_GregorianInterval = std::make_pair(gregorianDate, gregorianDate);
m_GtfsInterval = std::make_pair(gtfsDate, gtfsDate);
m_inited = true;
}
void AccumExceptionDates::AddRange()
{
osmoh::MonthdayRange range = GetMonthdayRangeFromDates(m_GtfsInterval.first, m_GtfsInterval.second);
m_ranges.push_back(range);
m_inited = false;
}
bool AccumExceptionDates::IsInited() const
{
return m_inited;
}
osmoh::Weekday ConvertWeekDayIndexToOsmoh(size_t index)
{
// Monday index in osmoh is 2.
index += 2;
if (index == 7)
return osmoh::Weekday::Saturday;
if (index == 8)
return osmoh::Weekday::Sunday;
return osmoh::ToWeekday(index);
}
std::vector<WeekdaysInterval> GetOpenCloseIntervals(std::vector<gtfs::CalendarAvailability> const & week)
{
std::vector<WeekdaysInterval> intervals;
WeekdaysInterval interval;
for (size_t i = 0; i < week.size(); ++i)
{
osmoh::RuleSequence::Modifier const status = week[i] == gtfs::CalendarAvailability::Available
? osmoh::RuleSequence::Modifier::DefaultOpen
: osmoh::RuleSequence::Modifier::Closed;
if (status == interval.m_status)
{
interval.m_end = i;
}
else
{
if (i > 0)
intervals.push_back(interval);
interval.m_start = i;
interval.m_end = i;
interval.m_status = status;
}
if (i == week.size() - 1)
intervals.push_back(interval);
}
return intervals;
}
void SetOpeningHoursRange(osmoh::MonthdayRange & range, gtfs::Date const & date, bool isStart)
{
if (!date.is_provided())
{
LOG(LINFO, ("Date is not provided in the calendar."));
return;
}
auto const & [year, month, day] = date.get_yyyy_mm_dd();
osmoh::MonthDay monthDay;
monthDay.SetYear(year);
monthDay.SetMonth(static_cast<osmoh::MonthDay::Month>(month));
monthDay.SetDayNum(day);
if (isStart)
range.SetStart(monthDay);
else
range.SetEnd(monthDay);
}
void GetServiceDaysOsmoh(gtfs::CalendarItem const & serviceDays, osmoh::TRuleSequences & rules)
{
osmoh::MonthdayRange range = GetMonthdayRangeFromDates(serviceDays.start_date, serviceDays.end_date);
osmoh::TMonthdayRanges const rangesMonths{range};
std::vector<gtfs::CalendarAvailability> const weekDayStatuses = {
serviceDays.monday, serviceDays.tuesday, serviceDays.wednesday, serviceDays.thursday,
serviceDays.friday, serviceDays.saturday, serviceDays.sunday};
auto const & intervals = GetOpenCloseIntervals(weekDayStatuses);
osmoh::RuleSequence ruleSeqOpen;
osmoh::RuleSequence ruleSeqClose;
for (auto const & interval : intervals)
{
osmoh::RuleSequence & ruleSeq =
interval.m_status == osmoh::RuleSequence::Modifier::DefaultOpen ? ruleSeqOpen : ruleSeqClose;
ruleSeq.SetMonths(rangesMonths);
ruleSeq.SetModifier(interval.m_status);
osmoh::WeekdayRange weekDayRange;
weekDayRange.SetStart(ConvertWeekDayIndexToOsmoh(interval.m_start));
weekDayRange.SetEnd(ConvertWeekDayIndexToOsmoh(interval.m_end));
osmoh::TWeekdayRanges weekDayRanges;
weekDayRanges.push_back(weekDayRange);
osmoh::Weekdays weekDays;
weekDays.SetWeekdayRanges(weekDayRanges);
ruleSeq.SetWeekdays(weekDays);
}
if (ruleSeqOpen.HasWeekdays())
rules.push_back(ruleSeqOpen);
if (ruleSeqClose.HasWeekdays())
rules.push_back(ruleSeqClose);
}
void AppendMonthRules(osmoh::RuleSequence::Modifier const & status, osmoh::TMonthdayRanges const & monthRanges,
osmoh::TRuleSequences & rules)
{
osmoh::RuleSequence ruleSeq;
ruleSeq.SetMonths(monthRanges);
ruleSeq.SetModifier(status);
rules.push_back(ruleSeq);
}
void GetServiceDaysExceptionsOsmoh(gtfs::CalendarDates const & exceptionDays, osmoh::TRuleSequences & rules)
{
if (exceptionDays.empty())
return;
AccumExceptionDates accumOpen;
AccumExceptionDates accumClosed;
for (size_t i = 0; i < exceptionDays.size(); ++i)
{
AccumExceptionDates & curAccum =
(exceptionDays[i].exception_type == gtfs::CalendarDateException::Added) ? accumOpen : accumClosed;
auto const [year, month, day] = exceptionDays[i].date.get_yyyy_mm_dd();
boost::gregorian::date const date{year, month, day};
if (!curAccum.IsInited())
{
curAccum.InitIntervals(date, exceptionDays[i].date);
}
else
{
auto & prevDate = curAccum.m_GregorianInterval.second;
boost::gregorian::date_duration duration = date - prevDate;
CHECK(!duration.is_negative(), ());
if (duration.days() == 1)
{
prevDate = date;
curAccum.m_GtfsInterval.second = exceptionDays[i].date;
}
else
{
curAccum.AddRange();
curAccum.InitIntervals(date, exceptionDays[i].date);
}
}
AccumExceptionDates & prevAccum =
(exceptionDays[i].exception_type == gtfs::CalendarDateException::Added) ? accumClosed : accumOpen;
if (prevAccum.IsInited())
prevAccum.AddRange();
if (i == exceptionDays.size() - 1)
curAccum.AddRange();
}
if (!accumOpen.m_ranges.empty())
AppendMonthRules(osmoh::RuleSequence::Modifier::DefaultOpen, accumOpen.m_ranges, rules);
if (!accumClosed.m_ranges.empty())
AppendMonthRules(osmoh::RuleSequence::Modifier::Closed, accumClosed.m_ranges, rules);
}
void MergeRules(osmoh::TRuleSequences & dstRules, osmoh::TRuleSequences const & srcRules)
{
for (auto const & rule : srcRules)
if (!base::IsExist(dstRules, rule))
dstRules.push_back(rule);
}
} // namespace transit

View file

@ -0,0 +1,43 @@
#pragma once
#include <string>
#include <vector>
#include "3party/just_gtfs/just_gtfs.h"
#include "3party/opening_hours/opening_hours.hpp"
namespace transit
{
// Creates osmoh::Time object from GTFS Time |gtfsTime|.
osmoh::Time GetTimeOsmoh(gtfs::Time const & gtfsTime);
// Creates osmoh::RuleSequence with Modifier::Open and osmoh::Timespan with |start| - |end|
// interval.
osmoh::RuleSequence GetRuleSequenceOsmoh(gtfs::Time const & start, gtfs::Time const & end);
// Converts week day |index| in range [0, 6] to the osmoh::Weekday object.
osmoh::Weekday ConvertWeekDayIndexToOsmoh(size_t index);
// Inclusive interval of days and corresponding Open/Closed status.
struct WeekdaysInterval
{
size_t m_start = 0;
size_t m_end = 0;
osmoh::RuleSequence::Modifier m_status = osmoh::RuleSequence::Modifier::DefaultOpen;
};
// Calculates open/closed intervals for |week|.
std::vector<WeekdaysInterval> GetOpenCloseIntervals(std::vector<gtfs::CalendarAvailability> const & week);
// Sets start or end |date| for |range|.
void SetOpeningHoursRange(osmoh::MonthdayRange & range, gtfs::Date const & date, bool isStart);
// Extracts open/closed service days ranges from |serviceDays| to |rules|.
void GetServiceDaysOsmoh(gtfs::CalendarItem const & serviceDays, osmoh::TRuleSequences & rules);
// Extracts open/closed exception service days ranges from |exceptionDays| to |rules|.
void GetServiceDaysExceptionsOsmoh(gtfs::CalendarDates const & exceptionDays, osmoh::TRuleSequences & rules);
// Adds |srcRules| to |dstRules| if they are not present.
void MergeRules(osmoh::TRuleSequences & dstRules, osmoh::TRuleSequences const & srcRules);
} // namespace transit

View file

@ -0,0 +1,539 @@
#include "transit/world_feed/feed_helpers.hpp"
#include "geometry/mercator.hpp"
#include "geometry/parametrized_segment.hpp"
#include "geometry/point2d.hpp"
#include "base/assert.hpp"
#include "base/logging.hpp"
#include "base/stl_helpers.hpp"
#include <cmath>
namespace
{
// Epsilon for m2::PointD comparison.
double constexpr kEps = 1e-5;
struct ProjectionData
{
// Projection to polyline.
m2::PointD m_proj;
// Index before which the projection will be inserted.
size_t m_indexOnShape = 0;
// Distance from point to its projection.
double m_distFromPoint = 0.0;
// Distance from the first ending (start for forward direction, end for backward) point on
// polyline to the projection.
double m_distFromEnding = 0.0;
// Point on polyline almost equal to the projection can already exist, so we don't need to
// insert projection. Or we insert it to the polyline.
bool m_needsInsertion = false;
};
// Returns true if |p1| is much closer to the first ending (start for forward direction, end for
// backward) then |p2| (parameter |distDeltaEnding|) and its distance to projections to polyline
// |m_distFromPoint| is comparable.
bool CloserToEndingAndOnSimilarDistToLine(ProjectionData const & p1, ProjectionData const & p2)
{
// Delta between two points distances from start point on polyline.
double constexpr distDeltaStart = 100.0;
// Delta between two points distances from their corresponding projections to polyline.
double constexpr distDeltaProj = 90.0;
return (p1.m_distFromEnding + distDeltaStart < p2.m_distFromEnding &&
std::abs(p2.m_distFromPoint - p1.m_distFromPoint) <= distDeltaProj);
}
} // namespace
namespace transit
{
ProjectionToShape ProjectStopOnTrack(m2::PointD const & stopPoint, m2::PointD const & point1, m2::PointD const & point2)
{
m2::PointD const stopProjection = m2::ParametrizedSegment<m2::PointD>(point1, point2).ClosestPointTo(stopPoint);
double const distM = mercator::DistanceOnEarth(stopProjection, stopPoint);
return {stopProjection, distM};
}
ProjectionData GetProjection(std::vector<m2::PointD> const & polyline, size_t index, Direction direction,
ProjectionToShape const & proj)
{
ProjectionData projData;
projData.m_distFromPoint = proj.m_dist;
projData.m_proj = proj.m_point;
int64_t const next = direction == Direction::Forward ? index + 1 : index - 1;
CHECK_GREATER_OR_EQUAL(next, 0, ());
CHECK_LESS(static_cast<size_t>(next), polyline.size(), ());
if (AlmostEqualAbs(proj.m_point, polyline[index], kEps))
{
projData.m_indexOnShape = index;
projData.m_needsInsertion = false;
}
else if (AlmostEqualAbs(proj.m_point, polyline[next], kEps))
{
projData.m_indexOnShape = next;
projData.m_needsInsertion = false;
}
else
{
projData.m_indexOnShape = direction == Direction::Forward ? next : index;
projData.m_needsInsertion = true;
}
return projData;
}
void FillProjections(std::vector<m2::PointD> & polyline, size_t startIndex, size_t endIndex, m2::PointD const & point,
double distStopsM, Direction direction, std::vector<ProjectionData> & projections)
{
CHECK_LESS_OR_EQUAL(startIndex, endIndex, ());
double distTravelledM = 0.0;
// Stop can't be further from its projection to line then |maxDistFromStopM|.
double constexpr maxDistFromStopM = 1000;
size_t const from = direction == Direction::Forward ? startIndex : endIndex;
auto const endCriterion = [&](size_t i) { return direction == Direction::Forward ? i < endIndex : i > startIndex; };
auto const move = [&](size_t & i)
{
direction == Direction::Forward ? ++i : --i;
CHECK_LESS_OR_EQUAL(i, polyline.size(), ());
};
for (size_t i = from; endCriterion(i); move(i))
{
auto const current = i;
auto const prev = direction == Direction::Forward ? i - 1 : i + 1;
auto const next = direction == Direction::Forward ? i + 1 : i - 1;
if (i != from)
distTravelledM += mercator::DistanceOnEarth(polyline[prev], polyline[current]);
auto proj =
GetProjection(polyline, current, direction, ProjectStopOnTrack(point, polyline[current], polyline[next]));
proj.m_distFromEnding = distTravelledM + mercator::DistanceOnEarth(polyline[current], proj.m_proj);
// The distance on the polyline between the projections of stops must not be less than the
// shortest possible distance between the stops themselves.
if (proj.m_distFromEnding < distStopsM)
continue;
if (proj.m_distFromPoint < maxDistFromStopM)
projections.emplace_back(proj);
}
}
std::pair<size_t, bool> PrepareNearestPointOnTrack(m2::PointD const & point,
std::optional<m2::PointD> const & prevPoint, size_t prevIndex,
Direction direction, std::vector<m2::PointD> & polyline)
{
// We skip 70% of the distance in a straight line between two stops for preventing incorrect
// projection of the |point| to the polyline of complex shape.
double const distStopsM = prevPoint ? mercator::DistanceOnEarth(point, *prevPoint) * 0.7 : 0.0;
std::vector<ProjectionData> projections;
// Reserve space for points on polyline which are relatively close to the shape.
// Approximately 1/4 of all points on shape.
auto const size = direction == Direction::Forward ? polyline.size() - prevIndex : prevIndex;
projections.reserve(size / 4);
auto const startIndex = direction == Direction::Forward ? prevIndex : 0;
auto const endIndex = direction == Direction::Forward ? polyline.size() - 1 : prevIndex;
FillProjections(polyline, startIndex, endIndex, point, distStopsM, direction, projections);
if (projections.empty())
return {polyline.size() + 1, false};
// We find the most fitting projection of the stop to the polyline. For two different projections
// with approximately equal distances to the stop the most preferable is the one that is closer
// to the beginning of the polyline segment.
auto const cmp = [](ProjectionData const & p1, ProjectionData const & p2)
{
if (CloserToEndingAndOnSimilarDistToLine(p1, p2))
return true;
if (CloserToEndingAndOnSimilarDistToLine(p2, p1))
return false;
if (p1.m_distFromPoint == p2.m_distFromPoint)
return p1.m_distFromEnding < p2.m_distFromEnding;
return p1.m_distFromPoint < p2.m_distFromPoint;
};
auto proj = std::min_element(projections.begin(), projections.end(), cmp);
// This case is possible not only for the first stop on the shape. We try to resolve situation
// when two stops are projected to the same point on the shape.
if (proj->m_indexOnShape == prevIndex)
{
proj = std::min_element(projections.begin(), projections.end(),
[](ProjectionData const & p1, ProjectionData const & p2)
{ return p1.m_distFromPoint < p2.m_distFromPoint; });
}
if (proj->m_needsInsertion)
polyline.insert(polyline.begin() + proj->m_indexOnShape, proj->m_proj);
return {proj->m_indexOnShape, proj->m_needsInsertion};
}
bool IsRelevantType(gtfs::RouteType const & routeType)
{
// All types and constants are described in GTFS:
// https://developers.google.com/transit/gtfs/reference
auto const isSubway = [](gtfs::RouteType const & routeType)
{
return routeType == gtfs::RouteType::Subway || routeType == gtfs::RouteType::MetroService ||
routeType == gtfs::RouteType::UndergroundService;
};
// We skip all subways because we extract subway data from OSM, not from GTFS.
if (isSubway(routeType))
return false;
auto const val = static_cast<size_t>(routeType);
// "Classic" GTFS route types.
if (val < 8 || (val > 10 && val < 13))
return true;
// Extended GTFS route types.
// We do not handle taxi services.
if (val >= 1500)
return false;
// Other not relevant types - school buses, lorry services etc.
static std::vector<gtfs::RouteType> const kNotRelevantTypes{gtfs::RouteType::CarTransportRailService,
gtfs::RouteType::LorryTransportRailService,
gtfs::RouteType::VehicleTransportRailService,
gtfs::RouteType::PostBusService,
gtfs::RouteType::SpecialNeedsBus,
gtfs::RouteType::MobilityBusService,
gtfs::RouteType::MobilityBusForRegisteredDisabled,
gtfs::RouteType::SchoolBus,
gtfs::RouteType::SchoolAndPublicServiceBus};
return !base::IsExist(kNotRelevantTypes, routeType);
}
std::string ToString(gtfs::RouteType const & routeType)
{
// GTFS route types.
switch (routeType)
{
case gtfs::RouteType::Tram: return "tram";
case gtfs::RouteType::Subway: return "subway";
case gtfs::RouteType::Rail: return "rail";
case gtfs::RouteType::Bus: return "bus";
case gtfs::RouteType::Ferry: return "ferry";
case gtfs::RouteType::CableTram: return "cable_tram";
case gtfs::RouteType::AerialLift: return "aerial_lift";
case gtfs::RouteType::Funicular: return "funicular";
case gtfs::RouteType::Trolleybus: return "trolleybus";
case gtfs::RouteType::Monorail: return "monorail";
default:
// Extended GTFS route types.
return ToStringExtendedType(routeType);
}
}
std::string ToStringExtendedType(gtfs::RouteType const & routeType)
{
// These constants refer to extended GTFS routes types.
auto const val = static_cast<size_t>(routeType);
if (val >= 100 && val < 200)
return "rail";
if (val >= 200 && val < 300)
return "bus";
if (val == 405)
return "monorail";
if (val >= 400 && val < 500)
return "rail";
if (val >= 700 && val < 800)
return "bus";
if (val == 800)
return "trolleybus";
if (val >= 900 && val < 1000)
return "tram";
if (val == 1000)
return "water_service";
if (val == 1100)
return "air_service";
if (val == 1200)
return "ferry";
if (val == 1300)
return "aerial_lift";
if (val == 1400)
return "funicular";
LOG(LINFO, ("Unrecognized route type", val));
return {};
}
gtfs::StopTimes GetStopTimesForTrip(gtfs::StopTimes const & allStopTimes, std::string const & tripId)
{
gtfs::StopTime reference;
reference.trip_id = tripId;
auto itStart =
std::lower_bound(allStopTimes.begin(), allStopTimes.end(), reference,
[](gtfs::StopTime const & t1, gtfs::StopTime const & t2) { return t1.trip_id < t2.trip_id; });
if (itStart == allStopTimes.end())
return {};
auto itEnd = itStart;
while (itEnd != allStopTimes.end() && itEnd->trip_id == tripId)
++itEnd;
gtfs::StopTimes res(itStart, itEnd);
std::sort(res.begin(), res.end(),
[](gtfs::StopTime const & t1, gtfs::StopTime const & t2) { return t1.stop_sequence < t2.stop_sequence; });
return res;
}
void UpdateLinePart(LineParts & lineParts, LineSegment const & segment, m2::PointD const & startPoint,
TransitId commonLineId, m2::PointD const & startPointParallel)
{
if (auto it = FindLinePart(lineParts, segment); it == lineParts.end())
{
LinePart lp;
lp.m_segment = segment;
lp.m_commonLines[commonLineId] = startPointParallel;
lp.m_firstPoint = startPoint;
lineParts.push_back(lp);
}
else
{
it->m_commonLines[commonLineId] = startPointParallel;
}
}
std::pair<LineSegments, LineSegments> FindIntersections(std::vector<m2::PointD> const & line1,
std::vector<m2::PointD> const & line2)
{
double constexpr eps = 1e-5;
size_t constexpr minIntersection = 2;
CHECK_GREATER_OR_EQUAL(line1.size(), minIntersection, ());
CHECK_GREATER_OR_EQUAL(line2.size(), minIntersection, ());
std::pair<LineSegments, LineSegments> intersections;
// Find start indexes of line1 and line2 intersections.
size_t i = 0;
while (i < line1.size() - minIntersection + 1)
{
size_t j = 0;
size_t delta = 1;
while (j < line2.size() - minIntersection + 1)
{
size_t intersection = 0;
size_t const len = std::min(line1.size() - i, line2.size() - j);
for (size_t k = 0; k < len; ++k)
{
if (!AlmostEqualAbs(line1[i + k], line2[j + k], eps))
break;
++intersection;
}
if (intersection >= minIntersection)
{
intersections.first.emplace_back(i, i + intersection - 1);
intersections.second.emplace_back(j, j + intersection - 1);
delta = intersection;
break;
}
++j;
}
i += delta;
}
CHECK_EQUAL(intersections.first.size(), intersections.second.size(), ());
return intersections;
}
LineParts::iterator FindLinePart(LineParts & lineParts, LineSegment const & segment)
{
return std::find_if(lineParts.begin(), lineParts.end(),
[&segment](LinePart const & linePart) { return linePart.m_segment == segment; });
}
std::optional<LineSegment> GetIntersection(size_t start1, size_t finish1, size_t start2, size_t finish2)
{
int const maxStart = static_cast<int>(std::max(start1, start2));
int const minFinish = static_cast<int>(std::min(finish1, finish2));
size_t const intersectionLen = std::max(minFinish - maxStart, 0);
if (intersectionLen == 0)
return std::nullopt;
return LineSegment(maxStart, static_cast<uint32_t>(maxStart + intersectionLen));
}
int CalcSegmentOrder(size_t segIndex, size_t totalSegCount)
{
int constexpr shapeOffsetIncrement = 2;
int const shapeOffset = -static_cast<int>(totalSegCount / 2) * 2 - static_cast<int>(totalSegCount % 2) + 1;
int const curSegOffset = shapeOffset + shapeOffsetIncrement * static_cast<int>(segIndex);
return curSegOffset;
}
bool StopIndexIsSet(size_t stopIndex)
{
return stopIndex != std::numeric_limits<size_t>::max();
}
std::pair<size_t, size_t> GetStopsRange(IdList const & lineStopIds, IdSet const & stopIdsInRegion)
{
size_t first = std::numeric_limits<size_t>::max();
size_t last = std::numeric_limits<size_t>::max();
for (size_t i = 0; i < lineStopIds.size(); ++i)
{
auto const & stopId = lineStopIds[i];
if (stopIdsInRegion.count(stopId) != 0)
{
if (!StopIndexIsSet(first))
first = i;
last = i;
}
}
if (StopIndexIsSet(first))
{
if (first > 0)
--first;
if (last < lineStopIds.size() - 1)
++last;
}
CHECK_GREATER_OR_EQUAL(last, first, ());
return {first, last};
}
// Returns indexes of nearest to the |point| elements in |shape|.
std::vector<size_t> GetMinDistIndexes(std::vector<m2::PointD> const & shape, m2::PointD const & point)
{
double minDist = std::numeric_limits<double>::max();
std::vector<size_t> indexes;
for (size_t i = 0; i < shape.size(); ++i)
{
double dist = mercator::DistanceOnEarth(shape[i], point);
if (AlmostEqualAbs(dist, minDist, kEps))
{
indexes.push_back(i);
continue;
}
if (dist < minDist)
{
minDist = dist;
indexes.clear();
indexes.push_back(i);
}
}
CHECK(std::is_sorted(indexes.begin(), indexes.end()), ());
return indexes;
}
// Returns minimal distance between |val| and element in |vals| and the nearest element value.
std::pair<size_t, size_t> FindMinDist(size_t val, std::vector<size_t> const & vals)
{
size_t minDist = std::numeric_limits<size_t>::max();
size_t minVal;
CHECK(!vals.empty(), ());
for (size_t curVal : vals)
{
auto const & [min, max] = std::minmax(val, curVal);
size_t const dist = max - min;
if (dist < minDist)
{
minVal = curVal;
minDist = dist;
}
}
return {minDist, minVal};
}
std::pair<size_t, size_t> FindSegmentOnShape(std::vector<m2::PointD> const & shape,
std::vector<m2::PointD> const & segment)
{
auto const & intersectionsShape = FindIntersections(shape, segment).first;
if (intersectionsShape.empty())
return {0, 0};
auto const & firstIntersection = intersectionsShape.front();
return {firstIntersection.m_startIdx, firstIntersection.m_endIdx};
}
std::pair<size_t, size_t> FindPointsOnShape(std::vector<m2::PointD> const & shape, m2::PointD const & p1,
m2::PointD const & p2)
{
// We find indexes of nearest points in |shape| to |p1| and |p2| correspondingly.
std::vector<size_t> const & indexes1 = GetMinDistIndexes(shape, p1);
std::vector<size_t> const & indexes2 = GetMinDistIndexes(shape, p2);
// We fill mapping of distance (between p1 and p2 on the shape) to pairs of indexes of p1 and p2.
std::map<size_t, std::pair<size_t, size_t>> distToIndexes;
for (size_t i1 : indexes1)
{
auto [minDist, i2] = FindMinDist(i1, indexes2);
distToIndexes.emplace(minDist, std::make_pair(i1, i2));
}
CHECK(!distToIndexes.empty(), ());
// If index of |p1| equals index of |p2| on the |shape| we return the next pair of the nearest
// indexes. It is possible in case if |p1| and |p2| are ends of the edge which represents the loop
// on the route.
auto const & [first, last] = distToIndexes.begin()->second;
if (first == last)
{
LOG(LINFO, ("Edge with equal indexes of first and last points on the shape. Index on the shape:", first));
CHECK_GREATER(distToIndexes.size(), 1, ());
auto const & nextPair = std::next(distToIndexes.begin());
CHECK_NOT_EQUAL(nextPair->second.first, nextPair->second.second, ());
return nextPair->second;
}
return distToIndexes.begin()->second;
}
} // namespace transit

View file

@ -0,0 +1,150 @@
#pragma once
#include "transit/transit_entities.hpp"
#include "geometry/point2d.hpp"
#include <algorithm>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "3party/just_gtfs/just_gtfs.h"
namespace transit
{
// Projection point and mercator distance to it.
struct ProjectionToShape
{
m2::PointD m_point;
double m_dist;
};
enum class Direction
{
Forward,
Backward
};
/// \returns |stopPoint| projection to the track segment [|point1|, |point2|] and
/// distance from the |stopPoint| to its projection.
ProjectionToShape ProjectStopOnTrack(m2::PointD const & stopPoint, m2::PointD const & point1,
m2::PointD const & point2);
/// \returns index of the nearest track point to the |point| and flag if it was inserted to the
/// shape. If this index doesn't match already existent points, the stop projection is inserted to
/// the |polyline| and the flag is set to true. New point should follow prevPoint in the direction
/// |direction|.
std::pair<size_t, bool> PrepareNearestPointOnTrack(m2::PointD const & point,
std::optional<m2::PointD> const & prevPoint, size_t prevIndex,
Direction direction, std::vector<m2::PointD> & polyline);
/// \returns true if we should not skip routes with this GTFS |routeType|.
bool IsRelevantType(gtfs::RouteType const & routeType);
/// \return string representation of the GTFS |routeType|.
std::string ToString(gtfs::RouteType const & routeType);
/// \return string representation of the extended GTFS |routeType|.
std::string ToStringExtendedType(gtfs::RouteType const & routeType);
/// \return stop times for trip with |tripId|.
gtfs::StopTimes GetStopTimesForTrip(gtfs::StopTimes const & allStopTimes, std::string const & tripId);
// Delete item from the |container| by its key.
template <class C, class K>
void DeleteIfExists(C & container, K const & key)
{
auto it = container.find(key);
if (it != container.end())
container.erase(it);
}
template <class K>
void DeleteIfExists(std::vector<K> & container, K const & key)
{
auto it = std::find(container.begin(), container.end(), key);
if (it != container.end())
container.erase(it);
}
// Delete items by keys in |keysForDel| from the |container|.
template <class C, class S>
void DeleteAllEntriesByIds(C & container, S const & keysForDel)
{
for (auto const & key : keysForDel)
DeleteIfExists(container, key);
}
// We have routes with multiple lines. Each line corresponds to the geometric polyline. Lines may
// be parallel in some segments. |LinePart| represents these operlapping segments for each line.
struct LinePart
{
// Start and end indexes on polyline.
LineSegment m_segment;
// Parallel line ids to its start points on the segment.
std::map<TransitId, m2::PointD> m_commonLines;
// First coordinate of current line on the segment. It is used for determining if the line is
// co-directional or reversed regarding the main line on the segment.
m2::PointD m_firstPoint;
};
using LineParts = std::vector<LinePart>;
// Returns iterator to the line part with equivalent segment.
LineParts::iterator FindLinePart(LineParts & lineParts, LineSegment const & segment);
// Data required for finding parallel polyline segments and calculating offsets for each line on the
// segment.
struct LineSchemeData
{
TransitId m_lineId = 0;
std::string m_color;
ShapeLink m_shapeLink;
LineParts m_lineParts;
};
// Returns overlapping segments between two polylines.
std::pair<LineSegments, LineSegments> FindIntersections(std::vector<m2::PointD> const & line1,
std::vector<m2::PointD> const & line2);
// Finds item in |lineParts| equal to |segment| and updates it. If it doesn't exist it is added to
// the |lineParts|.
void UpdateLinePart(LineParts & lineParts, LineSegment const & segment, m2::PointD const & startPoint,
TransitId commonLineId, m2::PointD const & startPointParallel);
// Calculates start and end indexes of intersection of two segments: [start1, finish1] and [start2,
// finish2].
std::optional<LineSegment> GetIntersection(size_t start1, size_t finish1, size_t start2, size_t finish2);
// Calculates line order on segment based on two parameters: line index between all parallel lines,
// total parallel lines count. Line order must be symmetrical with respect to the сentral axis of
// the polyline.
int CalcSegmentOrder(size_t segIndex, size_t totalSegCount);
// Returns true if |stopIndex| doesn't equal size_t maximum value.
bool StopIndexIsSet(size_t stopIndex);
// Gets interval of stop indexes on the line with |lineStopIds| which belong to the region and
// its vicinity. |stopIdsInRegion| is set of all stop ids in the region.
std::pair<size_t, size_t> GetStopsRange(IdList const & lineStopIds, IdSet const & stopIdsInRegion);
// Returns indexes of points |p1| and |p2| on the |shape| polyline. If there are more then 1
// occurrences of |p1| or |p2| in |shape|, indexes with minimum distance are returned.
std::pair<size_t, size_t> FindPointsOnShape(std::vector<m2::PointD> const & shape, m2::PointD const & p1,
m2::PointD const & p2);
// Returns indexes of first and last points of |segment| in the |shape| polyline. If |edgeShape|
// is not found returns pair of zeroes.
std::pair<size_t, size_t> FindSegmentOnShape(std::vector<m2::PointD> const & shape,
std::vector<m2::PointD> const & segment);
// Returns reversed vector.
template <class T>
std::vector<T> GetReversed(std::vector<T> vec)
{
std::reverse(vec.begin(), vec.end());
return vec;
}
} // namespace transit

View file

@ -0,0 +1,10 @@
project(gtfs_converter)
omim_add_executable(${PROJECT_NAME} gtfs_converter.cpp)
target_link_libraries(${PROJECT_NAME}
world_feed
generator
platform
gflags::gflags
)

View file

@ -0,0 +1,322 @@
#include "generator/affiliation.hpp"
#include "transit/world_feed/color_picker.hpp"
#include "transit/world_feed/subway_converter.hpp"
#include "transit/world_feed/world_feed.hpp"
#include "platform/platform.hpp"
#include "base/assert.hpp"
#include "base/file_name_utils.hpp"
#include "base/logging.hpp"
#include "base/timer.hpp"
#include <gflags/gflags.h>
DEFINE_string(path_mapping, "",
"Path to the mapping file of TransitId to GTFS hash for all transit entities except edges");
DEFINE_string(path_mapping_edges, "", "Path to the mapping file of TransitId to GTFS hash for edges");
// One of these two paths should be specified: |path_gtfs_feeds| and/or |path_subway_json|.
DEFINE_string(path_gtfs_feeds, "", "Directory with GTFS feeds subdirectories");
DEFINE_string(path_subway_json, "", "OMaps json file with subway data from OSM");
DEFINE_string(path_json, "", "Output directory for dumping json files");
DEFINE_string(path_resources, "", "OMaps resources directory");
DEFINE_string(start_feed, "", "Optional. Feed directory from which the process continues");
DEFINE_string(stop_feed, "", "Optional. Feed directory on which to stop the process");
// Finds subdirectories with feeds.
Platform::FilesList GetGtfsFeedsInDirectory(std::string const & path)
{
Platform::FilesList res;
Platform::TFilesWithType gtfsList;
Platform::GetFilesByType(path, Platform::EFileType::Directory, gtfsList);
for (auto const & item : gtfsList)
{
auto const & gtfsFeedDir = item.first;
if (gtfsFeedDir != "." && gtfsFeedDir != "..")
res.push_back(base::JoinPath(path, gtfsFeedDir));
}
return res;
}
// Handles the case when the directory consists of a single subdirectory with GTFS files.
void ExtendPath(std::string & path)
{
Platform::TFilesWithType csvFiles;
Platform::GetFilesByType(path, Platform::EFileType::Regular, csvFiles);
if (!csvFiles.empty())
return;
Platform::TFilesWithType subdirs;
Platform::GetFilesByType(path, Platform::EFileType::Directory, subdirs);
// If there are more subdirectories then ".", ".." and directory with feed, the feed is most
// likely corrupted.
if (subdirs.size() > 3)
return;
for (auto const & item : subdirs)
{
auto const & subdir = item.first;
if (subdir != "." && subdir != "..")
{
path = base::JoinPath(path, subdir);
LOG(LDEBUG, ("Found subdirectory with feed", path));
return;
}
}
}
bool SkipFeed(std::string const & feedPath, bool & pass)
{
if (!FLAGS_start_feed.empty() && pass)
{
if (base::FileNameFromFullPath(feedPath) != FLAGS_start_feed)
return true;
pass = false;
}
return false;
}
bool StopOnFeed(std::string const & feedPath)
{
if (!FLAGS_stop_feed.empty() && base::FileNameFromFullPath(feedPath) == FLAGS_stop_feed)
{
LOG(LINFO, ("Stop on", feedPath));
return true;
}
return false;
}
enum class FeedStatus
{
OK = 0,
CORRUPTED,
NO_SHAPES
};
FeedStatus ReadFeed(gtfs::Feed & feed)
{
// First we read shapes. If there are no shapes in feed we do not need to read all the required
// files - agencies, stops, etc.
if (auto res = feed.read_shapes(); res != gtfs::ResultCode::OK)
{
LOG(LWARNING, ("Could not get shapes.", res.message));
return FeedStatus::NO_SHAPES;
}
if (feed.get_shapes().empty())
return FeedStatus::NO_SHAPES;
// We try to parse required for json files and return error in case of invalid file content.
if (auto res = feed.read_agencies(); res != gtfs::ResultCode::OK)
{
LOG(LWARNING, ("Could not parse agencies.", res.message));
return FeedStatus::CORRUPTED;
}
if (auto res = feed.read_routes(); res != gtfs::ResultCode::OK)
{
LOG(LWARNING, ("Could not parse routes.", res.message));
return FeedStatus::CORRUPTED;
}
if (auto res = feed.read_trips(); res != gtfs::ResultCode::OK)
{
LOG(LWARNING, ("Could not parse trips.", res.message));
return FeedStatus::CORRUPTED;
}
if (auto res = feed.read_stops(); res != gtfs::ResultCode::OK)
{
LOG(LWARNING, ("Could not parse stops.", res.message));
return FeedStatus::CORRUPTED;
}
if (auto res = feed.read_stop_times(); res != gtfs::ResultCode::OK)
{
LOG(LWARNING, ("Could not parse stop times.", res.message));
return FeedStatus::CORRUPTED;
}
// We try to parse optional for json files and do not return error in case of invalid file
// content, only log warning message.
if (auto res = feed.read_calendar(); gtfs::ErrorParsingOptionalFile(res))
LOG(LINFO, ("Could not parse calendar.", res.message));
if (auto res = feed.read_calendar_dates(); gtfs::ErrorParsingOptionalFile(res))
LOG(LINFO, ("Could not parse calendar dates.", res.message));
if (auto res = feed.read_frequencies(); gtfs::ErrorParsingOptionalFile(res))
LOG(LINFO, ("Could not parse frequencies.", res.message));
if (auto res = feed.read_transfers(); gtfs::ErrorParsingOptionalFile(res))
LOG(LINFO, ("Could not parse transfers.", res.message));
if (feed.read_feed_info() == gtfs::ResultCode::OK)
LOG(LINFO, ("Feed info is present."));
return FeedStatus::OK;
}
// Reads GTFS feeds from directories in |FLAGS_path_gtfs_feeds|. Converts each feed to the WorldFeed
// object and saves to the |FLAGS_path_json| path in the new transit line-by-line json format.
bool ConvertFeeds(transit::IdGenerator & generator, transit::IdGenerator & generatorEdges,
transit::ColorPicker & colorPicker, feature::CountriesFilesAffiliation & mwmMatcher)
{
auto const gtfsFeeds = GetGtfsFeedsInDirectory(FLAGS_path_gtfs_feeds);
if (gtfsFeeds.empty())
{
LOG(LERROR, ("No subdirectories with GTFS feeds found in", FLAGS_path_gtfs_feeds));
return false;
}
std::vector<std::string> invalidFeeds;
size_t feedsWithNoShapesCount = 0;
size_t feedsNotDumpedCount = 0;
size_t feedsDumped = 0;
size_t feedsTotal = gtfsFeeds.size();
bool pass = true;
for (size_t i = 0; i < gtfsFeeds.size(); ++i)
{
base::Timer feedTimer;
auto feedPath = gtfsFeeds[i];
if (SkipFeed(feedPath, pass))
{
++feedsTotal;
LOG(LINFO, ("Skipped", feedPath));
continue;
}
bool stop = StopOnFeed(feedPath);
if (stop)
feedsTotal -= (gtfsFeeds.size() - i - 1);
ExtendPath(feedPath);
LOG(LINFO, ("Handling feed", feedPath));
gtfs::Feed feed(feedPath);
if (auto const res = ReadFeed(feed); res != FeedStatus::OK)
{
if (res == FeedStatus::NO_SHAPES)
feedsWithNoShapesCount++;
else
invalidFeeds.push_back(feedPath);
if (stop)
break;
continue;
}
transit::WorldFeed globalFeed(generator, generatorEdges, colorPicker, mwmMatcher);
if (!globalFeed.SetFeed(std::move(feed)))
{
LOG(LINFO, ("Error transforming feed for json representation."));
++feedsNotDumpedCount;
if (stop)
break;
continue;
}
bool const saved = globalFeed.Save(FLAGS_path_json, i == 0 /* overwrite */);
if (saved)
++feedsDumped;
else
++feedsNotDumpedCount;
LOG(LINFO, ("Merged:", saved ? "yes" : "no", "time", feedTimer.ElapsedSeconds(), "s"));
if (stop)
break;
}
LOG(LINFO, ("Corrupted feeds paths:", invalidFeeds));
LOG(LINFO, ("Corrupted feeds:", invalidFeeds.size(), "/", feedsTotal));
LOG(LINFO, ("Feeds with no shapes:", feedsWithNoShapesCount, "/", feedsTotal));
LOG(LINFO, ("Feeds parsed but not dumped:", feedsNotDumpedCount, "/", feedsTotal));
LOG(LINFO, ("Total dumped feeds:", feedsDumped, "/", feedsTotal));
return true;
}
// Reads subway json from |FLAGS_path_subway_json|, converts it to the WorldFeed object and saves
// to the |FLAGS_path_json| path in the new transit line-by-line json format.
bool ConvertSubway(transit::IdGenerator & generator, transit::IdGenerator & generatorEdges,
transit::ColorPicker & colorPicker, feature::CountriesFilesAffiliation & mwmMatcher, bool overwrite)
{
transit::WorldFeed globalFeed(generator, generatorEdges, colorPicker, mwmMatcher);
transit::SubwayConverter converter(FLAGS_path_subway_json, globalFeed);
if (!converter.Convert())
return false;
globalFeed.Save(FLAGS_path_json, overwrite);
return true;
}
int main(int argc, char ** argv)
{
gflags::SetUsageMessage("Reads GTFS feeds or subway transit.json, produces json with global ids for generator.");
gflags::ParseCommandLineFlags(&argc, &argv, true);
auto const toolName = base::FileNameFromFullPath(argv[0]);
if (FLAGS_path_gtfs_feeds.empty() && FLAGS_path_subway_json.empty())
{
LOG(LWARNING, ("Path to GTFS feeds directory or path to the subways json must be specified."));
gflags::ShowUsageWithFlagsRestrict(argv[0], toolName.c_str());
return EXIT_FAILURE;
}
if (FLAGS_path_mapping.empty() || FLAGS_path_mapping_edges.empty() || FLAGS_path_json.empty())
{
LOG(LWARNING, ("Some of the required options are not present."));
gflags::ShowUsageWithFlagsRestrict(argv[0], toolName.c_str());
return EXIT_FAILURE;
}
if ((!FLAGS_path_gtfs_feeds.empty() && !Platform::IsDirectory(FLAGS_path_gtfs_feeds)) ||
!Platform::IsDirectory(FLAGS_path_json) || !Platform::IsDirectory(FLAGS_path_resources) ||
(!FLAGS_path_subway_json.empty() && !Platform::IsFileExistsByFullPath(FLAGS_path_subway_json)))
{
LOG(LWARNING, ("Some paths set in options are not valid. Check the directories:", FLAGS_path_gtfs_feeds,
FLAGS_path_json, FLAGS_path_resources));
gflags::ShowUsageWithFlagsRestrict(argv[0], toolName.c_str());
return EXIT_FAILURE;
}
transit::IdGenerator generator(FLAGS_path_mapping);
transit::IdGenerator generatorEdges(FLAGS_path_mapping_edges);
GetPlatform().SetResourceDir(FLAGS_path_resources);
transit::ColorPicker colorPicker;
feature::CountriesFilesAffiliation mwmMatcher(GetPlatform().ResourcesDir(), false /* haveBordersForWholeWorld */);
// We convert GTFS feeds to the json format suitable for generator_tool and save it to the
// corresponding directory.
if (!FLAGS_path_gtfs_feeds.empty() && !ConvertFeeds(generator, generatorEdges, colorPicker, mwmMatcher))
return EXIT_FAILURE;
// We mixin data in our "old transit" (in fact subway-only) json format to the resulting files
// in experimental line-by-line json format which is processed by generator_tool for building
// experimental transit section. We use the same id |generator| so ids of subway and GTFS
// itineraries will not conflict.
if (!FLAGS_path_subway_json.empty() &&
!ConvertSubway(generator, generatorEdges, colorPicker, mwmMatcher, FLAGS_path_gtfs_feeds.empty() /* overwrite */))
{
return EXIT_FAILURE;
}
generator.Save();
return EXIT_SUCCESS;
}

View file

@ -0,0 +1,810 @@
#include "transit/world_feed/subway_converter.hpp"
#include "generator/transit_generator.hpp"
#include "routing/fake_feature_ids.hpp"
#include "base/assert.hpp"
#include "base/logging.hpp"
#include <algorithm>
#include <iterator>
#include <limits>
namespace transit
{
std::string const kHashPrefix = "mapsme_transit";
std::string const kDefaultLang = "default";
std::string const kSubwayRouteType = "subway";
namespace
{
double constexpr kEps = 1e-5;
// Returns route id of the line. Route id is calculated in the same way as in the script
// tools/transit/transit_graph_generator.py.
uint32_t GetSubwayRouteId(routing::transit::LineId lineId)
{
return static_cast<uint32_t>(lineId >> 4);
}
// Increments |lineSegment| indexes by |shapeLink| start index.
void ShiftSegmentOnShape(transit::LineSegment & lineSegment, transit::ShapeLink const & shapeLink)
{
lineSegment.m_startIdx += shapeLink.m_startIndex;
lineSegment.m_endIdx += shapeLink.m_startIndex;
}
// Returns segment edge points on the polyline.
std::pair<m2::PointD, m2::PointD> GetSegmentEdgesOnPolyline(std::vector<m2::PointD> const & polyline,
transit::LineSegment const & segment)
{
CHECK_GREATER(polyline.size(), std::max(segment.m_startIdx, segment.m_endIdx), ());
m2::PointD const startPoint = polyline[segment.m_startIdx];
m2::PointD const endPoint = polyline[segment.m_endIdx];
return {startPoint, endPoint};
}
// Calculates |segment| start and end indexes on the polyline with length |polylineSize| in
// assumption that this segment is reversed. Example: we have polyline [1, 2, 3, 4, 5, 6] and
// segment [5, 4]. We reversed this segment so it transformed to [4, 5] and found it on polyline.
// Its start and end indexes on the polyline are 3, 4. We want to calculate start and end indexes of
// the original segment [5, 4]. These indexes are 4, 3.
void UpdateReversedSegmentIndexes(transit::LineSegment & segment, size_t polylineSize)
{
size_t const len = segment.m_endIdx - segment.m_startIdx + 1;
segment.m_endIdx = static_cast<uint32_t>(polylineSize - segment.m_startIdx - 1);
segment.m_startIdx = static_cast<uint32_t>(segment.m_endIdx - len + 1);
CHECK_GREATER(segment.m_endIdx, segment.m_startIdx, ());
CHECK_GREATER(polylineSize, segment.m_endIdx, ());
}
} // namespace
SubwayConverter::SubwayConverter(std::string const & subwayJson, WorldFeed & feed)
: m_subwayJson(subwayJson)
, m_feed(feed)
{}
bool SubwayConverter::Convert()
{
routing::transit::OsmIdToFeatureIdsMap emptyMapping;
routing::transit::DeserializeFromJson(emptyMapping, m_subwayJson, m_graphData);
if (!ConvertNetworks())
return false;
if (!SplitEdges())
return false;
if (!ConvertLinesBasedData())
return false;
m_feed.ModifyLinesAndShapes();
MinimizeReversedLinesCount();
if (!ConvertStops())
return false;
ConvertTransfers();
// In contrast to the GTFS gates OSM gates for subways shouldn't be empty.
if (!ConvertGates())
return false;
if (!ConvertEdges())
return false;
m_feed.SplitFeedIntoRegions();
PrepareLinesMetadata();
return true;
}
bool SubwayConverter::ConvertNetworks()
{
auto const & networksSubway = m_graphData.GetNetworks();
m_feed.m_networks.m_data.reserve(networksSubway.size());
for (auto const & networkSubway : networksSubway)
{
// Subway network id is city id index approximately in interval (0, 400).
TransitId const networkId = networkSubway.GetId();
CHECK(!routing::FakeFeatureIds::IsTransitFeature(networkId), (networkId));
m_feed.m_networks.m_data.emplace(networkId, networkSubway.GetTitle());
}
LOG(LINFO, ("Converted", m_feed.m_networks.m_data.size(), "networks from subways to public transport."));
return !m_feed.m_networks.m_data.empty();
}
bool SubwayConverter::SplitEdges()
{
auto & edgesSubway = m_graphData.GetEdges();
for (size_t i = 0; i < edgesSubway.size(); ++i)
{
auto const & edgeSubway = edgesSubway[i];
if (edgeSubway.GetTransfer())
m_edgesTransferSubway.emplace(edgeSubway, i);
else
m_edgesSubway.emplace(edgeSubway, i);
}
return !m_edgesSubway.empty() && !m_edgesTransferSubway.empty();
}
std::pair<TransitId, RouteData> SubwayConverter::MakeRoute(routing::transit::Line const & lineSubway)
{
uint32_t routeSubwayId = GetSubwayRouteId(lineSubway.GetId());
std::string const routeHash =
BuildHash(kHashPrefix, std::to_string(lineSubway.GetNetworkId()), std::to_string(routeSubwayId));
TransitId const routeId = m_feed.m_idGenerator.MakeId(routeHash);
RouteData routeData;
routeData.m_title = lineSubway.GetNumber();
routeData.m_routeType = kSubwayRouteType;
routeData.m_networkId = lineSubway.GetNetworkId();
routeData.m_color = lineSubway.GetColor();
return {routeId, routeData};
}
std::pair<TransitId, GateData> SubwayConverter::MakeGate(routing::transit::Gate const & gateSubway)
{
// This id is used only for storing gates in gtfs_converter tool. It is not saved to json.
TransitId const gateId = m_feed.m_idGenerator.MakeId(BuildHash(kHashPrefix, std::to_string(gateSubway.GetOsmId())));
GateData gateData;
gateData.m_isEntrance = gateSubway.GetEntrance();
gateData.m_isExit = gateSubway.GetExit();
gateData.m_point = gateSubway.GetPoint();
gateData.m_osmId = gateSubway.GetOsmId();
for (auto stopIdSubway : gateSubway.GetStopIds())
{
gateData.m_weights.emplace_back(
TimeFromGateToStop(m_stopIdMapping[stopIdSubway] /* stopId */, gateSubway.GetWeight() /* timeSeconds */));
}
return {gateId, gateData};
}
std::pair<TransitId, TransferData> SubwayConverter::MakeTransfer(routing::transit::Transfer const & transferSubway)
{
TransitId const transferId =
m_feed.m_idGenerator.MakeId(BuildHash(kHashPrefix, std::to_string(transferSubway.GetId())));
TransferData transferData;
transferData.m_point = transferSubway.GetPoint();
for (auto stopIdSubway : transferSubway.GetStopIds())
transferData.m_stopsIds.emplace_back(m_stopIdMapping[stopIdSubway]);
return {transferId, transferData};
}
std::pair<TransitId, LineData> SubwayConverter::MakeLine(routing::transit::Line const & lineSubway, TransitId routeId)
{
TransitId const lineId = lineSubway.GetId();
CHECK(!routing::FakeFeatureIds::IsTransitFeature(lineId), (lineId));
LineData lineData;
lineData.m_routeId = routeId;
lineData.m_title = lineSubway.GetTitle();
lineData.m_schedule.SetDefaultFrequency(lineSubway.GetInterval());
return {lineId, lineData};
}
std::pair<EdgeId, EdgeData> SubwayConverter::MakeEdge(routing::transit::Edge const & edgeSubway, uint32_t index)
{
auto const lineId = edgeSubway.GetLineId();
EdgeId const edgeId(m_stopIdMapping[edgeSubway.GetStop1Id()], m_stopIdMapping[edgeSubway.GetStop2Id()], lineId);
EdgeData edgeData;
edgeData.m_weight = edgeSubway.GetWeight();
edgeData.m_featureId = index;
CHECK(m_feed.m_edgesOnShapes.find(edgeId) != m_feed.m_edgesOnShapes.end(), (lineId));
edgeData.m_shapeLink.m_shapeId = m_feed.m_lines.m_data[lineId].m_shapeLink.m_shapeId;
return {edgeId, edgeData};
}
std::pair<EdgeTransferId, EdgeData> SubwayConverter::MakeEdgeTransfer(routing::transit::Edge const & edgeSubway,
uint32_t index)
{
EdgeTransferId const edgeTransferId(m_stopIdMapping[edgeSubway.GetStop1Id()] /* fromStopId */,
m_stopIdMapping[edgeSubway.GetStop2Id()] /* toStopId */);
EdgeData edgeData;
edgeData.m_weight = edgeSubway.GetWeight();
edgeData.m_featureId = index;
return {edgeTransferId, edgeData};
}
std::pair<TransitId, StopData> SubwayConverter::MakeStop(routing::transit::Stop const & stopSubway)
{
TransitId const stopId = m_stopIdMapping[stopSubway.GetId()];
StopData stopData;
stopData.m_point = stopSubway.GetPoint();
stopData.m_osmId = stopSubway.GetOsmId();
if (stopSubway.GetFeatureId() != kInvalidFeatureId)
stopData.m_featureId = stopSubway.GetFeatureId();
return {stopId, stopData};
}
bool SubwayConverter::ConvertLinesBasedData()
{
auto const & linesSubway = m_graphData.GetLines();
m_feed.m_lines.m_data.reserve(linesSubway.size());
m_feed.m_shapes.m_data.reserve(linesSubway.size());
auto const & shapesSubway = m_graphData.GetShapes();
for (auto const & lineSubway : linesSubway)
{
auto const [routeId, routeData] = MakeRoute(lineSubway);
m_feed.m_routes.m_data.emplace(routeId, routeData);
auto [lineId, lineData] = MakeLine(lineSubway, routeId);
TransitId const shapeId =
m_feed.m_idGenerator.MakeId(BuildHash(kHashPrefix, std::string("shape"), std::to_string(lineId)));
lineData.m_shapeId = shapeId;
ShapeData shapeData;
shapeData.m_lineIds.insert(lineId);
CHECK_EQUAL(lineSubway.GetStopIds().size(), 1, ("Line shouldn't be split into ranges."));
auto const & stopIdsSubway = lineSubway.GetStopIds().front();
CHECK_GREATER(stopIdsSubway.size(), 1, ("Range must include at least two stops."));
for (size_t i = 0; i < stopIdsSubway.size(); ++i)
{
auto const stopIdSubway = stopIdsSubway[i];
std::string const stopHash = BuildHash(kHashPrefix, std::to_string(stopIdSubway));
TransitId const stopId = m_feed.m_idGenerator.MakeId(stopHash);
lineData.m_stopIds.emplace_back(stopId);
m_stopIdMapping.emplace(stopIdSubway, stopId);
if (i == 0)
continue;
auto const stopIdSubwayPrev = stopIdsSubway[i - 1];
CHECK(stopIdSubwayPrev != stopIdSubway, (stopIdSubway));
auto const & edge = FindEdge(stopIdSubwayPrev, stopIdSubway, lineId);
CHECK_LESS_OR_EQUAL(edge.GetShapeIds().size(), 1, (edge));
std::vector<m2::PointD> edgePoints;
m2::PointD const prevPoint = FindById(m_graphData.GetStops(), stopIdSubwayPrev)->GetPoint();
m2::PointD const curPoint = FindById(m_graphData.GetStops(), stopIdSubway)->GetPoint();
if (edge.GetShapeIds().empty())
{
edgePoints.push_back(prevPoint);
edgePoints.push_back(curPoint);
}
else
{
routing::transit::ShapeId shapeIdSubway = edge.GetShapeIds().back();
auto polyline = FindById(shapesSubway, shapeIdSubway)->GetPolyline();
CHECK(polyline.size() > 1, ());
double const distToPrevStop = mercator::DistanceOnEarth(polyline.front(), prevPoint);
double const distToNextStop = mercator::DistanceOnEarth(polyline.front(), curPoint);
if (distToPrevStop > distToNextStop)
std::reverse(polyline.begin(), polyline.end());
// We remove duplicate point from the shape before appending polyline to it.
if (!shapeData.m_points.empty() && shapeData.m_points.back() == polyline.front())
shapeData.m_points.pop_back();
shapeData.m_points.insert(shapeData.m_points.end(), polyline.begin(), polyline.end());
edgePoints = polyline;
}
EdgeId const curEdge(m_stopIdMapping[stopIdSubwayPrev], stopId, lineId);
auto [itEdgeOnShape, inserted] =
m_feed.m_edgesOnShapes.emplace(curEdge, std::vector<std::vector<m2::PointD>>{edgePoints});
if (inserted)
{
itEdgeOnShape->second.push_back(edgePoints);
LOG(LWARNING,
("Edge duplicate in subways. stop1_id", stopIdSubwayPrev, "stop2_id", stopIdSubway, "line_id", lineId));
}
}
m_feed.m_lines.m_data.emplace(lineId, lineData);
m_feed.m_shapes.m_data.emplace(shapeId, shapeData);
}
LOG(LDEBUG, ("Converted", m_feed.m_routes.m_data.size(), "routes,", m_feed.m_lines.m_data.size(), "lines."));
return !m_feed.m_lines.m_data.empty();
}
bool SubwayConverter::ConvertStops()
{
auto const & stopsSubway = m_graphData.GetStops();
m_feed.m_stops.m_data.reserve(stopsSubway.size());
for (auto const & stopSubway : stopsSubway)
m_feed.m_stops.m_data.emplace(MakeStop(stopSubway));
LOG(LINFO, ("Converted", m_feed.m_stops.m_data.size(), "stops."));
return !m_feed.m_stops.m_data.empty();
}
bool SubwayConverter::ConvertTransfers()
{
auto const & transfersSubway = m_graphData.GetTransfers();
m_feed.m_transfers.m_data.reserve(transfersSubway.size());
for (auto const & transferSubway : transfersSubway)
{
auto const [transferId, transferData] = MakeTransfer(transferSubway);
std::map<TransitId, std::set<TransitId>> routeToStops;
for (auto const & stopId : transferData.m_stopsIds)
{
for (auto const & [lineId, lineData] : m_feed.m_lines.m_data)
if (base::IsExist(lineData.m_stopIds, stopId))
routeToStops[lineData.m_routeId].insert(stopId);
}
// We don't count as transfers transfer points between lines on the same route, so we skip them.
if (routeToStops.size() < 2)
{
LOG(LINFO, ("Skip transfer on route", transferId));
continue;
}
m_feed.m_transfers.m_data.emplace(transferId, transferData);
// All stops are already present in |m_feed| before the |ConvertTransfers()| call.
for (auto stopId : transferData.m_stopsIds)
LinkTransferIdToStop(m_feed.m_stops.m_data.at(stopId), transferId);
}
LOG(LINFO, ("Converted", m_feed.m_transfers.m_data.size(), "transfers."));
return !m_feed.m_transfers.m_data.empty();
}
bool SubwayConverter::ConvertGates()
{
auto const & gatesSubway = m_graphData.GetGates();
m_feed.m_gates.m_data.reserve(gatesSubway.size());
for (auto const & gateSubway : gatesSubway)
{
auto const [gateId, gateData] = MakeGate(gateSubway);
m_feed.m_gates.m_data.emplace(gateId, gateData);
}
LOG(LINFO, ("Converted", m_feed.m_gates.m_data.size(), "gates."));
return !m_feed.m_gates.m_data.empty();
}
bool SubwayConverter::ConvertEdges()
{
for (auto const & [edgeSubway, index] : m_edgesSubway)
m_feed.m_edges.m_data.emplace(MakeEdge(edgeSubway, index));
LOG(LINFO, ("Converted", m_feed.m_edges.m_data.size(), "edges."));
for (auto const & [edgeTransferSubway, index] : m_edgesTransferSubway)
m_feed.m_edgesTransfers.m_data.emplace(MakeEdgeTransfer(edgeTransferSubway, index));
LOG(LINFO, ("Converted", m_feed.m_edgesTransfers.m_data.size(), "transfer edges."));
return !m_feed.m_edges.m_data.empty() && !m_feed.m_edgesTransfers.m_data.empty();
}
void SubwayConverter::MinimizeReversedLinesCount()
{
for (auto & [lineId, lineData] : m_feed.m_lines.m_data)
{
if (lineData.m_shapeLink.m_startIndex < lineData.m_shapeLink.m_endIndex)
continue;
auto revStopIds = GetReversed(lineData.m_stopIds);
bool reversed = false;
for (auto const & [lineIdStraight, lineDataStraight] : m_feed.m_lines.m_data)
{
if (lineIdStraight == lineId ||
lineDataStraight.m_shapeLink.m_startIndex > lineDataStraight.m_shapeLink.m_endIndex ||
lineDataStraight.m_shapeLink.m_shapeId != lineData.m_shapeLink.m_shapeId)
{
continue;
}
if (revStopIds == lineDataStraight.m_stopIds)
{
lineData.m_shapeLink = lineDataStraight.m_shapeLink;
LOG(LDEBUG, ("Reversed line", lineId, "to line", lineIdStraight, "shapeLink", lineData.m_shapeLink));
reversed = true;
break;
}
}
if (!reversed)
{
std::swap(lineData.m_shapeLink.m_startIndex, lineData.m_shapeLink.m_endIndex);
LOG(LDEBUG, ("Reversed line", lineId, "shapeLink", lineData.m_shapeLink));
}
}
}
std::vector<LineSchemeData> SubwayConverter::GetLinesOnScheme(
std::unordered_map<TransitId, LineSegmentInRegion> const & linesInRegion) const
{
// Color of line to shape link and one of line ids with this link.
std::map<std::string, std::map<ShapeLink, TransitId>> colorsToLines;
for (auto const & [lineId, lineData] : linesInRegion)
{
if (lineData.m_splineParent)
{
LOG(LINFO, ("Line is short spline. We skip it. Id", lineId));
continue;
}
auto itLine = m_feed.m_lines.m_data.find(lineId);
CHECK(itLine != m_feed.m_lines.m_data.end(), ());
TransitId const routeId = itLine->second.m_routeId;
auto itRoute = m_feed.m_routes.m_data.find(routeId);
CHECK(itRoute != m_feed.m_routes.m_data.end(), ());
std::string const & color = itRoute->second.m_color;
ShapeLink const & newShapeLink = lineData.m_shapeLink;
auto [it, inserted] = colorsToLines.emplace(color, std::map<ShapeLink, TransitId>());
if (inserted)
{
it->second[newShapeLink] = lineId;
continue;
}
bool insert = true;
std::vector<ShapeLink> linksForRemoval;
for (auto const & [shapeLink, lineId] : it->second)
{
if (shapeLink.m_shapeId != newShapeLink.m_shapeId)
continue;
// New shape link is fully included into the existing one.
if (shapeLink.m_startIndex <= newShapeLink.m_startIndex && shapeLink.m_endIndex >= newShapeLink.m_endIndex)
{
insert = false;
continue;
}
// Existing shape link is fully included into the new one. It should be removed.
if (newShapeLink.m_startIndex <= shapeLink.m_startIndex && newShapeLink.m_endIndex >= shapeLink.m_endIndex)
linksForRemoval.push_back(shapeLink);
}
for (auto const & sl : linksForRemoval)
it->second.erase(sl);
if (insert)
it->second[newShapeLink] = lineId;
}
std::vector<LineSchemeData> linesOnScheme;
for (auto const & [color, linksToLines] : colorsToLines)
{
CHECK(!linksToLines.empty(), (color));
for (auto const & [shapeLink, lineId] : linksToLines)
{
LineSchemeData data;
data.m_lineId = lineId;
data.m_color = color;
data.m_shapeLink = shapeLink;
linesOnScheme.push_back(data);
}
}
return linesOnScheme;
}
enum class LineSegmentState
{
Start = 0,
Finish
};
struct LineSegmentInfo
{
LineSegmentInfo() = default;
LineSegmentInfo(LineSegmentState const & state, bool codirectional) : m_state(state), m_codirectional(codirectional)
{}
LineSegmentState m_state = LineSegmentState::Start;
bool m_codirectional = false;
};
struct LinePointState
{
std::map<TransitId, LineSegmentInfo> parallelLineStates;
m2::PointD m_firstPoint;
};
using LineGeometry = std::vector<m2::PointD>;
using ColorToLinepartsCache = std::map<std::string, std::vector<LineGeometry>>;
bool Equal(LineGeometry const & line1, LineGeometry const & line2)
{
if (line1.size() != line2.size())
return false;
for (size_t i = 0; i < line1.size(); ++i)
if (!AlmostEqualAbs(line1[i], line2[i], kEps))
return false;
return true;
}
bool AddToCache(std::string const & color, LineGeometry const & linePart, ColorToLinepartsCache & cache)
{
auto [it, inserted] = cache.emplace(color, std::vector<LineGeometry>());
if (inserted)
{
it->second.push_back(linePart);
return true;
}
std::vector<m2::PointD> linePartRev = GetReversed(linePart);
for (LineGeometry const & cachedPart : it->second)
if (Equal(cachedPart, linePart) || Equal(cachedPart, linePartRev))
return false;
it->second.push_back(linePart);
return true;
}
void SubwayConverter::CalculateLinePriorities(std::vector<LineSchemeData> const & linesOnScheme)
{
ColorToLinepartsCache routeSegmentsCache;
for (auto const & lineSchemeData : linesOnScheme)
{
auto const lineId = lineSchemeData.m_lineId;
std::map<size_t, LinePointState> linePoints;
for (auto const & linePart : lineSchemeData.m_lineParts)
{
auto & startPointState = linePoints[linePart.m_segment.m_startIdx];
auto & endPointState = linePoints[linePart.m_segment.m_endIdx];
startPointState.m_firstPoint = linePart.m_firstPoint;
for (auto const & [parallelLineId, parallelFirstPoint] : linePart.m_commonLines)
{
bool const codirectional = AlmostEqualAbs(linePart.m_firstPoint, parallelFirstPoint, kEps);
startPointState.parallelLineStates[parallelLineId] = LineSegmentInfo(LineSegmentState::Start, codirectional);
endPointState.parallelLineStates[parallelLineId] = LineSegmentInfo(LineSegmentState::Finish, codirectional);
}
}
linePoints.emplace(lineSchemeData.m_shapeLink.m_startIndex, LinePointState());
linePoints.emplace(lineSchemeData.m_shapeLink.m_endIndex, LinePointState());
std::map<TransitId, bool> parallelLines;
for (auto it = linePoints.begin(); it != linePoints.end(); ++it)
{
auto itNext = std::next(it);
if (itNext == linePoints.end())
break;
auto & startLinePointState = it->second;
size_t startIndex = it->first;
size_t endIndex = itNext->first;
for (auto const & [id, info] : startLinePointState.parallelLineStates)
{
if (info.m_state == LineSegmentState::Start)
{
auto [itParLine, insertedParLine] = parallelLines.emplace(id, info.m_codirectional);
if (!insertedParLine)
CHECK_EQUAL(itParLine->second, info.m_codirectional, ());
}
else
{
parallelLines.erase(id);
}
}
TransitId const routeId = m_feed.m_lines.m_data.at(lineId).m_routeId;
std::string color = m_feed.m_routes.m_data.at(routeId).m_color;
std::map<std::string, bool> colors{{color, true /* codirectional */}};
bool colorCopy = false;
for (auto const & [id, codirectional] : parallelLines)
{
TransitId const parallelRoute = m_feed.m_lines.m_data.at(id).m_routeId;
auto const parallelColor = m_feed.m_routes.m_data.at(parallelRoute).m_color;
colors.emplace(parallelColor, codirectional);
if (parallelColor == color && id < lineId)
colorCopy = true;
}
if (colorCopy)
{
LOG(LINFO, ("Skip line segment with color copy", color, "line id", lineId));
continue;
}
LineSegmentOrder lso;
lso.m_segment = LineSegment(static_cast<uint32_t>(startIndex), static_cast<uint32_t>(endIndex));
auto const & polyline = m_feed.m_shapes.m_data.at(lineSchemeData.m_shapeLink.m_shapeId).m_points;
if (!AddToCache(color, GetPolylinePart(polyline, lso.m_segment.m_startIdx, lso.m_segment.m_endIdx),
routeSegmentsCache))
{
continue;
}
auto itColor = colors.find(color);
CHECK(itColor != colors.end(), ());
size_t const index = std::distance(colors.begin(), itColor);
lso.m_order = CalcSegmentOrder(index, colors.size());
bool reversed = false;
if (index > 0 && !colors.begin()->second /* codirectional */)
{
lso.m_order = -lso.m_order;
reversed = true;
}
m_feed.m_linesMetadata.m_data[lineId].push_back(lso);
LOG(LINFO, ("routeId", routeId, "lineId", lineId, "start", startIndex, "end", endIndex, "len",
endIndex - startIndex + 1, "order", lso.m_order, "index", index, "reversed", reversed,
"|| lines count:", parallelLines.size(), "colors count:", colors.size()));
}
}
}
void SubwayConverter::PrepareLinesMetadata()
{
for (auto const & [region, linesInRegion] : m_feed.m_splitting.m_lines)
{
LOG(LINFO, ("Preparing metadata for", region, "region"));
std::vector<LineSchemeData> linesOnScheme = GetLinesOnScheme(linesInRegion);
for (size_t i = 0; i < linesOnScheme.size() - 1; ++i)
{
auto & line1 = linesOnScheme[i];
auto const & shapeLink1 = linesInRegion.at(line1.m_lineId).m_shapeLink;
// |polyline1| is sub-polyline of the shapeLink1 geometry.
auto const polyline1 = GetPolylinePart(m_feed.m_shapes.m_data.at(shapeLink1.m_shapeId).m_points,
shapeLink1.m_startIndex, shapeLink1.m_endIndex);
for (size_t j = i + 1; j < linesOnScheme.size(); ++j)
{
auto & line2 = linesOnScheme[j];
auto const & shapeLink2 = linesInRegion.at(line2.m_lineId).m_shapeLink;
if (line1.m_shapeLink.m_shapeId == line2.m_shapeLink.m_shapeId)
{
CHECK_LESS(shapeLink1.m_startIndex, shapeLink1.m_endIndex, ());
CHECK_LESS(shapeLink2.m_startIndex, shapeLink2.m_endIndex, ());
std::optional<LineSegment> inter = GetIntersection(shapeLink1.m_startIndex, shapeLink1.m_endIndex,
shapeLink2.m_startIndex, shapeLink2.m_endIndex);
if (inter != std::nullopt)
{
LineSegment const segment = inter.value();
m2::PointD const & startPoint = polyline1[segment.m_startIdx];
UpdateLinePart(line1.m_lineParts, segment, startPoint, line2.m_lineId, startPoint);
UpdateLinePart(line2.m_lineParts, segment, startPoint, line1.m_lineId, startPoint);
}
}
else
{
// |polyline2| is sub-polyline of the shapeLink2 geometry.
auto polyline2 = GetPolylinePart(m_feed.m_shapes.m_data.at(shapeLink2.m_shapeId).m_points,
shapeLink2.m_startIndex, shapeLink2.m_endIndex);
auto [segments1, segments2] = FindIntersections(polyline1, polyline2);
if (segments1.empty())
{
auto polyline2Rev = GetReversed(polyline2);
std::tie(segments1, segments2) = FindIntersections(polyline1, polyline2Rev);
if (!segments1.empty())
for (auto & seg : segments2)
UpdateReversedSegmentIndexes(seg, polyline2.size());
}
if (!segments1.empty())
{
for (size_t k = 0; k < segments1.size(); ++k)
{
auto const & [startPoint1, endPoint1] = GetSegmentEdgesOnPolyline(polyline1, segments1[k]);
auto const & [startPoint2, endPoint2] = GetSegmentEdgesOnPolyline(polyline2, segments2[k]);
CHECK((AlmostEqualAbs(startPoint1, startPoint2, kEps) && AlmostEqualAbs(endPoint1, endPoint2, kEps)) ||
(AlmostEqualAbs(startPoint1, endPoint2, kEps) && AlmostEqualAbs(endPoint1, startPoint2, kEps)),
());
ShiftSegmentOnShape(segments1[k], shapeLink1);
ShiftSegmentOnShape(segments2[k], shapeLink2);
UpdateLinePart(line1.m_lineParts, segments1[k], startPoint1, line2.m_lineId, startPoint2);
UpdateLinePart(line2.m_lineParts, segments2[k], startPoint2, line1.m_lineId, startPoint1);
}
}
}
}
}
CalculateLinePriorities(linesOnScheme);
LOG(LINFO, ("Prepared metadata for lines in", region));
}
}
routing::transit::Edge SubwayConverter::FindEdge(routing::transit::StopId stop1Id, routing::transit::StopId stop2Id,
routing::transit::LineId lineId) const
{
routing::transit::Edge edge(stop1Id, stop2Id, 0 /* weight */, lineId, false /* transfer */, {} /* shapeIds */);
auto const itEdge = m_edgesSubway.find(edge);
CHECK(itEdge != m_edgesSubway.end(), (stop1Id, stop2Id, lineId));
return itEdge->first;
}
} // namespace transit

View file

@ -0,0 +1,88 @@
#pragma once
#include "generator/affiliation.hpp"
#include "transit/transit_entities.hpp"
#include "transit/transit_graph_data.hpp"
#include "transit/world_feed/feed_helpers.hpp"
#include "transit/world_feed/world_feed.hpp"
#include "geometry/mercator.hpp"
#include "geometry/point2d.hpp"
#include "defines.hpp"
#include <cstdint>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>
#include "3party/opening_hours/opening_hours.hpp"
namespace transit
{
using LineIdToStops = std::unordered_map<TransitId, IdList>;
// Converts public transport data from the MAPS.ME old transit.json format (which contains only
// subway data) to the new line-by-line jsons used for handling data extracted from GTFS.
class SubwayConverter
{
public:
SubwayConverter(std::string const & subwayJson, WorldFeed & feed);
// Parses subway json and converts it to entities in WorldFeed |m_feed|.
bool Convert();
private:
bool ConvertNetworks();
// Splits subway edges in two containers: |m_edgesSubway| for edges between two stops on the line
// and |m_edgesTransferSubway| for transfer edges.
bool SplitEdges();
// Converts lines, creates routes based on the lines data and constructs shapes.
bool ConvertLinesBasedData();
bool ConvertStops();
bool ConvertTransfers();
bool ConvertGates();
bool ConvertEdges();
// Tries to minimize the reversed lines count where it is possible by reversing line geometry.
void MinimizeReversedLinesCount();
// Returns line ids with corresponding shape links and route ids. There can be may lines inside
// the route with same shapeLink. We keep only one of them. These line ids are used in
// |PrepareLinesMetadata()|.
std::vector<LineSchemeData> GetLinesOnScheme(
std::unordered_map<TransitId, LineSegmentInRegion> const & linesInRegion) const;
// Finds common overlapping (parallel on the subway layer) segments on polylines. Motivation:
// we shouldn't draw parallel lines of different routes on top of each other so the user cant
// tell which lines go where (the only visible line is the one that is drawn last). We need these
// lines to be drawn in parallel in corresponding routes colours.
void PrepareLinesMetadata();
// Calculates order for each of the parallel lines in the overlapping segment. In drape frontend
// we use this order as an offset for drawing line.
void CalculateLinePriorities(std::vector<LineSchemeData> const & linesOnScheme);
// Methods for creating id & data pairs for |m_feed| based on the subway items.
std::pair<TransitId, RouteData> MakeRoute(routing::transit::Line const & lineSubway);
std::pair<TransitId, GateData> MakeGate(routing::transit::Gate const & gateSubway);
std::pair<TransitId, TransferData> MakeTransfer(routing::transit::Transfer const & transferSubway);
static std::pair<TransitId, LineData> MakeLine(routing::transit::Line const & lineSubway, TransitId routeId);
std::pair<EdgeId, EdgeData> MakeEdge(routing::transit::Edge const & edgeSubway, uint32_t index);
std::pair<EdgeTransferId, EdgeData> MakeEdgeTransfer(routing::transit::Edge const & edgeSubway, uint32_t index);
std::pair<TransitId, StopData> MakeStop(routing::transit::Stop const & stopSubway);
routing::transit::Edge FindEdge(routing::transit::StopId stop1Id, routing::transit::StopId stop2Id,
routing::transit::LineId lineId) const;
// Path to the file with subways json.
std::string m_subwayJson;
// Transit graph for deserializing json from |m_subwayJson|.
routing::transit::GraphData m_graphData;
// Destination feed for converted items from subway.
WorldFeed & m_feed;
// Mapping of subway stop id to transit stop id.
std::unordered_map<routing::transit::StopId, TransitId> m_stopIdMapping;
// Subset of the |m_graphData| edges with no transfers.
std::map<routing::transit::Edge, uint32_t> m_edgesSubway;
// Subset of the |m_graphData| edges with transfers.
std::map<routing::transit::Edge, uint32_t> m_edgesTransferSubway;
};
} // namespace transit

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,434 @@
#pragma once
#include "generator/affiliation.hpp"
#include "transit/transit_entities.hpp"
#include "transit/transit_schedule.hpp"
#include "transit/world_feed/color_picker.hpp"
#include "transit/world_feed/feed_helpers.hpp"
#include "geometry/mercator.hpp"
#include "geometry/point2d.hpp"
#include "defines.hpp"
#include <cstdint>
#include <fstream>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "3party/just_gtfs/just_gtfs.h"
namespace transit
{
static std::string const kDelimiter = "_";
// Generates globally unique TransitIds mapped to the GTFS entities hashes.
class IdGenerator
{
public:
IdGenerator() = default;
explicit IdGenerator(std::string const & idMappingPath);
void Save();
TransitId MakeId(std::string const & hash);
private:
std::unordered_map<std::string, TransitId> m_hashToId;
TransitId m_curId = 0;
std::string m_idMappingPath;
};
// Here are OMaps representations for GTFS entities, e.g. networks for GTFS agencies.
// https://developers.google.com/transit/gtfs/reference
struct Networks
{
void Write(IdSet const & ids, std::ofstream & stream) const;
// Id to agency name mapping.
std::unordered_map<TransitId, std::string> m_data;
};
struct RouteData
{
TransitId m_networkId = 0;
std::string m_routeType;
std::string m_title;
std::string m_color;
};
struct Routes
{
void Write(IdSet const & ids, std::ofstream & stream) const;
std::unordered_map<TransitId, RouteData> m_data;
};
struct LineData
{
TransitId m_routeId = 0;
ShapeLink m_shapeLink;
std::string m_title;
// Sequence of stops along the line from first to last.
IdList m_stopIds;
// Monthdays and weekdays ranges on which the line is at service.
// Exceptions in service schedule. Explicitly activates or disables service by dates.
// Transport intervals depending on the day timespans.
Schedule m_schedule;
// Fields not intended to be exported to json.
TransitId m_shapeId = 0;
std::string m_gtfsTripId;
std::unordered_set<std::string> m_gtfsServiceIds;
};
struct LineSegmentInRegion
{
// Stops in the region.
IdList m_stopIds;
// Spline line id to its parent line id mapping.
std::optional<TransitId> m_splineParent = std::nullopt;
// Indexes of the line shape link may differ in different regions.
ShapeLink m_shapeLink;
};
struct Lines
{
void Write(std::unordered_map<TransitId, LineSegmentInRegion> const & ids, std::ofstream & stream) const;
std::unordered_map<TransitId, LineData> m_data;
};
struct LinesMetadata
{
void Write(std::unordered_map<TransitId, LineSegmentInRegion> const & linesInRegion, std::ofstream & stream) const;
// Line id to line additional data (e.g. for rendering).
std::unordered_map<TransitId, LineSegmentsOrder> m_data;
};
struct ShapeData
{
ShapeData() = default;
explicit ShapeData(std::vector<m2::PointD> const & points);
std::vector<m2::PointD> m_points;
// Field not for dumping to json:
IdSet m_lineIds;
};
using ShapesIter = std::unordered_map<TransitId, ShapeData>::iterator;
struct Shapes
{
void Write(IdSet const & ids, std::ofstream & stream) const;
std::unordered_map<TransitId, ShapeData> m_data;
};
struct StopData
{
void UpdateTimetable(TransitId lineId, gtfs::StopTime const & stopTime);
m2::PointD m_point;
std::string m_title;
// If arrival time at a specific stop for a specific trip on a route is not available,
// |m_timetable| can be left empty.
TimeTable m_timetable;
// Ids of transfer nodes containing this stop.
IdList m_transferIds;
uint64_t m_osmId = 0;
uint32_t m_featureId = 0;
// Field not intended for dumping to json:
std::string m_gtfsParentId;
};
struct Stops
{
void Write(IdSet const & ids, std::ofstream & stream) const;
std::unordered_map<TransitId, StopData> m_data;
};
struct Edges
{
void Write(IdEdgeSet const & ids, std::ofstream & stream) const;
std::unordered_map<EdgeId, EdgeData, EdgeIdHasher> m_data;
};
struct EdgeTransferId
{
EdgeTransferId() = default;
EdgeTransferId(TransitId fromStopId, TransitId toStopId);
bool operator==(EdgeTransferId const & other) const;
TransitId m_fromStopId = 0;
TransitId m_toStopId = 0;
};
struct EdgeTransferIdHasher
{
size_t operator()(EdgeTransferId const & key) const;
};
using IdEdgeTransferSet = std::unordered_set<EdgeTransferId, EdgeTransferIdHasher>;
struct EdgesTransfer
{
void Write(IdEdgeTransferSet const & ids, std::ofstream & stream) const;
// Key is pair of stops and value is |EdgeData|, containing weight (in seconds).
std::unordered_map<EdgeTransferId, EdgeData, EdgeTransferIdHasher> m_data;
};
struct TransferData
{
m2::PointD m_point;
IdList m_stopsIds;
};
struct Transfers
{
void Write(IdSet const & ids, std::ofstream & stream) const;
std::unordered_map<TransitId, TransferData> m_data;
};
struct GateData
{
bool m_isEntrance = false;
bool m_isExit = false;
m2::PointD m_point;
std::vector<TimeFromGateToStop> m_weights;
uint64_t m_osmId = 0;
// Field not intended for dumping to json:
std::string m_gtfsId;
};
struct Gates
{
void Write(IdSet const & ids, std::ofstream & stream) const;
std::unordered_map<TransitId, GateData> m_data;
};
// Indexes for WorldFeed |m_gtfsIdToHash| field. For each type of GTFS entity, e.g. agency or stop,
// there is distinct mapping located by its own |FieldIdx| index in the |m_gtfsIdToHash|.
enum FieldIdx
{
AgencyIdx = 0,
StopsIdx,
RoutesIdx,
TripsIdx,
ShapesIdx,
IdxCount
};
using GtfsIdToHash = std::unordered_map<std::string, std::string>;
struct StopsOnLines
{
explicit StopsOnLines(IdList const & ids);
IdList m_stopSeq;
IdSet m_lines;
bool m_isValid = true;
transit::Direction m_direction = Direction::Forward;
};
using IdsInRegion = std::unordered_map<std::string, IdSet>;
using LinesInRegion = std::unordered_map<std::string, std::unordered_map<TransitId, LineSegmentInRegion>>;
using EdgeIdsInRegion = std::unordered_map<std::string, IdEdgeSet>;
using EdgeTransferIdsInRegion = std::unordered_map<std::string, IdEdgeTransferSet>;
using Regions = std::vector<std::string>;
struct TransitByRegion
{
IdsInRegion m_networks;
IdsInRegion m_routes;
LinesInRegion m_lines;
IdsInRegion m_shapes;
IdsInRegion m_stops;
EdgeIdsInRegion m_edges;
EdgeTransferIdsInRegion m_edgesTransfers;
IdsInRegion m_transfers;
IdsInRegion m_gates;
};
// Pair of points representing corresponding edge endings.
using EdgePoints = std::pair<m2::PointD, m2::PointD>;
// Class for merging scattered GTFS feeds into one World feed with static ids.
// The usage scenario consists of steps:
// 1) Initialize |WorldFeed| instance with |IdGenerator| for correct id assignment to GTFS entities,
// |ColorPicker| for choosing route colors from our palette, |CountriesFilesAffiliation| for
// splitting result feed into regions.
// 2) Call SetFeed(...) method to convert GTFS entities into objects convenient for dumping to json.
// 3) Call Save(...) to save result data as a set of json files in the specified directory.
class WorldFeed
{
public:
WorldFeed(IdGenerator & generator, IdGenerator & generatorEdges, ColorPicker & colorPicker,
feature::CountriesFilesAffiliation & mwmMatcher);
// Transforms GTFS feed into the global feed.
bool SetFeed(gtfs::Feed && feed);
// Dumps global feed to |world_feed_path|.
bool Save(std::string const & worldFeedDir, bool overwrite);
private:
friend class WorldFeedIntegrationTests;
friend class SubwayConverterTests;
friend class SubwayConverter;
void SaveRegions(std::string const & worldFeedDir, std::string const & region, bool overwrite);
bool SetFeedLanguage();
// Fills networks from GTFS agencies data.
bool FillNetworks();
// Fills routes from GTFS routes data.
bool FillRoutes();
// Fills lines and corresponding shapes from GTFS trips and shapes.
bool FillLinesAndShapes();
// Deletes shapes which are sub-shapes and refreshes corresponding links in lines.
void ModifyLinesAndShapes();
// Gets service monthday open/closed ranges, weekdays and exceptions in schedule.
void FillLinesSchedule();
// Gets frequencies of trips from GTFS.
// Adds shape with mercator points instead of WGS84 lat/lon.
void AddShape(GtfsIdToHash::iterator & iter, gtfs::Shape const & shapeItems, TransitId lineId);
// Fills stops data, corresponding fields in |m_lines| and builds edges for the road graph.
bool FillStopsEdges();
// Generates globally unique id and hash for the stop by its |stopGtfsId|.
std::pair<TransitId, std::string> GetStopIdAndHash(std::string const & stopGtfsId);
// Adds new stop with |stopId| and fills it with GTFS data by |gtfsId| or just
// links to it |lineId|.
bool UpdateStop(TransitId stopId, gtfs::StopTime const & stopTime, std::string const & stopHash, TransitId lineId);
std::optional<TransitId> GetParentLineForSpline(TransitId lineId) const;
bool PrepareEdgesInRegion(std::string const & region);
TransitId GetSplineParent(TransitId lineId, std::string const & region) const;
std::unordered_map<TransitId, std::vector<StopsOnLines>> GetStopsForShapeMatching();
// Adds stops projections to shapes. Updates corresponding links to shapes. Returns number of
// invalid and valid shapes.
std::pair<size_t, size_t> ModifyShapes();
// Fills transfers based on GTFS transfers.
void FillTransfers();
// Fills gates based on GTFS stops.
void FillGates();
// Recalculates 0-weights of edges based on the shape length.
bool UpdateEdgeWeights();
std::optional<Direction> ProjectStopsToShape(ShapesIter & itShape, StopsOnLines const & stopsOnLines,
std::unordered_map<TransitId, std::vector<size_t>> & stopsToIndexes);
// Splits data into regions.
void SplitFeedIntoRegions();
// Splits |m_stops|, |m_edges| and |m_edgesTransfer| into regions. The following stops are
// added to corresponding regions: stops inside borders; stops which are connected with an edge
// from |m_edges| or |m_edgesTransfer| with stops inside borders. Edge from |m_edges| or
// |m_edgesTransfer| is added to the region if one of its stop ids lies inside mwm.
void SplitStopsBasedData();
// Splits |m_lines|, |m_shapes|, |m_routes| and |m_networks| into regions. If one of the line
// stops (|m_stopIds|) lies inside region, then this line is added to this region. But only stops
// whose stop ids are contained in this region will be attached to the line in this region. Shape,
// route or network is added to the region if it is linked to the line in this region.
void SplitLinesBasedData();
// Splits |m_transfers| and |m_gates| into regions. Transfer is added to the region if there is
// stop in |m_stopsIds| which is inside this region. Gate is added to the region if there is stop
// in |m_weights| which is inside the region.
void SplitSupplementalData();
// Extend existing ids containers by appending to them |fromId| and |toId|. If one of the ids is
// present in region, then the other is also added.
std::pair<Regions, Regions> ExtendRegionsByPair(TransitId fromId, TransitId toId);
// Returns true if edge weight between two stops (stop ids are contained in |edgeId|)
// contradicts maximum transit speed.
bool SpeedExceedsMaxVal(EdgeId const & edgeId, EdgeData const & edgeData);
// Removes entities from feed which are linked only to the |corruptedLineIds|.
bool ClearFeedByLineIds(std::unordered_set<TransitId> const & corruptedLineIds);
// Current GTFS feed which is being merged to the global feed.
gtfs::Feed m_feed;
// Entities for json'izing and feeding to the generator_tool (Not split by regions).
Networks m_networks;
Routes m_routes;
Lines m_lines;
LinesMetadata m_linesMetadata;
Shapes m_shapes;
Stops m_stops;
Edges m_edges;
EdgesTransfer m_edgesTransfers;
Transfers m_transfers;
Gates m_gates;
// Mapping of the edge to its points on the shape polyline.
std::unordered_map<EdgeId, std::vector<std::vector<m2::PointD>>, EdgeIdHasher> m_edgesOnShapes;
// Ids of entities for json'izing, split by regions.
TransitByRegion m_splitting;
// Generator of ids, globally unique and constant between re-runs.
IdGenerator & m_idGenerator;
// Generator of ids for edges only.
IdGenerator & m_idGeneratorEdges;
// Color name picker of the nearest color for route RBG from our constant list of transfer colors.
ColorPicker & m_colorPicker;
// Mwm matcher for m2:Points representing stops and other entities.
feature::CountriesFilesAffiliation & m_affiliation;
// GTFS id -> entity hash mapping. Maps GTFS id string (unique only for current feed) to the
// globally unique hash.
std::vector<GtfsIdToHash> m_gtfsIdToHash;
// Unique hash characterizing each GTFS feed.
std::string m_gtfsHash;
// Unique hashes of all agencies handled by WorldFeed.
static std::unordered_set<std::string> m_agencyHashes;
// Count of corrupted stops sequences which could not be projected to the shape polyline.
static size_t m_badStopSeqCount;
// Agencies which are already handled by WorldFeed and should be copied to the resulting jsons.
std::unordered_set<std::string> m_agencySkipList;
// If the feed explicitly specifies its language, we use its value. Otherwise set to default.
std::string m_feedLanguage;
bool m_feedIsSplitIntoRegions = false;
};
// Creates concatenation of |values| separated by delimiter.
template <typename... Values>
auto BuildHash(Values... values)
{
size_t constexpr paramsCount = sizeof...(Values);
size_t const delimitersSize = (paramsCount - 1) * kDelimiter.size();
size_t const totalSize = (delimitersSize + ... + values.size());
std::string hash;
hash.reserve(totalSize);
(hash.append(values + kDelimiter), ...);
hash.pop_back();
return hash;
}
// Inserts |transferId| into the |stop| m_transferIds if it isn't already present there.
void LinkTransferIdToStop(StopData & stop, TransitId transferId);
} // namespace transit

View file

@ -0,0 +1,23 @@
project(world_feed_integration_tests)
set(SRC world_feed_integration_tests.cpp)
omim_add_test(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
world_feed
generator
)
# This test requires additional data from the other repository
if(NOT TEST_DATA_REPO_URL)
set(TEST_DATA_REPO_URL "https://github.com/organicmaps/world_feed_integration_tests_data.git")
endif()
set(DESTINATION_FOLDER "${OMIM_DATA_DIR}/test_data/world_feed_integration_tests_data/")
add_custom_command(
TARGET ${PROJECT_NAME}
POST_BUILD
COMMAND test -d ${DESTINATION_FOLDER} || (git clone ${TEST_DATA_REPO_URL} ${DESTINATION_FOLDER})
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)

View file

@ -0,0 +1,194 @@
#include "testing/testing.hpp"
#include "generator/affiliation.hpp"
#include "transit/world_feed/world_feed.hpp"
#include "platform/platform.hpp"
#include "base/assert.hpp"
#include "base/file_name_utils.hpp"
#include "base/string_utils.hpp"
#include <algorithm>
#include <string>
#include <utility>
namespace
{
std::string const kFeedsSubdir = "test_data/world_feed_integration_tests_data";
} // namespace
namespace transit
{
class WorldFeedIntegrationTests
{
public:
WorldFeedIntegrationTests()
: m_mwmMatcher(GetPlatform().ResourcesDir(), false /* haveBordersForWholeWorld */)
, m_globalFeed(m_generator, m_generatorEdges, m_colorPicker, m_mwmMatcher)
{
m_testPath = base::JoinPath(GetPlatform().WritableDir(), kFeedsSubdir);
CHECK(GetPlatform().IsFileExistsByFullPath(m_testPath), ());
m_generator = IdGenerator(base::JoinPath(m_testPath, "mapping.txt"));
m_generatorEdges = IdGenerator(base::JoinPath(m_testPath, "mapping_edges.txt"));
}
void ReadMinimalisticFeed()
{
gtfs::Feed feed(base::JoinPath(m_testPath, "minimalistic_feed"));
TEST_EQUAL(feed.read_feed().code, gtfs::ResultCode::OK, ());
TEST(m_globalFeed.SetFeed(std::move(feed)), ());
TEST_EQUAL(m_globalFeed.m_networks.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_routes.m_data.size(), 2, ());
// We check that lines with no entries in stop_times.txt are not added.
TEST_EQUAL(m_globalFeed.m_lines.m_data.size(), 2, ());
TEST_EQUAL(m_globalFeed.m_stops.m_data.size(), 7, ());
TEST_EQUAL(m_globalFeed.m_shapes.m_data.size(), 2, ());
// There are 2 lines with 3 and 4 stops correspondingly. So we have 5 edges.
TEST_EQUAL(m_globalFeed.m_edges.m_data.size(), 5, ());
}
void ReadRealLifeFeed()
{
gtfs::Feed feed(base::JoinPath(m_testPath, "real_life_feed"));
TEST_EQUAL(feed.read_feed().code, gtfs::ResultCode::OK, ());
TEST(m_globalFeed.SetFeed(std::move(feed)), ());
TEST_EQUAL(m_globalFeed.m_networks.m_data.size(), 21, ());
TEST_EQUAL(m_globalFeed.m_routes.m_data.size(), 87, ());
// All trips have unique service_id so each line corresponds to some trip.
TEST_EQUAL(m_globalFeed.m_lines.m_data.size(), 392, ());
TEST_EQUAL(m_globalFeed.m_stops.m_data.size(), 1008, ());
// 64 shapes contained in other shapes should be skipped.
TEST_EQUAL(m_globalFeed.m_shapes.m_data.size(), 329, ());
TEST_EQUAL(m_globalFeed.m_gates.m_data.size(), 0, ());
TEST_EQUAL(m_globalFeed.m_transfers.m_data.size(), 0, ());
TEST_EQUAL(m_globalFeed.m_edges.m_data.size(), 3999, ());
TEST_EQUAL(m_globalFeed.m_edgesTransfers.m_data.size(), 0, ());
}
void ReadFeedWithMultipleShapeProjections()
{
gtfs::Feed feed(base::JoinPath(m_testPath, "feed_with_multiple_shape_projections"));
TEST_EQUAL(feed.read_feed().code, gtfs::ResultCode::OK, ());
TEST(m_globalFeed.SetFeed(std::move(feed)), ());
TEST_EQUAL(m_globalFeed.m_networks.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_routes.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_lines.m_data.size(), 2, ());
TEST_EQUAL(m_globalFeed.m_stops.m_data.size(), 16, ());
TEST_EQUAL(m_globalFeed.m_shapes.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_gates.m_data.size(), 0, ());
TEST_EQUAL(m_globalFeed.m_transfers.m_data.size(), 2, ());
TEST_EQUAL(m_globalFeed.m_edges.m_data.size(), 27, ());
TEST_EQUAL(m_globalFeed.m_edgesTransfers.m_data.size(), 2, ());
}
void ReadFeedWithWrongStopsOrder()
{
gtfs::Feed feed(base::JoinPath(m_testPath, "feed_with_wrong_stops_order"));
TEST_EQUAL(feed.read_feed().code, gtfs::ResultCode::OK, ());
// Feed has wrong stops order (impossible for trip shape) and should be rejected.
TEST(!m_globalFeed.SetFeed(std::move(feed)), ());
}
void ReadFeedWithBackwardOrder()
{
gtfs::Feed feed(base::JoinPath(m_testPath, "feed_with_backward_order"));
TEST_EQUAL(feed.read_feed().code, gtfs::ResultCode::OK, ());
TEST(m_globalFeed.SetFeed(std::move(feed)), ());
TEST_EQUAL(m_globalFeed.m_networks.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_routes.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_lines.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_stops.m_data.size(), 16, ());
TEST_EQUAL(m_globalFeed.m_shapes.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_gates.m_data.size(), 0, ());
TEST_EQUAL(m_globalFeed.m_transfers.m_data.size(), 2, ());
TEST_EQUAL(m_globalFeed.m_edges.m_data.size(), 16, ());
TEST_EQUAL(m_globalFeed.m_edgesTransfers.m_data.size(), 2, ());
}
// Test for train itinerary that passes through 4 regions in Europe and consists of 4 stops
// (each in separate mwm) and 1 route with 1 line. This line passes through 4 stops:
// [1] Switzerland_Ticino -> [2] Switzerland_Eastern ->
// [3] Italy_Lombardy_Como -> [4] Italy_Lombardy_Monza and Brianza
void SplitFeedIntoMultipleRegions()
{
gtfs::Feed feed(base::JoinPath(m_testPath, "feed_long_itinerary"));
TEST_EQUAL(feed.read_feed().code, gtfs::ResultCode::OK, ());
TEST(m_globalFeed.SetFeed(std::move(feed)), ());
TEST_EQUAL(m_globalFeed.m_lines.m_data.size(), 1, ());
TEST_EQUAL(m_globalFeed.m_stops.m_data.size(), 4, ());
TEST_EQUAL(m_globalFeed.m_edges.m_data.size(), 3, ());
m_globalFeed.SplitFeedIntoRegions();
size_t const mwmCount = 4;
// We check that count of keys in each regions-to-ids mapping corresponds to the mwms count.
TEST_EQUAL(m_globalFeed.m_splitting.m_networks.size(), mwmCount, ());
TEST_EQUAL(m_globalFeed.m_splitting.m_routes.size(), mwmCount, ());
TEST_EQUAL(m_globalFeed.m_splitting.m_lines.size(), mwmCount, ());
TEST_EQUAL(m_globalFeed.m_splitting.m_stops.size(), mwmCount, ());
auto & stopsInRegions = m_globalFeed.m_splitting.m_stops;
auto & edgesInRegions = m_globalFeed.m_splitting.m_edges;
// First and last stops are connected through 1 edge with 1 nearest stop.
// Stops in the middle are connected through 2 edges with 2 nearest stops.
TEST_EQUAL(stopsInRegions["Switzerland_Ticino"].size(), 3, ());
TEST_EQUAL(edgesInRegions["Switzerland_Ticino"].size(), 1, ());
TEST_EQUAL(stopsInRegions["Switzerland_Eastern"].size(), 4, ());
TEST_EQUAL(edgesInRegions["Switzerland_Eastern"].size(), 2, ());
TEST_EQUAL(stopsInRegions["Italy_Lombardy_Como"].size(), 4, ());
TEST_EQUAL(edgesInRegions["Italy_Lombardy_Como"].size(), 2, ());
TEST_EQUAL(stopsInRegions["Italy_Lombardy_Monza and Brianza"].size(), 3, ());
TEST_EQUAL(edgesInRegions["Italy_Lombardy_Monza and Brianza"].size(), 1, ());
}
private:
std::string m_testPath;
IdGenerator m_generator;
IdGenerator m_generatorEdges;
transit::ColorPicker m_colorPicker;
feature::CountriesFilesAffiliation m_mwmMatcher;
WorldFeed m_globalFeed;
};
UNIT_CLASS_TEST(WorldFeedIntegrationTests, MinimalisticFeed)
{
ReadMinimalisticFeed();
}
UNIT_CLASS_TEST(WorldFeedIntegrationTests, RealLifeFeed)
{
ReadRealLifeFeed();
}
UNIT_CLASS_TEST(WorldFeedIntegrationTests, FeedWithLongItinerary)
{
SplitFeedIntoMultipleRegions();
}
UNIT_CLASS_TEST(WorldFeedIntegrationTests, FeedWithMultipleShapeProjections)
{
ReadFeedWithMultipleShapeProjections();
}
UNIT_CLASS_TEST(WorldFeedIntegrationTests, FeedWithWrongStopsOrder)
{
ReadFeedWithWrongStopsOrder();
}
UNIT_CLASS_TEST(WorldFeedIntegrationTests, FeedWithBackwardOrder)
{
ReadFeedWithBackwardOrder();
}
} // namespace transit

View file

@ -0,0 +1,14 @@
project(world_feed_tests)
set(SRC
subway_converter_tests.cpp
world_feed_tests.cpp
)
omim_add_test(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
generator
world_feed
transit
)

View file

@ -0,0 +1,388 @@
#include "testing/testing.hpp"
#include "transit/world_feed/subway_converter.hpp"
#include "transit/world_feed/world_feed.hpp"
#include "platform/platform.hpp"
#include "base/assert.hpp"
#include "base/file_name_utils.hpp"
#include <fstream>
#include <string>
#include <utility>
#include <vector>
namespace
{
std::string const kSubwayTestsDir = "transit_subway_converter_tests";
std::string const kSubwayJsonFile = "subways.json";
std::string const kMappingFile = "mapping.txt";
std::string const kMappingEdgesFile = "mapping_edges.txt";
void WriteStringToFile(std::string const & fileName, std::string const & data)
{
std::ofstream file;
file.open(fileName);
CHECK(file.is_open(), ("Could not open file", fileName));
file << data;
}
} // namespace
namespace transit
{
class SubwayConverterTests
{
public:
SubwayConverterTests() : m_mwmMatcher(GetPlatform().ResourcesDir(), false /* haveBordersForWholeWorld */)
{
CHECK(Platform::MkDirChecked(kSubwayTestsDir), ("Could not create directory for test data:", kSubwayTestsDir));
m_generator = transit::IdGenerator(base::JoinPath(kSubwayTestsDir, kMappingFile));
m_generatorEdges = transit::IdGenerator(base::JoinPath(kSubwayTestsDir, kMappingEdgesFile));
}
~SubwayConverterTests() { Platform::RmDirRecursively(kSubwayTestsDir); }
void ParseEmptySubway()
{
std::string const emptySubway = R"({
"networks":[],
"lines":[],
"edges":[],
"gates":[],
"shapes":[],
"stops":[],
"transfers":[]
})";
auto const & filePath = base::JoinPath(kSubwayTestsDir, kSubwayJsonFile);
WriteStringToFile(filePath, emptySubway);
transit::WorldFeed feed(m_generator, m_generatorEdges, m_colorPicker, m_mwmMatcher);
transit::SubwayConverter converter(filePath, feed);
TEST(!converter.Convert(), ());
}
// Subway consists of two lines with one transfer.
// 65687110 ---------> 76638582 ---------> 61447662 ---------> 61447702 | Line 244075840
// | /
// 61447663 ---------->/ | Line 244075841
void ParseValidSubway()
{
std::string const validSubway = R"({
"networks":[
{
"id":108,
"title":"Belo Horizonte"
}
],
"lines":[
{
"color":"orange_light",
"id":244075840,
"interval":150,
"network_id":108,
"number":"L1",
"stop_ids":[
65687110,
76638582,
61447662,
61447702
],
"title":"Metro L1R: Place des Martyrs - Ain Naâdja",
"type":"subway"
},
{
"color":"amber_dark",
"id":244075841,
"interval":320,
"network_id":108,
"number":"L1",
"stop_ids":[
61447663,
61447702
],
"title":"Metro L1R: Place des Martyrs - Ain Naâdja",
"type":"subway"
}
],
"stops":[
{
"id":65687110,
"line_ids":[
244075840
],
"osm_id":"4611686021817678228",
"point":{
"x":21.056,
"y":61.316
},
"title_anchors":[]
},
{
"id":76638582,
"line_ids":[
244075840
],
"osm_id":"4611686021817678229",
"point":{
"x":21.057,
"y":61.317
},
"title_anchors":[]
},
{
"id":61447662,
"line_ids":[
244075840
],
"osm_id":"4611686021817678230",
"point":{
"x":21.058,
"y":61.319
},
"title_anchors":[]
},
{
"id":61447702,
"line_ids":[
244075840,
244075841
],
"osm_id":"4611686021817678231",
"point":{
"x":21.059,
"y":61.321
},
"title_anchors":[]
},
{
"id":61447663,
"line_ids":[
244075841
],
"osm_id":"4611686021817678231",
"point":{
"x":21.068,
"y":61.399
},
"title_anchors":[]
}
],
"edges":[
{
"stop1_id":61447662,
"stop2_id":61447663,
"transfer":true,
"weight":225
},
{
"line_id":244075840,
"shape_ids":[
{
"stop1_id":65687110,
"stop2_id":76638582
}
],
"stop1_id":65687110,
"stop2_id":76638582,
"transfer":false,
"weight":33
},
{
"line_id":244075840,
"shape_ids":[
{
"stop1_id":76638582,
"stop2_id":61447662
}
],
"stop1_id":76638582,
"stop2_id":61447662,
"transfer":false,
"weight":46
},
{
"line_id":244075840,
"shape_ids":[
{
"stop1_id":61447662,
"stop2_id":61447702
}
],
"stop1_id":61447662,
"stop2_id":61447702,
"transfer":false,
"weight":32
},
{
"line_id":244075841,
"shape_ids":[
{
"stop1_id":61447662,
"stop2_id":61447702
}
],
"stop1_id":61447663,
"stop2_id":61447702,
"transfer":false,
"weight":40
}
],
"gates":[
{
"entrance":true,
"exit":true,
"osm_id":"4611686018706540014",
"point":{
"x":21.055938,
"y":61.31740794838219
},
"stop_ids":[
76638582
],
"weight":129
}
],
"shapes":[
{
"id":{
"stop1_id":65687110,
"stop2_id":76638582
},
"polyline":[
{
"x":3.062,
"y":39.616
},
{
"x":3.063,
"y":39.61
}
]
},
{
"id":{
"stop1_id":76638582,
"stop2_id":61447662
},
"polyline":[
{
"x":3.063,
"y":39.61
},
{
"x":3.0624,
"y":39.614
}
]
},
{
"id":{
"stop1_id":61447662,
"stop2_id":61447702
},
"polyline":[
{
"x":3.062,
"y":39.616
},
{
"x":3.065,
"y":39.61
}
]
},
{
"id":{
"stop1_id":61447663,
"stop2_id":61447702
},
"polyline":[
{
"x":3.068,
"y":39.64
},
{
"x":3.063,
"y":39.674
}
]
}
],
"transfers":[
{
"id":4611686018489133646,
"point":{
"x":-3.6761538,
"y":44.26760612521531
},
"stop_ids":[
61447662,
61447663
],
"title_anchors":[
]
}
]
})";
auto const & filePath = base::JoinPath(kSubwayTestsDir, kSubwayJsonFile);
WriteStringToFile(filePath, validSubway);
transit::WorldFeed feed(m_generator, m_generatorEdges, m_colorPicker, m_mwmMatcher);
transit::SubwayConverter converter(filePath, feed);
// We check that the conversion between old and new formats is successful.
TEST(converter.Convert(), ());
// We check that transit entities are converted correctly.
TEST_EQUAL(feed.m_networks.m_data.size(), 1, ());
TEST_EQUAL(feed.m_routes.m_data.size(), 1, ());
TEST_EQUAL(feed.m_lines.m_data.size(), 2, ());
TEST_EQUAL(feed.m_stops.m_data.size(), 5, ());
TEST_EQUAL(feed.m_edges.m_data.size(), 4, ());
TEST_EQUAL(feed.m_edgesTransfers.m_data.size(), 1, ());
TEST_EQUAL(feed.m_transfers.m_data.size(), 0, ());
TEST_EQUAL(feed.m_gates.m_data.size(), 1, ());
// Two initial shapes must be merged into one.
TEST_EQUAL(feed.m_shapes.m_data.size(), 1, ());
// Shape does not contain duplicate points (we have 5 points instead of 6).
TEST_EQUAL(feed.m_shapes.m_data.begin()->second.m_points.size(), 5, ());
// We check main relations consistency.
auto const networkIt = feed.m_networks.m_data.begin();
auto const routeIt = feed.m_routes.m_data.begin();
TEST_EQUAL(routeIt->second.m_networkId, networkIt->first, ());
for (auto const & [lineId, lineData] : feed.m_lines.m_data)
{
TEST_EQUAL(lineData.m_routeId, routeIt->first, ());
for (auto const stopId : lineData.m_stopIds)
{
auto const stopIt = feed.m_stops.m_data.find(stopId);
TEST(stopIt != feed.m_stops.m_data.end(), (stopId));
}
}
}
private:
transit::IdGenerator m_generator;
transit::IdGenerator m_generatorEdges;
transit::ColorPicker m_colorPicker;
feature::CountriesFilesAffiliation m_mwmMatcher;
};
UNIT_CLASS_TEST(SubwayConverterTests, SubwayConverter_ParseInvalidJson)
{
ParseEmptySubway();
}
UNIT_CLASS_TEST(SubwayConverterTests, SubwayConverter_ParseValidJson)
{
ParseValidSubway();
}
} // namespace transit

View file

@ -0,0 +1,541 @@
#include "testing/testing.hpp"
#include "transit/world_feed/color_picker.hpp"
#include "transit/world_feed/date_time_helpers.hpp"
#include "transit/world_feed/feed_helpers.hpp"
#include "transit/world_feed/world_feed.hpp"
#include "platform/platform.hpp"
#include "base/assert.hpp"
#include <algorithm>
#include <optional>
#include <string>
#include <vector>
#include "3party/just_gtfs/just_gtfs.h"
#include "3party/opening_hours/opening_hours.hpp"
namespace world_feed_tests
{
using namespace transit;
std::vector<gtfs::CalendarAvailability> GetCalendarAvailability(std::vector<size_t> const & data)
{
CHECK_EQUAL(data.size(), 7, ());
std::vector<gtfs::CalendarAvailability> res;
for (auto val : data)
if (val == 0)
res.push_back(gtfs::CalendarAvailability::NotAvailable);
else
res.push_back(gtfs::CalendarAvailability::Available);
return res;
}
gtfs::StopTimes GetFakeStopTimes(std::vector<std::string> const & transitIds)
{
auto ids = transitIds;
std::sort(ids.begin(), ids.end());
gtfs::StopTimes res;
for (size_t i = 0; i < ids.size(); ++i)
{
gtfs::StopTime st;
st.trip_id = ids[i];
st.stop_sequence = i;
res.emplace_back(st);
}
return res;
}
void TestInterval(WeekdaysInterval const & interval, size_t start, size_t end, osmoh::RuleSequence::Modifier status)
{
TEST_EQUAL(interval.m_start, start, ());
TEST_EQUAL(interval.m_end, end, ());
TEST_EQUAL(interval.m_status, status, ());
}
void TestExceptionIntervals(gtfs::CalendarDates const & dates, size_t intervalsCount,
std::string const & resOpeningHoursStr)
{
osmoh::TRuleSequences rules;
GetServiceDaysExceptionsOsmoh(dates, rules);
// TEST_EQUAL(rules.size(), intervalsCount, ());
auto const openingHours = ToString(osmoh::OpeningHours(rules));
TEST_EQUAL(openingHours, resOpeningHoursStr, ());
}
void TestStopsRange(IdList const & stopsOnLine, IdSet const & stopsInRegion, size_t firstIdxPlan, size_t lastIdxPlan)
{
auto const & [firstIdxFact, lastIdxFact] = GetStopsRange(stopsOnLine, stopsInRegion);
TEST_EQUAL(firstIdxFact, firstIdxPlan, ());
TEST_EQUAL(lastIdxFact, lastIdxPlan, ());
}
UNIT_TEST(Transit_GTFS_OpenCloseInterval1)
{
auto const & intervals = GetOpenCloseIntervals(GetCalendarAvailability({1, 1, 1, 1, 1, 0, 0}));
TEST_EQUAL(intervals.size(), 2, ());
TestInterval(intervals[0], 0, 4, osmoh::RuleSequence::Modifier::DefaultOpen);
TestInterval(intervals[1], 5, 6, osmoh::RuleSequence::Modifier::Closed);
}
UNIT_TEST(Transit_GTFS_OpenCloseInterval2)
{
auto const & intervals = GetOpenCloseIntervals(GetCalendarAvailability({0, 0, 0, 0, 0, 1, 0}));
TEST_EQUAL(intervals.size(), 3, ());
TestInterval(intervals[0], 0, 4, osmoh::RuleSequence::Modifier::Closed);
TestInterval(intervals[1], 5, 5, osmoh::RuleSequence::Modifier::DefaultOpen);
TestInterval(intervals[2], 6, 6, osmoh::RuleSequence::Modifier::Closed);
}
UNIT_TEST(Transit_GTFS_OpenCloseInterval3)
{
auto const & intervals = GetOpenCloseIntervals(GetCalendarAvailability({0, 0, 0, 0, 0, 0, 1}));
TEST_EQUAL(intervals.size(), 2, ());
TestInterval(intervals[0], 0, 5, osmoh::RuleSequence::Modifier::Closed);
TestInterval(intervals[1], 6, 6, osmoh::RuleSequence::Modifier::DefaultOpen);
}
UNIT_TEST(Transit_GTFS_GetTimeOsmoh)
{
size_t const hours = 21;
size_t const minutes = 5;
size_t const seconds = 30;
gtfs::Time const timeGtfs(hours, minutes, seconds);
auto const timeOsmoh = GetTimeOsmoh(timeGtfs);
TEST_EQUAL(timeOsmoh.GetMinutesCount(), minutes, ());
TEST_EQUAL(timeOsmoh.GetHoursCount(), hours, ());
}
UNIT_TEST(Transit_GTFS_ServiceDaysExceptions1)
{
gtfs::CalendarDates const exceptionDays{
{"serviceId1", gtfs::Date(2015, 01, 30), gtfs::CalendarDateException::Removed},
{"serviceId1", gtfs::Date(2015, 01, 31), gtfs::CalendarDateException::Removed},
{"serviceId1", gtfs::Date(2015, 02, 01), gtfs::CalendarDateException::Removed},
{"serviceId1", gtfs::Date(2015, 04, 03), gtfs::CalendarDateException::Added}};
TestExceptionIntervals(exceptionDays, 2 /* intervalsCount */,
"2015 Apr 03-2015 Apr 03; 2015 Jan 30-2015 Feb 01 closed" /* resOpeningHoursStr */);
}
UNIT_TEST(Transit_GTFS_ServiceDaysExceptions2)
{
gtfs::CalendarDates const exceptionDays{
{"serviceId2", gtfs::Date(1999, 11, 14), gtfs::CalendarDateException::Removed}};
TestExceptionIntervals(exceptionDays, 1 /* intervalsCount */,
"1999 Nov 14-1999 Nov 14 closed" /* resOpeningHoursStr */);
}
UNIT_TEST(Transit_GTFS_ServiceDaysExceptions3)
{
gtfs::CalendarDates const exceptionDays{
{"serviceId2", gtfs::Date(2005, 8, 01), gtfs::CalendarDateException::Added},
{"serviceId2", gtfs::Date(2005, 8, 12), gtfs::CalendarDateException::Added},
{"serviceId2", gtfs::Date(2005, 10, 11), gtfs::CalendarDateException::Removed},
{"serviceId2", gtfs::Date(2005, 10, 12), gtfs::CalendarDateException::Removed},
{"serviceId2", gtfs::Date(2005, 10, 13), gtfs::CalendarDateException::Added},
{"serviceId2", gtfs::Date(1999, 10, 14), gtfs::CalendarDateException::Removed}};
TestExceptionIntervals(exceptionDays, 2 /* intervalsCount */,
"2005 Aug 01-2005 Aug 01, 2005 Aug 12-2005 Aug 12, 2005 Oct 13-2005 Oct 13; 2005 Oct 11-2005 "
"Oct 12, 1999 Oct 14-1999 Oct 14 closed" /* resOpeningHoursStr */);
}
UNIT_TEST(Transit_GTFS_FindStopTimesByTransitId)
{
auto const allStopTimes = GetFakeStopTimes({"4", "5", "6", "2", "10", "2", "2", "6"});
auto const stopTimes1 = GetStopTimesForTrip(allStopTimes, "2");
TEST_EQUAL(stopTimes1.size(), 3, ());
auto const stopTimes10 = GetStopTimesForTrip(allStopTimes, "10");
TEST_EQUAL(stopTimes10.size(), 1, ());
auto const stopTimes6 = GetStopTimesForTrip(allStopTimes, "6");
TEST_EQUAL(stopTimes6.size(), 2, ());
auto const stopTimesNonExistent1 = GetStopTimesForTrip(allStopTimes, "11");
TEST(stopTimesNonExistent1.empty(), ());
auto const stopTimesNonExistent2 = GetStopTimesForTrip(allStopTimes, "1");
TEST(stopTimesNonExistent1.empty(), ());
}
UNIT_TEST(Transit_GTFS_FindStopTimesByTransitId2)
{
auto const allStopTimes = GetFakeStopTimes({"28", "28", "28", "28"});
auto const stopTimes = GetStopTimesForTrip(allStopTimes, "28");
TEST_EQUAL(stopTimes.size(), 4, ());
auto const stopTimesNonExistent = GetStopTimesForTrip(allStopTimes, "3");
TEST(stopTimesNonExistent.empty(), ());
}
// Stops are marked as *, points on polyline as +. Points have indexes, stops have letters.
//
// *A
//
// +----+---------------+----------------------+
// 0 1 2 3
//
// *B *C
//
UNIT_TEST(Transit_GTFS_ProjectStopToLine_Simple)
{
// index, was inserted
using ResT = std::pair<size_t, bool>;
double const y = 0.0002;
std::vector<m2::PointD> shape{{0.001, y}, {0.0015, y}, {0.004, y}, {0.005, y}};
m2::PointD const point_A{0.0012, 0.0003};
m2::PointD const point_B{0.00499, 0.0001};
m2::PointD const point_C{0.005, 0.0001};
// Test that point_A is projected between two existing polyline points and the new point is
// added in the place of its projection.
TEST_EQUAL(ResT(1, true),
PrepareNearestPointOnTrack(point_A, std::nullopt, 0 /* prevIndex */, Direction::Forward, shape), ());
TEST_EQUAL(shape.size(), 5, ());
TEST_EQUAL(shape[1 /* expectedIndex */], m2::PointD(point_A.x, y), ());
// Test that repeated point_A projection to the polyline doesn't lead to the second insertion.
// Expected point projection index is the same.
// But this projection is not inserted (it is already present).
TEST_EQUAL(ResT(1, false),
PrepareNearestPointOnTrack(point_A, std::nullopt, 0 /* prevIndex */, Direction::Forward, shape), ());
// So the shape size remains the same.
TEST_EQUAL(shape.size(), 5, ());
// Test that point_B insertion leads to addition of the new projection to the shape.
TEST_EQUAL(ResT(4, true),
PrepareNearestPointOnTrack(point_B, std::nullopt, 1 /* prevIndex */, Direction::Forward, shape), ());
// Test that point_C insertion does not lead to the addition of the new projection.
TEST_EQUAL(ResT(5, false),
PrepareNearestPointOnTrack(point_C, std::nullopt, 4 /* prevIndex */, Direction::Forward, shape), ());
// Test point_C projection in backward direction.
TEST_EQUAL(
ResT(5, false),
PrepareNearestPointOnTrack(point_C, std::nullopt, shape.size() - 1 /* prevIndex */, Direction::Backward, shape),
());
// Test point_B projection in backward direction.
TEST_EQUAL(ResT(4, false),
PrepareNearestPointOnTrack(point_B, std::nullopt, 5 /* prevIndex */, Direction::Backward, shape), ());
// Test point_A projection in backward direction.
TEST_EQUAL(ResT(1, false),
PrepareNearestPointOnTrack(point_A, std::nullopt, 4 /* prevIndex */, Direction::Backward, shape), ());
}
// Stop is on approximately the same distance from the segment (0, 1) and segment (1, 2).
// Its projection index and projection coordinate depend on the |startIndex| parameter.
//
// 1 +----------+ 2
// |
// | *A
// |
// 0 +
//
UNIT_TEST(Transit_GTFS_ProjectStopToLine_DifferentStartIndexes)
{
// index, was inserted
using ResT = std::pair<size_t, bool>;
std::vector<m2::PointD> const referenceShape{{0.001, 0.001}, {0.001, 0.002}, {0.003, 0.002}};
m2::PointD const point_A{0.0015, 0.0015};
// Test for |startIndex| = 0.
{
auto shape = referenceShape;
TEST_EQUAL(ResT(1, true),
PrepareNearestPointOnTrack(point_A, std::nullopt, 0 /* prevIndex */, Direction::Forward, shape), ());
TEST_EQUAL(shape.size(), 4, ());
TEST_EQUAL(shape[1 /* expectedIndex */], m2::PointD(0.001, point_A.y), ());
}
// Test for |startIndex| = 1.
{
auto shape = referenceShape;
TEST_EQUAL(ResT(2, true),
PrepareNearestPointOnTrack(point_A, std::nullopt, 1 /* prevIndex */, Direction::Forward, shape), ());
TEST_EQUAL(shape.size(), 4, ());
TEST_EQUAL(shape[2 /* expectedIndex */], m2::PointD(point_A.x, 0.002), ());
}
}
// Real-life example of stop being closer to the other side of the route (4, 5) then to its real
// destination (0, 1).
// We handle this type of situations by using constant max distance of departing from this stop
// on the polyline in |PrepareNearestPointOnTrack()|.
//
// 5 4
// +--------------------------------+---------------------------------------+ 3
// |
// /+-------------------------------------------------+ 2
// *A / 1
// /
// + 0
//
UNIT_TEST(Transit_GTFS_ProjectStopToLine_MaxDistance)
{
// index, was inserted
using ResT = std::pair<size_t, bool>;
std::vector<m2::PointD> shape{{0.002, 0.001}, {0.003, 0.003}, {0.010, 0.003},
{0.010, 0.0031}, {0.005, 0.0031}, {0.001, 0.0031}};
m2::PointD const point_A{0.0028, 0.0029};
TEST_EQUAL(ResT(1, true),
PrepareNearestPointOnTrack(point_A, std::nullopt, 0 /* prevIndex */, Direction::Forward, shape), ());
}
/* Complex shape with multiple points on it and multiple stops for projection.
*
* +-----+
* C* / \
* /+\ / \ *D
* + / \+/ \
* / +
* / | *E
* + +-----+
* | |
* | |
* +---+\ +-----+
* \ |
* B* + |
* A* \ +---------+
* + |
* | +
* + *F
*/
UNIT_TEST(Transit_GTFS_ProjectStopToLine_NearCircle)
{
// index, was inserted
using ResT = std::pair<size_t, bool>;
double constexpr kEps = 1e-5;
std::vector<m2::PointD> const initialShape{{0.003, 0.001}, {0.003, 0.0015}, {0.0025, 0.002}, {0.002, 0.0025},
{0.001, 0.0025}, {0.001, 0.0035}, {0.0015, 0.0045}, {0.0025, 0.005},
{0.0035, 0.0045}, {0.004, 0.0055}, {0.0055, 0.0055}, {0.0065, 0.0045},
{0.0065, 0.0035}, {0.0075, 0.0035}, {0.0075, 0.0025}, {0.0065, 0.0025},
{0.0065, 0.0015}, {0.004, 0.0015}, {0.004, 0.001}};
m2::PointD const point_A{0.0024, 0.0018};
m2::PointD const point_B{0.002499, 0.00199};
m2::PointD const point_C{0.0036, 0.0049};
m2::PointD const point_D{0.0063, 0.005};
m2::PointD const point_E{0.008, 0.004};
m2::PointD const point_F{0.0047, 0.0005};
// Forward
auto shape = initialShape;
TEST_EQUAL(ResT(2, true),
PrepareNearestPointOnTrack(point_A, std::nullopt, 0 /* prevIndex */, Direction::Forward, shape), ());
auto const coordA = shape[2];
TEST_EQUAL(ResT(3, false),
PrepareNearestPointOnTrack(point_B, std::nullopt, 2 /* prevIndex */, Direction::Forward, shape), ());
auto const coordB = shape[3];
TEST_EQUAL(ResT(10, true),
PrepareNearestPointOnTrack(point_C, std::nullopt, 3 /* prevIndex */, Direction::Forward, shape), ());
auto const coordC = shape[10];
TEST_EQUAL(ResT(12, false),
PrepareNearestPointOnTrack(point_D, std::nullopt, 10 /* prevIndex */, Direction::Forward, shape), ());
auto const coordD = shape[12];
TEST_EQUAL(ResT(14, true),
PrepareNearestPointOnTrack(point_E, std::nullopt, 12 /* prevIndex */, Direction::Forward, shape), ());
auto const coordE = shape[14];
TEST_EQUAL(ResT(20, true),
PrepareNearestPointOnTrack(point_F, std::nullopt, 14 /* prevIndex */, Direction::Forward, shape), ());
// Backward processing of reversed shape
shape = initialShape;
reverse(shape.begin(), shape.end());
TEST_EQUAL(
ResT(17, true),
PrepareNearestPointOnTrack(point_A, std::nullopt, shape.size() - 1 /* prevIndex */, Direction::Backward, shape),
());
TEST(AlmostEqualAbs(coordA, shape[17], kEps), (coordA, shape[17]));
TEST_EQUAL(ResT(16, false),
PrepareNearestPointOnTrack(point_B, std::nullopt, 17 /* prevIndex */, Direction::Backward, shape), ());
TEST(AlmostEqualAbs(coordB, shape[16], kEps), (coordA, shape[17]));
TEST_EQUAL(ResT(10, true),
PrepareNearestPointOnTrack(point_C, std::nullopt, 16 /* prevIndex */, Direction::Backward, shape), ());
TEST(AlmostEqualAbs(coordC, shape[10], kEps), (coordA, shape[17]));
TEST_EQUAL(ResT(8, false),
PrepareNearestPointOnTrack(point_D, std::nullopt, 10 /* prevIndex */, Direction::Backward, shape), ());
TEST(AlmostEqualAbs(coordD, shape[8], kEps), (coordA, shape[17]));
TEST_EQUAL(ResT(7, true),
PrepareNearestPointOnTrack(point_E, std::nullopt, 8 /* prevIndex */, Direction::Backward, shape), ());
TEST(AlmostEqualAbs(coordE, shape[7], kEps), (coordA, shape[17]));
// point_F has different position because we do not insert before point 0.
TEST_EQUAL(ResT(2, true),
PrepareNearestPointOnTrack(point_F, std::nullopt, 7 /* prevIndex */, Direction::Backward, shape), ());
}
UNIT_TEST(Transit_ColorPicker)
{
ColorPicker colorPicker;
// We check that we don't match with the 'text' colors subset. This is the color of transit
// text lime_light and we expect not to pick it.
TEST_EQUAL(colorPicker.GetNearestColor("827717"), "lime_dark", ());
// We check the default color for invalid input.
TEST_EQUAL(colorPicker.GetNearestColor("94141230"), "default", ());
// We check that we really find nearest colors. This input is really close to pink light.
TEST_EQUAL(colorPicker.GetNearestColor("d18aa2"), "pink_light", ());
}
UNIT_TEST(Transit_BuildHash1Arg)
{
TEST_EQUAL(BuildHash(std::string("Title")), "Title", ());
TEST_EQUAL(BuildHash(std::string("Id1"), std::string("Id2")), "Id1_Id2", ());
TEST_EQUAL(BuildHash(std::string("A"), std::string("B"), std::string("C")), "A_B_C", ());
}
UNIT_TEST(IntersectionSimple)
{
auto const & factRes = FindIntersections({{1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}},
{{4.0, 4.0}, {1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}, {4.0, 4.0}});
std::pair<LineSegments, LineSegments> planRes{{LineSegment(0, 2)}, {LineSegment(1, 3)}};
TEST(factRes == planRes, ());
}
std::vector<m2::PointD> Get2DVector(std::vector<size_t> const & v)
{
std::vector<m2::PointD> res;
res.reserve(v.size());
for (size_t val : v)
res.emplace_back(static_cast<double>(val), 1.0);
return res;
}
std::pair<LineSegments, LineSegments> GetIntersections(std::vector<size_t> const & line1,
std::vector<size_t> const & line2)
{
return FindIntersections(Get2DVector(line1), Get2DVector(line2));
}
UNIT_TEST(IntersectionShortest)
{
auto const & factRes = GetIntersections({10, 15}, {10, 15});
std::pair<LineSegments, LineSegments> planRes{{LineSegment(0, 1)}, {LineSegment(0, 1)}};
TEST(factRes == planRes, ());
}
UNIT_TEST(IntersectionNone)
{
auto const & factRes = GetIntersections({100, 105}, {101, 110});
std::pair<LineSegments, LineSegments> planRes{{}, {}};
TEST(factRes == planRes, ());
}
UNIT_TEST(IntersectionDouble)
{
auto const & factRes = GetIntersections({1, 2, 3, 4, 5, 6, 7, 8}, {3, 4, 5, 100, 7, 8});
std::pair<LineSegments, LineSegments> planRes{{LineSegment(2, 4), LineSegment(6, 7)},
{LineSegment(0, 2), LineSegment(4, 5)}};
TEST(factRes == planRes, ());
}
UNIT_TEST(IntersectionTriple)
{
auto const & factRes = GetIntersections({1, 2, 3, 6, 6, 6, 7, 8, 6, 6, 9, 10}, {0, 0, 1, 2, 3, 0, 7, 8, 0, 9, 10, 0});
std::pair<LineSegments, LineSegments> planRes{{LineSegment(0, 2), LineSegment(6, 7), LineSegment(10, 11)},
{LineSegment(2, 4), LineSegment(6, 7), LineSegment(9, 10)}};
TEST(factRes == planRes, ());
}
UNIT_TEST(GetIntersectionInner)
{
auto inter = GetIntersection(0, 5, 2, 4);
TEST(inter, ());
TEST_EQUAL(inter->m_startIdx, 2, ());
TEST_EQUAL(inter->m_endIdx, 4, ());
}
UNIT_TEST(GetIntersectionNone)
{
auto inter = GetIntersection(1, 10, 20, 40);
TEST(!inter, ());
}
UNIT_TEST(GetIntersectionLeft)
{
auto inter = GetIntersection(10, 100, 5, 15);
TEST(inter, ());
TEST_EQUAL(inter->m_startIdx, 10, ());
TEST_EQUAL(inter->m_endIdx, 15, ());
}
UNIT_TEST(GetIntersectionRight)
{
auto inter = GetIntersection(0, 8, 5, 10);
TEST(inter, ());
TEST_EQUAL(inter->m_startIdx, 5, ());
TEST_EQUAL(inter->m_endIdx, 8, ());
}
UNIT_TEST(GetIntersectionSingle)
{
auto inter = GetIntersection(0, 8, 8, 10);
TEST(!inter, ());
}
UNIT_TEST(CalcSegmentOrder)
{
TEST_EQUAL(CalcSegmentOrder(0 /* segIndex */, 1 /* totalSegCount */), 0, ());
TEST_EQUAL(CalcSegmentOrder(0 /* segIndex */, 2 /* totalSegCount */), -1, ());
TEST_EQUAL(CalcSegmentOrder(1 /* segIndex */, 2 /* totalSegCount */), 1, ());
TEST_EQUAL(CalcSegmentOrder(0 /* segIndex */, 3 /* totalSegCount */), -2, ());
TEST_EQUAL(CalcSegmentOrder(1 /* segIndex */, 3 /* totalSegCount */), 0, ());
TEST_EQUAL(CalcSegmentOrder(2 /* segIndex */, 3 /* totalSegCount */), 2, ());
TEST_EQUAL(CalcSegmentOrder(0 /* segIndex */, 4 /* totalSegCount */), -3, ());
TEST_EQUAL(CalcSegmentOrder(1 /* segIndex */, 4 /* totalSegCount */), -1, ());
TEST_EQUAL(CalcSegmentOrder(2 /* segIndex */, 4 /* totalSegCount */), 1, ());
TEST_EQUAL(CalcSegmentOrder(3 /* segIndex */, 4 /* totalSegCount */), 3, ());
TEST_EQUAL(CalcSegmentOrder(0 /* segIndex */, 5 /* totalSegCount */), -4, ());
TEST_EQUAL(CalcSegmentOrder(1 /* segIndex */, 5 /* totalSegCount */), -2, ());
TEST_EQUAL(CalcSegmentOrder(2 /* segIndex */, 5 /* totalSegCount */), 0, ());
TEST_EQUAL(CalcSegmentOrder(3 /* segIndex */, 5 /* totalSegCount */), 2, ());
TEST_EQUAL(CalcSegmentOrder(4 /* segIndex */, 5 /* totalSegCount */), 4, ());
}
UNIT_TEST(SplitLineToRegions)
{
TestStopsRange({1, 2, 3, 4, 5} /* stopsOnLine */, {1, 2, 3, 4, 5} /* stopsInRegion */, 0 /* firstIdxPlan */,
4 /* lastIdxPlan */);
TestStopsRange({1, 2, 3, 4, 5, 6, 7} /* stopsOnLine */, {1, 2, 3} /* stopsInRegion */, 0 /* firstIdxPlan */,
3 /* lastIdxPlan */);
TestStopsRange({1, 2, 3, 4, 5, 6, 7} /* stopsOnLine */, {3, 4} /* stopsInRegion */, 1 /* firstIdxPlan */,
4 /* lastIdxPlan */);
}
} // namespace world_feed_tests