/* 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/editorSwitchDesign.hpp" #include "editor/editorFormat.hpp" #include "utilities/utilities.h" #include #include #include #include #include namespace switch_design { namespace { double constexpr kPi{3.14159265358979323846}; double constexpr kMinRadius{10.0}; double constexpr kTransitionStep{5.0}; double constexpr kLink{1.0}; glm::dvec2 heading_of(double const Angle) { return {std::sin(Angle), std::cos(Angle)}; } glm::dvec3 spatial(glm::dvec2 const &Plan) { return {Plan.x, 0.0, Plan.y}; } double curvature_of(double const Radius) { return Radius != 0.0 ? 1.0 / Radius : 0.0; } segment_data piece(glm::dvec2 const &From, glm::dvec2 const &To, double const Heading0, double const Heading1, double const Handle0, double const Handle1, double const Radius) { segment_data result; result.points[segment_data::point::start] = spatial(From); result.points[segment_data::point::end] = spatial(To); if (Handle0 > 0.0 || Handle1 > 0.0) { result.points[segment_data::point::control1] = spatial(heading_of(Heading0) * Handle0); result.points[segment_data::point::control2] = spatial(-heading_of(Heading1) * Handle1); } result.radius = static_cast(std::abs(Radius)); return result; } struct walker { glm::dvec2 at{0.0}; double heading{0.0}; double sharpest{0.0}; std::vector pieces; void note(double const Radius) { if (Radius != 0.0 && (sharpest == 0.0 || std::abs(Radius) < sharpest)) sharpest = std::abs(Radius); } void straight(double const Length) { auto const to{at + heading_of(heading) * Length}; pieces.push_back(piece(at, to, heading, heading, 0.0, 0.0, 0.0)); at = to; } void arc(double const Length, double const Radius) { note(Radius); auto const angle{Length / Radius}; auto const count{std::max(1, static_cast(std::ceil(std::abs(angle) / (kPi * 0.5) - 1e-9)))}; auto const step{angle / count}; auto const handle{4.0 / 3.0 * std::abs(Radius * std::tan(step * 0.25))}; for (int i = 0; i < count; ++i) { auto const chord{2.0 * std::abs(Radius * std::sin(step * 0.5))}; auto const to{at + heading_of(heading + step * 0.5) * chord}; pieces.push_back(piece(at, to, heading, heading + step, handle, handle, Radius)); at = to; heading += step; } } void transition(double const Length, double const Radius1, double const Radius2) { note(Radius1); note(Radius2); auto const k1{curvature_of(Radius1)}; auto const k2{curvature_of(Radius2)}; auto const start{heading}; auto const heading_at = [&](double const S) { return start + k1 * S + (k2 - k1) * S * S / (2.0 * Length); }; auto const count{std::max(2, static_cast(std::ceil(Length / kTransitionStep)))}; auto const step{Length / count}; for (int i = 0; i < count; ++i) { auto const s0{step * i}; int const samples{32}; glm::dvec2 offset{0.0}; for (int j = 0; j <= samples; ++j) { auto const weight{j == 0 || j == samples ? 1.0 : (j % 2 == 1 ? 4.0 : 2.0)}; offset += heading_of(heading_at(s0 + step * j / samples)) * weight; } offset *= step / (3.0 * samples); auto const h0{heading_at(s0)}; auto const h1{heading_at(s0 + step)}; auto const middle{curvature_of(Radius1) + (k2 - k1) * (s0 + step * 0.5) / Length}; auto const handle{glm::length(offset) / 3.0}; pieces.push_back(piece(at, at + offset, h0, h1, handle, handle, middle != 0.0 ? 1.0 / middle : 0.0)); at += offset; } heading = heading_at(Length); } }; void mirror(segment_data &Path) { auto const flip = [](glm::dvec3 const &Point) { return glm::dvec3{-Point.x, Point.y, Point.z}; }; Path.transform(flip, flip); } void shift(segment_data &Path, double const Along) { Path.transform([&](glm::dvec3 const &Point) { return Point + glm::dvec3{0.0, 0.0, Along}; }, [](glm::dvec3 const &Vector) { return Vector; }); } segment_data straight_path(double const From, double const To) { segment_data result; result.points[segment_data::point::start] = {0.0, 0.0, From}; result.points[segment_data::point::end] = {0.0, 0.0, To}; return result; } std::string checked(std::vector const &Parts) { if (Parts.empty()) return "The diverging track has no parts"; for (auto const &part : Parts) { if (part.length < 0.05) return "A part of the diverging track is shorter than 5 cm"; for (auto const radius : {part.radius1, part.radius2}) if (radius != 0.0 && std::abs(radius) < kMinRadius) return "A radius of the diverging track is under 10 m"; } return {}; } // the point where the tangent at the end of the diverging track meets the main track, as the distance along it double tangent_point(glm::dvec2 const &End, double const Angle) { return End.y - End.x / std::tan(Angle); } std::string word(std::string const &Text) { if (Text.empty()) return "-"; auto result{Text}; std::replace(result.begin(), result.end(), ' ', '_'); return result; } std::string unword(std::string const &Text) { if (Text == "-") return {}; auto result{Text}; std::replace(result.begin(), result.end(), '_', ' '); return result; } std::filesystem::path own_path() { auto path{user_config_path("eu07_switch_templates.txt")}; return path.empty() ? std::filesystem::path{"eu07_switch_templates.txt"} : path; } void write_parts(std::ostream &Output, std::vector const &Parts) { Output << ' ' << Parts.size(); for (auto const &part : Parts) Output << ' ' << format("%.4f %.4f %.4f", part.length, part.radius1, part.radius2); } bool read_parts(std::istream &Input, std::vector &Parts) { std::size_t count{0}; Input >> count; if (Input.fail() || count > 64) return false; Parts.resize(count); for (auto &part : Parts) Input >> part.length >> part.radius1 >> part.radius2; return false == Input.fail(); } } // namespace bool double_switch(std::string const &Type) { return Type == "Rpd" || Type == "Rpj"; } path diverging(std::vector const &Parts, double const Lead, double const Entry) { walker walk; if (Lead > 0.0) walk.straight(Lead); auto const start{Lead > 0.0 ? 0.0 : glm::radians(Entry)}; walk.heading += glm::radians(Entry); for (auto const &part : Parts) { if (part.radius1 == 0.0 && part.radius2 == 0.0) walk.straight(part.length); else if (part.radius1 == part.radius2) walk.arc(part.length, part.radius1); else walk.transition(part.length, part.radius1, part.radius2); } path result; result.angle = walk.heading; result.radius = walk.sharpest; if (walk.pieces.size() == 1) { result.whole = walk.pieces.front(); return result; } auto const end{walk.at}; auto handle0{glm::length(end) / 3.0}; auto handle1{handle0}; if (std::abs(walk.heading) > 1e-6) { auto const along{tangent_point(end, walk.heading)}; auto const back{end.x / std::sin(walk.heading)}; if (along > 0.0 && back > 0.0) { handle0 = along * 2.0 / 3.0; handle1 = back * 2.0 / 3.0; } } result.whole = piece({0.0, 0.0}, end, start, walk.heading, handle0, handle1, walk.sharpest); result.whole.pieces = std::move(walk.pieces); return result; } shape build(design const &Design, std::string &Error) { shape result; Error.clear(); bool const twofold{double_switch(Design.type)}; if (Design.type != "Rz" && false == twofold) { Error = "Unknown type of switch"; return result; } Error = checked(Design.parts); if (Error.empty() && twofold && false == Design.parts2.empty()) Error = checked(Design.parts2); if (Error.empty() && (Design.lead < 0.0 || Design.lead > 30.0)) Error = "The pre-blade stretch has to be between 0 and 30 m"; if (Error.empty() && (Design.entry < 0.0 || Design.entry > 5.0)) Error = "The angle of the blade has to be between 0 and 5 degrees"; if (false == Error.empty()) return result; auto const first{diverging(Design.parts, Design.lead, Design.entry)}; if (first.angle <= 1e-4 || first.angle >= kPi / 4.0) { Error = "The diverging track has to leave to the side at an angle between 0 and 45 degrees"; return result; } auto const end{glm::dvec2{first.whole.points[segment_data::point::end].x, first.whole.points[segment_data::point::end].z}}; result.ratio = 1.0 / std::tan(first.angle); result.radius = first.radius; result.offset = end.x; result.tip = Design.lead; result.a = Design.a > 0.0 ? Design.a : tangent_point(end, first.angle); if (false == twofold) { result.length = Design.main > 0.0 ? Design.main : end.y; if (result.length < 1.0) { Error = "The main track is shorter than 1 m"; return result; } result.b = Design.b > 0.0 ? Design.b : result.length - result.a; result.paths = {straight_path(0.0, result.length), first.whole}; return result; } auto const reach{end.y}; if (Design.stagger < 1.0 || Design.stagger > reach - 1.0) { Error = "The second switch has to start at least 1 m along the main track and 1 m before the end of the diverging track"; return result; } auto const lead2{Design.lead2 >= 0.0 ? Design.lead2 : Design.lead}; auto const entry2{Design.entry2 >= 0.0 ? Design.entry2 : Design.entry}; if (lead2 > 30.0 || entry2 > 5.0) { Error = "The pre-blade stretch of the second switch has to be at most 30 m and the angle of its blade at most 5 degrees"; return result; } auto second{diverging(Design.parts2.empty() ? Design.parts : Design.parts2, lead2, entry2)}; if (second.angle <= 1e-4 || second.angle >= kPi / 4.0) { Error = "The diverging track of the second switch has to leave to the side at an angle between 0 and 45 degrees"; return result; } if (Design.type == "Rpd") mirror(second.whole); shift(second.whole, Design.stagger); auto const secondend{second.whole.points[segment_data::point::end].z}; result.length = Design.main > 0.0 ? Design.main : std::max(reach, secondend); if (result.length < Design.stagger + 1.0) { Error = "The main track ends less than 1 m after the start of the second switch"; return result; } result.b = Design.b > 0.0 ? Design.b : reach - result.a; result.stagger = Design.stagger; result.tip2 = Design.stagger + lead2; result.radius = std::min(first.radius, second.radius > 0.0 ? second.radius : first.radius); // the simulation joins switches only to plain tracks, so a short one lies between the two auto const link{std::min(kLink, Design.stagger * 0.5)}; result.paths = {straight_path(0.0, Design.stagger - link), first.whole, straight_path(Design.stagger, result.length), second.whole, straight_path(Design.stagger - link, Design.stagger)}; return result; } std::vector catalogue() { struct ordinary { char const *name; double radius; double ratio; double a; double b; }; ordinary const ordinaries[] = { {"Rz 1:9 R190", 190.0, 9.0, 10.523, 16.615}, {"Rz 1:9 R300", 300.0, 9.0, 16.615, 16.615}, {"Rz 1:12 R500", 500.0, 12.0, 20.797, 0.0}, {"Rz 1:14 R760", 760.0, 14.0, 0.0, 0.0}, {"Rz 1:18,5 R1200", 1200.0, 18.5, 32.409, 34.943}, {"Rz 1:26,5 R2500", 2500.0, 26.5, 0.0, 0.0}, }; auto const make = [](ordinary const &Ordinary) { design entry; entry.name = Ordinary.name; auto const angle{std::atan(1.0 / Ordinary.ratio)}; entry.a = Ordinary.a > 0.0 ? Ordinary.a : Ordinary.radius * std::tan(angle * 0.5); auto const estimated{Ordinary.b <= 0.0}; entry.b = estimated ? 1.87 * Ordinary.ratio : Ordinary.b; entry.parts.push_back({Ordinary.radius * angle, Ordinary.radius, Ordinary.radius}); auto const rest{(entry.a + entry.b - Ordinary.radius * std::sin(angle)) / std::cos(angle)}; if (rest > 0.01) { entry.parts.push_back({rest, 0.0, 0.0}); entry.main = entry.a + entry.b; } else entry.b = 0.0; if (estimated) entry.note = "(b est.)"; return entry; }; std::vector result; for (auto const &ordinary : ordinaries) result.push_back(make(ordinary)); { design curved; curved.name = "Rz 1:7,5 R190"; auto const angle{std::atan(1.0 / 7.5)}; curved.parts.push_back({190.0 * angle, 190.0, 190.0}); curved.note = "(curved to the end)"; result.push_back(curved); } struct twofold { char const *name; char const *type; std::size_t ordinary; double stagger; }; twofold const twofolds[] = { {"Rpd 1:9 R190", "Rpd", 0, 5.0}, {"Rpj 1:9 R190", "Rpj", 0, 5.0}, {"Rpd 1:9 R300", "Rpd", 1, 6.0}, {"Rpj 1:9 R300", "Rpj", 1, 6.0}, }; for (auto const &entry : twofolds) { auto made{make(ordinaries[entry.ordinary])}; made.name = entry.name; made.type = entry.type; made.stagger = entry.stagger; made.main = 0.0; made.b = 0.0; made.note = format("(second switch %.1f m on, assumed)", entry.stagger); result.push_back(made); } return result; } std::vector own() { std::vector result; std::ifstream file{own_path()}; if (false == file.is_open()) return result; std::string text; while (std::getline(file, text)) { if (text.empty() || text.front() == '#') continue; std::istringstream line{text}; design entry; entry.own = true; std::string keyword; std::string name; line >> keyword >> name; if (keyword != "switch") continue; entry.name = unword(name); bool good{true}; while (good && line >> keyword) { if (keyword == "type") line >> entry.type; else if (keyword == "rail") { line >> entry.rail; if (entry.rail.empty()) entry.rail = "-"; } else if (keyword == "main") line >> entry.main; else if (keyword == "stagger") line >> entry.stagger; else if (keyword == "lead") line >> entry.lead; else if (keyword == "entry") line >> entry.entry; else if (keyword == "lead2") line >> entry.lead2; else if (keyword == "entry2") line >> entry.entry2; else if (keyword == "a") line >> entry.a; else if (keyword == "b") line >> entry.b; else if (keyword == "parts") good = read_parts(line, entry.parts); else if (keyword == "parts2") good = read_parts(line, entry.parts2); else good = false; good = good && false == line.fail(); } if (good && false == entry.name.empty()) result.push_back(std::move(entry)); } return result; } bool save_own(std::vector const &Designs, std::string &Error) { auto const path{own_path()}; std::error_code error; if (path.has_parent_path()) std::filesystem::create_directories(path.parent_path(), error); std::ofstream file{path, std::ios::trunc}; if (false == file.is_open()) { Error = "Can't write " + path.string(); return false; } file << "# switch templates made in the track editor of MaSzyna\n"; file << "# switch type rail main stagger a b lead entry [lead2 entry2 ] parts ( )... parts2 ...\n"; file << "# R 0 is a straight, the same R at both ends an arc, different ones a transition curve; 0 for main, a, b: from the geometry\n"; for (auto const &entry : Designs) { file << "switch " << word(entry.name) << " type " << entry.type << " rail " << word(entry.rail) << ' ' << format("main %.4f stagger %.4f a %.4f b %.4f lead %.4f entry %.4f", entry.main, entry.stagger, entry.a, entry.b, entry.lead, entry.entry); if (entry.lead2 >= 0.0 || entry.entry2 >= 0.0) file << format(" lead2 %.4f entry2 %.4f", entry.lead2, entry.entry2); file << " parts"; write_parts(file, entry.parts); if (false == entry.parts2.empty()) { file << " parts2"; write_parts(file, entry.parts2); } file << '\n'; } if (file.fail()) { Error = "Can't write " + path.string(); return false; } return true; } } // namespace switch_design