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" +