From ece61056ffc87d37e8ea001fd811763868373c15 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 25 Oct 2026 00:29:05 +0200 Subject: [PATCH] feat(editor): a switch laid into a curve is checked by ST-T1-A6 as it's placed: the radii of its main and diverging tracks at its start and its end shown at it; warnings where the curvature of a track turns from one side to the other within it (KZW II, 9.7 pkt 12), where the diverging track is sharper or the main track bent more than table 11.9 allows the basic switch (the values allowed by way of exception named), where a switch of 1:9.403 or steeper is bent (11.4 pkt 4), where the cant changes within it (11.4 pkt 7), where a cant ramp starts or ends within it or under 6 m off it (9.6 pkt 16-17) and where the cant is over 100 mm (table 9.6); a note on a transition curve, which only an exception allows. The ends of the cant ramps are marked on the curve, and the start of the switch pressed within 3 m of where it starts or ends at an end of a ramp, or keeps the 6 m off it, is drawn there. The warnings stay in the status once it's laid. The checks stand on their own in editor/editorSwitchCurve, with unit tests --- CMakeLists.txt | 1 + application/editormode.h | 11 ++ application/editormodeswitchplace.cpp | 109 ++++++++++-- application/editormodetrack.cpp | 58 ++++++- editor/editorSwitchCurve.cpp | 229 ++++++++++++++++++++++++++ editor/editorSwitchCurve.hpp | 61 +++++++ lang/editor_pl.po | 72 ++++++++ tests/unit/CMakeLists.txt | 2 + tests/unit/test_switch_curve.cpp | 102 ++++++++++++ 9 files changed, 633 insertions(+), 12 deletions(-) create mode 100644 editor/editorSwitchCurve.cpp create mode 100644 editor/editorSwitchCurve.hpp create mode 100644 tests/unit/test_switch_curve.cpp 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()); +}