From 8f8a1518b7ede01cc887c08752341fe6ca8ffa61 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 11:07:58 +0200 Subject: [PATCH] feat(editor): the signalling mode places the signals of a station and names them as PKP PLK does, the entry ones out on the lines with the circuits before them, the exit ones at the ends of the station tracks, the shunting ones at the shorter tracks, and renames the signals standing in their places --- application/editormode.h | 4 +- application/editormodesignal.cpp | 114 +++++++++++ application/editormodesrk.cpp | 323 ++++++++++++++++++++++++++++++- application/editormodesrk.h | 16 +- application/editortool.h | 42 +++- editor/editorStation.cpp | 277 ++++++++++++++++++++++++++ editor/editorStation.hpp | 38 ++++ lang/editor_pl.po | 33 ++++ 8 files changed, 840 insertions(+), 7 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index e2692fc8..dd42ffba 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1890,6 +1890,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t move_templates(std::vector> const &Moves) override; // takes them out, as one step of the undo std::size_t remove_templates(std::vector const &Instances) override; + bool standing_signals(std::vector &Signals, bool const Wait) override; + std::size_t signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) override; struct signal_template { @@ -2381,7 +2383,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri hekto_tool m_hekto{*this, *this, *this, *this, *this, *this}; fouling_tool m_fouling{*this, *this, *this, *this, *this}; parallel_tool m_parallel{*this, *this, *this, *this}; - srk_tool m_srk{*this, *this, *this}; + srk_tool m_srk{*this, *this, *this, *this}; std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel, &m_srk}; void infra_rebase(); diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 5366b5fa..55ad4950 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -31,6 +31,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace { @@ -874,6 +875,119 @@ void editor_mode::signal_renamed(std::string const &Before, std::string const &A Snapshot.node_name = Snapshot.tracks.front().first->name(); } +bool editor_mode::standing_signals(std::vector &Signals, bool const Wait) +{ + Signals.clear(); + signal_scan(Wait); + if (m_signal.templates.empty()) + return false; + signal_refresh(true); + for (auto const &standing : m_signal.standing) + if (standing.kind >= 0) + Signals.push_back({standing.include.instance, standing.name, kKinds[standing.kind], standing.include.location, standing.include.yaw}); + return true; +} + +std::size_t editor_mode::signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) +{ + auto &tool{m_signal}; + signal_scan(); + signal_refresh(true); + std::vector items; + std::vector> reads; + for (auto const &order : Placed) + { + auto const kind{kind_index(order.kind)}; + auto const *item{kind >= 0 && tool.chosen[kind] >= 0 && tool.chosen[kind] < static_cast(tool.templates.size()) ? &tool.templates[tool.chosen[kind]] : nullptr}; + if (item == nullptr) + { + Error = format(STR_C("no template of the kind %s"), STR(kKindLabels[std::max(0, kind)]).c_str()); + break; + } + signal_spot spot; + if (order.track == nullptr || false == signal_spot_at(*order.track, order.path, order.point, order.point, order.facing, order.side, *item, spot)) + continue; + template_item placing; + placing.file = signal_variant(*item, spot); + placing.location = spot.position; + placing.yaw = spot.yaw; + placing.described = true; + placing.values["name"] = order.name; + auto const *variant{signal_template_of(placing.file)}; + if (variant == nullptr) + variant = item; + if (variant->plate) + placing.values["#6"] = plate_for(std::string{}, order.plate); + if (variant->linked) + placing.values["#7"] = "none"; + items.push_back(std::move(placing)); + reads.emplace_back(spot, variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, {order.name})); + } + // the names passed round among the signals go through ones of their own first, or a read event renamed would be renamed again + EditorSnapshot step; + step.tracks = std::move(States); + std::vector> directives; + for (auto const temporary : {true, false}) + for (auto const &rename : Renamed) + { + auto const *standing{signal_standing_of(rename.instance)}; + auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; + auto const before{scene::Layers.directive(rename.instance)}; + std::string file; + std::vector values; + if (item == nullptr || false == editor_includes::parse_directive(before, file, values) || values.empty()) + continue; + auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; + values[0] = rename.name + (temporary ? "~" : ""); + if (item->plate && values.size() > 5) + values[5] = plate_for(std::string{}, rename.plate); + auto const read_after{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; + auto const directive{editor_includes::compose_directive(file, values)}; + if (directive == before) + continue; + if (temporary) + directives.emplace_back(rename.instance, before); + set_include_directive(rename.instance, directive); + if (read_before != read_after) + signal_renamed(read_before, read_after, step); + } + std::vector instances; + if (Error.empty() && false == items.empty()) + { + if (place_templates(items, Error) == 0) + reads.clear(); + else + { + auto placed{std::move(m_history.back())}; + m_history.pop_back(); + instances = std::move(placed.instances); + } + } + else + reads.clear(); + std::unordered_set const created(Created.begin(), Created.end()); + for (auto const &[spot, read] : reads) + { + if (read.empty() || false == spot.track->Dynamics.empty() || false == scene::Layers.editable(spot.track)) + continue; + if (created.count(spot.track) == 0 && std::none_of(step.tracks.begin(), step.tracks.end(), [&](auto const &State) { return State.first == spot.track; })) + step.tracks.emplace_back(spot.track, editor_track::capture(*spot.track)); + editor_track::events(*spot.track, spot.list).emplace_back(ToLower(read), nullptr); + std::string missing; + editor_track::bind_events(*spot.track, missing); + spot.track->mark_dirty(); + } + auto const count{instances.size()}; + if (step.tracks.empty() && Created.empty() && instances.empty() && directives.empty()) + return 0; + push_track_snapshot(std::move(step.tracks), std::move(Created)); + auto &pushed{m_history.back()}; + pushed.instances = std::move(instances); + pushed.directives.insert(pushed.directives.end(), directives.begin(), directives.end()); + signal_refresh(true); + return count; +} + void editor_mode::signal_select(scene::instance_handle const Instance) { m_signal.selected = Instance; diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 26b92ce0..cddcd5aa 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/Logs.h" +#include "utilities/utilities.h" #include "world/Track.h" #include "imgui/imgui.h" @@ -31,6 +32,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -44,6 +46,7 @@ ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; ImU32 constexpr kApproach{IM_COL32(95, 100, 112, 255)}; ImU32 constexpr kHovered{IM_COL32(255, 255, 255, 255)}; +ImU32 constexpr kSignal{IM_COL32(240, 95, 85, 255)}; bool is_switch(TTrack const &Track) { @@ -82,18 +85,56 @@ std::string unit_number(station::unit const &Unit, std::string const &Stem, std: return std::to_string(Index + 1); } -// the number of the station track a circuit is named after: bpd_it5b of track 5; empty for the other names +// the number of the station track a circuit is named after: bpd_it5b of track 5; empty for the other names, bpd_ita of the +// circuit before the entry signal A among them std::string track_of(std::string const &Name, std::string const &Stem) { auto const head{Stem + "it"}; if (Stem.empty() || Name.size() <= head.size() || Name.compare(0, head.size(), head) != 0) return {}; auto number{Name.substr(head.size())}; + if (std::none_of(number.begin(), number.end(), [](char const Letter) { return std::isdigit(static_cast(Letter)) != 0; })) + return {}; if (number.size() > 1 && std::isalpha(static_cast(number.back())) && std::isdigit(static_cast(number[number.size() - 2]))) number.pop_back(); return number; } +// the circuit between an entry signal and the station: bpd_ita +bool entry_circuit(std::string const &Name, std::string const &Stem) +{ + auto const head{Stem + "it"}; + return false == Stem.empty() && Name.size() > head.size() && Name.compare(0, head.size(), head) == 0 && + std::all_of(Name.begin() + head.size(), Name.end(), [](char const Letter) { return std::isalpha(static_cast(Letter)) != 0; }); +} + +// the name of a signal as its plate and the schematic show it: bpd_tm3 as Tm3, bpd_f1 as F1 +std::string signal_shown(std::string const &Name, std::string const &Stem) +{ + auto text{false == Stem.empty() && Name.size() > Stem.size() && ToLower(Name).compare(0, Stem.size(), Stem) == 0 ? Name.substr(Stem.size()) : Name}; + std::transform(text.begin(), text.end(), text.begin(), [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); + if (text.size() > 2 && text[0] == 'T' && (text[1] == 'M' || text[1] == 'O')) + text[1] = static_cast(std::tolower(static_cast(text[1]))); + return text; +} + +// the way the trains a signal faces run, in the plan: it stands turned to them +glm::dvec2 travel_of(double const Yaw) +{ + auto const yaw{glm::radians(Yaw + 180.0)}; + return {std::sin(yaw), std::cos(yaw)}; +} + +// how far the point lies from the tracks of the station, in the plan +double gap_to(station::layout const &Layout, glm::dvec3 const &Point) +{ + auto best{std::numeric_limits::max()}; + for (auto const &path : Layout.paths) + for (auto const &point : path.points) + best = std::min(best, glm::distance(glm::dvec2{point.x, point.z}, glm::dvec2{Point.x, Point.z})); + return best; +} + 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)}; @@ -270,6 +311,8 @@ void srk_tool::update() } if ((m_layout.has_value() || m_anchor != nullptr) && m_history.history_size() != m_history_size) refresh(); + else if (m_layout.has_value() && false == m_signals_known) + survey_signals(); // a track of another station selected opens it auto *track{m_selection.selected_track()}; if (track == nullptr || track == m_picked) @@ -425,6 +468,14 @@ void srk_tool::render_options() "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. Run again after changes, it keeps " "the numbers the tracks have. One step of undo"); + step_heading(3, STR_C("Signals")); + if (ImGui::Button(STR_C("Set the signals"), ImVec2(-1.0f, 0.0f))) + set_signals(); + item_tooltip("Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on " + "each line, A, B... from the start of the km, Z, Y... from the end, the circuit from it to the station bpd_ita; an exit " + "signal at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a " + "shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is " + "doubled. The kinds of signals chosen in the Signals tool. One step of undo"); if (auto const *selected{m_selection.selected_track()}; selected != nullptr && false == selected->Isolated.empty()) if (auto const number{track_of(selected->Isolated.front()->asName, stem_of_open())}; false == number.empty() && begin_fields("##srktrack")) { @@ -716,6 +767,232 @@ void srk_tool::renumber_track(TTrack const &Track, std::string const &Number) m_status = format(STR_C("Track %s is track %s now"), from.c_str(), to.c_str()); } +void srk_tool::set_signals() +{ + m_status.clear(); + auto const stem{stem_of_open()}; + if (stem.empty()) + { + m_status = STR_C("Give the station its code first: the signals are named after it"); + return; + } + auto const &layout{*m_layout}; + // the station tracks by their numbers, the circuits as the tracks have them + std::map>> numbers; + std::unordered_set inside; + for (auto const &path : layout.paths) + { + if (path.track->Isolated.empty() || inside.count(path.track) > 0) + continue; + auto const &name{path.track->Isolated.front()->asName}; + if (name.size() <= stem.size() || name.compare(0, stem.size(), stem) != 0 || entry_circuit(name, stem)) + continue; + inside.insert(path.track); + if (auto const number{track_of(name, stem)}; false == number.empty()) + numbers[number][name].push_back(path.track); + } + if (numbers.empty()) + { + m_status = STR_C("Set the track circuits first: the signals stand at their joints"); + return; + } + std::vector tracks; + for (auto &[number, circuits] : numbers) + { + station::numbered entry; + entry.number = number; + for (auto &[name, members] : circuits) + entry.circuits.push_back(std::move(members)); + tracks.push_back(std::move(entry)); + } + auto plan{station::signals(layout, tracks, [&](TTrack const &Track) { return inside.count(&Track) > 0; })}; + std::vector standing; + m_signals.standing_signals(standing, true); + standing.erase(std::remove_if(standing.begin(), standing.end(), [&](standing_signal const &Signal) { return gap_to(layout, Signal.position) > 8.0; }), standing.end()); + auto const full = [&](station::signal const &Signal) { return stem + ToLower(Signal.name); }; + // the standing ones by their places, then by their names the ones moved away + std::vector match(plan.size(), -1); + std::vector used(standing.size(), 0); + std::vector> candidates; + for (std::size_t i = 0; i < plan.size(); ++i) + { + auto const &planned{plan[i]}; + glm::dvec2 const travel{glm::normalize(glm::dvec2{planned.travel.x, planned.travel.z} + glm::dvec2{1e-12, 0.0})}; + auto const reach{planned.kind == station::signal::role::entry ? 400.0 : planned.kind == station::signal::role::exit ? 60.0 : 40.0}; + for (std::size_t k = 0; k < standing.size(); ++k) + { + auto const &signal{standing[k]}; + if ((signal.kind == "shunt") != (planned.kind == station::signal::role::shunting) || (signal.kind != "shunt" && signal.kind != "main") || glm::dot(travel_of(signal.yaw), travel) < 0.87) + continue; + glm::dvec2 const offset{signal.position.x - planned.point.x, signal.position.z - planned.point.z}; + auto const along{glm::dot(offset, travel)}; + auto const across{std::abs(offset.x * travel.y - offset.y * travel.x)}; + if (std::abs(along) < reach && across < 3.0) + candidates.emplace_back(std::abs(along) + across * 10.0, i, k); + } + } + std::sort(candidates.begin(), candidates.end()); + for (auto const &[score, i, k] : candidates) + if (match[i] < 0 && 0 == used[k]) + { + match[i] = static_cast(k); + used[k] = 1; + } + for (std::size_t i = 0; i < plan.size(); ++i) + for (std::size_t k = 0; k < standing.size() && match[i] < 0; ++k) + if (0 == used[k] && ToLower(standing[k].name) == full(plan[i])) + { + match[i] = static_cast(k); + used[k] = 1; + } + // the circuits from the entry signals to the station: the line cut by each one, all of it checked before any is + struct cut + { + std::size_t signal{0}; + TTrack *track{nullptr}; + double parameter{0.0}; + glm::dvec3 point{0.0}; + bool outward{false}; + std::vector named; // between the station and the cut + }; + std::vector cuts; + for (std::size_t i = 0; i < plan.size(); ++i) + { + auto const &planned{plan[i]}; + if (planned.kind != station::signal::role::entry || planned.run.empty()) + continue; + auto const at{match[i] >= 0 ? standing[match[i]].position : planned.point}; + cut entry; + entry.signal = i; + auto best{std::numeric_limits::max()}; + std::size_t nearest{0}; + for (std::size_t k = 0; k < planned.run.size(); ++k) + { + auto const t{editor_track::nearest_parameter(*planned.run[k].first, at)}; + auto const point{editor_track::point_at(*planned.run[k].first, t)}; + if (auto const distance{glm::distance(glm::dvec2{point.x, point.z}, glm::dvec2{at.x, at.z})}; distance < best) + { + best = distance; + nearest = k; + entry.parameter = t; + entry.point = point; + } + } + for (std::size_t k = 0; k < nearest; ++k) + entry.named.push_back(planned.run[k].first); + entry.track = planned.run[nearest].first; + entry.outward = planned.run[nearest].second; + auto const &ends{entry.track->m_paths.front().points}; + auto const to_start{glm::distance(entry.point, ends[segment_data::point::start])}; + auto const to_end{glm::distance(entry.point, ends[segment_data::point::end])}; + std::string reason; + if (std::min(to_start, to_end) <= 0.3) + { + // at an end already + if ((to_end < to_start) == entry.outward) + entry.named.push_back(entry.track); + entry.track = nullptr; + } + else if (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 *part : entry.named) + if (false == scene::Layers.editable(part, &reason)) + { + m_status = format(STR_C("%s can't be changed: %s"), name_of(*part).c_str(), reason.c_str()); + return; + } + cuts.push_back(std::move(entry)); + } + std::vector> before; + std::vector created; + std::unordered_set captured; + for (auto &entry : cuts) + { + auto &planned{plan[entry.signal]}; + if (entry.track != nullptr) + { + captured.insert(entry.track); + before.emplace_back(entry.track, editor_track::capture(*entry.track)); + if (auto *piece{editor_track::split_path(*entry.track, entry.parameter)}) + { + created.push_back(piece); + captured.insert(piece); + entry.named.push_back(entry.outward ? entry.track : piece); + planned.track = entry.outward ? piece : entry.track; + planned.point = entry.point; + } + } + auto const name{stem + "it" + ToLower(planned.name)}; + for (auto *part : entry.named) + { + if (part->Isolated.size() == 1 && part->Isolated.front()->asName == name) + continue; + if (captured.insert(part).second) + before.emplace_back(part, editor_track::capture(*part)); + editor_track::isolated(*part, {name}); + } + } + std::vector placed; + std::vector renamed; + for (std::size_t i = 0; i < plan.size(); ++i) + { + auto const &planned{plan[i]}; + auto const plate{ToLower(planned.name)}; + if (match[i] >= 0) + { + if (ToLower(standing[match[i]].name) != full(planned)) + renamed.push_back({standing[match[i]].instance, full(planned), plate}); + continue; + } + signal_order order; + order.track = planned.track; + order.point = planned.point; + order.facing = planned.facing; + order.side = planned.side; + order.kind = planned.kind == station::signal::role::shunting ? "shunt" : "main"; + order.name = full(planned); + order.plate = plate; + placed.push_back(std::move(order)); + } + // the others keep their names, unless one of these takes it + for (std::size_t k = 0; k < standing.size(); ++k) + if (0 == used[k] && std::any_of(plan.begin(), plan.end(), [&](station::signal const &Signal) { return full(Signal) == ToLower(standing[k].name); })) + renamed.push_back({standing[k].instance, ToLower(standing[k].name) + "_old", signal_shown(standing[k].name, stem) + "_old"}); + if (placed.empty() && renamed.empty() && before.empty() && created.empty()) + { + m_status = STR_C("The signals are set already"); + return; + } + std::string error; + auto const count{m_signals.signal_batch(placed, renamed, std::move(before), std::move(created), error)}; + if (false == error.empty()) + m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()); + else + m_status = format(STR_P("%zu signal placed", "%zu signals placed", count), count) + ", " + format(STR_P("%zu renamed", "%zu renamed", renamed.size()), renamed.size()); +} + +void srk_tool::survey_signals() +{ + m_signal_marks.clear(); + std::vector standing; + m_signals_known = m_layout.has_value() && m_signals.standing_signals(standing, false); + if (false == m_signals_known) + return; + auto const stem{stem_of_open()}; + for (auto const &signal : standing) + { + if (gap_to(*m_layout, signal.position) > 8.0) + continue; + auto const travel{travel_of(signal.yaw)}; + auto const at{station::project(*m_layout, signal.position)}; + auto const ahead{station::project(*m_layout, signal.position + glm::dvec3{travel.x, 0.0, travel.y} * 5.0)}; + m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem)}); + } +} + void srk_tool::survey() { m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); @@ -767,11 +1044,15 @@ void srk_tool::survey() on = point; } mark.at = on; + auto const entry{entry_circuit(mark.text, stem)}; // 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()))); + // bpd_ita, before the entry signal A, as ItA + if (entry && mark.text.size() > 2) + std::transform(mark.text.begin() + 2, mark.text.end(), mark.text.begin() + 2, [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); } // the joints: the ends where paths of different circuits meet std::vector> ends; @@ -791,15 +1072,24 @@ void srk_tool::survey() 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)); + survey_signals(); } void srk_tool::fit_view(float const Width, float const Height) { auto const &layout{*m_layout}; auto &view{m_view_state}; - auto const span{std::max(50.0, layout.to - layout.from)}; - view.from = layout.from - span * 0.08 - 30.0; - view.to = layout.to + span * 0.08 + 30.0; + // the switches and the signals out on the lines + auto from{layout.from}; + auto to{layout.to}; + for (auto const &mark : m_signal_marks) + { + from = std::min(from, mark.at.x); + to = std::max(to, mark.at.x); + } + auto const span{std::max(50.0, to - from)}; + view.from = from - span * 0.04 - 30.0; + view.to = to + span * 0.04 + 30.0; view.centre = (layout.low + layout.high) * 0.5; auto const across{(view.to - view.from) / std::max(1.0f, Width)}; auto const vertical{(layout.high - layout.low + 20.0) / std::max(1.0f, Height * 0.75f)}; @@ -958,6 +1248,14 @@ void srk_tool::render_canvas(float const Height) else label(text, kSwitch, {under, over}); } + // the signals: a flag pointing the way of the trains they face + for (auto const &mark : m_signal_marks) + { + auto const at{screen_of(mark.at)}; + auto const way{mark.ahead ? 1.0f : -1.0f}; + draw->AddTriangleFilled(ImVec2(at.x - 4.0f * way, at.y - 4.0f), ImVec2(at.x - 4.0f * way, at.y + 4.0f), ImVec2(at.x + 4.0f * way, at.y), kSignal); + label(mark.text, kSignal, {ImVec2(at.x, at.y - height - 5.0f), ImVec2(at.x, at.y + 5.0f)}); + } std::vector order; for (std::size_t index = 0; index < m_circuits.size(); ++index) if (m_circuits[index].labelled) @@ -1126,6 +1424,23 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog("SELFTEST srktrack: " + m_status); return editor_selftest_result::handled; } + if (Command == "srksignals") + { + // srksignals: the signals of the open station set + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srksignals: no station open"); + return editor_selftest_result::handled; + } + set_signals(); + WriteLog("SELFTEST srksignals: " + m_status); + refresh(); + std::string list; + for (auto const &mark : m_signal_marks) + list += format(" %s@%.0f/%.1f%s", mark.text.c_str(), mark.at.x, mark.at.y, mark.ahead ? ">" : "<"); + WriteLog(format("SELFTEST srksignals %zu:", m_signal_marks.size()) + list); + return editor_selftest_result::handled; + } if (Command == "srkstations") { load(); diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 2201e790..240babd4 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -24,7 +24,7 @@ http://mozilla.org/MPL/2.0/. class srk_tool final : public editor_tool { public: - srk_tool(editor_selection &Selection, editor_view &View, editor_history &History) : m_selection{Selection}, m_view{View}, m_history{History} {} + srk_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_signals &Signals) : m_selection{Selection}, m_view{View}, m_history{History}, m_signals{Signals} {} void update() override; void draw_overlay() const override; void render_options() override; @@ -39,6 +39,7 @@ class srk_tool final : public editor_tool editor_selection &m_selection; editor_view &m_view; editor_history &m_history; + editor_signals &m_signals; // a station described: found again by a switch of it struct record { @@ -82,6 +83,14 @@ class srk_tool final : public editor_tool 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 + struct signal_mark + { + glm::dvec2 at{0.0}; // in the schematic + bool ahead{true}; // the trains it faces run along the chainage + std::string text; + }; + std::vector m_signal_marks; + bool m_signals_known{false}; // the signals standing surveyed, once their templates are found std::string m_status; void load(); @@ -99,6 +108,11 @@ class srk_tool final : public editor_tool void set_circuits(); // the track circuits as the tracks have them, for the schematic void survey(); + // the signals placed where the station has none and the standing ones named, the circuits before the entry signals set too; one + // step of the history + void set_signals(); + // the signals standing by the station, for the schematic + void survey_signals(); // the names of the switches and the circuits of the open station begin with it, empty while it has no code std::string stem_of_open() const; // the circuits of the station track of the track renamed after another number; one step of the history diff --git a/application/editortool.h b/application/editortool.h index 744a7646..bbe506da 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -73,6 +73,33 @@ struct standing_template std::vector values; }; +// a signal to stand by a track, of the kind of the signal templates: main, shunt... +struct signal_order +{ + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 point{0.0}; // on the axis + int facing{1}; // the trains it faces run 1: towards the end of the path, -1: towards its start + int side{1}; // 1: on the right of the path, -1: on the left + std::string kind; + std::string name; + std::string plate; // the name on the plate under the head, when the template has one +}; +struct signal_rename +{ + scene::instance_handle instance{0}; + std::string name; + std::string plate; +}; +struct standing_signal +{ + scene::instance_handle instance{0}; + std::string name; + std::string kind; + glm::dvec3 position{0.0}; + double yaw{0.0}; +}; + struct nmt_place { glm::dvec3 position{0.0}; // on the axis of the path @@ -145,6 +172,19 @@ class editor_templates ~editor_templates() = default; }; +class editor_signals +{ + public: + // false while the signal templates are still being looked for + virtual bool standing_signals(std::vector &Signals, bool const Wait) = 0; + // the signals placed by the templates chosen for their kinds and the standing ones renamed, with the tracks changed before: + // one step of the history. returns: how many were placed + virtual std::size_t signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) = 0; + + protected: + ~editor_signals() = default; +}; + class editor_ground { public: @@ -180,7 +220,7 @@ class editor_guide ~editor_guide() = default; }; -class editor_services : public editor_selection, public editor_view, public editor_history, public editor_templates, public editor_ground, public editor_track_design, public editor_guide +class editor_services : public editor_selection, public editor_view, public editor_history, public editor_templates, public editor_signals, public editor_ground, public editor_track_design, public editor_guide { protected: ~editor_services() = default; diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index ef2c78f1..66c8016a 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -236,6 +237,11 @@ double constexpr kReach{120.0}; // m, along a branch looked for its fouling poin 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 double constexpr kThrough{400.0}; // m, of the switches a station track is followed through for its circuits beyond +double constexpr kEntry{300.0}; // m, from the joint of the station out to its entry signal +double constexpr kJoint{3.5}; // m, from a signal to the joint of the circuit it guards +double constexpr kStopping{200.0}; // m, the shortest circuit of a station track a train is sent out of by an exit signal +double constexpr kSignalSide{2.25}; // m, from the axis to a signal +double constexpr kSignalClear{2.5}; // m, the least from a signal to the axis of another track // 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 @@ -977,4 +983,275 @@ std::vector switches_of(TTrack &Switch, options const &Options) return collect(Switch, Options).switches; } +namespace +{ + +// the point the distance in from the end of the plain track, and the way to the end there +std::pair in_from(TTrack const &Track, bool const Atend, double const Distance) +{ + double length{0.0}; + auto points{dense_points(Track, 0, length)}; + if (Atend) + std::reverse(points.begin(), points.end()); + double along{0.0}; + for (std::size_t i = 1; i < points.size(); ++i) + { + auto const step{plan_distance(points[i - 1], points[i])}; + if (along + step >= Distance || i + 1 == points.size()) + { + auto const t{step > 1e-9 ? std::clamp((Distance - along) / step, 0.0, 1.0) : 0.0}; + return {glm::mix(points[i - 1], points[i], t), points[i - 1] - points[i]}; + } + along += step; + } + return {points.front(), glm::dvec3{0.0, 0.0, 1.0}}; +} + +// how far a signal on the side of the track at the point keeps from the other tracks; Way: of the path there +double side_gap(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const &Way, int const Side) +{ + glm::dvec2 const way{glm::normalize(glm::dvec2{Way.x, Way.z} + glm::dvec2{1e-12, 0.0})}; + glm::dvec2 const right{-way.y, way.x}; + auto const spot{glm::dvec2{Point.x, Point.z} + right * (Side * kSignalSide)}; + glm::dvec3 const at{spot.x, Point.y, spot.y}; + auto gap{std::numeric_limits::max()}; + for (auto const &path : Layout.paths) + if (plan_gap(Point, path.points) > 1.0) + gap = std::min(gap, plan_gap(at, path.points)); + return gap; +} + +// on the right of the trains, unless another track runs too close by there and less so on the left +int side_for(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const &Way, int const Facing) +{ + auto const right{side_gap(Layout, Point, Way, Facing)}; + if (right >= kSignalClear) + return Facing; + auto const left{side_gap(Layout, Point, Way, -Facing)}; + return left >= kSignalClear || left > right ? -Facing : Facing; +} + +// the ends of the tracks out of the set, Free where no track goes on +struct track_end +{ + TTrack *track{nullptr}; + bool atend{false}; + glm::dvec3 point{0.0}; + bool free{false}; +}; + +std::vector ends_out(std::vector const &Tracks) +{ + std::unordered_set const own(Tracks.begin(), Tracks.end()); + std::vector result; + for (auto *track : Tracks) + { + if (node(*track)) + continue; + for (auto const atend : {false, true}) + { + auto const *next{neighbour(*track, 0, atend)}; + if (next != nullptr && rail(*next) && own.count(next) > 0) + continue; + result.push_back({track, atend, end_of(*track, 0, atend), next == nullptr || false == rail(*next)}); + } + } + return result; +} + +char next_letter(char Letter, int const Step) +{ + do + Letter = static_cast(Letter + Step); + while (Letter == 'I' || Letter == 'Q'); + return Letter; +} + +} // namespace + +std::vector signals(layout const &Layout, std::vector const &Tracks, std::function const &Inside) +{ + std::vector result; + auto const middle{(Layout.from + Layout.to) * 0.5}; + // the entry signals: out along each line from where it leaves the circuits of the station + struct entry + { + signal placed; + double offset{0.0}; + bool start{false}; + }; + std::vector entries; + for (auto const &line : Layout.lines) + { + auto const outside{std::find_if(line.begin(), line.end(), [&](TTrack const *Track) { return false == Inside(*Track); })}; + if (outside == line.end() || node(**outside)) + continue; + // the joint: the end of the first track out of the station by the track before it + auto *track{*outside}; + bool atend{false}; + if (outside != line.begin()) + { + int path{0}; + bool end{false}; + atend = false == (end_at(**std::prev(outside), end_of(*track, 0, false), path, end)); + } + else + { + auto const start{end_of(*track, 0, false)}; + atend = std::none_of(Layout.switches.begin(), Layout.switches.end(), [&](TTrack const *Switch) { + return std::any_of(Switch->m_paths.begin(), Switch->m_paths.end(), [&](auto const &Path) { return glm::distance(Path.points[segment_data::point::start], start) < same_point || glm::distance(Path.points[segment_data::point::end], start) < same_point; }); + }); + } + // out from the joint: along the path when the joint is its start + entry found; + auto const joint{end_of(*track, 0, atend)}; + found.offset = project(Layout, joint).y; + found.start = project(Layout, joint).x < middle; + double along{0.0}; + auto point{joint}; + auto *next{track}; + std::unordered_set seen; + while (next != nullptr && rail(*next) && false == node(*next) && seen.insert(next).second) + { + int path{0}; + bool end{false}; + if (false == end_at(*next, point, path, end)) + break; + auto const outward{false == end}; + auto const length{plan_length(*next, 0)}; + found.placed.run.emplace_back(next, outward); + if (along + length >= kEntry) + { + auto const [at, way] = in_from(*next, end, kEntry - along); + found.placed.track = next; + found.placed.point = at; + found.placed.facing = outward ? -1 : 1; + found.placed.side = side_for(Layout, at, outward ? -way : way, found.placed.facing); + found.placed.travel = way; + break; + } + along += length; + point = end_of(*next, 0, outward); + next = neighbour(*next, 0, outward); + } + if (found.placed.track == nullptr && false == found.placed.run.empty() && along > 30.0) + { + // the line ends sooner, by the edge of the scenery: as far out as it goes + auto const &[last, outward] = found.placed.run.back(); + auto const length{plan_length(*last, 0)}; + auto const [at, way] = in_from(*last, outward, std::min(10.0, length * 0.5)); + found.placed.track = last; + found.placed.point = at; + found.placed.facing = outward ? -1 : 1; + found.placed.side = side_for(Layout, at, outward ? way : -way, found.placed.facing); + found.placed.travel = -way; + } + if (found.placed.track != nullptr) + entries.push_back(std::move(found)); + } + std::sort(entries.begin(), entries.end(), [](entry const &A, entry const &B) { return A.offset > B.offset; }); + char from_start{'A'}; + char from_end{'Z'}; + char last_start{'@'}; + char first_end{'['}; + for (auto &found : entries) + { + auto &letter{found.start ? from_start : from_end}; + found.placed.kind = signal::role::entry; + found.placed.name = std::string(1, letter); + (found.start ? last_start : first_end) = letter; + letter = next_letter(letter, found.start ? 1 : -1); + result.push_back(std::move(found.placed)); + } + // the exit signals, a letter for the odd tracks and one for the even ones at each end, clear of the entry ones + auto letter_after = [](char Letter, char const Limit, int const Step) { + while (Step > 0 ? Letter <= Limit : Letter >= Limit) + Letter = next_letter(Letter, Step); + return Letter; + }; + auto const odd_start{letter_after('F', last_start, 1)}; + auto const even_start{next_letter(odd_start, 1)}; + auto const odd_end{letter_after('R', first_end, -1)}; + auto const even_end{next_letter(odd_end, -1)}; + auto const lines_at = [&](bool const Start) { return std::any_of(entries.begin(), entries.end(), [&](entry const &Entry) { return Entry.start == Start; }); }; + auto const starts{lines_at(true)}; + auto const ends{lines_at(false)}; + std::vector shunting; + for (auto const &track : Tracks) + { + auto const digit{std::find_if(track.number.rbegin(), track.number.rend(), [](char const Letter) { return std::isdigit(static_cast(Letter)) != 0; })}; + auto const odd{digit == track.number.rend() || (*digit - '0') % 2 == 1}; + auto upper{track.number}; + std::transform(upper.begin(), upper.end(), upper.begin(), [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); + // the outer ends of the outermost circuits long enough, else of the outermost ones + struct reach + { + track_end low; + track_end high; + double low_x{std::numeric_limits::max()}; + double high_x{std::numeric_limits::lowest()}; + }; + reach longest; + reach any; + for (auto const &circuit : track.circuits) + { + double length{0.0}; + for (auto const *part : circuit) + length += node(*part) ? 0.0 : plan_length(*part, 0); + for (auto const &end : ends_out(circuit)) + { + auto const x{project(Layout, end.point).x}; + for (auto *found : {&any, length >= kStopping ? &longest : static_cast(nullptr)}) + { + if (found == nullptr) + continue; + if (x < found->low_x) + { + found->low_x = x; + found->low = end; + } + if (x > found->high_x) + { + found->high_x = x; + found->high = end; + } + } + } + } + auto const stopping{longest.low.track != nullptr}; + auto const &chosen{stopping ? longest : any}; + for (auto const start : {true, false}) + { + auto const &end{start ? chosen.low : chosen.high}; + if (end.track == nullptr || end.free) + continue; + signal placed; + auto const [at, way] = in_from(*end.track, end.atend, std::min(kJoint, plan_length(*end.track, 0) * 0.5)); + placed.track = end.track; + placed.point = at; + placed.facing = end.atend ? 1 : -1; + placed.side = side_for(Layout, at, end.atend ? way : -way, placed.facing); + placed.travel = way; + if (stopping && (start ? starts : ends)) + { + placed.kind = signal::role::exit; + placed.name = std::string(1, start ? (odd ? odd_start : even_start) : (odd ? odd_end : even_end)) + upper; + result.push_back(std::move(placed)); + } + else + { + placed.kind = signal::role::shunting; + shunting.push_back(std::move(placed)); + } + } + } + std::sort(shunting.begin(), shunting.end(), [&](signal const &A, signal const &B) { return project(Layout, A.point).x < project(Layout, B.point).x; }); + for (std::size_t index = 0; index < shunting.size(); ++index) + { + shunting[index].name = "Tm" + std::to_string(index + 1); + result.push_back(std::move(shunting[index])); + } + return result; +} + } // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index 89ffe9fd..f8fcceeb 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include #include @@ -107,4 +108,41 @@ std::vector track_numbers(layout const &Layout, std::vector switches_of(TTrack &Switch, options const &Options = {}); +// a station track by its number, with its track circuits +struct numbered +{ + std::string number; + std::vector> circuits; +}; + +// a signal of the station, as PKP PLK places and names them +struct signal +{ + enum class role + { + entry, + exit, + shunting + }; + role kind{role::entry}; + std::string name; // without the code of the station: A, F1, Tm3 + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 point{0.0}; // on the axis + int facing{1}; // the trains it faces run 1: towards the end of the path, -1: towards its start + int side{1}; // 1: on the right of the path, -1: on the left + glm::dvec3 travel{0.0}; // the way the trains it faces run, in the plan + // of an entry signal: the tracks of its line from the joint of the station out to it, each with the way out along it, true + // towards the end of the path; a circuit named after the signal runs between the two + std::vector> run; +}; + +// the signals of the station: the entry ones 300 m out along the lines beyond the circuits of the station, A, B... from the start +// of the chainage, Z, Y... from its end, the leftmost first; the exit ones facing out at the outer ends of the outermost circuits +// at least 200 m long of the station tracks, a letter for the odd tracks and one for the even ones at each end, with the number: +// F1, G2 towards the start, R1, P2 towards the end; the shunting ones at the ends of the shorter tracks, Tm1, Tm2... along the +// chainage. Each on the right of the trains, unless another track runs too close by there. Inside: a track of the circuits of the +// station +std::vector signals(layout const &Layout, std::vector const &Tracks, std::function const &Inside); + } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index e7f9adf4..79ca1635 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10455,3 +10455,36 @@ msgstr "Stacja ma już tor %s" msgid "Track %s is track %s now" msgstr "Tor %s ma teraz numer %s" +msgid "Set the signals" +msgstr "Ustaw semafory" + +msgid "Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on each line, A, B... from the start of the km, Z, Y... from the end, the circuit from it to the station bpd_ita; an exit signal at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is doubled. The kinds of signals chosen in the Signals tool. One step of undo" +msgstr "Stawia semafory stacji jak na liniach PKP PLK i nazywa je od jej skrótu: wjazdowy 300 m przed stacją na każdej linii, A, B... od początku km, Z, Y... od końca, a odcinek od niego do stacji to bpd_ita; wyjazdowy na końcu każdego toru, z którego wyprawia się pociągi, F1, G2 w stronę początku, R1, P2 w stronę końca; tarcza manewrowa Tm1, Tm2... na końcu krótszego toru. Semafor, który już stoi w miejscu któregoś z nich, dostaje tylko jego nazwę, nic się nie dubluje. Modele semaforów te wybrane w narzędziu Sygnalizatory. Jeden krok cofania" + +msgid "Give the station its code first: the signals are named after it" +msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy semaforów" + +msgid "Set the track circuits first: the signals stand at their joints" +msgstr "Najpierw ustaw izolacje: semafory stają przy stykach" + +msgid "The signals are set already" +msgstr "Semafory są już ustawione" + +msgid "The signals couldn't be placed: %s" +msgstr "Nie udało się postawić semaforów: %s" + +msgid "no template of the kind %s" +msgstr "brak szablonu rodzaju %s" + +msgid "%zu signal placed" +msgid_plural "%zu signals placed" +msgstr[0] "Postawiono %zu semafor" +msgstr[1] "Postawiono %zu semafory" +msgstr[2] "Postawiono %zu semaforów" + +msgid "%zu renamed" +msgid_plural "%zu renamed" +msgstr[0] "%zu przemianowany" +msgstr[1] "%zu przemianowane" +msgstr[2] "%zu przemianowanych" +