diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 3c128de2..2cf93026 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -27,8 +27,10 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include #include +#include #include #include +#include #include namespace @@ -62,6 +64,29 @@ std::string shown(std::string const &Word) return Word == "-" ? std::string{} : Word; } +// the number of the switch of a unit: of its name once named after the station, of its record, else of its place +std::string unit_number(station::unit const &Unit, std::string const &Stem, std::size_t const Index) +{ + auto const &name{Unit.parts.front()->name()}; + if (name.size() > Stem.size() && name.compare(0, Stem.size(), Stem) == 0) + { + auto digits{name.substr(Stem.size())}; + while (false == digits.empty() && std::isalpha(static_cast(digits.back()))) + digits.pop_back(); + if (false == digits.empty() && std::all_of(digits.begin(), digits.end(), [](char const Digit) { return std::isdigit(static_cast(Digit)) != 0; })) + return digits; + } + if (auto const record{switch_store::named(*Unit.parts.front())}; record && record->number != "-") + return record->number; + return std::to_string(Index + 1); +} + +ImU32 circuit_colour(std::string const &Name) +{ + static std::array const palette{IM_COL32(110, 200, 250, 255), IM_COL32(250, 150, 90, 255), IM_COL32(150, 220, 120, 255), IM_COL32(230, 120, 200, 255), IM_COL32(240, 220, 100, 255), IM_COL32(150, 140, 250, 255)}; + return palette[std::hash{}(Name) % palette.size()]; +} + template std::string anchor_of(Record_ const &Entry) { return Entry.track != nullptr && false == Entry.track->m_editorremoved ? Entry.track->name() : Entry.anchor; @@ -156,6 +181,7 @@ void srk_tool::open(TTrack &Track) m_record = index; } m_anchor = m_record >= 0 ? m_records[m_record].track : m_layout->switches.front(); + survey(); } void srk_tool::refresh() @@ -374,10 +400,17 @@ void srk_tool::render_options() {{0, STR_C("From the left"), layout.kilometrage ? "From the lower km, as PKP PLK numbers them" : "From the west end of the station"}, {1, STR_C("From the right"), layout.kilometrage ? "From the higher km" : "From the east end of the station"}})) m_reversed = from == 1; - if (main_button(STR_C("Number the switches"), ImVec2(-1.0f, tall_button_height()))) + if (ImGui::Button(STR_C("Number the switches"), ImVec2(-1.0f, 0.0f))) number_switches(); item_tooltip("Numbers the switches 1, 2, 3... along the station and names them after its code, as on the lines of PKP PLK: bpd_1, " "the parts of a slip bpd_15a to bpd_15d. The drives follow their switches, events written by hand keep the old names. One step of undo"); + step_heading(2, STR_C("Track circuits")); + if (main_button(STR_C("Set the track circuits"), ImVec2(-1.0f, tall_button_height()))) + set_circuits(); + item_tooltip("Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: " + "a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle " + "of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, " + "track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. One step of undo"); if (false == m_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); ImGui::Spacing(); @@ -497,6 +530,150 @@ void srk_tool::number_switches() m_status = format(STR_P("%zu switch numbered", "%zu switches numbered", units.size()), units.size()); } +void srk_tool::set_circuits() +{ + m_status.clear(); + auto const code{m_record >= 0 ? m_records[m_record].code : std::string{}}; + if (code.empty()) + { + m_status = STR_C("Give the station its code first: the track circuits are named after it"); + return; + } + auto const units{station::numbering(*m_layout, m_reversed)}; + auto const planned{station::joints(*m_layout, units)}; + for (auto const &entry : planned) + if (std::string reason; entry.track != nullptr && false == editor_track::can_edit_geometry(*entry.track, reason)) + { + m_status = format(STR_C("%s can't be cut: %s"), name_of(*entry.track).c_str(), reason.c_str()); + return; + } + for (auto const &path : m_layout->paths) + if (std::string reason; false == path.approach && false == scene::Layers.editable(path.track, &reason)) + { + m_status = format(STR_C("%s can't be changed: %s"), name_of(*path.track).c_str(), reason.c_str()); + return; + } + // the cuts: each track from its end back, so the pieces left to cut stay in the track + std::vector> before; + std::vector created; + std::vector points; + std::vector>> cuts; + for (auto const &entry : planned) + { + points.push_back(entry.point); + if (entry.track == nullptr) + continue; + auto found{std::find_if(cuts.begin(), cuts.end(), [&](auto const &Cut) { return Cut.first == entry.track; })}; + if (found == cuts.end()) + found = cuts.insert(cuts.end(), {entry.track, {}}); + found->second.push_back(entry.point); + } + for (auto &[track, at] : cuts) + { + before.emplace_back(track, editor_track::capture(*track)); + auto *const piece{track}; + std::sort(at.begin(), at.end(), [&](glm::dvec3 const &A, glm::dvec3 const &B) { return editor_track::nearest_parameter(*piece, A) > editor_track::nearest_parameter(*piece, B); }); + for (auto const &point : at) + if (auto *cut{editor_track::split_path(*track, editor_track::nearest_parameter(*track, point))}) + created.push_back(cut); + } + // the circuits of the station as cut + std::size_t count{0}; + if (auto const found{station::find(*m_anchor)}) + { + auto const circuits{station::circuits(*found, units, points)}; + auto const numbers{station::track_numbers(*found, circuits)}; + auto const stem{station::prefix(code)}; + std::unordered_set captured(created.begin(), created.end()); + for (auto const &[track, state] : before) + captured.insert(track); + for (std::size_t index = 0; index < circuits.size(); ++index) + { + auto const &circuit{circuits[index]}; + auto const name{circuit.unit >= 0 ? stem + "iz" + unit_number(units[circuit.unit], stem, circuit.unit) : stem + "it" + numbers[index]}; + for (auto *track : circuit.tracks) + { + if (track->Isolated.size() == 1 && track->Isolated.front()->asName == name) + continue; + if (captured.insert(track).second) + before.emplace_back(track, editor_track::capture(*track)); + editor_track::isolated(*track, {name}); + } + } + count = circuits.size(); + } + if (before.empty() && created.empty()) + { + m_status = STR_C("The track circuits are set already"); + return; + } + m_history.push_track_snapshot(std::move(before), created); + m_status = format(STR_P("%zu track circuit", "%zu track circuits", count), count) + ", " + format(STR_P("%zu cut", "%zu cuts", created.size()), created.size()); +} + +void srk_tool::survey() +{ + m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); + m_circuits.clear(); + m_ticks.clear(); + if (false == m_layout.has_value()) + return; + auto const &layout{*m_layout}; + auto const stem{m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}}; + std::vector weights; + for (std::size_t index = 0; index < layout.paths.size(); ++index) + { + auto const &path{layout.paths[index]}; + if (path.track->Isolated.empty()) + continue; + // of the lines leading out only the stretches up to the joints of the station + auto const &name{path.track->Isolated.front()->asName}; + if (path.approach && (stem.empty() || name.compare(0, stem.size(), stem) != 0)) + continue; + auto found{std::find_if(m_circuits.begin(), m_circuits.end(), [&](circuit_mark const &Mark) { return Mark.text == name; })}; + if (found == m_circuits.end()) + { + found = m_circuits.insert(m_circuits.end(), {glm::dvec2{0.0}, name, circuit_colour(name)}); + weights.push_back(0.0); + } + auto const circuit{static_cast(found - m_circuits.begin())}; + m_circuit_of[index] = static_cast(circuit); + for (auto const &point : path.schematic) + { + found->at += point; + weights[circuit] += 1.0; + } + } + for (std::size_t index = 0; index < m_circuits.size(); ++index) + { + auto &mark{m_circuits[index]}; + mark.at /= std::max(1.0, weights[index]); + // bpd_it3 shown as It3 + if (false == stem.empty() && mark.text.compare(0, stem.size(), stem) == 0) + mark.text.erase(0, stem.size()); + if (false == mark.text.empty()) + mark.text.front() = static_cast(std::toupper(static_cast(mark.text.front()))); + } + // the joints: the ends where paths of different circuits meet + std::vector> ends; + for (std::size_t index = 0; index < layout.paths.size(); ++index) + { + auto const &path{layout.paths[index]}; + if (path.approach && m_circuit_of[index] < 0) + { + for (auto const &point : {path.points.front(), path.points.back()}) + ends.emplace_back(point, -1); + continue; + } + for (auto const &point : {path.points.front(), path.points.back()}) + ends.emplace_back(point, m_circuit_of[index]); + } + for (std::size_t i = 0; i < ends.size(); ++i) + for (std::size_t j = i + 1; j < ends.size(); ++j) + if (ends[i].second != ends[j].second && (ends[i].second >= 0 || ends[j].second >= 0) && glm::distance(ends[i].first, ends[j].first) < 0.05) + m_ticks.push_back(station::project(layout, ends[i].first)); +} + void srk_tool::fit_view(float const Width, float const Height) { auto const &layout{*m_layout}; @@ -616,17 +793,21 @@ void srk_tool::render_canvas(float const Height) for (auto const &path : layout.paths) if (path.approach) draw_path(path, kApproach, 2.0f); - for (auto const &path : layout.paths) - if (false == path.approach) - draw_path(path, is_switch(*path.track) ? kSwitch : kPlain, 2.5f); + for (std::size_t index = 0; index < layout.paths.size(); ++index) + if (auto const &path{layout.paths[index]}; false == path.approach || m_circuit_of[index] >= 0) + draw_path(path, m_circuit_of[index] >= 0 ? m_circuits[m_circuit_of[index]].colour : is_switch(*path.track) ? kSwitch : kPlain, 2.5f); + for (auto const &tick : m_ticks) + { + auto const at{screen_of(tick)}; + draw->AddLine(ImVec2(at.x, at.y - 5.0f), ImVec2(at.x, at.y + 5.0f), IM_COL32(255, 255, 255, 220), 2.0f); + } for (auto const &path : layout.paths) if (path.track == selected) draw_path(path, overlay_color::selected, 4.0f); if (m_hover >= 0) draw_path(layout.paths[m_hover], kHovered, 4.0f); // the switches: a dot at the start, the name over or under it in turn; the number alone, once named after the station - auto const prefix{m_record >= 0 && false == m_records[m_record].code.empty() ? station::switch_name(m_records[m_record].code, 0, 0, 1) : std::string{}}; - auto const stem{prefix.empty() ? std::string{} : prefix.substr(0, prefix.size() - 1)}; + auto const stem{m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}}; for (std::size_t index = 0; index < layout.switches.size(); ++index) { auto const *track{layout.switches[index]}; @@ -640,6 +821,12 @@ void srk_tool::render_canvas(float const Height) draw->AddRectFilled(ImVec2(place.x - 2.0f, place.y - 1.0f), ImVec2(place.x + extent.x + 2.0f, place.y + extent.y + 1.0f), IM_COL32(22, 24, 28, 200), 2.0f); draw->AddText(place, kSwitch, text.c_str()); } + for (auto const &mark : m_circuits) + { + auto const at{screen_of(mark.at)}; + auto const extent{ImGui::CalcTextSize(mark.text.c_str())}; + draw->AddText(ImVec2(at.x - extent.x * 0.5f, at.y + 4.0f), mark.colour, mark.text.c_str()); + } draw->PopClipRect(); if (hovered && m_hover >= 0 && false == m_dragging) { @@ -650,6 +837,8 @@ void srk_tool::render_canvas(float const Height) ImGui::TextDisabled("%s, %.1f m", is_switch(*path.track) ? STR_C("switch") : path.approach ? STR_C("line out of the station") : STR_C("track"), path.length); if (layout.kilometrage) ImGui::TextDisabled("km %.3f", station::km_of(layout, along)); + if (false == path.track->Isolated.empty()) + ImGui::TextDisabled(STR_C("track circuit %s"), path.track->Isolated.front()->asName.c_str()); ImGui::TextDisabled("%s", STR_C("LMB: select it")); ImGui::EndTooltip(); } @@ -719,6 +908,7 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri tracks.insert(path.track); WriteLog(format("SELFTEST srkopen %s: %zu switches, %zu tracks, %zu paths out, axis %.0f m, switches over %.0f..%.0f, offsets %.1f..%.1f, km %s, record %d", m_anchor->name().c_str(), layout.switches.size(), tracks.size(), approach, layout.chainage.back(), layout.from, layout.to, layout.low, layout.high, layout.kilometrage ? format("%.3f", layout.km).c_str() : "-", m_record)); + WriteLog(format("SELFTEST srkopen lines %zu, main %zu tracks, second %zu tracks", layout.lines.size(), layout.main.size(), layout.second.size())); for (auto const *track : layout.switches) { auto const at{station::project(layout, track->m_paths.front().points[segment_data::point::start])}; @@ -749,6 +939,31 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog("SELFTEST srknumber names:" + list); return editor_selftest_result::handled; } + if (Command == "srkcircuits") + { + // srkcircuits: the open station cut into its track circuits + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkcircuits: no station open"); + return editor_selftest_result::handled; + } + set_circuits(); + WriteLog("SELFTEST srkcircuits: " + m_status); + refresh(); + std::map> found; + for (auto const &path : m_layout->paths) + if (false == path.track->Isolated.empty()) + { + auto &entry{found[path.track->Isolated.front()->asName]}; + ++entry.first; + entry.second += path.length; + } + std::string list; + for (auto const &[name, entry] : found) + list += format(" %s:%zu/%.0f", name.c_str(), entry.first, entry.second); + WriteLog(format("SELFTEST srkcircuits %zu named, %zu joints:", found.size(), m_ticks.size()) + list); + return editor_selftest_result::handled; + } if (Command == "srkstations") { load(); diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 557c4711..0ef8fc8b 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -13,6 +13,8 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorStation.hpp" #include "scene/scenelayers.h" +#include "imgui/imgui.h" + #include #include #include @@ -69,6 +71,15 @@ class srk_tool final : public editor_tool int m_hover{-1}; // path of the layout under the mouse in the schematic bool m_dragging{false}; std::vector> m_found; // the stations of the scenery: a switch and the count of them + struct circuit_mark + { + glm::dvec2 at{0.0}; // in the schematic + std::string text; + ImU32 colour{0}; + }; + std::vector m_circuit_of; // of each path of the layout, an index into m_circuits, -1 for none + std::vector m_circuits; + std::vector m_ticks; // the joints between the circuits, in the schematic std::string m_status; void load(); @@ -82,4 +93,8 @@ class srk_tool final : public editor_tool void find_all(); // the switches numbered along the station and named after its code; one step of the history void number_switches(); + // the station cut at the joints of its track circuits and the circuits named; one step of the history + void set_circuits(); + // the track circuits as the tracks have them, for the schematic + void survey(); }; diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 31e3d245..a35ccc79 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -20,6 +20,8 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include +#include #include #include @@ -96,11 +98,17 @@ struct chain double length{0.0}; TTrack *reached{nullptr}; // the switch met at its other end bool ended{false}; // at a free end + TTrack const *from{nullptr}; // the switch it leads out of, with the end of it + int path{0}; + bool atend{false}; }; chain follow(TTrack const &From, int const Path, bool const Atend, double const Limit) { chain result; + result.from = &From; + result.path = Path; + result.atend = Atend; auto point{end_of(From, Path, Atend)}; auto *next{neighbour(From, Path, Atend)}; while (next != nullptr && result.length <= Limit) @@ -131,6 +139,7 @@ struct area std::vector switches; std::vector plain; std::vector exits; + std::vector ends; // tracks ending free: sidings, or lines cut off by the edge of the scenery }; area collect(TTrack &From, options const &Options) @@ -159,9 +168,9 @@ area collect(TTrack &From, options const &Options) for (auto const &[path, atend] : ends_of(from)) { auto line{follow(from, path, atend, Options.link)}; - if (line.reached == nullptr && false == line.ended) + if (line.reached == nullptr) { - result.exits.push_back(std::move(line)); + (line.ended ? result.ends : result.exits).push_back(std::move(line)); continue; } for (auto *track : line.tracks) @@ -171,6 +180,22 @@ area collect(TTrack &From, options const &Options) result.switches.push_back(line.reached); } } + // a link longer than the limit between two switches of the station is a track of it, not two lines out + auto exits{std::move(result.exits)}; + result.exits.clear(); + for (auto &line : exits) + { + if (false == line.ended) + if (auto const whole{follow(*line.from, line.path, line.atend, std::numeric_limits::max())}; whole.reached != nullptr && known.count(whole.reached) > 0) + { + for (auto *track : whole.tracks) + if (known.insert(track).second) + result.plain.push_back(track); + continue; + } + if (std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return known.count(Track) > 0; })) + result.exits.push_back(std::move(line)); + } return result; } @@ -185,12 +210,143 @@ std::vector points_of(TTrack const &Track, int const Path, double &L return result; } +// the points of the path a metre or so apart, for the joints to come out the same however the tracks are cut +std::vector dense_points(TTrack const &Track, int const Path, double &Length) +{ + bezier const curve{Track.m_paths[Path]}; + Length = curve.plan_length(64); + auto const count{std::clamp(static_cast(std::ceil(Length)), 2, 4000)}; + std::vector result; + for (int i = 0; i <= count; ++i) + result.push_back(curve.point(static_cast(i) / count)); + return result; +} + // the map shows the east to the right and the north up: the x of the scenery runs to the west glm::dvec2 map_of(glm::dvec3 const &Point) { return {-Point.x, Point.z}; } +double constexpr kStart{1.5}; // m, a joint before the start of a switch +double constexpr kClear{4.0}; // m, between the axes at a joint beyond a fouling point +double constexpr kReach{120.0}; // m, along a branch looked for its fouling point +double constexpr kSection{30.0}; // m, the shortest stretch between two joints made a track circuit of its own +double constexpr kSnap{0.3}; // m, a joint this close to the end of a track lies at the end + +// the points of the way out of an end of the track, as a train runs: along the main path of a switch run into from its start, +// back along the path of one run into from the end of it +std::vector ahead(TTrack const &From, int const Path, bool const Atend, double const Length) +{ + std::vector result{end_of(From, Path, Atend)}; + auto *next{neighbour(From, Path, Atend)}; + std::unordered_set seen{&From}; + double length{0.0}; + while (next != nullptr && length < Length && rail(*next) && seen.insert(next).second) + { + int path{0}; + bool atend{false}; + if (false == end_at(*next, result.back(), path, atend)) + break; + if (next->eType == tt_Switch && false == atend) + path = 0; + double plan{0.0}; + auto points{dense_points(*next, path, plan)}; + if (atend) + std::reverse(points.begin(), points.end()); + result.insert(result.end(), points.begin() + 1, points.end()); + length += plan; + auto const *track{next}; + next = neighbour(*track, path, false == atend); + } + return result; +} + +// the path of a switch a train run into from its start goes on along: the one turning the least +int straighter(TTrack const &Switch) +{ + if (Switch.eType != tt_Switch || Switch.m_paths.size() < 2) + return 0; + auto const turn = [&](int const Path) { + bezier const curve{Switch.m_paths[Path]}; + auto const from{curve.first(0.0)}; + auto const to{curve.first(1.0)}; + return std::abs(std::atan2(from.x * to.z - from.z * to.x, from.x * to.x + from.z * to.z)); + }; + return turn(1) < turn(0) ? 1 : 0; +} + +// the tracks of the run from the end of the track at the point on, through the switches along their straighter paths +std::vector straight_run(TTrack &Track, glm::dvec3 Point, double const Limit) +{ + std::vector result{&Track}; + std::unordered_set seen{&Track}; + int path{0}; + bool atend{false}; + if (false == end_at(Track, Point, path, atend)) + return result; + auto *next{neighbour(Track, path, atend)}; + double length{0.0}; + while (next != nullptr && length < Limit && rail(*next) && seen.insert(next).second) + { + if (false == end_at(*next, Point, path, atend)) + break; + if (next->eType == tt_Switch && false == atend) + path = straighter(*next); + result.push_back(next); + length += plan_length(*next, path); + Point = end_of(*next, path, false == atend); + auto const *track{next}; + next = neighbour(*track, path, false == atend); + } + return result; +} + +double plan_gap(glm::dvec3 const &Point, std::vector const &Line) +{ + glm::dvec2 const point{Point.x, Point.z}; + auto best{glm::distance(point, glm::dvec2{Line.front().x, Line.front().z})}; + for (std::size_t i = 1; i < Line.size(); ++i) + { + glm::dvec2 const a{Line[i - 1].x, Line[i - 1].z}; + glm::dvec2 const along{glm::dvec2{Line[i].x, Line[i].z} - a}; + auto const length2{glm::dot(along, along)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, along) / length2, 0.0, 1.0) : 0.0}; + best = std::min(best, glm::distance(point, a + along * t)); + } + return best; +} + +// how far from the end of the switch its joint lies: beyond the fouling point at a branch, before the start elsewhere +double clearance(TTrack const &Switch, int const Path, bool const Atend) +{ + auto const point{end_of(Switch, Path, Atend)}; + std::optional> other; + auto nearest{kClear}; + for (auto const &[path, atend] : ends_of(Switch)) + if (auto const distance{plan_distance(end_of(Switch, path, atend), point)}; distance > same_point && distance < nearest) + { + nearest = distance; + other = std::make_pair(path, atend); + } + if (false == other.has_value()) + return kStart; + auto const mine{ahead(Switch, Path, Atend, kReach)}; + auto const theirs{ahead(Switch, other->first, other->second, kReach + 50.0)}; + double along{0.0}; + auto gap{plan_gap(mine.front(), theirs)}; + for (std::size_t i = 1; i < mine.size(); ++i) + { + auto const step{plan_distance(mine[i - 1], mine[i])}; + auto const next{plan_gap(mine[i], theirs)}; + if (next >= kClear) + return along + (next > gap ? step * std::clamp((kClear - gap) / (next - gap), 0.0, 1.0) : step); + along += step; + gap = next; + } + return along; +} + } // namespace std::optional find(TTrack &From, options const &Options) @@ -201,9 +357,10 @@ std::optional find(TTrack &From, options const &Options) std::unordered_set switches(found.switches.begin(), found.switches.end()); // the axis: the run through the station along the main paths of its switches which passes the most of them std::vector starts; - for (auto const &line : found.exits) - if (false == line.tracks.empty()) - starts.push_back(line.tracks.front()); + for (auto const *lines : {&found.exits, &found.ends}) + for (auto const &line : *lines) + if (false == line.tracks.empty()) + starts.push_back(line.tracks.front()); if (starts.empty()) starts.push_back(found.switches.front()); editor_track::route best; @@ -298,6 +455,33 @@ std::optional find(TTrack &From, options const &Options) std::reverse(result.axis.begin(), result.axis.end()); measure(); } + // a long track ending free far beyond the switches is a line cut off by the edge of the scenery, else a siding + std::vector exits{found.exits}; + std::vector plain{found.plain}; + { + auto low{std::numeric_limits::max()}; + auto high{std::numeric_limits::lowest()}; + for (auto const *track : found.switches) + { + auto const along{project(result, end_of(*track, 0, false)).x}; + low = std::min(low, along); + high = std::max(high, along); + } + for (auto const &line : found.ends) + { + auto beyond{0.0}; + for (auto const *track : line.tracks) + for (auto const atend : {false, true}) + { + auto const at{project(result, end_of(*track, 0, atend))}; + beyond = std::max({beyond, low - at.x, at.x - high}); + } + if (beyond > Options.approach * 0.5 && line.length > Options.approach) + exits.push_back(line); + else + plain.insert(plain.end(), line.tracks.begin(), line.tracks.end()); + } + } // the paths, laid out along the axis auto const add = [&](TTrack &Track, bool const Approach) { for (int index = 0; index < static_cast(Track.m_paths.size()); ++index) @@ -314,16 +498,18 @@ std::optional find(TTrack &From, options const &Options) }; for (auto *track : found.switches) add(*track, false); - for (auto *track : found.plain) + for (auto *track : plain) add(*track, false); - for (auto const &line : found.exits) + for (auto const &line : exits) { double length{0.0}; + result.lines.emplace_back(); for (auto *track : line.tracks) { if (length >= Options.approach) break; add(*track, true); + result.lines.back().push_back(track); length += plan_length(*track, 0); } } @@ -348,6 +534,40 @@ std::optional find(TTrack &From, options const &Options) } result.switches = found.switches; std::sort(result.switches.begin(), result.switches.end(), [&](TTrack const *A, TTrack const *B) { return project(result, end_of(*A, 0, false)).x < project(result, end_of(*B, 0, false)).x; }); + // a double-track line: another line leaves the station by the one along the axis a few metres aside; the run from it along + // the main paths of the switches is the other main track + for (auto const &span : best.spans) + result.main.push_back(span.track); + std::unordered_set const axis(result.main.begin(), result.main.end()); + std::vector> leaving; + for (std::size_t index = 0; index < result.lines.size(); ++index) + { + std::vector points; + for (auto const &path : result.paths) + if (path.approach && std::find(result.lines[index].begin(), result.lines[index].end(), path.track) != result.lines[index].end()) + points.insert(points.end(), path.schematic.begin(), path.schematic.end()); + auto const beyond = [&](glm::dvec2 const &Point) { return std::abs(std::max(result.from - Point.x, Point.x - result.to) - 60.0); }; + if (false == points.empty()) + leaving.emplace_back(index, *std::min_element(points.begin(), points.end(), [&](glm::dvec2 const &A, glm::dvec2 const &B) { return beyond(A) < beyond(B); })); + } + for (auto const &[along, at] : leaving) + { + if (axis.count(result.lines[along].front()) == 0 || false == result.second.empty()) + continue; + for (auto const &[aside, there] : leaving) + { + if (aside == along || axis.count(result.lines[aside].front()) > 0 || (at.x < result.from) != (there.x < result.from) || std::abs(there.y - at.y) < 3.0 || std::abs(there.y - at.y) > 7.0) + continue; + // the run goes in from the end of the line at the station + auto &first{*result.lines[aside].front()}; + auto const *start{neighbour(first, 0, false)}; + auto const inwards{start != nullptr && switches.count(start) > 0 ? false : true}; + for (auto *track : straight_run(first, end_of(first, 0, inwards), result.chainage.back() + 2.0 * Options.approach)) + if (axis.count(track) == 0 && std::find(result.second.begin(), result.second.end(), track) == result.second.end()) + result.second.push_back(track); + break; + } + } return result; } @@ -365,7 +585,8 @@ glm::dvec2 project(layout const &Layout, glm::dvec3 const &Point) if (length < 1e-9) continue; auto const unit{along / length}; - auto const t{std::clamp(glm::dot(point - a, unit), 0.0, length)}; + // beyond the ends of the axis it goes on straight + auto const t{std::clamp(glm::dot(point - a, unit), i == 1 ? std::numeric_limits::lowest() : 0.0, i + 1 == Layout.axis.size() ? std::numeric_limits::max() : length)}; auto const foot{a + unit * t}; auto const distance{glm::distance(point, foot)}; if (distance >= best) @@ -430,17 +651,261 @@ std::vector numbering(layout const &Layout, bool const Reversed) return result; } -std::string switch_name(std::string const &Code, int const Number, std::size_t const Part, std::size_t const Parts) +std::string prefix(std::string const &Code) { std::string result; for (auto const c : Code) result += static_cast(std::tolower(static_cast(c))); - result += '_' + std::to_string(Number); + return result + '_'; +} + +std::string switch_name(std::string const &Code, int const Number, std::size_t const Part, std::size_t const Parts) +{ + auto result{prefix(Code) + std::to_string(Number)}; if (Parts > 1) result += static_cast('a' + std::min(Part, 25)); return result; } +std::vector joints(layout const &Layout, std::vector const &Units, options const &Options) +{ + std::unordered_map unit_of; + for (std::size_t index = 0; index < Units.size(); ++index) + for (auto const *part : Units[index].parts) + unit_of[part] = index; + auto const one_unit = [&](TTrack const *A, TTrack const *B) { + auto const a{unit_of.find(A)}; + auto const b{unit_of.find(B)}; + return a != unit_of.end() && b != unit_of.end() && a->second == b->second; + }; + std::unordered_set through; + std::vector meets; + std::vector result; + for (auto *from : Layout.switches) + for (auto const &[path, atend] : ends_of(*from)) + { + auto line{follow(*from, path, atend, std::numeric_limits::max())}; + auto const start{end_of(*from, path, atend)}; + // out of the station: to a switch of another one, along a line cut off by the edge of the scenery + if ((line.reached != nullptr && std::find(Layout.switches.begin(), Layout.switches.end(), line.reached) == Layout.switches.end()) || + (line.ended && false == line.tracks.empty() && std::any_of(Layout.lines.begin(), Layout.lines.end(), [&](std::vector const &Line) { return false == Line.empty() && Line.front() == line.tracks.front(); }))) + { + line.reached = nullptr; + line.ended = false; + } + if (line.reached == nullptr && false == line.ended) + { + double length{0.0}; + auto const kept{std::find_if(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return (length += plan_length(*Track, 0)) > kReach * 2.0; })}; + line.tracks.erase(kept == line.tracks.end() ? kept : std::next(kept), line.tracks.end()); + } + if (line.tracks.empty()) + { + // two switches meeting: the joint lies where they meet, unless they're parts of one + if (line.reached == nullptr || one_unit(from, line.reached) || std::any_of(meets.begin(), meets.end(), [&](glm::dvec3 const &Meet) { return glm::distance(Meet, start) < same_point; })) + continue; + meets.push_back(start); + result.push_back({nullptr, start}); + continue; + } + if (through.count(line.tracks.front()) > 0) + continue; + through.insert(line.tracks.begin(), line.tracks.end()); + // the link as one line from the switch on, and where each of its tracks ends along it + std::vector points{start}; + std::vector lengths{0.0}; + std::vector> pieces; + for (auto *track : line.tracks) + { + double plan{0.0}; + auto samples{dense_points(*track, 0, plan)}; + if (plan_distance(samples.back(), points.back()) < plan_distance(samples.front(), points.back())) + std::reverse(samples.begin(), samples.end()); + for (std::size_t i = 1; i < samples.size(); ++i) + { + lengths.push_back(lengths.back() + plan_distance(points.back(), samples[i])); + points.push_back(samples[i]); + } + pieces.emplace_back(lengths.back(), track); + } + auto const length{lengths.back()}; + auto const own{clearance(*from, path, atend)}; + std::vector places; + if (line.reached != nullptr) + { + int far_path{0}; + bool far_atend{false}; + if (one_unit(from, line.reached) || false == end_at(*line.reached, points.back(), far_path, far_atend)) + continue; + auto const theirs{clearance(*line.reached, far_path, far_atend)}; + if (length - own - theirs >= kSection) + places = {own, length - theirs}; + else + places = {(own + length - theirs) * 0.5}; + } + else if (false == line.ended || length - own >= kSection) + places = {own}; + for (auto const place : places) + { + auto distance{std::clamp(place, 0.0, length)}; + TTrack *track{nullptr}; + auto previous{0.0}; + for (auto const &[end, piece] : pieces) + { + if (std::abs(distance - previous) <= kSnap || std::abs(distance - end) <= kSnap) + { + distance = std::abs(distance - previous) <= kSnap ? previous : end; + track = nullptr; + break; + } + if (distance < end) + { + track = piece; + break; + } + previous = end; + } + auto const after{std::min(std::upper_bound(lengths.begin(), lengths.end(), distance) - lengths.begin(), lengths.size() - 1)}; + auto const before{after > 0 ? after - 1 : 0}; + auto const span{lengths[after] - lengths[before]}; + result.push_back({track, glm::mix(points[before], points[after], span > 1e-9 ? (distance - lengths[before]) / span : 0.0)}); + } + } + return result; +} + +std::vector circuits(layout const &Layout, std::vector const &Units, std::vector const &Joints) +{ + std::unordered_map unit_of; + for (std::size_t index = 0; index < Units.size(); ++index) + for (auto const *part : Units[index].parts) + unit_of[part] = index; + std::vector tracks{Layout.switches}; + std::unordered_set members(tracks.begin(), tracks.end()); + std::unordered_set approach; + for (auto const &entry : Layout.paths) + { + if (members.insert(entry.track).second) + tracks.push_back(entry.track); + if (entry.approach) + approach.insert(entry.track); + } + auto const barrier = [&](glm::dvec3 const &Point) { return std::any_of(Joints.begin(), Joints.end(), [&](glm::dvec3 const &Joint) { return glm::distance(Joint, Point) < 0.05; }); }; + std::unordered_set taken; + std::vector result; + for (auto *seed : tracks) + { + if (taken.count(seed) > 0) + continue; + taken.insert(seed); + circuit entry; + entry.tracks.push_back(seed); + for (std::size_t i = 0; i < entry.tracks.size(); ++i) + { + auto const &track{*entry.tracks[i]}; + auto const ends{node(track) ? ends_of(track) : std::vector>{{0, false}, {0, true}}}; + for (auto const &[path, atend] : ends) + { + if (barrier(end_of(track, path, atend))) + continue; + auto *next{neighbour(track, path, atend)}; + if (next != nullptr && members.count(next) > 0 && taken.insert(next).second) + entry.tracks.push_back(next); + } + } + for (auto const *track : entry.tracks) + if (auto const found{unit_of.find(track)}; found != unit_of.end()) + { + entry.unit = static_cast(found->second); + break; + } + if (entry.unit < 0 && std::any_of(entry.tracks.begin(), entry.tracks.end(), [&](TTrack const *Track) { return approach.count(Track) > 0; })) + continue; + entry.from = std::numeric_limits::max(); + entry.to = std::numeric_limits::lowest(); + double weighted{0.0}; + std::unordered_set own(entry.tracks.begin(), entry.tracks.end()); + for (auto const &path : Layout.paths) + { + if (own.count(path.track) == 0) + continue; + for (std::size_t i = 1; i < path.schematic.size(); ++i) + { + auto const step{glm::distance(path.points[i - 1], path.points[i])}; + weighted += step * (path.schematic[i - 1].y + path.schematic[i].y) * 0.5; + entry.length += step; + } + for (auto const &point : path.schematic) + { + entry.from = std::min(entry.from, point.x); + entry.to = std::max(entry.to, point.x); + } + } + entry.offset = entry.length > 0.0 ? weighted / entry.length : 0.0; + result.push_back(std::move(entry)); + } + return result; +} + +std::vector track_numbers(layout const &Layout, std::vector const &Circuits) +{ + std::unordered_set const main(Layout.main.begin(), Layout.main.end()); + std::unordered_set const second(Layout.second.begin(), Layout.second.end()); + auto const on = [&](circuit const &Circuit, std::unordered_set const &Tracks) { return std::any_of(Circuit.tracks.begin(), Circuit.tracks.end(), [&](TTrack const *Track) { return Tracks.count(Track) > 0; }); }; + std::vector axis; + std::vector other; + std::vector right; + std::vector left; + for (std::size_t index = 0; index < Circuits.size(); ++index) + if (Circuits[index].unit >= 0) + continue; + else if (on(Circuits[index], main)) + axis.push_back(index); + else if (on(Circuits[index], second)) + other.push_back(index); + else + (Circuits[index].offset < 0.0 ? right : left).push_back(index); + auto const mean = [&](std::vector const &Group) { + double weighted{0.0}; + double length{0.0}; + for (auto const index : Group) + { + weighted += Circuits[index].offset * Circuits[index].length; + length += Circuits[index].length; + } + return length > 0.0 ? weighted / length : 0.0; + }; + // track 1 on the right along the chainage: the other main track of a double-track line when it lies to the right of the axis + auto const swapped{false == other.empty() && mean(other) < mean(axis)}; + std::vector, int>> tracks{{axis, swapped ? 2 : 1}}; + if (false == other.empty()) + tracks.emplace_back(other, swapped ? 1 : 2); + // beyond the main tracks the circuits side by side, the nearest first, the odd numbers to the right, the even ones to the left + for (auto *side : {&right, &left}) + { + std::sort(side->begin(), side->end(), [&](std::size_t A, std::size_t B) { return std::abs(Circuits[A].offset) < std::abs(Circuits[B].offset); }); + auto number{side == &right ? 3 : other.empty() ? 2 : 4}; + for (std::size_t index = 0; index < side->size(); ++index) + { + if (index == 0 || std::abs(Circuits[(*side)[index]].offset) - std::abs(Circuits[(*side)[index - 1]].offset) > 2.0) + { + if (index > 0) + number += 2; + tracks.emplace_back(std::vector{}, number); + } + tracks.back().first.push_back((*side)[index]); + } + } + std::vector result(Circuits.size()); + for (auto &[members, number] : tracks) + { + std::sort(members.begin(), members.end(), [&](std::size_t A, std::size_t B) { return Circuits[A].from < Circuits[B].from; }); + for (std::size_t index = 0; index < members.size(); ++index) + result[members[index]] = std::to_string(number) + (members.size() > 1 ? std::string(1, static_cast('a' + std::min(index, 25))) : std::string{}); + } + return result; +} + std::vector switches_of(TTrack &Switch, options const &Options) { return collect(Switch, Options).switches; diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index a6a85ec1..0efda3b0 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -50,6 +50,9 @@ struct layout double high{0.0}; bool kilometrage{false}; // the chainage runs with the km of the switches double km{0.0}; // at chainage 0, when it does + std::vector> lines; // the lines leading out, their tracks from the station on, as far as the approach + std::vector main; // the tracks of the axis + std::vector second; // of the other main track of a double-track line, none on a single-track one }; // what shares a number of the station: a switch, or the switches of a slip, the two of a double switch, the paths of a crossing @@ -60,6 +63,24 @@ struct unit double chainage{0.0}; // of the start of its first part along the station }; +// a joint of the track circuits; Track is the plain track to be cut there, none where two tracks meet already +struct joint +{ + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; +}; + +// a track circuit of the station, once its tracks are cut at the joints: of a switch, or of a station track +struct circuit +{ + std::vector tracks; + int unit{-1}; // of the switch, -1 for a station track + double offset{0.0}; // the mean one of a station track, in the schematic + double from{0.0}; // the chainage it spans + double to{0.0}; + double length{0.0}; +}; + // the station of the switch, or of the plain track near one std::optional find(TTrack &From, options const &Options = {}); // the chainage along the axis and the offset to the left of it, as the map shows it @@ -70,8 +91,19 @@ double km_of(layout const &Layout, double const Chainage); std::vector stations(options const &Options = {}); // the switches of the station by their numbers: along the chainage, or against it when Reversed; turntables have none std::vector numbering(layout const &Layout, bool const Reversed); +// the names of the switches and the track circuits of the station begin with it: bpd_ +std::string prefix(std::string const &Code); // _, each part of a unit of several with a letter of its own: bpd_15a std::string switch_name(std::string const &Code, int const Number, std::size_t const Part, std::size_t const Parts); +// the joints of the track circuits: 1.5 m before the start of a switch, beyond the fouling points of its branches where the axes +// are 4 m apart, in the middle of the links between switches too short for both, and so where the lines leave the station +std::vector joints(layout const &Layout, std::vector const &Units, options const &Options = {}); +// the track circuits the joints part the station into; the tracks of the lines beyond the joints belong to none +std::vector circuits(layout const &Layout, std::vector const &Units, std::vector const &Joints); +// the numbers of the station tracks as PKP PLK gives them, one for each circuit, empty for those of switches: 1 and 2 the main +// tracks, track 1 on the right along the chainage, the odd ones beyond it on its side, the even ones on the other; a track of +// several circuits has them lettered along the chainage: 5a, 5b +std::vector track_numbers(layout const &Layout, std::vector const &Circuits); // the switches of the station of the switch, without laying it out std::vector switches_of(TTrack &Switch, options const &Options = {}); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index a9d74cd5..b725c4c0 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10410,3 +10410,36 @@ msgstr[0] "Ponumerowano %zu rozjazd" msgstr[1] "Ponumerowano %zu rozjazdy" msgstr[2] "Ponumerowano %zu rozjazdów" +msgid "Set the track circuits" +msgstr "Ustaw izolacje" + +msgid "Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. One step of undo" +msgstr "Tnie tory stacji na stykach izolowanych i nazywa odcinki jak na liniach PKP PLK: styk 1,5 m przed początkiem rozjazdu, za ukresem każdej gałęzi tam, gdzie osie są 4 m od siebie, w połowie łącznika za krótkiego na oba. Każdy rozjazd dostaje bpd_iz, każdy tor stacyjny bpd_it: 1 i 2 to tory główne, tor 1 po prawej w stronę rosnącego km, nieparzyste za nim, parzyste po drugiej stronie. Jeden krok cofania" + +msgid "Give the station its code first: the track circuits are named after it" +msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy odcinków izolowanych" + +msgid "%s can't be cut: %s" +msgstr "Nie można przeciąć %s: %s" + +msgid "%s can't be changed: %s" +msgstr "Nie można zmienić %s: %s" + +msgid "The track circuits are set already" +msgstr "Izolacje są już ustawione" + +msgid "%zu track circuit" +msgid_plural "%zu track circuits" +msgstr[0] "%zu odcinek izolowany" +msgstr[1] "%zu odcinki izolowane" +msgstr[2] "%zu odcinków izolowanych" + +msgid "%zu cut" +msgid_plural "%zu cuts" +msgstr[0] "%zu cięcie" +msgstr[1] "%zu cięcia" +msgstr[2] "%zu cięć" + +msgid "track circuit %s" +msgstr "odcinek izolowany %s" +