diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index a06c9c31..f55c9e0a 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -66,6 +66,9 @@ 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)}; 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 bool is_switch(TTrack const &Track) { @@ -175,6 +178,52 @@ template std::string anchor_of(Record_ const &Entry) return Entry.track != nullptr && false == Entry.track->m_editorremoved ? Entry.track->name() : Entry.anchor; } +// the name without the code of the station: bpd_4 as 4 +std::string without_stem(std::string const &Name, std::string const &Stem) +{ + return false == Stem.empty() && Name.size() > Stem.size() && Name.compare(0, Stem.size(), Stem) == 0 ? Name.substr(Stem.size()) : Name; +} + +// a limit of a useful length in words, as "from" and "to" take it +std::string limit_text(station::limit const &Limit, std::string const &Stem) +{ + auto const name{without_stem(Limit.name, Stem)}; + switch (Limit.what) + { + case station::limit::kind::signal: + return format(STR_C("signal %s"), name.c_str()); + case station::limit::kind::fouling: + return format(STR_C("the fouling point of switch %s"), name.c_str()); + case station::limit::kind::start: + return format(STR_C("the start of switch %s"), name.c_str()); + case station::limit::kind::buffer: + return STR_C("the buffer stop"); + case station::limit::kind::crossing: + return STR_C("the level crossing"); + default: + return name.empty() ? std::string{STR_C("the joint")} : format(STR_C("the joint by switch %s"), name.c_str()); + } +} + +// the points of the track from one distance along it to the other +std::vector stretch_of(station::lengths const &Track, double const From, double const To) +{ + auto const at = [&](double const Along) { + auto const after{std::min(std::upper_bound(Track.distances.begin(), Track.distances.end(), Along) - Track.distances.begin(), Track.distances.size() - 1)}; + auto const before{after > 0 ? after - 1 : 0}; + auto const span{Track.distances[after] - Track.distances[before]}; + return glm::mix(Track.points[before], Track.points[after], span > 1e-9 ? std::clamp((Along - Track.distances[before]) / span, 0.0, 1.0) : 0.0); + }; + std::vector result{at(std::min(From, To))}; + for (std::size_t i = 0; i < Track.distances.size(); ++i) + if (Track.distances[i] > std::min(From, To) + 0.05 && Track.distances[i] < std::max(From, To) - 0.05) + result.push_back(Track.points[i]); + result.push_back(at(std::max(From, To))); + if (From > To) + std::reverse(result.begin(), result.end()); + return result; +} + glm::dvec3 axis_point(station::layout const &Layout, double const Chainage) { auto const after{std::lower_bound(Layout.chainage.begin(), Layout.chainage.end(), Chainage)}; @@ -360,6 +409,9 @@ void srk_tool::update() refresh(); else if (m_layout.has_value() && false == m_signals_known) survey_signals(); + m_length_shown = m_length_hover >= 0 ? m_length_hover : m_length_canvas >= 0 ? m_length_canvas : length_of(m_selection.selected_track()); + if (m_length_shown >= static_cast(m_lengths.size())) + m_length_shown = -1; // a track of another station selected opens it auto *track{m_selection.selected_track()}; if (track == nullptr || track == m_picked) @@ -454,6 +506,37 @@ void srk_tool::draw_overlay() const drawlist->AddRectFilled(ImVec2(screen.x - 3.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x + 3.0f, screen.y), IM_COL32(0, 0, 0, 160), 3.0f); drawlist->AddText(ImVec2(screen.x, screen.y - size.y - 2.0f), kSwitch, text.c_str()); } + // the useful lengths of the station track shown, beside it on the right of the trains, a post where they stop + if (m_length_shown >= 0 && m_length_shown < static_cast(m_lengths.size())) + { + auto const &track{m_lengths[m_length_shown]}; + for (auto const &part : track.ways) + { + if (part.length <= 0.0) + continue; + auto const points{stretch_of(track, part.rear.along, part.head.along)}; + std::vector beside; + for (std::size_t i = 0; i < points.size(); ++i) + { + auto const way{points[std::min(i + 1, points.size() - 1)] - points[i > 0 && i + 1 == points.size() ? i - 1 : i]}; + glm::dvec2 const along{glm::normalize(glm::dvec2{way.x, way.z} + glm::dvec2{1e-12, 0.0})}; + beside.push_back(points[i] + lift + glm::dvec3{-along.y, 0.1, along.x} * 0.9); + } + for (std::size_t i = 1; i < beside.size(); ++i) + if (near_enough(beside[i], kOverlayRange)) + projection.line(drawlist, beside[i - 1], beside[i], kUseful, 3.0f); + auto const top{beside.back() + glm::dvec3{0.0, 2.5, 0.0}}; + projection.line(drawlist, beside.front(), beside.front() + glm::dvec3{0.0, 1.0, 0.0}, kUseful, 2.0f); + projection.line(drawlist, beside.back(), top, kUseful, 2.0f); + ImVec2 screen; + if (false == near_enough(top, kLabelRange) || false == projection.project(top, screen)) + continue; + auto const text{format(STR_C("track %s: %.0f m"), track.number.c_str(), part.length)}; + 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), IM_COL32(0, 0, 0, 180), 3.0f); + drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), kUseful, text.c_str()); + } + } for (auto const &mark : m_signal_marks) { ImVec2 screen; @@ -611,6 +694,10 @@ void srk_tool::render_options() if (false == m_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); ImGui::Spacing(); + m_length_hover = -1; + if (ImGui::CollapsingHeader(format("%s (%zu)###srklengths", STR_C("Track lengths"), m_lengths.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + render_lengths(); + 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))))) @@ -657,6 +744,138 @@ void srk_tool::render_options() item_tooltip("Leaves this station; LMB on a switch opens another one"); } +void srk_tool::render_lengths() +{ + auto const dimmed = [](char const *Text) { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", Text); + ImGui::PopStyleColor(); + }; + if (m_lengths.empty()) + { + if (stem_of_open().empty()) + dimmed(STR_C("Give the station its code first: its tracks are numbered and measured after it")); + else if (m_numbered) + dimmed(STR_C("The station tracks have no ends to be measured between")); + else + { + dimmed(STR_C("Set the track circuits first: the station tracks are measured by their numbers, from their joints, the fouling points and the signals")); + if (ImGui::Button(STR_C("Set the track circuits"), ImVec2(-1.0f, 0.0f))) + set_circuits(); + } + return; + } + auto const stem{stem_of_open()}; + char const *const ways[] = {STR_C("To the right"), STR_C("To the left")}; + if (ImGui::BeginTable("##srklengthlist", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (m_lengths.size() + 1.5f))))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Track"), ImGuiTableColumnFlags_WidthStretch, 0.16f); + ImGui::TableSetupColumn(ways[0], ImGuiTableColumnFlags_WidthStretch, 0.28f); + ImGui::TableSetupColumn(ways[1], ImGuiTableColumnFlags_WidthStretch, 0.28f); + ImGui::TableSetupColumn(STR_C("Train up to"), ImGuiTableColumnFlags_WidthStretch, 0.28f); + ImGui::TableHeadersRow(); + for (std::size_t index = 0; index < m_lengths.size(); ++index) + { + auto const &track{m_lengths[index]}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable((track.number + "##srklength" + std::to_string(index)).c_str(), static_cast(index) == m_length_shown, ImGuiSelectableFlags_SpanAllColumns)) + { + // the track at the middle of its useful length selected, the camera over it + auto const &way{track.ways[0]}; + auto const middle{stretch_of(track, way.rear.along, way.head.along)}; + auto const at{middle[middle.size() / 2]}; + station::path const *nearest{nullptr}; + auto best{std::numeric_limits::max()}; + for (auto const &path : m_layout->paths) + for (auto const &point : path.points) + if (auto const distance{glm::distance(glm::dvec2{point.x, point.z}, glm::dvec2{at.x, at.z})}; false == path.approach && distance < best) + { + best = distance; + nearest = &path; + } + if (nearest != nullptr) + { + m_selection.set_current_node(nearest->track); + m_picked = nearest->track; + m_view.focus_track(*nearest->track, nearest->index, at); + } + } + if (ImGui::IsItemHovered()) + { + m_length_hover = static_cast(index); + ImGui::BeginTooltip(); + ImGui::TextUnformatted(format(STR_C("Track %s"), track.number.c_str()).c_str()); + for (int way = 0; way < 2; ++way) + { + auto const &part{track.ways[way]}; + ImGui::TextUnformatted(format(STR_C("%s: %.0f m, from %s to %s"), ways[way], part.length, limit_text(part.rear, stem).c_str(), limit_text(part.head, stem).c_str()).c_str()); + } + ImGui::TextDisabled(STR_C("Construction length %.0f m: between the switches at its ends, the switches within it left out"), track.construction); + if (std::abs(track.ways[0].length - track.ways[1].length) > kUnequal) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("The two ways differ: their trains stop on other stretches of the track, between other switches or signals")); + ImGui::TextDisabled("%s", STR_C("LMB: show it")); + ImGui::EndTooltip(); + } + auto const unequal{std::abs(track.ways[0].length - track.ways[1].length) > kUnequal}; + for (int way = 0; way < 2; ++way) + { + ImGui::TableNextColumn(); + auto const &part{track.ways[way]}; + if (unequal) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, part.head.what == station::limit::kind::signal ? 1.0f : 0.6f), "%.0f m", part.length); + else if (part.head.what == station::limit::kind::signal) + ImGui::Text("%.0f m", part.length); + else + ImGui::TextDisabled("%.0f m", part.length); + } + ImGui::TableNextColumn(); + ImGui::Text("%.0f m", std::max(0.0, std::min(track.ways[0].length, track.ways[1].length) - kTrainMargin)); + } + ImGui::EndTable(); + } + if (std::none_of(m_lengths.begin(), m_lengths.end(), [](station::lengths const &Track) { return Track.ways[0].head.what == station::limit::kind::signal || Track.ways[1].head.what == station::limit::kind::signal; })) + { + dimmed(STR_C("No signal stands by the station tracks yet, the lengths run between the fouling points: set the signals for the useful ones")); + if (ImGui::Button(STR_C("Set the signals"), ImVec2(-1.0f, 0.0f))) + set_signals(); + } + if (ImGui::Button(STR_C("Copy the list of tracks"), ImVec2(-1.0f, 0.0f))) + { + ImGui::SetClipboardText(lengths_table().c_str()); + m_status = format(STR_P("The lengths of %zu track copied", "The lengths of %zu tracks copied", m_lengths.size()), m_lengths.size()); + } + item_tooltip("The lengths of the station tracks as a table to paste into a spreadsheet: the construction length and the useful one each way with its limits, as the list of the tracks of a station gives them"); + 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")); +} + +std::string srk_tool::lengths_table() const +{ + auto const stem{stem_of_open()}; + std::string result{STR_C("Track\tConstruction length [m]\tUseful to the right [m]\tfrom\tto\tUseful to the left [m]\tfrom\tto")}; + result += '\n'; + for (auto const &track : m_lengths) + { + result += format("%s\t%.0f", track.number.c_str(), track.construction); + for (auto const &part : track.ways) + result += format("\t%.0f\t%s\t%s", part.length, limit_text(part.rear, stem).c_str(), limit_text(part.head, stem).c_str()); + result += '\n'; + } + return result; +} + +int srk_tool::length_of(TTrack const *Track) const +{ + if (Track == nullptr || Track->Isolated.empty()) + return -1; + auto const number{track_of(Track->Isolated.front()->asName, stem_of_open())}; + for (std::size_t index = 0; index < m_lengths.size() && false == number.empty(); ++index) + if (m_lengths[index].number == number) + return static_cast(index); + return -1; +} + void srk_tool::number_switches() { m_status.clear(); @@ -941,7 +1160,7 @@ bool srk_tool::station_tracks(std::vector &Tracks, std::unord std::map>> numbers; for (auto const &path : m_layout->paths) { - if (path.track->Isolated.empty() || Inside.count(path.track) > 0) + if (path.track->m_editorremoved || 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)) @@ -1697,8 +1916,13 @@ void srk_tool::survey_signals() std::vector standing; m_signals_known = m_layout.has_value() && m_signals.standing_signals(standing, false); if (false == m_signals_known) + { + if (false == m_measured) + measure({}); return; + } auto const stem{stem_of_open()}; + std::vector stops; for (auto const &signal : standing) { if (gap_to(*m_layout, signal.position) > 8.0) @@ -1707,7 +1931,30 @@ void srk_tool::survey_signals() 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}}); + if (signal.kind != "main" && signal.kind != "shunt") + continue; + station::signal stop; + stop.name = signal_shown(signal.name, stem); + stop.point = signal.position; + stop.travel = glm::dvec3{travel.x, 0.0, travel.y}; + stops.push_back(std::move(stop)); } + measure(stops); +} + +void srk_tool::measure(std::vector const &Signals) +{ + m_measured = true; + m_lengths.clear(); + std::vector tracks; + std::unordered_set inside; + m_numbered = m_layout.has_value() && station_tracks(tracks, inside); + if (false == m_numbered) + return; + m_lengths = station::track_lengths(*m_layout, tracks, Signals); + std::sort(m_lengths.begin(), m_lengths.end(), [](station::lengths const &A, station::lengths const &B) { + return std::make_pair(std::atoi(A.number.c_str()), A.number) < std::make_pair(std::atoi(B.number.c_str()), B.number); + }); } void srk_tool::survey() @@ -1715,6 +1962,9 @@ void srk_tool::survey() m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); m_circuits.clear(); m_ticks.clear(); + m_lengths.clear(); + m_measured = false; + m_numbered = false; if (false == m_layout.has_value()) return; auto const &layout{*m_layout}; @@ -1889,6 +2139,7 @@ void srk_tool::render_canvas(float const Height) } else if (false == hovered && false == ImGui::IsItemActive()) m_hover = -1; + m_length_canvas = m_hover >= 0 && false == m_dragging ? length_of(layout.paths[m_hover].track) : -1; if (ImGui::IsItemDeactivated()) { if (false == m_dragging && m_hover >= 0) @@ -1942,6 +2193,35 @@ void srk_tool::render_canvas(float const Height) draw_path(path, overlay_color::selected, 4.0f); if (m_hover >= 0) draw_path(layout.paths[m_hover], kHovered, 4.0f); + // the useful lengths of the station track shown: of the trains to the right over it, to the left under it, an arrowhead where + // they stop + std::vector> useful; + if (m_length_shown >= 0 && m_length_shown < static_cast(m_lengths.size())) + { + auto const &track{m_lengths[m_length_shown]}; + for (int way = 0; way < 2; ++way) + { + auto const &part{track.ways[way]}; + if (part.length <= 0.0) + continue; + auto const shift{way == 0 ? -7.0f : 7.0f}; + std::vector line; + for (auto const &point : stretch_of(track, part.rear.along, part.head.along)) + { + auto at{screen_of(station::project(layout, point))}; + at.y += shift; + line.push_back(at); + } + draw->AddPolyline(line.data(), static_cast(line.size()), kUseful, ImDrawFlags_None, 2.0f); + auto const &rear{line.front()}; + auto const &head{line.back()}; + draw->AddLine(ImVec2(rear.x, rear.y - 4.0f), ImVec2(rear.x, rear.y + 4.0f), kUseful, 2.0f); + auto const way_x{head.x >= line[line.size() > 1 ? line.size() - 2 : 0].x ? 1.0f : -1.0f}; + draw->AddTriangleFilled(ImVec2(head.x - 7.0f * way_x, head.y - 4.5f), ImVec2(head.x - 7.0f * way_x, head.y + 4.5f), ImVec2(head.x + 1.0f * way_x, head.y), kUseful); + auto const &middle{line[line.size() / 2]}; + useful.emplace_back(format("%.0f m", part.length), ImVec2(middle.x, way == 0 ? middle.y - ImGui::GetTextLineHeight() - 3.0f : middle.y + 3.0f)); + } + } // the labels, none over another: the switches first, a dot at the start and the name over or under it in turn, the number // alone once named after the station; then the station tracks under or over them, the longest first std::vector placed; @@ -1958,6 +2238,8 @@ void srk_tool::render_canvas(float const Height) return; } }; + for (auto const &[text, at] : useful) + label(text, kUseful, {at}); auto const stem{stem_of_open()}; auto const height{ImGui::GetTextLineHeight()}; for (std::size_t index = 0; index < layout.switches.size(); ++index) @@ -2006,6 +2288,11 @@ void srk_tool::render_canvas(float const Height) ImGui::TextDisabled("km %.3f", station::km_of(layout, along)); if (false == path.track->Isolated.empty()) ImGui::TextDisabled(STR_C("track circuit %s"), path.track->Isolated.front()->asName.c_str()); + if (m_length_canvas >= 0 && m_length_canvas < static_cast(m_lengths.size())) + { + auto const &track{m_lengths[m_length_canvas]}; + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kUseful), STR_C("Track %s, useful length %.0f m to the right, %.0f m to the left"), track.number.c_str(), track.ways[0].length, track.ways[1].length); + } ImGui::TextDisabled("%s", STR_C("LMB: select it")); ImGui::EndTooltip(); } @@ -2013,6 +2300,7 @@ void srk_tool::render_canvas(float const Height) void srk_tool::render_windows() { + m_length_canvas = -1; if (false == m_open || false == m_strip || false == m_layout.has_value()) return; auto const &io{ImGui::GetIO()}; @@ -2219,6 +2507,47 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri } return editor_selftest_result::handled; } + if (Command == "srklengths") + { + // srklengths [show=]: the lengths of the station tracks of the open station, the one shown drawn + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srklengths: no station open"); + return editor_selftest_result::handled; + } + std::vector standing; + m_signals.standing_signals(standing, true); + refresh(); + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srklengths: the station is gone"); + return editor_selftest_result::handled; + } + auto const values{selftest_values(Words)}; + auto const show{values.find("show")}; + auto const code = [](station::limit const &Limit) { + char const *const kinds[] = {"S", "J", "F", "St", "B", "X"}; + return std::string{kinds[static_cast(Limit.what)]} + ":" + Limit.name + format("@%.1f", Limit.along); + }; + for (std::size_t index = 0; index < m_lengths.size(); ++index) + { + auto const &track{m_lengths[index]}; + WriteLog(format("SELFTEST srklengths %s: right %.1f (%s .. %s), left %.1f (%s .. %s), construction %.1f", track.number.c_str(), track.ways[0].length, code(track.ways[0].rear).c_str(), code(track.ways[0].head).c_str(), track.ways[1].length, + code(track.ways[1].rear).c_str(), code(track.ways[1].head).c_str(), track.construction)); + } + WriteLog(format("SELFTEST srklengths: %zu tracks", m_lengths.size())); + if (auto const copy{values.find("copy")}; copy != values.end() && copy->second == "1") + WriteLog("SELFTEST srklengths table:\n" + lengths_table()); + // the track shown selected + for (auto const &path : m_layout->paths) + if (show != values.end() && false == path.track->Isolated.empty() && track_of(path.track->Isolated.front()->asName, stem_of_open()) == show->second) + { + m_selection.set_current_node(path.track); + m_picked = path.track; + break; + } + 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 a5f965af..605676c3 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -107,6 +107,12 @@ class srk_tool final : public editor_tool }; std::vector m_signal_marks; bool m_signals_known{false}; // the signals standing surveyed, once their templates are found + std::vector m_lengths; // of the station tracks, by their numbers + bool m_measured{false}; + bool m_numbered{false}; // the station has tracks numbered by their circuits + 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 std::string m_status; void load(); @@ -129,6 +135,14 @@ class srk_tool final : public editor_tool void set_signals(); // the signals standing by the station, for the schematic void survey_signals(); + // the lengths of the station tracks, the signals given by them + void measure(std::vector const &Signals); + // 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 + std::string lengths_table() const; + // of the lengths, the one of the station track the track is a part of, -1 for none + int length_of(TTrack const *Track) const; // the signals of the lines out of the station: on a double-track line the block signals to the next station and the block // sections between them, on a single-track one a warning disc before the entry signal; one step of the history void set_lines(); diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 1ae54d43..042fd4e8 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -330,8 +330,9 @@ double plan_gap(glm::dvec3 const &Point, std::vector const &Line) return best; } -// how far from the end of the switch its joint lies: 4.2 m beyond the fouling point at a branch, before the start elsewhere -double clearance(TTrack const &Switch, int const Path, bool const Atend) +// how far out from the end of the switch along the track leading on its fouling point lies, below 0 within the switch: where the +// axes of the path there and of the other one are 3.5 m apart; none at the start of a switch +std::optional fouling(TTrack const &Switch, int const Path, bool const Atend) { auto const point{end_of(Switch, Path, Atend)}; std::optional> other; @@ -343,7 +344,7 @@ double clearance(TTrack const &Switch, int const Path, bool const Atend) other = std::make_pair(path, atend); } if (false == other.has_value()) - return kStart; + return std::nullopt; auto const theirs{ahead(Switch, other->first, other->second, kReach + 50.0)}; if (plan_gap(point, theirs) >= kFouling) { @@ -356,7 +357,7 @@ double clearance(TTrack const &Switch, int const Path, bool const Atend) double back{0.0}; for (std::size_t i = 1; i < mine.size() && plan_gap(mine[i], own) >= kFouling; ++i) back += plan_distance(mine[i - 1], mine[i]); - return std::max(0.0, kBeyond - back); + return -back; } auto const mine{ahead(Switch, Path, Atend, kReach)}; double along{0.0}; @@ -366,11 +367,18 @@ double clearance(TTrack const &Switch, int const Path, bool const Atend) auto const step{plan_distance(mine[i - 1], mine[i])}; auto const next{plan_gap(mine[i], theirs)}; if (next >= kFouling) - return along + (next > gap ? step * std::clamp((kFouling - gap) / (next - gap), 0.0, 1.0) : step) + kBeyond; + return along + (next > gap ? step * std::clamp((kFouling - gap) / (next - gap), 0.0, 1.0) : step); along += step; gap = next; } - return along; + return along - kBeyond; +} + +// how far from the end of the switch its joint lies: 4.2 m beyond the fouling point at a branch, before the start elsewhere +double clearance(TTrack const &Switch, int const Path, bool const Atend) +{ + auto const point{fouling(Switch, Path, Atend)}; + return point.has_value() ? std::max(0.0, *point + kBeyond) : kStart; } } // namespace @@ -1539,4 +1547,500 @@ std::string block_number(double const Km, int const Track, bool const Rising) return std::to_string(number); } +namespace +{ + +double constexpr kBeside{4.5}; // m, from the axis of a station track to a signal of it, at most +double constexpr kFacing{0.5}; // of the cosine between the way of a signal and the track, at least +double constexpr kLevel{1.5}; // m, the most a road crossing a track lies above or below it at a level crossing +double constexpr kSkew{0.2}; // of the sine between a road and a track, the least a level crossing is taken at +int constexpr kSearch{4096}; // of the steps of the way through the switches within a station track, the most tried + +// the points of the path from its end, Length: in the plan +std::vector from_end(TTrack const &Track, int const Path, bool const Atend, double &Length) +{ + auto points{dense_points(Track, Path, Length)}; + if (Atend) + std::reverse(points.begin(), points.end()); + return points; +} + +// a path of a track along a station track, run along from its end +struct step +{ + TTrack *track{nullptr}; + int path{0}; + bool atend{false}; +}; + +// the shortest way out of the end of the track through the tracks of others, the switches within a station track, on to one of +// its own not taken yet, no longer than the limit; its last step on that one, empty for none +std::vector way_on(TTrack const &Track, bool const Atend, std::unordered_set const &Own, std::unordered_set const &Taken, double const Limit) +{ + std::vector best; + auto shortest{Limit}; + std::vector current; + auto budget{kSearch}; + std::function search = [&](TTrack const &From, int const Path, bool const End, double const Length) { + auto const point{end_of(From, Path, End)}; + auto *next{neighbour(From, Path, End)}; + if (--budget < 0 || next == nullptr || false == rail(*next) || Length >= shortest || std::any_of(current.begin(), current.end(), [&](step const &Step) { return Step.track == next; })) + return; + if (Own.count(next) > 0) + { + int path{0}; + bool atend{false}; + if (Taken.count(next) == 0 && end_at(*next, point, path, atend)) + { + best = current; + best.push_back({next, 0, atend}); + shortest = Length; + } + return; + } + for (int path = 0; path < static_cast(next->m_paths.size()); ++path) + for (auto const atend : {false, true}) + if (glm::distance(end_of(*next, path, atend), point) < same_point) + { + current.push_back({next, path, atend}); + search(*next, path, false == atend, Length + plan_length(*next, path)); + current.pop_back(); + } + }; + search(Track, 0, Atend, 0.0); + return best; +} + +// out of an end of a station track to the switch beyond it or to the end of the tracks +struct beyond +{ + TTrack const *node{nullptr}; // the switch, none at the end of the tracks + bool open{false}; // neither within reach: the circuit of the track ends by another of plain track + double out{0.0}; // from the end of the track to the end of the switch, or to the end of the tracks + std::optional fouling; // from the end of the track out to the fouling point of the switch + std::vector points; // out from the end of the track, into the switch + std::vector distances; +}; + +beyond out_of(TTrack const &Track, bool const Atend) +{ + beyond result; + auto point{end_of(Track, 0, Atend)}; + result.points.push_back(point); + result.distances.push_back(0.0); + auto *next{neighbour(Track, 0, Atend)}; + std::unordered_set seen{&Track}; + while (next != nullptr && rail(*next) && seen.insert(next).second) + { + int path{0}; + bool atend{false}; + if (false == end_at(*next, point, path, atend) || result.out > kReach) + break; + double length{0.0}; + auto const points{from_end(*next, path, atend, length)}; + for (std::size_t i = 1; i < points.size(); ++i) + { + result.distances.push_back(result.distances.back() + plan_distance(result.points.back(), points[i])); + result.points.push_back(points[i]); + } + if (node(*next)) + { + result.node = next; + if (auto const at{fouling(*next, path, atend)}) + result.fouling = result.out - *at; + return result; + } + result.out = result.distances.back(); + point = end_of(*next, 0, false == atend); + next = neighbour(*next, 0, false == atend); + } + result.open = next != nullptr && rail(*next); + return result; +} + +glm::dvec3 point_along(std::vector const &Points, std::vector const &Distances, double const Along) +{ + auto const after{std::min(std::upper_bound(Distances.begin(), Distances.end(), Along) - Distances.begin(), Distances.size() - 1)}; + auto const before{after > 0 ? after - 1 : 0}; + auto const span{Distances[after] - Distances[before]}; + return glm::mix(Points[before], Points[after], span > 1e-9 ? std::clamp((Along - Distances[before]) / span, 0.0, 1.0) : 0.0); +} + +// an end of a stretch of a station track between the switches: at a switch within the track or at an end of it, the distances along +// the track +struct bound +{ + TTrack const *node{nullptr}; // the switch, none at the end of the tracks + bool open{false}; // no switch: the circuit ends by another of plain track + double joint{0.0}; // the end of the circuit of the stretch, the edge of a level crossing + std::optional fouling; // of the switch + double start{0.0}; // of the switch, where the stretch meets it at its start; else the end of the tracks + bool crossing{false}; // a level crossing +}; + +// the limit of a stretch at its end, Inward: the way into it along the track. Behind the trains stopped by a signal the joint, or the +// fouling point where it lies within; else the fouling point, the start of the switch or the end of the tracks +limit limit_at(bound const &Bound, double const Inward, bool const Signalled) +{ + limit found; + found.along = Bound.joint; + if (Bound.crossing) + found.what = limit::kind::crossing; + if (Bound.open || Bound.crossing) + return found; + if (Bound.node == nullptr) + { + found.what = limit::kind::buffer; + found.along = Bound.start; + return found; + } + found.name = Bound.node->name(); + if (Bound.fouling.has_value()) + { + if (false == Signalled || (*Bound.fouling - Bound.joint) * Inward > 0.0) + { + found.what = limit::kind::fouling; + found.along = *Bound.fouling; + } + } + else if (false == Signalled) + { + found.what = limit::kind::start; + found.along = Bound.start; + } + return found; +} + +// the roads by the station, each as points along its axis with its width +struct road +{ + std::vector points; + double width{0.0}; + glm::dvec2 low{0.0}; // the corners of the plan around it + glm::dvec2 high{0.0}; +}; + +std::vector roads_by(layout const &Layout) +{ + std::vector result; + glm::dvec2 low{std::numeric_limits::max()}; + glm::dvec2 high{std::numeric_limits::lowest()}; + for (auto const &path : Layout.paths) + for (auto const &point : path.points) + { + low = glm::min(low, glm::dvec2{point.x, point.z}); + high = glm::max(high, glm::dvec2{point.x, point.z}); + } + low -= glm::dvec2{50.0}; + high += glm::dvec2{50.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || (track->iCategoryFlag & 2) == 0 || track->m_editorremoved) + continue; + for (auto const &segment : track->m_paths) + { + bezier const curve{segment}; + auto const length{curve.plan_length(16)}; + if (false == std::isfinite(length)) + continue; + auto const count{std::clamp(static_cast(length / 2.0), 2, 64)}; + road entry; + entry.width = std::max(track->fTrackWidth, track->fTrackWidth2); + entry.low = glm::dvec2{std::numeric_limits::max()}; + entry.high = glm::dvec2{std::numeric_limits::lowest()}; + for (int i = 0; i <= count; ++i) + { + entry.points.push_back(curve.point(static_cast(i) / count)); + entry.low = glm::min(entry.low, glm::dvec2{entry.points.back().x, entry.points.back().z}); + entry.high = glm::max(entry.high, glm::dvec2{entry.points.back().x, entry.points.back().z}); + } + if (entry.low.x <= high.x && entry.high.x >= low.x && entry.low.y <= high.y && entry.high.y >= low.y) + result.push_back(std::move(entry)); + } + } + return result; +} + +double cross(glm::dvec2 const &A, glm::dvec2 const &B) +{ + return A.x * B.y - A.y * B.x; +} + +// the stretches along the track the roads crossing it on its level take, from the edge to the edge of each, in order +std::vector> crossings_of(std::vector const &Points, std::vector const &Distances, std::vector const &Roads) +{ + std::vector> result; + glm::dvec2 low{std::numeric_limits::max()}; + glm::dvec2 high{std::numeric_limits::lowest()}; + for (auto const &point : Points) + { + low = glm::min(low, glm::dvec2{point.x, point.z}); + high = glm::max(high, glm::dvec2{point.x, point.z}); + } + for (auto const &road : Roads) + { + if (road.low.x > high.x || road.high.x < low.x || road.low.y > high.y || road.high.y < low.y) + continue; + for (std::size_t k = 1; k < road.points.size(); ++k) + { + glm::dvec2 const r0{road.points[k - 1].x, road.points[k - 1].z}; + glm::dvec2 const across{glm::dvec2{road.points[k].x, road.points[k].z} - r0}; + if (glm::dot(across, across) < 1e-9) + continue; + for (std::size_t i = 1; i < Points.size(); ++i) + { + glm::dvec2 const p0{Points[i - 1].x, Points[i - 1].z}; + glm::dvec2 const along{glm::dvec2{Points[i].x, Points[i].z} - p0}; + auto const determinant{cross(along, across)}; + if (std::abs(determinant) < 1e-9) + continue; + auto const u{cross(r0 - p0, across) / determinant}; + auto const t{cross(r0 - p0, along) / determinant}; + if (u < 0.0 || u > 1.0 || t < 0.0 || t > 1.0) + continue; + if (std::abs(glm::mix(road.points[k - 1].y, road.points[k].y, t) - glm::mix(Points[i - 1].y, Points[i].y, u)) > kLevel) + continue; + auto const sine{std::abs(determinant) / (glm::length(along) * glm::length(across))}; + auto const half{road.width * 0.5 / std::max(sine, kSkew)}; + auto const at{Distances[i - 1] + (Distances[i] - Distances[i - 1]) * u}; + result.emplace_back(at - half, at + half); + } + } + } + std::sort(result.begin(), result.end()); + std::vector> merged; + for (auto const &part : result) + if (false == merged.empty() && part.first <= merged.back().second) + merged.back().second = std::max(merged.back().second, part.second); + else + merged.push_back(part); + return merged; +} + +} // namespace + +std::vector track_lengths(layout const &Layout, std::vector const &Tracks, std::vector const &Signals) +{ + std::vector result; + auto const roads{roads_by(Layout)}; + for (auto const &track : Tracks) + { + std::vector own; + std::unordered_map circuit_of; + for (std::size_t circuit = 0; circuit < track.circuits.size(); ++circuit) + for (auto *part : track.circuits[circuit]) + if (false == node(*part) && circuit_of.emplace(part, circuit).second) + own.push_back(part); + auto const ends{ends_out(own)}; + if (ends.empty()) + continue; + // from the end at the lower chainage along its tracks, through the switches between them: the stretches between the switches + // and the joints, each between the end of one bound and the start of the next + auto const first{std::min_element(ends.begin(), ends.end(), [&](track_end const &A, track_end const &B) { return project(Layout, A.point).x < project(Layout, B.point).x; })}; + std::unordered_set const members(own.begin(), own.end()); + std::unordered_set taken; + std::vector points; + std::vector distances; + std::vector> paths; + std::vector> bounds; // of the stretch before, of the one after + double within{0.0}; + auto const add = [&](TTrack const &Track, int const Path, bool const Atend) { + double length{0.0}; + auto const along{from_end(Track, Path, Atend, length)}; + for (std::size_t i = points.empty() ? 0 : 1; i < along.size(); ++i) + { + distances.push_back(points.empty() ? 0.0 : distances.back() + plan_distance(points.back(), along[i])); + points.push_back(along[i]); + } + paths.emplace_back(&Track, Path); + }; + auto const low{out_of(*first->track, first->atend)}; + { + bound after; + after.node = low.node; + after.open = low.open; + if (low.fouling.has_value()) + after.fouling = -*low.fouling; + after.start = -low.out; + bounds.emplace_back(bound{}, after); + } + auto *at{first->track}; + auto atend{first->atend}; + taken.insert(at); + add(*at, 0, atend); + while (true) + { + auto const way{way_on(*at, false == atend, members, taken, kReach)}; + if (way.empty()) + break; + if (way.size() == 1 && circuit_of[way.front().track] == circuit_of[at]) + { + at = way.front().track; + atend = way.front().atend; + taken.insert(at); + add(*at, 0, atend); + continue; + } + bound before; + bound after; + before.joint = distances.back(); + for (std::size_t i = 0; i < way.size(); ++i) + { + auto const &step{way[i]}; + auto const from{distances.back()}; + if (i + 1 == way.size()) + after.joint = from; + add(*step.track, step.path, step.atend); + if (false == node(*step.track)) + continue; + within += distances.back() - from; + if (before.node == nullptr) + { + before.node = step.track; + before.start = from; + if (auto const out{fouling(*step.track, step.path, step.atend)}) + before.fouling = from - *out; + } + after.node = step.track; + after.start = distances.back(); + after.fouling.reset(); + if (auto const out{fouling(*step.track, step.path, false == step.atend)}) + after.fouling = distances.back() + *out; + } + before.open = after.open = before.node == nullptr; + bounds.emplace_back(before, after); + at = way.back().track; + atend = way.back().atend; + taken.insert(at); + } + auto const length{distances.back()}; + auto const high{out_of(*at, false == atend)}; + { + bound before; + before.node = high.node; + before.open = high.open; + before.joint = length; + if (high.fouling.has_value()) + before.fouling = length + *high.fouling; + before.start = length + high.out; + bounds.emplace_back(before, bound{}); + } + // the level crossings within the stretches part them + for (auto const &[from, to] : crossings_of(points, distances, roads)) + { + auto const middle{(from + to) * 0.5}; + for (std::size_t index = 0; index + 1 < bounds.size(); ++index) + if (middle > bounds[index].second.joint && middle < bounds[index + 1].first.joint) + { + bound before; + before.crossing = true; + before.joint = from; + bound after; + after.crossing = true; + after.joint = to; + bounds.insert(bounds.begin() + index + 1, {before, after}); + break; + } + } + lengths entry; + entry.number = track.number; + entry.construction = length + (low.open ? 0.0 : low.out) + (high.open ? 0.0 : high.out) - within; + for (std::size_t i = low.points.size(); i-- > 1;) + { + entry.points.push_back(low.points[i]); + entry.distances.push_back(-low.distances[i]); + } + entry.points.insert(entry.points.end(), points.begin(), points.end()); + entry.distances.insert(entry.distances.end(), distances.begin(), distances.end()); + for (std::size_t i = 1; i < high.points.size(); ++i) + { + entry.points.push_back(high.points[i]); + entry.distances.push_back(length + high.distances[i]); + } + // the signals by it: nearer to it than to any other track, facing along it + struct stop + { + double along{0.0}; + int facing{1}; + std::string name; + }; + std::vector stops; + for (auto const &signal : Signals) + { + glm::dvec2 const point{signal.point.x, signal.point.z}; + auto gap{std::numeric_limits::max()}; + double along{0.0}; + glm::dvec2 tangent{0.0, 1.0}; + for (std::size_t i = 1; i < points.size(); ++i) + { + glm::dvec2 const a{points[i - 1].x, points[i - 1].z}; + glm::dvec2 const segment{glm::dvec2{points[i].x, points[i].z} - a}; + auto const length2{glm::dot(segment, segment)}; + if (length2 < 1e-12) + continue; + auto const t{std::clamp(glm::dot(point - a, segment) / length2, 0.0, 1.0)}; + if (auto const distance{glm::distance(point, a + segment * t)}; distance < gap) + { + gap = distance; + along = distances[i - 1] + (distances[i] - distances[i - 1]) * t; + tangent = segment / std::sqrt(length2); + } + } + if (gap > kBeside || std::any_of(Layout.paths.begin(), Layout.paths.end(), [&](path const &Path) { + return std::none_of(paths.begin(), paths.end(), [&](auto const &Own) { return Own.first == Path.track && Own.second == Path.index; }) && plan_gap(signal.point, Path.points) < gap - 0.01; + })) + continue; + auto const cosine{glm::dot(glm::normalize(glm::dvec2{signal.travel.x, signal.travel.z} + glm::dvec2{1e-12, 0.0}), tangent)}; + if (std::abs(cosine) >= kFacing) + stops.push_back({along, cosine > 0.0 ? 1 : -1, signal.name}); + } + // each way the longest: from a signal facing the trains back to the end of its stretch behind them, else of the stretches + // between the fouling points + for (int way = 0; way < 2; ++way) + { + auto const forward{way == 0}; + auto const inward{forward ? 1.0 : -1.0}; + auto &part{entry.ways[way]}; + part.length = -1.0; + for (std::size_t index = 0; index + 1 < bounds.size(); ++index) + { + auto const &before{bounds[index].second}; + auto const &after{bounds[index + 1].first}; + auto const rear{forward ? limit_at(before, inward, true) : limit_at(after, inward, true)}; + for (auto const &stop : stops) + { + if (stop.facing != (forward ? 1 : -1) || stop.along < before.joint || stop.along > after.joint) + continue; + auto const length{(stop.along - rear.along) * inward}; + if (length > 0.0 && length > part.length) + { + part.rear = rear; + part.head.what = limit::kind::signal; + part.head.name = stop.name; + part.head.along = stop.along; + part.length = length; + } + } + } + if (part.length < 0.0) + for (std::size_t index = 0; index + 1 < bounds.size(); ++index) + { + auto const lower{limit_at(bounds[index].second, 1.0, false)}; + auto const upper{limit_at(bounds[index + 1].first, -1.0, false)}; + if (auto const length{upper.along - lower.along}; length > part.length) + { + part.rear = forward ? lower : upper; + part.head = forward ? upper : lower; + part.length = length; + } + } + part.length = std::max(0.0, part.length); + part.head.point = point_along(entry.points, entry.distances, part.head.along); + part.rear.point = point_along(entry.points, entry.distances, part.rear.along); + } + result.push_back(std::move(entry)); + } + return result; +} + } // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index 5be6b707..ff676c15 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 @@ -186,4 +187,43 @@ double distance_on(line const &Line, glm::dvec3 const &Point, double &Gap); // signal odd on track 1, even on track 2: 241, 242; Rising: its trains run towards the higher km std::string block_number(double const Km, int const Track, bool const Rising); +// a limit of the part of a station track a train stands on +struct limit +{ + enum class kind + { + signal, + joint, // of the track circuits + fouling, // the fouling point of a switch + start, // of a switch + buffer, // the end of the tracks, where the buffer stop stands + crossing // the edge of a road crossing the track on its level + }; + kind what{kind::joint}; + std::string name; // of the signal, of the switch for the others + double along{0.0}; // along the track, from its end at the lower chainage + glm::dvec3 point{0.0}; +}; +// the part of a station track the trains running one way stand on +struct stand +{ + limit head; // the trains stop by it + limit rear; // behind them + double length{0.0}; +}; +// the lengths of a station track, as the list of the tracks of a station gives them: the rozporządzenie of the minister of +// infrastructure on the railway structures (Dz.U. 2026 poz. 1225) § 2 +struct lengths +{ + std::string number; + double construction{0.0}; // between the ends of the switches at its ends, or to the buffer stop, the switches within it left out + // the useful one, of the trains running along the chainage, then of those running against it: from the signal they stop by back + // to the joint, the fouling point or the level crossing behind them, between those where no signal stands + std::array ways; + std::vector points; // along the track from its end at the lower chainage, through the switches within it + std::vector distances; // of the points, from that end +}; +// 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); + } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 851fb7dd..7178f8c3 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10801,3 +10801,87 @@ msgstr "Słupki pójdą według kilometra podanego w punkcie i kierunku, w któr msgid "Follow the km of line %s" msgstr "Według kilometrażu linii %s" +msgid "signal %s" +msgstr "sygnalizatora %s" + +msgid "the fouling point of switch %s" +msgstr "ukresu rozjazdu %s" + +msgid "the start of switch %s" +msgstr "początku rozjazdu %s" + +msgid "the buffer stop" +msgstr "kozła oporowego" + +msgid "the joint" +msgstr "złącza izolowanego" + +msgid "the joint by switch %s" +msgstr "złącza izolowanego przy rozjeździe %s" + +msgid "Track lengths" +msgstr "Długości torów" + +msgid "Set the track circuits first: the station tracks are measured by their numbers, from their joints, the fouling points and the signals" +msgstr "Najpierw ustaw izolacje: tory stacyjne mierzy się po ich numerach, od złączy izolowanych, ukresów i sygnalizatorów" + +msgid "To the right" +msgstr "W prawo" + +msgid "To the left" +msgstr "W lewo" + +msgid "Train up to" +msgstr "Pociąg do" + +msgid "Track %s" +msgstr "Tor %s" + +msgid "%s: %.0f m, from %s to %s" +msgstr "%s: %.0f m, od %s do %s" + +msgid "Construction length %.0f m: between the switches at its ends, the switches within it left out" +msgstr "Długość budowlana %.0f m: między rozjazdami na jego końcach, bez rozjazdów pośrednich" + +msgid "LMB: show it" +msgstr "LPM: pokaż go" + +msgid "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" +msgstr "Długość użyteczna: miejsce postoju pociągu, od sygnalizatora, przy którym staje, do złącza izolowanego, ukresu albo przejazdu za nim, a gdzie sygnalizatora nie ma, między nimi (wyszarzone). Najdłuższy pociąg zostawia z niej 25 m: 10 m na zatrzymanie, 10 m na widoczność sygnału, 5 m do złącza" + +msgid "track %s: %.0f m" +msgstr "tor %s: %.0f m" + +msgid "the level crossing" +msgstr "przejazdu kolejowo-drogowego" + +msgid "Give the station its code first: its tracks are numbered and measured after it" +msgstr "Najpierw nadaj stacji skrót: po nim numeruje się i mierzy jej tory" + +msgid "The station tracks have no ends to be measured between" +msgstr "Tory stacyjne nie mają końców, między którymi dałoby się je zmierzyć" + +msgid "The two ways differ: their trains stop on other stretches of the track, between other switches or signals" +msgstr "Kierunki się różnią: pociągi każdego z nich stają na innym odcinku toru, między innymi rozjazdami albo sygnalizatorami" + +msgid "No signal stands by the station tracks yet, the lengths run between the fouling points: set the signals for the useful ones" +msgstr "Przy torach stacyjnych nie stoi jeszcze żaden sygnalizator, długości liczą się między ukresami: ustaw semafory, żeby wyszły użyteczne" + +msgid "Copy the list of tracks" +msgstr "Kopiuj wykaz torów" + +msgid "The lengths of %zu track copied" +msgid_plural "The lengths of %zu tracks copied" +msgstr[0] "Skopiowano długości %zu toru" +msgstr[1] "Skopiowano długości %zu torów" +msgstr[2] "Skopiowano długości %zu torów" + +msgid "The lengths of the station tracks as a table to paste into a spreadsheet: the construction length and the useful one each way with its limits, as the list of the tracks of a station gives them" +msgstr "Długości torów stacyjnych jako tabela do wklejenia w arkusz: długość budowlana i użyteczna w każdą stronę z jej granicami, tak jak podaje je wykaz torów stacji" + +msgid "Track\tConstruction length [m]\tUseful to the right [m]\tfrom\tto\tUseful to the left [m]\tfrom\tto" +msgstr "Tor\tDługość budowlana [m]\tUżyteczna w prawo [m]\tod\tdo\tUżyteczna w lewo [m]\tod\tdo" + +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" +