diff --git a/application/editormode.h b/application/editormode.h index 9fd03efa..8cb25919 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1904,6 +1904,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri 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(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) override; + bool signal_heads(std::vector &Heads, bool const Wait) override; struct signal_template { @@ -1918,6 +1919,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri int parameters{0}; bool plate{false}; bool linked{false}; + std::vector speeds; + bool informs{false}; }; struct signal_spot { diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 596f8fea..7bccb392 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -264,6 +264,7 @@ bool editor_mode::signal_scan(bool const Wait) if (parameter.id == 7) item.linked = true; } + editor_includes::signal_speeds(entry.file, item.speeds, item.informs); tool.templates.push_back(std::move(item)); } std::sort(tool.templates.begin(), tool.templates.end(), [](signal_template const &A, signal_template const &B) { @@ -698,7 +699,7 @@ std::string editor_mode::signal_variant(signal_template const &Template, signal_ auto best{std::numeric_limits::max()}; for (auto const &other : m_signal.templates) { - if (other.kind != Template.kind || group_of(other.mount, other.lamps) != group || lean_of(other.lean) != wanted) + if (other.kind != Template.kind || group_of(other.mount, other.lamps) != group || lean_of(other.lean) != wanted || other.speeds != Template.speeds) continue; auto const distance{edit_distance(other.name, Template.name)}; if (distance < best) @@ -891,6 +892,27 @@ bool editor_mode::standing_signals(std::vector &Signals, bool c auto const *item{signal_template_of(standing.include.file)}; auto const linked{item != nullptr && item->linked && editor_includes::parse_directive(scene::Layers.directive(standing.include.instance), file, values) && values.size() > 6}; Signals.push_back({standing.include.instance, standing.name, kKinds[standing.kind], standing.include.location, standing.include.yaw, linked ? values[6] : std::string{}}); + auto &signal{Signals.back()}; + signal.file = standing.include.file; + signal.track = standing.track; + signal.facing = standing.track == nullptr ? 0 : standing.list % 3 == 2 ? 1 : standing.list % 3 == 1 ? -1 : 0; + } + return true; +} + +bool editor_mode::signal_heads(std::vector &Heads, bool const Wait) +{ + Heads.clear(); + signal_scan(Wait); + if (m_signal.templates.empty()) + return false; + for (int index = 0; index < static_cast(m_signal.templates.size()); ++index) + { + auto const &item{m_signal.templates[index]}; + if (item.lamps.empty()) + continue; + auto const kind{kind_index(item.kind)}; + Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, item.informs, kind >= 0 && m_signal.chosen[kind] == index}); } return true; } @@ -906,6 +928,8 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector { 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 (auto const *wanted{order.file.empty() ? nullptr : signal_template_of(order.file)}; wanted != nullptr && wanted->kind == order.kind) + item = wanted; if (item == nullptr) { Error = format(STR_C("no template of the kind %s"), STR(kKindLabels[std::max(0, kind)]).c_str()); @@ -980,6 +1004,50 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector set_include_directive(instance, directive); } } + for (auto const &[instance, wanted] : Changes.files) + { + auto const *standing{signal_standing_of(instance)}; + auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; + auto const *chosen{signal_template_of(wanted)}; + auto const before{scene::Layers.directive(instance)}; + std::string file; + std::vector values; + if (item == nullptr || chosen == nullptr || chosen->kind != item->kind || false == editor_includes::parse_directive(before, file, values) || values.empty()) + continue; + signal_spot spot; + auto const *variant{signal_standing_spot(*standing, spot) ? signal_template_of(signal_variant(*chosen, spot)) : chosen}; + if (variant == nullptr) + variant = chosen; + if (variant->file == item->file) + continue; + include_info had; + include_info info; + std::string missing; + editor_includes::load(item->file, had, missing); + editor_includes::load(variant->file, info, missing); + auto const role_of = [](include_info &Info, int const Id) { return Info.parameter(Id).role; }; + std::vector after(std::max(variant->parameters, 1)); + for (int id = 1; id <= static_cast(after.size()); ++id) + { + auto &value{after[id - 1]}; + auto const role{role_of(info, id)}; + for (int old = 1; old <= static_cast(values.size()) && value.empty(); ++old) + if (role != "free" && role != "text" && role_of(had, old) == role) + value = values[old - 1]; + if (id == 7 && variant->linked) + value = item->linked && values.size() > 6 ? values[6] : std::string{"none"}; + if (value.empty() && id == 6 && variant->plate) + value = plate_for(std::string{}, values[0].substr(values[0].rfind('_') + 1)); + if (value.empty()) + value = info.parameter(id).value.empty() ? std::string{"none"} : info.parameter(id).value; + } + auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; + auto const read_after{variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, after)}; + record(instance, before); + set_include_directive(instance, editor_includes::compose_directive(variant->file, after)); + if (false == read_before.empty() && false == read_after.empty() && ToLower(read_before) != ToLower(read_after)) + signal_renamed(read_before, read_after, step); + } std::vector instances; if (Error.empty() && false == items.empty()) { diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 45385c72..5ba0c5f4 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -52,6 +52,7 @@ double constexpr kRailHead{0.18}; // m, above the axis of a path double constexpr kLineLimit{30000.0}; // m, of a line followed out to the next station double constexpr kBlockShare{1.1}; // of the braking distance, the block sections towards the end of the tracks double constexpr kPredicted{305.0}; // m, out from the next station to its entry signal, while it has none +// the events of a block section, switching the block signal guarding it char const *const kControl{"sbl_odstep.inc"}; // m, of a train at the speed, as Ie-4 § 8 takes it: the least block section of the automatic block, a warning disc that far before @@ -59,7 +60,7 @@ char const *const kControl{"sbl_odstep.inc"}; double braking_distance(double const Speed) { return Speed <= 60.0 ? 400.0 : Speed <= 100.0 ? 700.0 : Speed <= 140.0 ? 1000.0 : 1300.0; -} // the events of a block section, switching the block signal guarding it +} ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; @@ -70,6 +71,9 @@ ImU32 constexpr kJoint{IM_COL32(255, 255, 255, 235)}; ImU32 constexpr kUseful{IM_COL32(120, 230, 255, 255)}; double constexpr kTrainMargin{25.0}; // m, of a useful length the train leaves: 10 to stop, 10 to see the signal, 5 to the joint, ST-T1-A6 double constexpr kUnequal{20.0}; // m, the useful lengths of the two ways differing more are shown: ST-T1-A6 wants them the same +ImU32 constexpr kUnfit{IM_COL32(255, 190, 40, 255)}; +double constexpr kFastest{250.0}; // km/h, a track set faster is taken for one without a limit +ImVec4 constexpr kWrong{1.0f, 0.42f, 0.36f, 1.0f}; bool is_switch(TTrack const &Track) { @@ -237,6 +241,224 @@ glm::dvec3 axis_point(station::layout const &Layout, double const Chainage) return glm::mix(Layout.axis[i - 1], Layout.axis[i], step > 1e-9 ? (Chainage - Layout.chainage[i - 1]) / step : 0.0); } +// km/h, a signal shows for the route at the speed: 40, 60 or 100 with a light strip, Ie-1; 0 for the highest the line allows +int shown_speed(double const Speed, double const Line) +{ + if (Speed <= 0.0 || Speed >= Line - 1.0) + return 0; + auto const shown{Speed >= 98.0 ? 100 : Speed >= 58.0 ? 60 : 40}; + return shown >= Line ? 0 : shown; +} + +// km/h, a signal slows the trains to with the next one at stop standing that far on, Ie-4 § 8 ust. 5; 140 with a W21 indicator +int shortened_speed(double const Length) +{ + return Length < 400.0 ? 40 : Length < 700.0 ? 60 : Length < 1000.0 ? 100 : 140; +} + +// what the lights of a signal must show for the routes of its trains +struct head_need +{ + std::vector speeds; // km/h, the lower ones + bool informs{false}; + bool white{false}; + int shortened{0}; + bool indicator{false}; +}; + +head_need need_of(std::vector const &Routes, bool const Main, double const Line, double const Braking) +{ + head_need need; + need.white = Main; + auto const add = [&](int const Speed) { + if (Speed > 0 && std::find(need.speeds.begin(), need.speeds.end(), Speed) == need.speeds.end()) + need.speeds.push_back(Speed); + }; + for (auto const &route : Routes) + { + auto const shown{Main ? shown_speed(route.speed, Line) : 0}; + add(shown); + auto const told{route.next >= 0 && route.warning < 0}; + need.informs = need.informs || told; + if (false == Main || false == told || route.length >= Braking) + continue; + auto const slower{shortened_speed(route.length)}; + if (slower >= (shown > 0 ? shown : Line)) + continue; + if (slower > 100) + need.indicator = true; + else + { + add(slower); + need.shortened = need.shortened == 0 ? slower : std::min(need.shortened, slower); + } + } + std::sort(need.speeds.begin(), need.speeds.end()); + return need; +} + +bool has_speed(std::vector const &Speeds, int const Speed) +{ + return std::find(Speeds.begin(), Speeds.end(), Speed) != Speeds.end(); +} + +// the template has the lights the need takes: a lower orange one for a lower speed, a strip for 60 and 100, Ie-1 § 7, two orange +// ones to show a lower speed with the next signal at stop +bool shows(signal_head const &Head, head_need const &Need) +{ + auto const &lamps{Head.lamps}; + auto const count = [&](char const Lamp) { return std::count(lamps.begin(), lamps.end(), Lamp); }; + if (count('z') == 0 || count('c') == 0 || (Need.white && count('b') == 0)) + return false; + if (Need.informs && (false == Head.informs || count('p') == 0)) + return false; + if (Need.speeds.empty()) + return true; + if (lamps.find('p', lamps.find('c')) == std::string::npos || (Need.informs && count('p') < 2)) + return false; + return std::all_of(Need.speeds.begin(), Need.speeds.end(), [&](int const Speed) { return has_speed(Head.speeds, Speed); }); +} + +// the more lights and strips beyond the need, the more +int surplus(signal_head const &Head, head_need const &Need) +{ + auto result{static_cast(Head.lamps.size()) * 4}; + for (auto const speed : {60, 100}) + if (has_speed(Head.speeds, speed) && false == has_speed(Need.speeds, speed)) + ++result; + return result; +} + +// of the templates of the kind and the mount that fit the need, the one with the least beyond it, the most like the one named; +// -1 for none +int best_head(std::vector const &Heads, std::string const &Kind, std::string const &Mount, std::string const &Like, head_need const &Need) +{ + auto best{-1}; + std::tuple score; + for (int index = 0; index < static_cast(Heads.size()); ++index) + { + auto const &head{Heads[index]}; + if (head.kind != Kind || head.mount != Mount || false == shows(head, Need)) + continue; + std::size_t common{0}; + while (common < head.name.size() && common < Like.size() && head.name[common] == Like[common]) + ++common; + std::tuple const candidate{surplus(head, Need), -static_cast(common), head.name.size()}; + if (best < 0 || candidate < score) + { + best = index; + score = candidate; + } + } + return best; +} + +// the lights of the need in words: green, two orange, red, white; 40, 60 km/h with an orange strip +std::string need_text(head_need const &Need) +{ + std::string text{STR("green")}; + if (Need.informs && false == Need.speeds.empty()) + text += ", " + STR("two orange"); + else if (Need.informs || false == Need.speeds.empty()) + text += ", " + STR("orange"); + text += ", " + STR("red"); + if (Need.white) + text += ", " + STR("white"); + if (Need.speeds.empty()) + return text; + std::string speeds; + for (auto const speed : Need.speeds) + speeds += (speeds.empty() ? "" : ", ") + std::to_string(speed); + text += "; " + format(STR_C("%s km/h"), speeds.c_str()); + if (has_speed(Need.speeds, 60)) + text += ", " + STR("an orange strip"); + if (has_speed(Need.speeds, 100)) + text += ", " + STR("a green strip"); + return text; +} + +ImU32 lamp_colour(char const Lamp) +{ + switch (Lamp) + { + case 'z': return IM_COL32(40, 210, 80, 255); + case 'c': return IM_COL32(230, 40, 40, 255); + case 'p': return IM_COL32(255, 160, 30, 255); + case 'b': return IM_COL32(235, 235, 235, 255); + case 'n': return IM_COL32(60, 110, 255, 255); + default: return IM_COL32(70, 70, 70, 255); + } +} + +// the lights of a template in a row, from its top on the left, its strips after them +void lamp_row(std::string const &Lamps, std::vector const &Speeds) +{ + auto const height{ImGui::GetTextLineHeight()}; + auto const radius{height * 0.3f}; + auto const origin{ImGui::GetCursorScreenPos()}; + auto *draw{ImGui::GetWindowDrawList()}; + auto x{origin.x + radius}; + for (auto const lamp : Lamps) + { + draw->AddCircleFilled(ImVec2(x, origin.y + height * 0.5f), radius, lamp_colour(lamp)); + x += radius * 2.6f; + } + for (auto const &[speed, colour] : {std::make_pair(60, lamp_colour('p')), std::make_pair(100, lamp_colour('z'))}) + if (has_speed(Speeds, speed)) + { + draw->AddRectFilled(ImVec2(x, origin.y + height * 0.38f), ImVec2(x + radius * 3.0f, origin.y + height * 0.62f), colour, 1.0f); + x += radius * 3.6f; + } + ImGui::Dummy(ImVec2(std::max(1.0f, x - origin.x), height)); +} + +// m, the routes of the trains are followed from a signal at most, beyond it the next one needs no telling of +double route_reach(double const Braking) +{ + return std::max(3000.0, 3.0 * Braking); +} + +// a standing signal by the track its trains read it on, no track for a signal of another kind or by none +station::post post_of(standing_signal const &Signal) +{ + using role = station::signal::role; + station::post post; + if (Signal.kind == "main") + post.kind = role::entry; + else if (Signal.kind == "block") + post.kind = role::block; + else if (Signal.kind == "warning") + post.kind = role::warning; + else if (Signal.kind == "shunt") + post.kind = role::shunting; + else + return post; + post.point = Signal.position; + if (Signal.track != nullptr && Signal.facing != 0 && false == Signal.track->m_editorremoved) + { + post.track = Signal.track; + post.facing = Signal.facing; + return post; + } + int path{0}; + post.track = kilometrage::track_near(Signal.position, 4.5, path); + if (post.track == nullptr) + return post; + geometry::bezier const curve{post.track->m_paths[path]}; + auto parameter{0.0}; + auto best{std::numeric_limits::max()}; + for (int k = 0; k <= 64; ++k) + if (auto const distance{geometry::plan_distance(curve.point(k / 64.0), Signal.position)}; distance < best) + { + best = distance; + parameter = k / 64.0; + } + auto const tangent{curve.first(parameter)}; + auto const travel{travel_of(Signal.yaw)}; + post.facing = travel.x * tangent.x + travel.y * tangent.z >= 0.0 ? 1 : -1; + return post; +} + } // namespace void srk_tool::load() @@ -265,6 +487,8 @@ void srk_tool::load() words >> entry.km; else if (word == "from") entry.reversed = (words >> word) && word == "right"; + else if (word == "braking") + words >> entry.braking; else if (word == "anchor") words >> entry.anchor; else if (word == "name") @@ -301,7 +525,8 @@ void srk_tool::store() continue; if (lines.empty()) lines.emplace_back("# stations described in the track editor, the simulation skips these lines"); - lines.push_back(format("station %s line %s km %s from %s anchor %s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), entry.name.c_str())); + auto const braking{entry.braking > 0 ? format(" braking %d", entry.braking) : std::string{}}; + lines.push_back(format("station %s line %s km %s from %s anchor %s%s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), braking.c_str(), entry.name.c_str())); } scene::Layers.mark(layer, kMark, std::move(lines)); } @@ -539,9 +764,20 @@ void srk_tool::draw_overlay() const ImVec2 screen; if (false == near_enough(mark.head, kLabelRange) || false == projection.project(mark.head, screen)) continue; - auto const size{ImGui::CalcTextSize(mark.text.c_str())}; - drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x * 0.5f + 4.0f, screen.y), kSignal, 3.0f); - drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), IM_COL32(255, 255, 255, 255), mark.text.c_str()); + auto const text{mark.unfit ? mark.text + " !" : mark.text}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x * 0.5f + 4.0f, screen.y), mark.unfit ? kUnfit : kSignal, 3.0f); + drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), mark.unfit ? IM_COL32(0, 0, 0, 255) : IM_COL32(255, 255, 255, 255), text.c_str()); + } + if (m_head_hover >= 0 && m_head_hover < static_cast(m_heads.size())) + { + auto const &head{m_heads[m_head_hover]}; + ImVec2 foot, top; + if (projection.project(head.position, foot) && projection.project(head.position + glm::dvec3{0.0, 6.0, 0.0}, top)) + { + drawlist->AddLine(foot, top, kHovered, 3.0f); + drawlist->AddCircle(top, 9.0f, kHovered, 0, 3.0f); + } } } @@ -695,6 +931,10 @@ void srk_tool::render_options() if (ImGui::CollapsingHeader(format("%s (%zu)###srklengths", STR_C("Track lengths"), m_lengths.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) render_lengths(); ImGui::Spacing(); + m_head_hover = -1; + if (ImGui::CollapsingHeader(format("%s (%zu)###srkheads", STR_C("Signal heads"), static_cast(std::count_if(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return Head.station; }))).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + render_heads(); + ImGui::Spacing(); if (ImGui::CollapsingHeader(format("%s (%zu)###srkswitches", STR_C("Switches"), layout.switches.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginTable("##srkswitchlist", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (layout.switches.size() + 1.5f))))) @@ -847,6 +1087,186 @@ void srk_tool::render_lengths() dimmed(STR_C("The useful length: where a train stands, from the signal it stops by back to the joint, the fouling point or the level crossing behind it, between those where no signal stands (dimmed). The longest train leaves 25 m of it: 10 m to stop, 10 m to see the signal, 5 m to the joint")); } +void srk_tool::render_heads() +{ + auto const dimmed = [](char const *Text) { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", Text); + ImGui::PopStyleColor(); + }; + if (begin_fields("##srkbraking")) + { + field_row("Line speed"); + auto const set{set_speed_of_open()}; + if (set > 0.0) + ImGui::TextWrapped("%s", format(STR_C("%.0f km/h, the highest set on its main tracks"), set).c_str()); + else + ImGui::TextColored(kWrong, "%s", format(STR_C("%.0f km/h taken: no track of the station has a speed set"), speed_of_open()).c_str()); + item_tooltip("The speed of the trains through the station, the highest one its main tracks and the lines out of it are set to in the Track tool, up to 250 km/h: the braking distance and the lights of the signals go by it. Without one 160 km/h is taken, or the speed of the braking distance set by hand"); + field_row("Braking distance"); + auto const by_hand{m_record >= 0 ? m_records[m_record].braking : 0}; + auto const after{format(STR_C("%.0f m, after the speed"), braking_distance(set > 0.0 ? set : 160.0))}; + auto changed{false}; + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::BeginCombo("##srkbrakingset", by_hand > 0 ? format(STR_C("%d m, set by hand"), by_hand).c_str() : after.c_str())) + { + if (ImGui::Selectable(after.c_str(), by_hand == 0)) + { + record_of_open().braking = 0; + changed = true; + } + for (auto const distance : {400, 700, 1000, 1300}) + if (ImGui::Selectable(format(STR_C("%d m, set by hand"), distance).c_str(), by_hand == distance)) + { + record_of_open().braking = distance; + changed = true; + } + ImGui::EndCombo(); + } + item_tooltip("The braking distance of the trains on the line, Ie-4 § 8: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. The block sections and the warning discs stand no nearer, a signal telling of the next one standing nearer slows the trains. Set it by hand where the line has another one"); + ImGui::EndTable(); + if (changed) + { + m_changed = true; + survey_signals(); + } + } + if (false == m_heads_known) + { + dimmed(STR_C("Looking for the signal templates...")); + return; + } + if (std::none_of(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return Head.station; })) + { + dimmed(STR_C("No main signal stands by the station yet: its lights are checked once one does")); + if (ImGui::Button((std::string{STR_C("Set the signals")} + "##srkheads").c_str(), ImVec2(-1.0f, 0.0f))) + set_signals(); + return; + } + auto const line{speed_of_open()}; + std::vector rows; + std::size_t fine{0}; + for (std::size_t index = 0; index < m_heads.size(); ++index) + { + auto const &head{m_heads[index]}; + if (head.station || false == head.fits || false == head.problems.empty()) + rows.push_back(index); + else + ++fine; + } + if (ImGui::BeginTable("##srkheadlist", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (rows.size() + 1.5f))))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Signal"), ImGuiTableColumnFlags_WidthStretch, 0.25f); + ImGui::TableSetupColumn(STR_C("Speeds"), ImGuiTableColumnFlags_WidthStretch, 0.23f); + ImGui::TableSetupColumn(STR_C("Lights"), ImGuiTableColumnFlags_WidthStretch, 0.25f); + ImGui::TableSetupColumn(STR_C("State"), ImGuiTableColumnFlags_WidthStretch, 0.27f); + ImGui::TableHeadersRow(); + for (auto const index : rows) + { + auto const &head{m_heads[index]}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable((head.name + "##srkhead" + std::to_string(index)).c_str(), static_cast(index) == m_head_hover, ImGuiSelectableFlags_SpanAllColumns)) + m_view.fly_to(head.position + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{head.travel.x, -0.3, head.travel.y}), 30.0); + if (ImGui::IsItemHovered()) + { + m_head_hover = static_cast(index); + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 32.0f); + auto const *kind{head.kind == "main" ? STR_C("Main signal %s") : head.kind == "block" ? STR_C("Block signal %s") : STR_C("Warning disc %s")}; + ImGui::TextUnformatted(format(kind, head.name.c_str()).c_str()); + ImGui::TextDisabled("%s", head.file.c_str()); + for (std::size_t k = 0; k < head.routes.size(); ++k) + { + auto const &route{head.routes[k]}; + std::string text; + if (route.next >= 0) + text = format(STR_C("To %s: %.0f m"), head.nexts[k].c_str(), route.length); + else if (route.ended) + text = format(STR_C("To the end of the tracks: %.0f m"), route.length); + else + text = format(STR_C("No signal within %.0f m"), route.length); + if (head.kind == "main") + text += ", " + (route.speed > 0.0 && shown_speed(route.speed, line) > 0 ? format(STR_C("%d km/h through the switches"), shown_speed(route.speed, line)) : format(STR_C("%.0f km/h"), line)); + std::string turnouts; + for (auto const *turnout : route.turnouts) + turnouts += (turnouts.empty() ? "" : ", ") + without_stem(turnout->name(), stem_of_open()); + if (false == turnouts.empty()) + text += ", " + format(STR_C("turning at %s"), turnouts.c_str()); + if (route.warning >= 0) + text += ", " + STR("past a warning disc"); + ImGui::BulletText("%s", text.c_str()); + } + if (head.kind != "warning") + { + head_need need; + need.speeds = head.speeds; + need.informs = head.informs; + need.white = head.white; + ImGui::TextUnformatted(format(STR_C("Needs: %s"), need_text(need).c_str()).c_str()); + } + if (head.shortened > 0) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", format(STR_C("The next signal stands nearer than the braking distance: with it at stop this one slows the trains to %d km/h, Ie-4 § 8 ust. 5"), head.shortened).c_str()); + if (head.indicator) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("Give it a W21 indicator \"14\": 140 km/h with the next signal at stop, Ie-4 § 8 ust. 5")); + if (head.lamps.empty() && head.kind != "warning") + ImGui::TextDisabled("%s", STR_C("A semaphore with arms: its lights aren't checked")); + else if (false == head.fits && head.fit.empty()) + ImGui::TextColored(kWrong, "%s", STR_C("No template of its kind and mount has these lights")); + else if (false == head.fits) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", format(STR_C("Its lights don't show all of it: %s does"), head.fit.c_str()).c_str()); + else if (head.more) + ImGui::TextDisabled("%s", format(STR_C("It has more lights than it needs: %s will do"), head.fit.c_str()).c_str()); + for (auto const &problem : head.problems) + ImGui::TextColored(kWrong, "%s", problem.c_str()); + ImGui::TextDisabled("%s", STR_C("LMB: fly to it")); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + ImGui::TableNextColumn(); + if (head.kind == "main") + { + auto text{format("%.0f", line)}; + for (auto speed{head.speeds.rbegin()}; speed != head.speeds.rend(); ++speed) + text += ", " + std::to_string(*speed); + ImGui::TextUnformatted(text.c_str()); + } + else + ImGui::TextDisabled("-"); + ImGui::TableNextColumn(); + if (head.lamps.empty()) + ImGui::TextDisabled("%s", head.kind == "warning" ? STR_C("disc") : STR_C("arms")); + else + lamp_row(head.lamps, head.own); + ImGui::TableNextColumn(); + if (head.lamps.empty()) + ImGui::TextDisabled("-"); + else if (false == head.fits) + ImGui::TextColored(head.fit.empty() ? kWrong : ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", head.fit.empty() ? STR_C("no template") : STR_C("lights missing")); + else if (head.more) + ImGui::TextDisabled("%s", STR_C("more than needed")); + else + ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "%s", STR_C("fits")); + if (false == head.problems.empty()) + { + ImGui::SameLine(); + ImGui::TextColored(kWrong, "%s", STR_C("too near")); + } + } + ImGui::EndTable(); + } + if (fine > 0) + ImGui::TextDisabled("%s", format(STR_P("%zu signal of the lines fits", "%zu signals of the lines fit", fine), fine).c_str()); + auto const fitting{static_cast(std::count_if(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return false == Head.fit.empty() && Head.fit != Head.file && (false == Head.fits || Head.more); }))}; + ImGui::BeginDisabled(fitting == 0); + if (main_button(format("%s (%zu)###srkfitheads", STR_C("Fit the lights"), fitting).c_str(), ImVec2(-1.0f, 0.0f))) + fit_heads(); + ImGui::EndDisabled(); + item_tooltip("Gives each signal marked the template of its kind and mount with the lights its routes need and the least beyond them, the name, the plate and the place kept, the trains reading it as before. One step of undo"); + dimmed(STR_C("The lights go by the routes of the trains: 40 km/h through the switches 1:9, 60 through 1:12 and 1:14, 100 through 1:18.5, by their radii, for these a lower orange light, with an orange strip for 60, a green one for 100, Ie-1; an upper orange one where the next signal can show stop; the white one of the substitute signal. A signal telling of the next one standing nearer than the braking distance slows the trains with it at stop, Ie-4 § 8 ust. 5")); +} + std::string srk_tool::lengths_table() const { auto const stem{stem_of_open()}; @@ -1408,12 +1828,53 @@ void srk_tool::set_signals() m_status = STR_C("The signals are set already"); return; } + // the lights of the main signals placed after the routes of their trains, to the signals standing and to these + std::vector unfit; + std::vector heads; + m_signals.signal_heads(heads, true); + auto const chosen{std::find_if(heads.begin(), heads.end(), [](signal_head const &Head) { return Head.kind == "main" && Head.chosen; })}; + if (chosen != heads.end()) + { + std::vector everywhere; + m_signals.standing_signals(everywhere, true); + std::vector posts; + for (auto const &signal : everywhere) + if (auto const post{post_of(signal)}; post.track != nullptr) + posts.push_back(post); + std::vector from; + for (auto const &order : placed) + { + if (order.kind == "main") + from.push_back(posts.size()); + posts.push_back({order.track, order.facing, order.point, order.kind == "shunt" ? station::signal::role::shunting : station::signal::role::entry}); + } + auto const found{station::routes(posts, from, route_reach(braking_of_open()))}; + auto const line{speed_of_open()}; + std::size_t next{0}; + for (auto &order : placed) + { + if (order.kind != "main") + continue; + auto const need{need_of(found[next++], true, line, braking_of_open())}; + if (auto const best{best_head(heads, "main", chosen->mount, chosen->name, need)}; best >= 0) + order.file = heads[best].file; + else + unfit.push_back(signal_shown(order.name, stem)); + } + } std::string error; - auto const count{m_signals.signal_batch({placed, renamed, {}, {}}, std::move(before), std::move(created), 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()); + if (false == unfit.empty()) + { + std::string names; + for (auto const &name : unfit) + names += (names.empty() ? "" : ", ") + name; + m_status += "\n" + format(STR_C("No template has the lights %s need, they got the one chosen in the Signals tool: see Signal heads"), names.c_str()); + } } void srk_tool::set_lines() @@ -1523,13 +1984,15 @@ void srk_tool::set_lines() return (growth > 0) != lines[Index].inbound; return (km_on(Index, 1000.0) > km_on(Index, 0.0)) != lines[Index].inbound; }; - // the braking distance at the highest speed out along the line, at 160 km/h while its tracks have none + // the braking distance at the highest speed out along the line, at 160 km/h while its tracks have none, unless one is set by hand auto const braking = [&](std::size_t const Index) { + if (m_record >= 0 && m_records[m_record].braking > 0) + return static_cast(m_records[m_record].braking); auto speed{0.0}; auto const &line{lines[Index]}; for (std::size_t k = 0; k < line.points.size(); ++k) - if (line.distances[k] >= 0.0) - speed = std::max(speed, line.tracks[line.owners[k]]->VelocityGet()); + if (auto const set{line.tracks[line.owners[k]]->VelocityGet()}; line.distances[k] >= 0.0 && set <= kFastest) + speed = std::max(speed, set); return braking_distance(speed > 0.0 ? speed : 160.0); }; for (std::size_t index = 0; index < lines.size(); ++index) @@ -1914,10 +2377,13 @@ void srk_tool::survey_signals() m_signals_known = m_layout.has_value() && m_signals.standing_signals(standing, false); if (false == m_signals_known) { + m_heads.clear(); + m_heads_known = false; if (false == m_measured) measure({}); return; } + check_heads(standing); auto const stem{stem_of_open()}; std::vector stops; for (auto const &signal : standing) @@ -1927,7 +2393,8 @@ void srk_tool::survey_signals() 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), signal.position + glm::dvec3{0.0, signal.kind == "shunt" ? 3.0 : 7.0, 0.0}}); + auto const unfit{std::any_of(m_heads.begin(), m_heads.end(), [&](head_check const &Head) { return Head.instance == signal.instance && (false == Head.fits || false == Head.problems.empty()); })}; + m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem), signal.position + glm::dvec3{0.0, signal.kind == "shunt" ? 3.0 : 7.0, 0.0}, unfit}); if (signal.kind != "main" && signal.kind != "shunt") continue; station::signal stop; @@ -1954,6 +2421,211 @@ void srk_tool::measure(std::vector const &Signals) }); } +double srk_tool::set_speed_of_open() const +{ + if (false == m_layout.has_value()) + return 0.0; + auto const highest = [](auto const &Tracks) { + auto speed{0.0}; + for (auto *track : Tracks) + if (auto const set{track->VelocityGet()}; track->eType == tt_Normal && set <= kFastest) + speed = std::max(speed, set); + return speed; + }; + std::vector through{m_layout->main}; + through.insert(through.end(), m_layout->second.begin(), m_layout->second.end()); + for (auto const &path : m_layout->paths) + if (path.approach) + through.push_back(path.track); + if (auto const speed{highest(through)}; speed > 0.0) + return speed; + std::vector all; + for (auto const &path : m_layout->paths) + all.push_back(path.track); + return highest(all); +} + +double srk_tool::speed_of_open() const +{ + if (auto const speed{set_speed_of_open()}; speed > 0.0) + return speed; + auto const set{m_record >= 0 ? m_records[m_record].braking : 0}; + return set <= 0 ? 160.0 : set <= 400 ? 60.0 : set <= 700 ? 100.0 : set <= 1000 ? 140.0 : 160.0; +} + +double srk_tool::braking_of_open() const +{ + auto const set{m_record >= 0 ? m_records[m_record].braking : 0}; + return set > 0 ? static_cast(set) : braking_distance(speed_of_open()); +} + +void srk_tool::check_heads(std::vector const &Standing) +{ + using role = station::signal::role; + m_heads.clear(); + std::vector heads; + m_heads_known = m_layout.has_value() && m_signals.signal_heads(heads, false); + if (false == m_heads_known) + return; + auto const &layout{*m_layout}; + auto const stem{stem_of_open()}; + auto const line{speed_of_open()}; + auto const braking{braking_of_open()}; + auto const middle{axis_point(layout, (layout.from + layout.to) * 0.5)}; + std::vector posts; + std::vector owners; + for (std::size_t k = 0; k < Standing.size(); ++k) + if (geometry::plan_distance(Standing[k].position, middle) <= kLineLimit) + if (auto const post{post_of(Standing[k])}; post.track != nullptr) + { + posts.push_back(post); + owners.push_back(k); + } + // the main signals of the station, the block signals and the warning discs of the lines in and out of it + std::vector from; + std::vector own(posts.size(), 0); + for (std::size_t index = 0; index < posts.size(); ++index) + { + if (posts[index].kind == role::entry && gap_to(layout, posts[index].point) <= 8.0) + own[index] = 1; + if (own[index] || posts[index].kind == role::block || posts[index].kind == role::warning) + from.push_back(index); + } + auto const found{station::routes(posts, from, route_reach(braking))}; + std::vector routes_of(posts.size(), -1); + for (std::size_t k = 0; k < from.size(); ++k) + routes_of[from[k]] = static_cast(k); + std::vector checked(own); + std::vector order; + for (std::size_t index = 0; index < posts.size(); ++index) + if (own[index]) + order.push_back(index); + for (std::size_t at = 0; at < order.size(); ++at) + for (auto const &route : found[routes_of[order[at]]]) + for (auto const index : {route.warning, route.next}) + if (index >= 0 && 0 == checked[index] && posts[index].kind != role::entry && routes_of[index] >= 0) + { + checked[index] = 1; + order.push_back(static_cast(index)); + } + for (auto grown{true}; grown;) + { + grown = false; + for (std::size_t index = 0; index < posts.size(); ++index) + { + if (checked[index] || routes_of[index] < 0 || posts[index].kind == role::entry) + continue; + auto const &routes{found[routes_of[index]]}; + if (std::none_of(routes.begin(), routes.end(), [&](station::route const &Route) { return Route.next >= 0 && checked[Route.next]; })) + continue; + checked[index] = 1; + order.push_back(index); + grown = true; + } + } + std::unordered_map by_file; + for (std::size_t index = 0; index < heads.size(); ++index) + by_file.emplace(heads[index].file, index); + for (auto const index : order) + { + auto const &signal{Standing[owners[index]]}; + auto const &post{posts[index]}; + head_check check; + check.instance = signal.instance; + check.name = signal_shown(signal.name, stem); + check.kind = signal.kind; + check.position = signal.position; + check.travel = travel_of(signal.yaw); + check.file = signal.file; + check.station = own[index] != 0; + check.routes = found[routes_of[index]]; + for (auto const &route : check.routes) + check.nexts.push_back(route.next >= 0 ? signal_shown(Standing[owners[route.next]].name, stem) : std::string{}); + auto const known{by_file.find(signal.file)}; + auto const *template_of{known != by_file.end() ? &heads[known->second] : nullptr}; + if (template_of != nullptr) + { + check.lamps = template_of->lamps; + check.own = template_of->speeds; + } + if (post.kind == role::warning) + { + for (std::size_t k = 0; k < check.routes.size(); ++k) + { + auto const &route{check.routes[k]}; + if (route.next < 0) + check.problems.push_back(format(STR_C("No main signal within %.0f m ahead of it to tell of"), route.length)); + else if (route.length < braking - 1.0) + check.problems.push_back(format(STR_C("It stands %.0f m before %s, less than the braking distance of %.0f m: the trains don't stop by the signal in time"), route.length, check.nexts[k].c_str(), braking)); + } + m_heads.push_back(std::move(check)); + continue; + } + auto const main{post.kind == role::entry}; + auto const need{need_of(check.routes, main, line, braking)}; + check.speeds = need.speeds; + check.informs = need.informs; + check.white = need.white; + check.shortened = need.shortened; + check.indicator = need.indicator; + if (false == main) + for (std::size_t k = 0; k < check.routes.size(); ++k) + { + auto const &route{check.routes[k]}; + if (route.next >= 0 && route.warning < 0 && route.length < braking - 1.0) + check.problems.push_back(format(STR_C("The block section to %s is %.0f m long, shorter than the braking distance of %.0f m, Ie-4 § 8 ust. 1"), check.nexts[k].c_str(), route.length, braking)); + } + if (template_of != nullptr) + { + check.fits = shows(*template_of, need); + auto const best{best_head(heads, signal.kind, template_of->mount, template_of->name, need)}; + if (best >= 0) + { + check.fit = heads[best].file; + check.more = check.fits && surplus(heads[best], need) < surplus(*template_of, need); + } + } + m_heads.push_back(std::move(check)); + } + std::stable_sort(m_heads.begin(), m_heads.end(), [&](head_check const &A, head_check const &B) { + if (A.station != B.station) + return A.station; + if (A.station) + return station::project(layout, A.position).x < station::project(layout, B.position).x; + return geometry::plan_distance(A.position, middle) < geometry::plan_distance(B.position, middle); + }); +} + +void srk_tool::fit_heads() +{ + m_status.clear(); + signal_changes changes; + for (auto const &head : m_heads) + if (false == head.fit.empty() && head.fit != head.file && (false == head.fits || head.more)) + changes.files.emplace_back(head.instance, head.fit); + if (changes.files.empty()) + { + m_status = std::any_of(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return false == Head.fits; }) ? STR_C("No template has the lights the signals marked need") : STR_C("The lights of the signals fit already"); + return; + } + std::string error; + m_signals.signal_batch(changes, {}, {}, error); + if (false == error.empty()) + { + m_status = format(STR_C("The signals couldn't be changed: %s"), error.c_str()); + return; + } + std::vector standing; + m_signals.standing_signals(standing, true); + std::size_t changed{0}; + for (auto const &head : m_heads) + changed += static_cast(std::count_if(standing.begin(), standing.end(), [&](standing_signal const &Signal) { return Signal.instance == head.instance && Signal.file != head.file; })); + if (changed == 0) + m_status = STR_C("The lights of the signals fit already"); + else + m_status = format(STR_P("%zu signal got the lights it needs, Ctrl+Z takes it back", "%zu signals got the lights they need, Ctrl+Z takes it back", changed), changed); +} + void srk_tool::survey() { m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); @@ -2259,8 +2931,9 @@ void srk_tool::render_canvas(float const Height) { 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)}); + auto const colour{mark.unfit ? kUnfit : kSignal}; + 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), colour); + label(mark.unfit ? mark.text + " !" : mark.text, colour, {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) @@ -2545,6 +3218,58 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri } return editor_selftest_result::handled; } + if (Command == "srkheads") + { + // srkheads [braking=] [fit=1]: the lights of the signals of the open station checked, fitted + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkheads: no station open"); + return editor_selftest_result::handled; + } + auto const values{selftest_values(Words)}; + if (auto const braking{values.find("braking")}; braking != values.end()) + { + record_of_open().braking = std::atoi(braking->second.c_str()); + m_changed = true; + } + std::vector standing; + m_signals.standing_signals(standing, true); + std::vector heads; + m_signals.signal_heads(heads, true); + refresh(); + if (auto const fit{values.find("fit")}; fit != values.end() && fit->second == "1" && m_layout.has_value()) + { + fit_heads(); + WriteLog("SELFTEST srkheads fit: " + m_status); + refresh(); + } + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkheads: the station is gone"); + return editor_selftest_result::handled; + } + WriteLog(format("SELFTEST srkheads: speed %.0f (set %.0f), braking %.0f, %zu checked, templates %zu", speed_of_open(), set_speed_of_open(), braking_of_open(), m_heads.size(), heads.size())); + auto const list = [](std::vector const &Speeds) { + std::string text; + for (auto const speed : Speeds) + text += (text.empty() ? "" : "/") + std::to_string(speed); + return text.empty() ? std::string{"-"} : text; + }; + for (auto const &head : m_heads) + { + std::string routes; + for (std::size_t k = 0; k < head.routes.size(); ++k) + { + auto const &route{head.routes[k]}; + routes += format(" %s:%.0f:%.0f%s%s", route.next >= 0 ? head.nexts[k].c_str() : route.ended ? "end" : "none", route.length, route.speed, route.warning >= 0 ? ":to" : "", route.turnouts.empty() ? "" : format(":%zut", route.turnouts.size()).c_str()); + } + WriteLog(format("SELFTEST srkheads %s %s%s %s lamps=%s own=%s need=%s%s%s short=%d%s fits=%d more=%d fit=%s routes%s problems=%zu", head.name.c_str(), head.kind.c_str(), head.station ? "" : "(line)", head.file.c_str(), head.lamps.c_str(), list(head.own).c_str(), list(head.speeds).c_str(), head.informs ? "+inform" : "", + head.white ? "+white" : "", head.shortened, head.indicator ? " w21" : "", head.fits ? 1 : 0, head.more ? 1 : 0, head.fit.empty() ? "-" : head.fit.c_str(), routes.c_str(), head.problems.size())); + for (auto const &problem : head.problems) + WriteLog("SELFTEST srkheads " + problem); + } + return editor_selftest_result::handled; + } if (Command == "srkstrip") { // srkstrip shown=0: the schematic hidden, the 3d view whole for a screenshot diff --git a/application/editormodesrk.h b/application/editormodesrk.h index d5c79e34..61a4b890 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -56,6 +56,7 @@ class srk_tool final : public editor_tool std::string line; // the number of the railway line std::string km; // of the axis of the station along the line, empty while unknown bool reversed{false}; // the km rise to the left of the schematic, the switches numbered from there + int braking{0}; // m, the braking distance of the line set by hand, 0: after its speed }; // the schematic: the chainage across, the offsets upwards, exaggerated struct view @@ -104,6 +105,7 @@ class srk_tool final : public editor_tool bool ahead{true}; // the trains it faces run along the chainage std::string text; glm::dvec3 head{0.0}; // above its lamps, in the scenery + bool unfit{false}; // its lights don't show what its routes need }; std::vector m_signal_marks; bool m_signals_known{false}; // the signals standing surveyed, once their templates are found @@ -113,6 +115,33 @@ class srk_tool final : public editor_tool int m_length_hover{-1}; // of the lengths, the row under the mouse int m_length_canvas{-1}; // of the lengths, of the track under the mouse in the schematic int m_length_shown{-1}; // of the lengths, the one drawn: of the row or the track under the mouse, else of the track selected + // a signal of the station or of its lines, its lights as the routes of its trains need them: Ie-1, Ie-4 § 8 + struct head_check + { + scene::instance_handle instance{0}; + std::string name; // shown, without the code of the station + std::string kind; + glm::dvec3 position{0.0}; + glm::dvec2 travel{0.0, 1.0}; + std::string file; // of its template + std::string lamps; + std::vector own; // the speeds of its template + bool station{false}; // stands by the station, not out on a line + std::vector routes; + std::vector nexts; // of the routes, the signals they end at, shown + std::vector speeds; // km/h, the lower ones it must show: 40, 60, 100 + bool informs{false}; // it must tell the next signal shows stop + bool white{false}; // it must give the substitute signal + int shortened{0}; // km/h, it slows the trains to, the next signal nearer than the braking distance: Ie-4 § 8 ust. 5 + bool indicator{false}; // a W21 "14" beside it, for 140 km/h so + bool fits{true}; // its template shows all of it + bool more{false}; // its template has more than it needs + std::string fit; // the template that fits best, empty for none + std::vector problems; // of its place + }; + std::vector m_heads; + bool m_heads_known{false}; // the templates found + int m_head_hover{-1}; // of the heads, the row under the mouse std::string m_status; void load(); @@ -137,6 +166,17 @@ class srk_tool final : public editor_tool void survey_signals(); // the lengths of the station tracks, the signals given by them void measure(std::vector const &Signals); + // km/h, the highest of the speeds the tracks of the open station are set to, 0 while none is + double set_speed_of_open() const; + // km/h, the trains run the station at: the highest set on its tracks, else after the braking distance set by hand, else 160 + double speed_of_open() const; + // m, the braking distance of the line of the open station: set by hand, else after its speed, as Ie-4 § 8 gives it + double braking_of_open() const; + // the lights of the signals checked against the routes of their trains and the places of the signals they tell of + void check_heads(std::vector const &Standing); + void render_heads(); + // the signals given the templates whose lights fit their routes; one step of the history + void fit_heads(); // the lengths of the station tracks as the list of the tracks gives them, the useful ones each way void render_lengths(); // the lengths as a table to paste into a spreadsheet, a line a track, the columns parted by tabs diff --git a/application/editortool.h b/application/editortool.h index 267350ed..6dbecc6a 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -85,6 +85,7 @@ struct signal_order std::string name; std::string plate; // the name on the plate under the head, when the template has one std::string linked{"none"}; // the signal before it, when the template takes one: a block signal shows its aspects by it + std::string file; // the template, of its kind; empty for the one chosen for the kind }; struct signal_rename { @@ -101,6 +102,21 @@ struct standing_signal glm::dvec3 position{0.0}; double yaw{0.0}; std::string linked; // of a template that takes one, else empty + std::string file; // of its template + TTrack *track{nullptr}; // the trains read it on, nullptr while none does + int facing{0}; // the trains it faces run 1: towards the end of the path of the track, -1: towards its start, 0 unknown +}; +// a signal template as the signalling picks one: the lights it has and the speeds its events show +struct signal_head +{ + std::string file; + std::string name; + std::string kind; + std::string mount; + std::string lamps; // its chambers from the top: z green, c red, p orange, b white, n blue, x blind + std::vector speeds; // of the signal itself, km/h: -1 the highest allowed, 20 the substitute signal + bool informs{false}; // it tells the next signal shows stop + bool chosen{false}; // the Signals tool places it for its kind }; // one step of the history of the signalling: signals placed, standing ones renamed, other templates placed with them, the values // of standing ones replaced @@ -110,6 +126,7 @@ struct signal_changes std::vector renamed; std::vector others; std::vector>> values; + std::vector> files; // standing signals given another template, of the same kind, their values kept }; struct nmt_place @@ -195,6 +212,8 @@ class editor_signals // the signals placed by the templates chosen for their kinds and the rest of the changes, with the tracks changed before: one // step of the history. returns: how many signals were placed virtual std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) = 0; + // the signal templates with lights, false while they are still being looked for + virtual bool signal_heads(std::vector &Heads, bool const Wait) = 0; protected: ~editor_signals() = default; diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 55b7cd4f..64bac01b 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -501,6 +501,49 @@ int parameter_count(std::string const &File) return read_template(File, text, error) ? count_parameters(text) : 0; } +bool signal_speeds(std::string const &File, std::vector &Speeds, bool &Informs) +{ + Speeds.clear(); + Informs = false; + template_text text; + std::string error; + if (false == read_template(File, text, error)) + { + return false; + } + auto const tokens{tokenize(text.content)}; + auto const speed = [](std::string const &Token, int &Speed) { + char *end{nullptr}; + auto const value{std::strtod(Token.c_str(), &end)}; + if (end == Token.c_str() || *end != '\0') + { + return false; + } + Speed = static_cast(std::lround(value)); + return true; + }; + for (std::size_t idx = 0; idx + 2 < tokens.size(); ++idx) + { + if (tokens[idx] != "setvelocity") + { + continue; + } + int own{0}; + int next{0}; + auto const shown{speed(tokens[idx + 1], own)}; + if (shown && own != 0 && std::find(Speeds.begin(), Speeds.end(), own) == Speeds.end()) + { + Speeds.push_back(own); + } + if (speed(tokens[idx + 2], next) && next == 0 && (tokens[idx + 1] == "*" || (shown && own != 0 && own != 20))) + { + Informs = true; + } + } + std::sort(Speeds.begin(), Speeds.end()); + return true; +} + void suggest(std::string const &File, include_info &Info) { template_text text; diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index 4b70de17..97099fef 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -68,6 +68,9 @@ bool load(std::string const &File, include_info &Info, std::string &Error); bool save(std::string const &File, include_info const &Info, std::string &Error); // highest number of (pN) placeholder used by specified template, 0 if it has none int parameter_count(std::string const &File); +// the speeds the events of a signal template set it to show, by their SetVelocity: of the signal itself, km/h, -1 the highest +// allowed, 20 the substitute signal. Informs: one of them tells the next signal shows stop. returns: false if the file can't be read +bool signal_speeds(std::string const &File, std::vector &Speeds, bool &Informs); // sets roles of the parameters whose use in the template makes their meaning clear. roles already set are left alone void suggest(std::string const &File, include_info &Info); // true if value of a parameter with specified role is supplied by the editor when the template is placed diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 042fd4e8..d7d79757 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -2043,4 +2043,190 @@ std::vector track_lengths(layout const &Layout, std::vector c return result; } +namespace +{ + +double constexpr kStraight{5000.0}; // m, a path curved less goes for a straight one +int constexpr kPieces{20000}; // of the tracks passed by the routes of a signal, at most + +struct reach +{ + TTrack *track{nullptr}; + int path{0}; + int facing{1}; + double from{0.0}; // m, along the path from its start, where the route comes in + double length{0.0}; // of the route up to there + double speed{0.0}; + int warning{-1}; + std::vector turnouts; + std::vector> passed; +}; + +} // namespace + +double path_speed(TTrack const &Track, int const Path) +{ + bezier const curve{Track.m_paths[Path]}; + int constexpr count{32}; + int constexpr span{4}; + auto least{std::numeric_limits::max()}; + for (int i = 0; i + 2 * span <= count; ++i) + { + auto const a{map_of(curve.point(static_cast(i) / count))}; + auto const b{map_of(curve.point(static_cast(i + span) / count))}; + auto const c{map_of(curve.point(static_cast(i + 2 * span) / count))}; + auto const twice{std::abs((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))}; + if (twice > 1e-9) + least = std::min(least, glm::distance(a, b) * glm::distance(b, c) * glm::distance(c, a) / (2.0 * twice)); + } + return least > kStraight ? 0.0 : std::sqrt(8.424 * least); +} + +std::vector> routes(std::vector const &Posts, std::vector const &From, double const Limit) +{ + std::vector> all; + struct along + { + std::vector points; + std::vector distances; + }; + std::map, along> paths; + auto const along_of = [&](TTrack const &Track, int const Path) -> along const & { + auto &found{paths[{&Track, Path}]}; + if (found.points.empty()) + { + double length{0.0}; + found.points = points_of(Track, Path, length); + found.distances.assign(1, 0.0); + for (std::size_t k = 1; k < found.points.size(); ++k) + found.distances.push_back(found.distances.back() + plan_distance(found.points[k - 1], found.points[k])); + } + return found; + }; + auto const project_on = [&](along const &Path, glm::dvec3 const &Point, double &Gap) { + auto best{std::numeric_limits::max()}; + double at{0.0}; + for (std::size_t k = 0; k + 1 < Path.points.size(); ++k) + { + auto const a{map_of(Path.points[k])}; + auto const ab{map_of(Path.points[k + 1]) - a}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(map_of(Point) - a, ab) / length2, 0.0, 1.0) : 0.0}; + if (auto const distance{glm::distance(a + ab * t, map_of(Point))}; distance < best) + { + best = distance; + at = Path.distances[k] + (Path.distances[k + 1] - Path.distances[k]) * t; + } + } + Gap = best; + return at; + }; + std::unordered_map> by_track; + for (std::size_t index = 0; index < Posts.size(); ++index) + if (Posts[index].track != nullptr) + by_track[Posts[index].track].push_back(index); + auto const routes_from = [&](std::size_t const Start) { + std::vector result; + auto const &start{Posts[Start]}; + auto path{0}; + auto nearest{std::numeric_limits::max()}; + double from{0.0}; + for (int candidate = 0; candidate < static_cast(start.track->m_paths.size()); ++candidate) + { + double gap{0.0}; + auto const at{project_on(along_of(*start.track, candidate), start.point, gap)}; + if (gap < nearest - 1e-3) + { + nearest = gap; + path = candidate; + from = at; + } + } + std::vector stack{{start.track, path, start.facing, from, 0.0, 0.0, -1, {}, {}}}; + auto pieces{0}; + while (false == stack.empty() && pieces++ < kPieces) + { + auto here{std::move(stack.back())}; + stack.pop_back(); + auto const &track{*here.track}; + auto const &line{along_of(track, here.path)}; + auto const total{line.distances.back()}; + here.passed.emplace_back(&track, here.path * 2 + (here.facing > 0 ? 1 : 0)); + if (node(track)) + { + auto const speed{path_speed(track, here.path)}; + auto limit{speed}; + auto const set{track.SwitchExtension != nullptr ? std::abs(static_cast(track.SwitchExtension->fVelocity)) : here.track->VelocityGet()}; + if (speed > 0.0 && set >= 2.0) + limit = std::min(speed, set); + if (limit > 0.0 && (here.speed <= 0.0 || limit < here.speed)) + here.speed = limit; + if (speed > 0.0 && (track.eType != tt_Switch || here.path != straighter(track))) + here.turnouts.push_back(here.track); + } + // the signals ahead on the path, the nearest first + std::vector> ahead; + if (auto const found{by_track.find(&track)}; found != by_track.end()) + for (auto const index : found->second) + { + auto const &other{Posts[index]}; + if (index == Start || other.facing != here.facing || other.kind == signal::role::shunting) + continue; + double gap{0.0}; + auto const at{project_on(line, other.point, gap)}; + auto const distance{(at - here.from) * here.facing}; + if (gap < 4.5 && distance > 0.1) + ahead.emplace_back(distance, index); + } + std::sort(ahead.begin(), ahead.end()); + auto stopped{false}; + for (auto const &[distance, index] : ahead) + { + if (here.length + distance > Limit) + break; + if (Posts[index].kind == signal::role::warning) + { + if (here.warning < 0) + here.warning = static_cast(index); + continue; + } + result.push_back({static_cast(index), here.warning, here.length + distance, here.speed, here.turnouts, false}); + stopped = true; + break; + } + if (stopped) + continue; + auto const length{here.length + (here.facing > 0 ? total - here.from : here.from)}; + if (length > Limit) + { + result.push_back({-1, here.warning, Limit, here.speed, here.turnouts, false}); + continue; + } + auto const atend{here.facing > 0}; + auto const point{end_of(track, here.path, atend)}; + auto *next{neighbour(track, here.path, atend)}; + auto went{false}; + if (next != nullptr && rail(*next)) + for (int other = 0; other < static_cast(next->m_paths.size()); ++other) + for (auto const end : {false, true}) + { + if (glm::distance(end_of(*next, other, end), point) >= same_point) + continue; + auto const facing{end ? -1 : 1}; + if (std::find(here.passed.begin(), here.passed.end(), std::make_pair(static_cast(next), other * 2 + (facing > 0 ? 1 : 0))) != here.passed.end()) + continue; + auto const &entered{along_of(*next, other)}; + stack.push_back({next, other, facing, end ? entered.distances.back() : 0.0, length, here.speed, here.warning, here.turnouts, here.passed}); + went = true; + } + if (false == went) + result.push_back({-1, here.warning, length, here.speed, here.turnouts, true}); + } + return result; + }; + for (auto const from : From) + all.push_back(from < Posts.size() && Posts[from].track != nullptr && false == Posts[from].track->m_paths.empty() ? routes_from(from) : std::vector{}); + return all; +} + } // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index ff676c15..8272c075 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -226,4 +226,27 @@ struct lengths // the lengths of the station tracks, the signals standing by them among the signals given std::vector track_lengths(layout const &Layout, std::vector const &Tracks, std::vector const &Signals); +// a signal by the track the trains read it on +struct post +{ + TTrack *track{nullptr}; + int facing{1}; // the trains it faces run 1: towards the end of the path, -1: towards its start + glm::dvec3 point{0.0}; + signal::role kind{signal::role::entry}; // entry for any main signal +}; +// the way of the trains from a signal on to the next main or block signal facing them +struct route +{ + int next{-1}; // of the posts, -1 for none within the limit + int warning{-1}; // the warning disc passed on the way, of the posts + double length{0.0}; // m, in the plan + double speed{0.0}; // km/h, the least the switches on it allow on their curved paths, by the radius and by the speed set; 0 for no limit + std::vector turnouts; // the switches it turns off the straight at + bool ended{false}; // at the end of the tracks, before the limit +}; +// km/h, a train runs the path at by its least radius, 0.65 m/s² of the acceleration left unbalanced without cant; 0 for a straight one +double path_speed(TTrack const &Track, int const Path); +// the routes of the trains from each of the posts given on through the switches, none longer than the limit +std::vector> routes(std::vector const &Posts, std::vector const &From, double const Limit); + } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 7178f8c3..8b55cf7a 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10885,3 +10885,180 @@ msgstr "Tor\tDługość budowlana [m]\tUżyteczna w prawo [m]\tod\tdo\tUżyteczn msgid "Track %s, useful length %.0f m to the right, %.0f m to the left" msgstr "Tor %s, długość użyteczna %.0f m w prawo, %.0f m w lewo" +msgid "green" +msgstr "zielone" + +msgid "two orange" +msgstr "dwa pomarańczowe" + +msgid "red" +msgstr "czerwone" + +msgid "%s km/h" +msgstr "%s km/h" + +msgid "an orange strip" +msgstr "pomarańczowy pas świetlny" + +msgid "a green strip" +msgstr "zielony pas świetlny" + +msgid "Signal heads" +msgstr "Komory semaforów" + +msgid "Line speed" +msgstr "Prędkość linii" + +msgid "%.0f km/h, the highest set on its main tracks" +msgstr "%.0f km/h, najwyższa ustawiona na torach głównych" + +msgid "%.0f km/h taken: no track of the station has a speed set" +msgstr "przyjęto %.0f km/h: żaden tor stacji nie ma ustawionej prędkości" + +msgid "The speed of the trains through the station, the highest one its main tracks and the lines out of it are set to in the Track tool, up to 250 km/h: the braking distance and the lights of the signals go by it. Without one 160 km/h is taken, or the speed of the braking distance set by hand" +msgstr "Prędkość pociągów przez stację: najwyższa, jaką jej tory główne i wychodzące z niej linie mają ustawioną w narzędziu Tor, do 250 km/h. Od niej zależą droga hamowania i światła semaforów. Gdy żaden tor jej nie ma, przyjmuje się 160 km/h albo prędkość drogi hamowania ustawionej ręcznie" + +msgid "Braking distance" +msgstr "Droga hamowania" + +msgid "%.0f m, after the speed" +msgstr "%.0f m, według prędkości" + +msgid "%d m, set by hand" +msgstr "%d m, ustawiona ręcznie" + +msgid "The braking distance of the trains on the line, Ie-4 § 8: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. The block sections and the warning discs stand no nearer, a signal telling of the next one standing nearer slows the trains. Set it by hand where the line has another one" +msgstr "Droga hamowania pociągów na linii, Ie-4 § 8: 400 m do 60 km/h, 700 m do 100, 1000 m do 140, 1300 m powyżej. Odstępy blokady i tarcze ostrzegawcze nie są krótsze, a semafor zapowiadający następny, który stoi bliżej, zmniejsza prędkość pociągów. Ustaw ją ręcznie, gdy linia ma inną" + +msgid "Looking for the signal templates..." +msgstr "Szukam szablonów semaforów..." + +msgid "No main signal stands by the station yet: its lights are checked once one does" +msgstr "Przy stacji nie stoi jeszcze żaden semafor: światła sprawdzę, gdy jakiś stanie" + +msgid "Speeds" +msgstr "Prędkości" + +msgid "Lights" +msgstr "Światła" + +msgid "State" +msgstr "Stan" + +msgid "Main signal %s" +msgstr "Semafor %s" + +msgid "Block signal %s" +msgstr "Semafor SBL %s" + +msgid "Warning disc %s" +msgstr "Tarcza ostrzegawcza %s" + +msgid "To %s: %.0f m" +msgstr "Do %s: %.0f m" + +msgid "To the end of the tracks: %.0f m" +msgstr "Do końca torów: %.0f m" + +msgid "No signal within %.0f m" +msgstr "Żadnego semafora przez %.0f m" + +msgid "%d km/h through the switches" +msgstr "%d km/h przez rozjazdy" + +msgid "%.0f km/h" +msgstr "%.0f km/h" + +msgid "turning at %s" +msgstr "zwrot na %s" + +msgid "past a warning disc" +msgstr "za tarczą ostrzegawczą" + +msgid "Needs: %s" +msgstr "Potrzebne: %s" + +msgid "The next signal stands nearer than the braking distance: with it at stop this one slows the trains to %d km/h, Ie-4 § 8 ust. 5" +msgstr "Następny semafor stoi bliżej niż droga hamowania: gdy on pokazuje stój, ten zmniejsza prędkość pociągów do %d km/h, Ie-4 § 8 ust. 5" + +msgid "Give it a W21 indicator \"14\": 140 km/h with the next signal at stop, Ie-4 § 8 ust. 5" +msgstr "Dostaw mu wskaźnik W21 „14”: 140 km/h, gdy następny semafor pokazuje stój, Ie-4 § 8 ust. 5" + +msgid "A semaphore with arms: its lights aren't checked" +msgstr "Semafor kształtowy: jego świateł nie sprawdzam" + +msgid "No template of its kind and mount has these lights" +msgstr "Żaden szablon tego rodzaju i mocowania nie ma tych świateł" + +msgid "Its lights don't show all of it: %s does" +msgstr "Jego światła nie pokażą wszystkiego, %s pokaże" + +msgid "It has more lights than it needs: %s will do" +msgstr "Ma więcej świateł, niż trzeba: wystarczy %s" + +msgid "LMB: fly to it" +msgstr "LPM: leć do niego" + +msgid "disc" +msgstr "tarcza" + +msgid "arms" +msgstr "ramiona" + +msgid "no template" +msgstr "brak szablonu" + +msgid "lights missing" +msgstr "brak świateł" + +msgid "more than needed" +msgstr "za dużo" + +msgid "fits" +msgstr "pasuje" + +msgid "too near" +msgstr "za blisko" + +msgid "Fit the lights" +msgstr "Dopasuj światła" + +msgid "Gives each signal marked the template of its kind and mount with the lights its routes need and the least beyond them, the name, the plate and the place kept, the trains reading it as before. One step of undo" +msgstr "Daje każdemu zaznaczonemu semaforowi szablon tego samego rodzaju i mocowania ze światłami, jakich wymagają jego przebiegi, i jak najmniejszym nadmiarem. Nazwa, tablica i miejsce zostają, pociągi odczytują go jak dotąd. Jeden krok cofania" + +msgid "The lights go by the routes of the trains: 40 km/h through the switches 1:9, 60 through 1:12 and 1:14, 100 through 1:18.5, by their radii, for these a lower orange light, with an orange strip for 60, a green one for 100, Ie-1; an upper orange one where the next signal can show stop; the white one of the substitute signal. A signal telling of the next one standing nearer than the braking distance slows the trains with it at stop, Ie-4 § 8 ust. 5" +msgstr "Światła wynikają z przebiegów pociągów: 40 km/h przez rozjazdy 1:9, 60 przez 1:12 i 1:14, 100 przez 1:18,5, liczone z ich promieni; dla nich dolne światło pomarańczowe, z pomarańczowym pasem dla 60 i zielonym dla 100, Ie-1; górne pomarańczowe, gdy następny semafor może pokazać stój; białe dla sygnału zastępczego. Semafor zapowiadający następny, który stoi bliżej niż droga hamowania, zmniejsza prędkość pociągów, gdy tamten pokazuje stój, Ie-4 § 8 ust. 5" + +msgid "No template has the lights %s need, they got the one chosen in the Signals tool: see Signal heads" +msgstr "Żaden szablon nie ma świateł potrzebnych dla %s, dostały ten wybrany w narzędziu Sygnalizatory: zobacz Komory semaforów" + +msgid "No main signal within %.0f m ahead of it to tell of" +msgstr "Przez %.0f m przed nią nie ma semafora, który by zapowiadała" + +msgid "It stands %.0f m before %s, less than the braking distance of %.0f m: the trains don't stop by the signal in time" +msgstr "Stoi %.0f m przed %s, mniej niż droga hamowania %.0f m: pociąg nie zdąży się zatrzymać przed semaforem" + +msgid "The block section to %s is %.0f m long, shorter than the braking distance of %.0f m, Ie-4 § 8 ust. 1" +msgstr "Odstęp do %s ma %.0f m, mniej niż droga hamowania %.0f m, Ie-4 § 8 ust. 1" + +msgid "No template has the lights the signals marked need" +msgstr "Żaden szablon nie ma świateł potrzebnych zaznaczonym semaforom" + +msgid "The lights of the signals fit already" +msgstr "Światła semaforów już pasują" + +msgid "The signals couldn't be changed: %s" +msgstr "Nie udało się zmienić semaforów: %s" + +msgid "%zu signal of the lines fits" +msgid_plural "%zu signals of the lines fit" +msgstr[0] "%zu semafor linii pasuje" +msgstr[1] "%zu semafory linii pasują" +msgstr[2] "%zu semaforów linii pasuje" + +msgid "%zu signal got the lights it needs, Ctrl+Z takes it back" +msgid_plural "%zu signals got the lights they need, Ctrl+Z takes it back" +msgstr[0] "%zu semafor dostał potrzebne światła, Ctrl+Z cofa" +msgstr[1] "%zu semafory dostały potrzebne światła, Ctrl+Z cofa" +msgstr[2] "%zu semaforów dostało potrzebne światła, Ctrl+Z cofa" +