17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-26 18:14:55 +01:00

feat(editor): the SRK tool measures the station tracks: the construction length and the useful one each way, as the rozporządzenie on the railway structures (Dz.U. 2026 poz. 1225) § 2 defines it, from the signal the trains stop by back to the joint, the fouling point or the level crossing behind them, between those where no signal stands; the switches within a track part it into stretches. A list in the panel with the longest train, the two ways marked where they differ, the track under the mouse or selected drawn in the schematic and the scenery, the list copied as a table

This commit is contained in:
maj00r
2026-10-22 15:24:02 +02:00
parent 1f8a477ec4
commit 43346b61e7
5 changed files with 978 additions and 7 deletions

View File

@@ -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 <typename Record_> 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<glm::dvec3> stretch_of(station::lengths const &Track, double const From, double const To)
{
auto const at = [&](double const Along) {
auto const after{std::min<std::size_t>(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<glm::dvec3> 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<int>(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<int>(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<glm::dvec3> 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<int>(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<double>::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<int>(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<int>(index);
return -1;
}
void srk_tool::number_switches()
{
m_status.clear();
@@ -941,7 +1160,7 @@ bool srk_tool::station_tracks(std::vector<station::numbered> &Tracks, std::unord
std::map<std::string, std::map<std::string, std::vector<TTrack *>>> 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_signal> 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<station::signal> 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<station::signal> const &Signals)
{
m_measured = true;
m_lengths.clear();
std::vector<station::numbered> tracks;
std::unordered_set<TTrack const *> 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<std::pair<std::string, ImVec2>> useful;
if (m_length_shown >= 0 && m_length_shown < static_cast<int>(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<ImVec2> 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<int>(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<ImVec4> 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<int>(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=<number>]: 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_signal> 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<int>(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

View File

@@ -107,6 +107,12 @@ class srk_tool final : public editor_tool
};
std::vector<signal_mark> m_signal_marks;
bool m_signals_known{false}; // the signals standing surveyed, once their templates are found
std::vector<station::lengths> 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<station::signal> 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();

View File

@@ -330,8 +330,9 @@ double plan_gap(glm::dvec3 const &Point, std::vector<glm::dvec3> 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<double> fouling(TTrack const &Switch, int const Path, bool const Atend)
{
auto const point{end_of(Switch, Path, Atend)};
std::optional<std::pair<int, bool>> 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<glm::dvec3> 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<step> way_on(TTrack const &Track, bool const Atend, std::unordered_set<TTrack const *> const &Own, std::unordered_set<TTrack const *> const &Taken, double const Limit)
{
std::vector<step> best;
auto shortest{Limit};
std::vector<step> current;
auto budget{kSearch};
std::function<void(TTrack const &, int, bool, double)> 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<int>(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<double> fouling; // from the end of the track out to the fouling point of the switch
std::vector<glm::dvec3> points; // out from the end of the track, into the switch
std::vector<double> 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<TTrack const *> 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<glm::dvec3> const &Points, std::vector<double> const &Distances, double const Along)
{
auto const after{std::min<std::size_t>(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<double> 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<glm::dvec3> points;
double width{0.0};
glm::dvec2 low{0.0}; // the corners of the plan around it
glm::dvec2 high{0.0};
};
std::vector<road> roads_by(layout const &Layout)
{
std::vector<road> result;
glm::dvec2 low{std::numeric_limits<double>::max()};
glm::dvec2 high{std::numeric_limits<double>::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<int>(length / 2.0), 2, 64)};
road entry;
entry.width = std::max(track->fTrackWidth, track->fTrackWidth2);
entry.low = glm::dvec2{std::numeric_limits<double>::max()};
entry.high = glm::dvec2{std::numeric_limits<double>::lowest()};
for (int i = 0; i <= count; ++i)
{
entry.points.push_back(curve.point(static_cast<double>(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<std::pair<double, double>> crossings_of(std::vector<glm::dvec3> const &Points, std::vector<double> const &Distances, std::vector<road> const &Roads)
{
std::vector<std::pair<double, double>> result;
glm::dvec2 low{std::numeric_limits<double>::max()};
glm::dvec2 high{std::numeric_limits<double>::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<std::pair<double, double>> 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<lengths> track_lengths(layout const &Layout, std::vector<numbered> const &Tracks, std::vector<signal> const &Signals)
{
std::vector<lengths> result;
auto const roads{roads_by(Layout)};
for (auto const &track : Tracks)
{
std::vector<TTrack *> own;
std::unordered_map<TTrack const *, std::size_t> 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<TTrack const *> const members(own.begin(), own.end());
std::unordered_set<TTrack const *> taken;
std::vector<glm::dvec3> points;
std::vector<double> distances;
std::vector<std::pair<TTrack const *, int>> paths;
std::vector<std::pair<bound, bound>> 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<stop> stops;
for (auto const &signal : Signals)
{
glm::dvec2 const point{signal.point.x, signal.point.z};
auto gap{std::numeric_limits<double>::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

View File

@@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include <array>
#include <functional>
#include <optional>
#include <string>
@@ -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<stand, 2> ways;
std::vector<glm::dvec3> points; // along the track from its end at the lower chainage, through the switches within it
std::vector<double> distances; // of the points, from that end
};
// the lengths of the station tracks, the signals standing by them among the signals given
std::vector<lengths> track_lengths(layout const &Layout, std::vector<numbered> const &Tracks, std::vector<signal> const &Signals);
} // namespace station

View File

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