diff --git a/CMakeLists.txt b/CMakeLists.txt index acd2e35e..15648179 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,6 +192,7 @@ set(SOURCES "editor/editorGroundFollow.cpp" "editor/editorAlignmentStore.cpp" "editor/editorSwitchStore.cpp" +"editor/editorSwitchCurve.cpp" "editor/editorKilometrage.cpp" "editor/editorStation.cpp" "editor/editorSwitchDesign.cpp" diff --git a/application/editormode.h b/application/editormode.h index 1743486d..7ed04eb4 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -35,6 +35,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorAlignment.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" +#include "editor/editorSwitchCurve.hpp" #include "editor/editorKilometrage.hpp" #include "editor/editorSpeed.hpp" #include "editor/editorRoad.hpp" @@ -1240,6 +1241,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri int last{0}; // template armed the last time bool placing{false}; double along{0.0}; + double pressed{0.0}; // along a curve: where the press was, the start of the switch drawn from it to the ends of the cant ramps glm::dvec3 point{0.0}; glm::dvec3 mouse{0.0}; editor_track::straight line; @@ -1528,6 +1530,15 @@ class editor_mode : public application_mode, private editor_track::observer, pri void switch_placed(std::vector const &Created); static std::vector sample_chain(editor_track::chain const &Chain); static curve_sample sample_at(std::vector const &Frame, double const Station); + // the curve a switch goes into as the norms look at it: its headings and its cant along it + std::vector frame_points(std::vector const &Frame) const; + // the way the switch being placed into a curve goes along it and the side its diverging track leaves to, from the drag + void curved_switch_pose(int &Direction, int &Side) const; + // the start of the switch being placed into a curve: where it was pressed, drawn to where the switch meets an end of a cant ramp or + // keeps 6 m off it; What receives which + double curved_switch_start(int const Direction, std::string *What = nullptr) const; + // the switch made of the paths, laid into the curve at the station, as ST-T1-A6 sees it + switch_curve::switch_report curved_switch_report(std::vector const &Paths, double const Station, int const Direction) const; static double nearest_station(std::vector const &Frame, glm::dvec3 const &Point); static std::vector curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side); void insert_curved_switch(int const Direction, int const Side); diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp index 1ab64d54..5b87c716 100644 --- a/application/editormodeswitchplace.cpp +++ b/application/editormodeswitchplace.cpp @@ -51,12 +51,10 @@ std::vector editor_mode::switch_preview() const return {}; if (tool.curved) { - auto const sample{sample_at(tool.frame, tool.along)}; - glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; - auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; - glm::dvec2 const direction{sample.tangent * static_cast(forward)}; - auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; - return curved_switch_paths(tool.templates[tool.armed], tool.frame, tool.along, forward, side); + int forward; + int side; + curved_switch_pose(forward, side); + return curved_switch_paths(tool.templates[tool.armed], tool.frame, curved_switch_start(forward), forward, side); } if (switch_link plan; plan_switch_link(plan)) { @@ -116,6 +114,7 @@ bool editor_mode::start_switch_placement() return false; tool.curved = true; tool.along = nearest_station(tool.frame, ground); + tool.pressed = tool.along; tool.point = sample_at(tool.frame, tool.along).position; if (plan_distance(tool.point, ground) > kSwitchReach) return false; @@ -167,11 +166,10 @@ void editor_mode::finish_switch_placement() } if (tool.curved) { - auto const sample{sample_at(tool.frame, tool.along)}; - glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; - auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; - glm::dvec2 const direction{sample.tangent * static_cast(forward)}; - auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + int forward; + int side; + curved_switch_pose(forward, side); + tool.along = curved_switch_start(forward); insert_curved_switch(forward, side); return; } @@ -1256,6 +1254,89 @@ double editor_mode::nearest_station(std::vector const &Frame, glm: return station; } +std::vector editor_mode::frame_points(std::vector const &Frame) const +{ + auto const gauge{m_route.design.norms.gauge}; + std::vector points; + for (auto const &sample : Frame) + { + switch_curve::track_point point; + point.station = sample.station; + point.heading = std::atan2(sample.tangent.y, sample.tangent.x); + // the headings go on through a half turn, so that their differences are the turns between them + if (false == points.empty()) + point.heading = points.back().heading + std::remainder(point.heading - points.back().heading, 2.0 * glm::pi()); + point.cant = gauge * std::sin(glm::radians(static_cast(sample.roll))); + points.push_back(point); + } + return points; +} + +void editor_mode::curved_switch_pose(int &Direction, int &Side) const +{ + auto const &tool{m_switch}; + auto const sample{sample_at(tool.frame, tool.pressed)}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + Direction = glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1; + glm::dvec2 const direction{sample.tangent * static_cast(Direction)}; + Side = direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1; +} + +double editor_mode::curved_switch_start(int const Direction, std::string *What) const +{ + auto const &tool{m_switch}; + if (What != nullptr) + What->clear(); + if (tool.frame.size() < 2 || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return tool.pressed; + auto const length{tool.templates[tool.armed].length}; + std::string what; + auto const start{switch_curve::snapped_start(switch_curve::ramp_ends(frame_points(tool.frame)), tool.pressed, length, Direction, 3.0, &what)}; + // a start which would put the switch off the curve isn't one + auto const total{tool.frame.back().station}; + if (Direction > 0 ? start < 0.0 || start > total - length : start < length || start > total) + return tool.pressed; + if (What != nullptr) + *What = what; + return start; +} + +switch_curve::switch_report editor_mode::curved_switch_report(std::vector const &Paths, double const Station, int const Direction) const +{ + auto const &tool{m_switch}; + if (Paths.size() < 2 || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return {}; + // a path of the switch from its start, in the plan + auto const along = [](segment_data const &Path) { + std::vector points; + auto const add = [&](segment_data const &Piece) { + bezier const curve{Piece}; + auto const steps{std::max(4, static_cast(std::ceil(curve.plan_length() / 0.25)))}; + for (int k = points.empty() ? 0 : 1; k <= steps; ++k) + { + auto const t{static_cast(k) / steps}; + auto const position{plan_of(curve.point(t))}; + auto const tangent{plan_of(curve.first(t))}; + switch_curve::track_point point; + point.heading = std::atan2(tangent.y, tangent.x); + if (false == points.empty()) + { + point.station = points.back().station + glm::distance(position, plan_of(curve.point(static_cast(k - 1) / steps))); + point.heading = points.back().heading + std::remainder(point.heading - points.back().heading, 2.0 * glm::pi()); + } + points.push_back(point); + } + }; + if (Path.pieces.empty()) + add(Path); + for (auto const &piece : Path.pieces) + add(piece); + return points; + }; + auto const &shape{tool.templates[tool.armed]}; + return switch_curve::check(along(Paths[0]), along(Paths[1]), frame_points(tool.frame), Station, Station + Direction * shape.length, shape.radius, shape.ratio); +} + std::vector editor_mode::curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side) { auto const map = [&](glm::dvec3 const &Local, glm::dvec2 &Tangent, glm::dvec2 const &Localtangent) { @@ -1393,10 +1474,16 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) } } auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)}; + auto const report{curved_switch_report(paths, station, Direction)}; auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; created.insert(created.end(), made.begin(), made.end()); if (made.size() == 1) tool.status = format(STR_C("Switch %s inserted into the curve, Ctrl+Z takes it back"), made.front()->name().c_str()); + // what the norms say of it stays in the status + if (report.transition) + tool.status += "\n" + report.notes.front(); + for (auto const &warning : report.warnings) + tool.status += "\n" + warning; auto const placed{created}; push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 450e97fe..973d8a6c 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -2354,8 +2354,64 @@ void editor_mode::draw_build_overlay() const } else if (m_switch.placing) { - for (auto const &piece : switch_preview()) + auto const preview{switch_preview()}; + for (auto const &piece : preview) drawpath(piece, overlay_color::marked); + // into a curve: the ends of the cant ramps, what the start was drawn to, the radii at the ends of the switch and the norms + if (auto const &tool{m_switch}; tool.curved && tool.connection < 0 && preview.size() >= 2) + { + auto const points{frame_points(tool.frame)}; + for (auto const end : switch_curve::ramp_ends(points)) + { + auto const sample{sample_at(tool.frame, end)}; + glm::dvec3 const across{-sample.tangent.y * 2.5, 0.0, sample.tangent.x * 2.5}; + projection.line(drawlist, sample.position - across, sample.position + across, IM_COL32(255, 170, 40, 255), 3.0f); + ImVec2 screen; + if (projection.project(sample.position + across, screen)) + drawlist->AddText({screen.x + 4.0f, screen.y - 7.0f}, IM_COL32(255, 170, 40, 255), STR_C("ramp")); + } + int forward; + int side; + curved_switch_pose(forward, side); + std::string snapped; + auto const station{curved_switch_start(forward, &snapped)}; + auto const report{curved_switch_report(preview, station, forward)}; + // a radius counted positive the way the line turns, the other way with a minus + auto const radius = [](double const Curvature) { return std::abs(Curvature) < 2e-5 ? std::string{STR_C("straight")} : format("%.0f m", 1.0 / Curvature); }; + std::vector> lines; + if (false == snapped.empty()) + lines.emplace_back(snapped, IM_COL32(255, 170, 40, 255)); + if (report.curved) + { + lines.emplace_back(format(STR_C("main track R %s at the start, %s at the end"), radius(report.main_start).c_str(), radius(report.main_end).c_str()), IM_COL32(255, 255, 255, 235)); + lines.emplace_back(format(STR_C("diverging track R %s at the start, %s at the end"), radius(report.branch_start).c_str(), radius(report.branch_end).c_str()), IM_COL32(255, 255, 255, 235)); + } + lines.emplace_back(format(STR_C("cant %.0f mm at the start, %.0f mm at the end"), report.cant_start, report.cant_end), IM_COL32(255, 255, 255, 235)); + for (auto const ¬e : report.notes) + lines.emplace_back(note, IM_COL32(170, 200, 230, 235)); + for (auto const &warning : report.warnings) + lines.emplace_back(warning, overlay_color::invalid); + ImVec2 screen; + if (projection.project(sample_at(tool.frame, station).position, screen)) + { + auto const wrap{460.0f}; + auto height{0.0f}; + auto width{0.0f}; + for (auto const &[text, colour] : lines) + { + auto const size{ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap)}; + height += size.y + 2.0f; + width = std::max(width, size.x); + } + ImVec2 corner{screen.x + 24.0f, screen.y + 24.0f}; + drawlist->AddRectFilled({corner.x - 6.0f, corner.y - 4.0f}, {corner.x + width + 6.0f, corner.y + height + 4.0f}, IM_COL32(10, 20, 30, 215), 4.0f); + for (auto const &[text, colour] : lines) + { + drawlist->AddText(ImGui::GetFont(), ImGui::GetFontSize(), corner, colour, text.c_str(), nullptr, wrap); + corner.y += ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap).y + 2.0f; + } + } + } switch_link plan; auto const linked{plan_switch_link(plan)}; ImVec2 screen; diff --git a/editor/editorSwitchCurve.cpp b/editor/editorSwitchCurve.cpp new file mode 100644 index 00000000..d1393b43 --- /dev/null +++ b/editor/editorSwitchCurve.cpp @@ -0,0 +1,229 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorSwitchCurve.hpp" +#include "editor/editorFormat.hpp" +#include "utilities/translation.h" + +#include +#include +#include + +namespace switch_curve +{ + +namespace +{ + +// 1/m: a curvature below it is a straight, the curvature measured over a few metres never quite 0 +constexpr double kStraight{2e-5}; +// m: an end of a ramp this close to an end of the switch is at it +constexpr double kAtEnd{0.3}; +// m: how far off the switch a ramp keeps, 9.6 pkt 16 +constexpr double kRampClear{6.0}; + +// the curved switches bent from the basic ones, table 11.9: the radius of the basic switch, the limiting curvature of the +// diverging track and the least radius of the main track, the values allowed by way of exception in the brackets, 0 for none +struct bent_limit +{ + double radius; + double branch; + double branch_exception; + double main; + double main_exception; +}; +constexpr bent_limit kBent[]{ + {300.0, 4.000e-3, 4.673e-3, 1505.522, 749.722}, {500.0, 4.000e-3, 4.673e-3, 501.730, 375.638}, {760.0, 3.333e-3, 0.0, 497.250, 0.0}, {1200.0, 2.169e-3, 0.0, 750.001, 0.0}, {2500.0, 1.063e-3, 0.0, 1510.0, 0.0}}; + +double heading_at(std::vector const &Track, double const Station) +{ + if (Station <= Track.front().station) + return Track.front().heading; + if (Station >= Track.back().station) + return Track.back().heading; + auto const next{std::lower_bound(Track.begin(), Track.end(), Station, [](track_point const &Point, double const Value) { return Point.station < Value; })}; + auto const previous{std::prev(next)}; + auto const span{std::max(1e-9, next->station - previous->station)}; + return previous->heading + (next->heading - previous->heading) * (Station - previous->station) / span; +} + +double cant_at(std::vector const &Track, double const Station) +{ + if (Station <= Track.front().station) + return Track.front().cant; + if (Station >= Track.back().station) + return Track.back().cant; + auto const next{std::lower_bound(Track.begin(), Track.end(), Station, [](track_point const &Point, double const Value) { return Point.station < Value; })}; + auto const previous{std::prev(next)}; + auto const span{std::max(1e-9, next->station - previous->station)}; + return previous->cant + (next->cant - previous->cant) * (Station - previous->station) / span; +} + +} // namespace + +double curvature_at(std::vector const &Track, double const Station) +{ + if (Track.size() < 2) + return 0.0; + auto const from{std::max(Track.front().station, Station - 1.0)}; + auto const to{std::min(Track.back().station, Station + 1.0)}; + if (to - from < 0.2) + return 0.0; + return (heading_at(Track, to) - heading_at(Track, from)) / (to - from); +} + +std::vector ramp_ends(std::vector const &Track) +{ + std::vector ends; + auto ramping{false}; + for (std::size_t i = 0; i + 1 < Track.size(); ++i) + { + auto const span{Track[i + 1].station - Track[i].station}; + if (span < 1e-6) + continue; + // a change of the cant of 1 mm over 20 m and more + auto const changing{std::abs(Track[i + 1].cant - Track[i].cant) / span > 0.05}; + if (changing == ramping) + continue; + ramping = changing; + // a ramp going on after a break of under a metre, where the paths meet, is the same ramp + if (changing && false == ends.empty() && Track[i].station - ends.back() < 1.0) + ends.pop_back(); + else + ends.push_back(Track[i].station); + } + if (ramping) + ends.push_back(Track.back().station); + return ends; +} + +switch_report check(std::vector const &Main, std::vector const &Branch, std::vector const &Line, double const From, double const To, double const Radius, + double const Ratio) +{ + switch_report report; + if (Main.size() < 2 || Branch.size() < 2 || Line.size() < 2) + return report; + // the curvatures counted positive the way the line turns, as the switch goes along it + auto const direction{To >= From ? 1.0 : -1.0}; + auto const sign{(Line.back().heading - Line.front().heading < 0.0 ? -1.0 : 1.0) * direction}; + struct span + { + double first{0.0}; + double last{0.0}; + double low{std::numeric_limits::max()}; + double high{std::numeric_limits::lowest()}; + double sharpest{0.0}; + }; + auto const along = [&](std::vector const &Track) { + span result; + auto const length{Track.back().station - Track.front().station}; + auto const margin{std::min(1.0, length * 0.25)}; + result.first = sign * curvature_at(Track, Track.front().station + margin); + result.last = sign * curvature_at(Track, Track.back().station - margin); + for (auto s{Track.front().station + margin}; s <= Track.back().station - margin + 1e-9; s += 0.5) + { + auto const curvature{sign * curvature_at(Track, s)}; + result.low = std::min(result.low, curvature); + result.high = std::max(result.high, curvature); + result.sharpest = std::max(result.sharpest, std::abs(curvature)); + } + return result; + }; + auto const main{along(Main)}; + auto const branch{along(Branch)}; + report.main_start = main.first; + report.main_end = main.last; + report.branch_start = branch.first; + report.branch_end = branch.last; + report.cant_start = std::abs(cant_at(Line, From)); + report.cant_end = std::abs(cant_at(Line, To)); + report.curved = main.sharpest > kStraight; + report.transition = report.curved && std::abs(main.last - main.first) > std::max(2e-6, 0.05 * std::max(std::abs(main.first), std::abs(main.last))); + + if (report.transition) + report.notes.emplace_back(STR_C("On a transition curve: only by way of exception, by consent of the Zaklad Linii Kolejowych (9.7 pkt 12); the plan gives the radii of both tracks at the start and at the end of the switch")); + // the curvature of each track of one sign all along it, the cases KZW I and KZW III of 9.7 pkt 12, not KZW II + if (branch.low < -kStraight && branch.high > kStraight) + report.warnings.emplace_back(STR_C("The curvature of the diverging track turns from one side to the other within the switch (KZW II), which isn't allowed (9.7 pkt 12)")); + if (main.low < -kStraight && main.high > kStraight) + report.warnings.emplace_back(STR_C("The curvature of the main track turns from one side to the other within the switch, which isn't allowed (9.7 pkt 12)")); + if (report.curved) + { + auto const limit{std::find_if(std::begin(kBent), std::end(kBent), [&](bent_limit const &Limit) { return std::abs(Limit.radius - Radius) < 2.0; })}; + if (limit == std::end(kBent)) + report.notes.emplace_back(format(STR_C("The basic switch of R %.0f m isn't in table 11.9, its limiting curvature isn't checked"), Radius)); + else + { + if (branch.sharpest > limit->branch + 1e-9) + report.warnings.emplace_back(limit->branch_exception > 0.0 && branch.sharpest <= limit->branch_exception + 1e-9 ? + format(STR_C("The curvature of the diverging track %.3f 1/km is over the limiting %.3f 1/km of table 11.9, up to %.3f 1/km only by consent of the Zaklad Linii Kolejowych"), branch.sharpest * 1000.0, + limit->branch * 1000.0, limit->branch_exception * 1000.0) : + format(STR_C("The curvature of the diverging track %.3f 1/km is over the limiting %.3f 1/km for the switch of R %.0f m (table 11.9)"), branch.sharpest * 1000.0, limit->branch * 1000.0, Radius)); + if (main.sharpest > kStraight && 1.0 / main.sharpest < limit->main - 1e-6) + report.warnings.emplace_back(limit->main_exception > 0.0 && 1.0 / main.sharpest >= limit->main_exception - 1e-6 ? + format(STR_C("The main track bent to R %.0f m, below the least %.0f m of table 11.9, down to %.0f m only by consent of the Zaklad Linii Kolejowych"), 1.0 / main.sharpest, limit->main, limit->main_exception) : + format(STR_C("The main track bent to R %.0f m, below the least %.0f m for the switch of R %.0f m (table 11.9)"), 1.0 / main.sharpest, limit->main, Radius)); + } + if (Ratio > 0.0 && Ratio < 9.41) + report.warnings.emplace_back(format(STR_C("A switch of 1:%.3f isn't bent in the running and the main tracks: none of 1:9.403 nor steeper than 1:9 (11.4 pkt 4)"), Ratio)); + } + + // the cant: the same all along the switch, a ramp off it by 6 m at least or ending at its ends (11.4 pkt 7, 9.6 pkt 16-17) + if (std::abs(report.cant_end - report.cant_start) > 1.0) + report.warnings.emplace_back(format(STR_C("The cant changes within the switch, %.0f to %.0f mm: it's to stay the same over it (11.4 pkt 7); a straight ramp through the switch only by consent of the Zaklad Linii Kolejowych"), + report.cant_start, report.cant_end)); + auto const low{std::min(From, To)}; + auto const high{std::max(From, To)}; + for (auto const end : ramp_ends(Line)) + { + if (end > low + kAtEnd && end < high - kAtEnd) + report.warnings.emplace_back(format(STR_C("A cant ramp starts or ends within the switch, %.1f m from its start: at the start or the end of the switch, or 6 m off it at least (9.6 pkt 16-17)"), std::abs(end - From))); + else if (std::abs(end - low) <= kAtEnd || std::abs(end - high) <= kAtEnd) + report.notes.emplace_back(STR_C("A cant ramp starts or ends at an end of the switch")); + else if (auto const off{end < low ? low - end : end - high}; off < kRampClear) + report.warnings.emplace_back(format(STR_C("A cant ramp ends %.1f m off the switch, 6 m at least (9.6 pkt 16)"), off)); + } + auto const cant{std::max(report.cant_start, report.cant_end)}; + if (report.curved && cant > 100.0 + 1e-6) + report.warnings.emplace_back(format(STR_C("Cant %.0f mm in a bent switch, over 100 mm (table 9.6)"), cant)); + else if (report.curved && cant > 60.0 + 1e-6) + report.notes.emplace_back(format(STR_C("Cant %.0f mm: over the 60 mm recommended for a bent switch, up to 100 mm (table 9.6)"), cant)); + else if (false == report.curved && cant > 1.0) + report.notes.emplace_back(format(STR_C("Cant %.0f mm in a straight switch: none recommended, up to 100 mm (table 9.6)"), cant)); + return report; +} + +double snapped_start(std::vector const &Ramps, double const Pressed, double const Length, int const Direction, double const Reach, std::string *What) +{ + auto const d{Direction < 0 ? -1.0 : 1.0}; + auto best{Pressed}; + auto nearest{Reach}; + std::string name; + auto const consider = [&](double const Start, char const *Name) { + if (auto const distance{std::abs(Start - Pressed)}; distance <= nearest) + { + nearest = distance; + best = Start; + name = Name; + } + }; + for (auto const end : Ramps) + { + consider(end, STR_C("the switch starts at an end of the cant ramp")); + consider(end - d * Length, STR_C("the switch ends at an end of the cant ramp")); + consider(end + d * kRampClear, STR_C("the switch starts 6 m off the cant ramp")); + consider(end - d * (kRampClear + Length), STR_C("the switch ends 6 m off the cant ramp")); + } + if (What != nullptr) + *What = name; + return best; +} + +} // namespace switch_curve diff --git a/editor/editorSwitchCurve.hpp b/editor/editorSwitchCurve.hpp new file mode 100644 index 00000000..bde0f459 --- /dev/null +++ b/editor/editorSwitchCurve.hpp @@ -0,0 +1,61 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +// a switch laid into a curve or into a transition curve, as the standard of PKP PLK ST-T1-A6 sees it: the curvatures of its tracks +// (9.7 pkt 12, 11.4, table 11.9) and the cant along it, which is to stay the same over it or be ramped from its ends (9.6 pkt +// 16-17, 11.4 pkt 7, table 9.6) +namespace switch_curve +{ + +// a point of a track in the plan: how far along it, which way it heads (radians, growing as it turns to the left), its cant (mm) +struct track_point +{ + double station{0.0}; + double heading{0.0}; + double cant{0.0}; +}; + +// the curvature of the track at the station, from its headings a metre either side of it, 1/m positive to the left +double curvature_at(std::vector const &Track, double const Station); + +// the stations where the cant of the track starts or stops changing: the ends of its cant ramps +std::vector ramp_ends(std::vector const &Track); + +struct switch_report +{ + // at the start and at the end of the switch, 1/m, positive turning the way the line does + double main_start{0.0}; + double main_end{0.0}; + double branch_start{0.0}; + double branch_end{0.0}; + // of the main track, mm + double cant_start{0.0}; + double cant_end{0.0}; + bool curved{false}; // the main track bent + bool transition{false}; // its curvature changing along the switch + std::vector notes; + std::vector warnings; +}; + +// Main, Branch: the tracks of the switch from its start, the stations from 0 there; Line: the track it's laid into, From and To the +// stations of the start and the end of the switch along it, From after To where it goes back along the line. Radius and Ratio: +// of the diverging track of the basic switch it's bent from +switch_report check(std::vector const &Main, std::vector const &Branch, std::vector const &Line, double const From, double const To, double const Radius, + double const Ratio); + +// the start of a switch Length long, going Direction along the line, drawn to where its start or its end meets an end of a cant ramp, +// or to where it keeps the 6 m off it, when the station pressed is within Reach of that; What receives which of them, empty for none +double snapped_start(std::vector const &Ramps, double const Pressed, double const Length, int const Direction, double const Reach, std::string *What = nullptr); + +} // namespace switch_curve diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 464111a9..676586f6 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11974,3 +11974,75 @@ msgstr "W%d R%d %.0f m przechyłka %.0f mm" msgid "W%d R %.0f m cant %.0f mm" msgstr "W%d R %.0f m przechyłka %.0f mm" + +msgid "On a transition curve: only by way of exception, by consent of the Zaklad Linii Kolejowych (9.7 pkt 12); the plan gives the radii of both tracks at the start and at the end of the switch" +msgstr "Na krzywej przejściowej: tylko wyjątkowo, za zgodą Zakładu Linii Kolejowych (9.7 pkt 12); na planie podaje się promienie obu torów na początku i na końcu rozjazdu" + +msgid "The curvature of the diverging track turns from one side to the other within the switch (KZW II), which isn't allowed (9.7 pkt 12)" +msgstr "Krzywizna toru zwrotnego zmienia znak w obrębie rozjazdu (KZW II), co jest niedozwolone (9.7 pkt 12)" + +msgid "The curvature of the main track turns from one side to the other within the switch, which isn't allowed (9.7 pkt 12)" +msgstr "Krzywizna toru zasadniczego zmienia znak w obrębie rozjazdu, co jest niedozwolone (9.7 pkt 12)" + +msgid "The basic switch of R %.0f m isn't in table 11.9, its limiting curvature isn't checked" +msgstr "Rozjazdu podstawowego R %.0f m nie ma w tabeli 11.9, jego krzywizna graniczna nie jest sprawdzana" + +msgid "The curvature of the diverging track %.3f 1/km is over the limiting %.3f 1/km of table 11.9, up to %.3f 1/km only by consent of the Zaklad Linii Kolejowych" +msgstr "Krzywizna toru zwrotnego %.3f 1/km ponad graniczną %.3f 1/km z tabeli 11.9; do %.3f 1/km tylko za zgodą Zakładu Linii Kolejowych" + +msgid "The curvature of the diverging track %.3f 1/km is over the limiting %.3f 1/km for the switch of R %.0f m (table 11.9)" +msgstr "Krzywizna toru zwrotnego %.3f 1/km ponad graniczną %.3f 1/km dla rozjazdu R %.0f m (tabela 11.9)" + +msgid "The main track bent to R %.0f m, below the least %.0f m of table 11.9, down to %.0f m only by consent of the Zaklad Linii Kolejowych" +msgstr "Tor zasadniczy wygięty do R %.0f m, poniżej najmniejszego %.0f m z tabeli 11.9; do %.0f m tylko za zgodą Zakładu Linii Kolejowych" + +msgid "The main track bent to R %.0f m, below the least %.0f m for the switch of R %.0f m (table 11.9)" +msgstr "Tor zasadniczy wygięty do R %.0f m, poniżej najmniejszego %.0f m dla rozjazdu R %.0f m (tabela 11.9)" + +msgid "A switch of 1:%.3f isn't bent in the running and the main tracks: none of 1:9.403 nor steeper than 1:9 (11.4 pkt 4)" +msgstr "Rozjazdu 1:%.3f nie łukuje się w torach szlakowych i głównych: ani 1:9,403, ani o skosie większym niż 1:9 (11.4 pkt 4)" + +msgid "The cant changes within the switch, %.0f to %.0f mm: it's to stay the same over it (11.4 pkt 7); a straight ramp through the switch only by consent of the Zaklad Linii Kolejowych" +msgstr "Przechyłka zmienia się w obrębie rozjazdu, z %.0f na %.0f mm: ma być stała na całej jego długości (11.4 pkt 7); prostoliniowa rampa w rozjeździe tylko za zgodą Zakładu Linii Kolejowych" + +msgid "A cant ramp starts or ends within the switch, %.1f m from its start: at the start or the end of the switch, or 6 m off it at least (9.6 pkt 16-17)" +msgstr "Rampa przechyłkowa zaczyna się albo kończy w obrębie rozjazdu, %.1f m od jego początku: ma się zaczynać na początku albo końcu rozjazdu, albo co najmniej 6 m od niego (9.6 pkt 16-17)" + +msgid "A cant ramp starts or ends at an end of the switch" +msgstr "Rampa przechyłkowa zaczyna się albo kończy na początku lub końcu rozjazdu" + +msgid "A cant ramp ends %.1f m off the switch, 6 m at least (9.6 pkt 16)" +msgstr "Koniec rampy przechyłkowej %.1f m od rozjazdu, ma być co najmniej 6 m (9.6 pkt 16)" + +msgid "Cant %.0f mm in a bent switch, over 100 mm (table 9.6)" +msgstr "Przechyłka %.0f mm w rozjeździe łukowym, ponad 100 mm (tabela 9.6)" + +msgid "Cant %.0f mm: over the 60 mm recommended for a bent switch, up to 100 mm (table 9.6)" +msgstr "Przechyłka %.0f mm: ponad zalecane 60 mm dla rozjazdu łukowego, dopuszczalne do 100 mm (tabela 9.6)" + +msgid "Cant %.0f mm in a straight switch: none recommended, up to 100 mm (table 9.6)" +msgstr "Przechyłka %.0f mm w rozjeździe zwyczajnym: zalecana żadna, dopuszczalne do 100 mm (tabela 9.6)" + +msgid "the switch starts at an end of the cant ramp" +msgstr "początek rozjazdu na krańcu rampy przechyłkowej" + +msgid "the switch ends at an end of the cant ramp" +msgstr "koniec rozjazdu na krańcu rampy przechyłkowej" + +msgid "the switch starts 6 m off the cant ramp" +msgstr "początek rozjazdu 6 m od rampy przechyłkowej" + +msgid "the switch ends 6 m off the cant ramp" +msgstr "koniec rozjazdu 6 m od rampy przechyłkowej" + +msgid "ramp" +msgstr "rampa" + +msgid "main track R %s at the start, %s at the end" +msgstr "tor zasadniczy R %s na początku, %s na końcu" + +msgid "diverging track R %s at the start, %s at the end" +msgstr "tor zwrotny R %s na początku, %s na końcu" + +msgid "cant %.0f mm at the start, %.0f mm at the end" +msgstr "przechyłka %.0f mm na początku, %.0f mm na końcu" diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index c91f7ebf..12974602 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -20,6 +20,8 @@ add_executable(${UNIT_TESTS_NAME} # and the heights of the tracks laid "${CMAKE_SOURCE_DIR}/editor/editorGroundFollow.cpp" "${CMAKE_SOURCE_DIR}/editor/editorProfile.cpp" + # and the switches laid into the curves + "${CMAKE_SOURCE_DIR}/editor/editorSwitchCurve.cpp" ${UNIT_TEST_CASES}) # the core library brings the game's build settings along diff --git a/tests/unit/test_switch_curve.cpp b/tests/unit/test_switch_curve.cpp new file mode 100644 index 00000000..a370e5ca --- /dev/null +++ b/tests/unit/test_switch_curve.cpp @@ -0,0 +1,102 @@ +#include "stdafx.h" +#include "doctest.h" +#include "editor/editorSwitchCurve.hpp" + +#include +#include +#include + +namespace { + +// a line every half metre: a straight, a transition curve from it to the arc of the radius, with a straight cant ramp along it, and +// the arc, each of the lengths given +std::vector line(double const Straight, double const Transition, double const Arc, double const Radius, double const Cant) { + std::vector points; + double heading{0.0}; + auto const curvature = [&](double const S) { return S <= Straight ? 0.0 : S <= Straight + Transition ? (S - Straight) / Transition / Radius : 1.0 / Radius; }; + auto const cant = [&](double const S) { return S <= Straight ? 0.0 : S <= Straight + Transition ? Cant * (S - Straight) / Transition : Cant; }; + for (double s{0.0}; s <= Straight + Transition + Arc + 1e-9; s += 0.5) { + if (s > 0.0) + heading += 0.5 * (curvature(s - 0.5) + curvature(s)) * 0.5; + points.push_back({s, heading, cant(s)}); + } + return points; +} + +// the tracks of a switch Length long laid into the line from the station: the main one along it, the diverging one turning off it +// to the side by the curvature of the basic switch +std::pair, std::vector> laid(std::vector const &Line, double const From, double const Length, double const Radius, int const Side) { + std::vector main; + std::vector branch; + auto const first{std::lower_bound(Line.begin(), Line.end(), From, [](auto const &Point, double const Value) { return Point.station < Value; })}; + for (auto point{first}; point != Line.end() && point->station <= From + Length + 1e-9; ++point) { + auto const s{point->station - From}; + main.push_back({s, point->heading - first->heading, 0.0}); + branch.push_back({s, point->heading - first->heading + Side * s / Radius, 0.0}); + } + return {main, branch}; +} + +bool says(std::vector const &Lines, char const *Text) { + return std::any_of(Lines.begin(), Lines.end(), [&](std::string const &Line) { return Line.find(Text) != std::string::npos; }); +} + +} // namespace + +TEST_CASE("switch on a curve: the curvatures of its tracks on a transition curve, read the way the line turns") { + auto const track{line(100.0, 120.0, 200.0, 1000.0, 60.0)}; + auto const [main, branch] {laid(track, 150.0, 30.0, 500.0, -1)}; + auto const report{switch_curve::check(main, branch, track, 150.0, 180.0, 500.0, 12.0)}; + CHECK(report.curved); + CHECK(report.transition); + CHECK(report.main_start == doctest::Approx(51.0 / 120.0 / 1000.0).epsilon(0.02)); + CHECK(report.main_end == doctest::Approx(79.0 / 120.0 / 1000.0).epsilon(0.02)); + // turning off to the outside of the curve: against it all along, the case KZW I + CHECK(report.branch_start < 0.0); + CHECK(report.branch_end < 0.0); + CHECK(false == says(report.warnings, "KZW II")); + CHECK(says(report.notes, "transition curve")); + // laid on the ramp, the cant changing under it + CHECK(report.cant_start == doctest::Approx(25.0).epsilon(0.02)); + CHECK(report.cant_end == doctest::Approx(40.0).epsilon(0.02)); + CHECK(says(report.warnings, "The cant changes within the switch")); +} + +TEST_CASE("switch on a curve: the diverging track turning from one side to the other is the case KZW II") { + // a transition growing past the curvature of the basic switch along the switch + auto const track{line(100.0, 60.0, 100.0, 300.0, 0.0)}; + auto const [main, branch] {laid(track, 125.0, 30.0, 500.0, -1)}; + auto const report{switch_curve::check(main, branch, track, 125.0, 155.0, 500.0, 12.0)}; + CHECK(report.branch_start < 0.0); + CHECK(report.branch_end > 0.0); + CHECK(says(report.warnings, "KZW II")); +} + +TEST_CASE("switch on a curve: the ends of a cant ramp at the ends of the switch or 6 m off it") { + auto const track{line(100.0, 120.0, 200.0, 1000.0, 60.0)}; + auto const ramps{switch_curve::ramp_ends(track)}; + REQUIRE(ramps.size() == 2); + CHECK(ramps[0] == doctest::Approx(100.0).epsilon(0.01)); + CHECK(ramps[1] == doctest::Approx(220.0).epsilon(0.01)); + + auto const at = [&](double const From) { + auto const [main, branch] {laid(track, From, 30.0, 500.0, 1)}; + return switch_curve::check(main, branch, track, From, From + 30.0, 500.0, 12.0); + }; + CHECK(says(at(210.0).warnings, "within the switch")); + CHECK(says(at(222.0).warnings, "6 m at least")); + CHECK(false == says(at(220.0).warnings, "within the switch")); + CHECK(says(at(220.0).notes, "at an end of the switch")); + CHECK(false == says(at(230.0).warnings, "cant ramp")); + + // the start pressed near those places drawn to them + std::string what; + CHECK(switch_curve::snapped_start(ramps, 218.5, 30.0, 1, 3.0, &what) == doctest::Approx(220.0)); + CHECK(what.find("starts at an end") != std::string::npos); + CHECK(switch_curve::snapped_start(ramps, 228.0, 30.0, 1, 3.0, &what) == doctest::Approx(226.0)); + CHECK(what.find("6 m off") != std::string::npos); + CHECK(switch_curve::snapped_start(ramps, 72.0, 30.0, 1, 3.0, &what) == doctest::Approx(70.0)); + CHECK(what.find("ends at an end") != std::string::npos); + CHECK(switch_curve::snapped_start(ramps, 160.0, 30.0, 1, 3.0, &what) == doctest::Approx(160.0)); + CHECK(what.empty()); +}