From d14b4534ee5583f5a68444b2d91eb20a17deebe6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 12:51:58 +0200 Subject: [PATCH] feat(editor): the station schematic generalized: each station track on a row of its own, the stretches between the switches shortened, the connections slanted between the rows, the signals and the useful lengths on the drawn tracks --- application/editormodesrk.cpp | 180 +++++++++++--- application/editormodesrk.h | 5 + editor/editorStation.cpp | 429 ++++++++++++++++++++++++++++++++++ editor/editorStation.hpp | 24 ++ 4 files changed, 602 insertions(+), 36 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 23a261ec..251182ac 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -834,6 +834,7 @@ void srk_tool::open(TTrack &Track) return; } m_layout = std::move(found); + m_plot = station::generalize(*m_layout); m_hover = -1; m_view_state.exaggeration = 0.0f; m_history_revision = m_history.history_revision(); @@ -3656,6 +3657,18 @@ void srk_tool::survey_signals() auto const ahead{station::project(*m_layout, signal.position + glm::dvec3{travel.x, 0.0, travel.y} * 5.0)}; auto const unfit{std::any_of(m_heads.begin(), m_heads.end(), [&](head_check const &Head) { return Head.instance == signal.instance && (false == Head.fits || false == Head.problems.empty()); })}; m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem), signal.position + glm::dvec3{0.0, signal.kind == "shunt" ? 3.0 : 7.0, 0.0}, unfit}); + // in the plot on the track it stands by, to its side + auto &mark{m_signal_marks.back()}; + mark.drawn = m_plot.at(at); + auto nearest{6.0}; + for (std::size_t path = 0; path < m_layout->paths.size(); ++path) + for (std::size_t k = 0; k < m_layout->paths[path].schematic.size(); ++k) + if (auto const &point{m_layout->paths[path].schematic[k]}; glm::distance(point, at) < nearest) + { + nearest = glm::distance(point, at); + mark.drawn = m_plot.paths[path][k]; + mark.side = at.y > point.y ? 1.0f : -1.0f; + } if (signal.kind != "main" && signal.kind != "shunt") continue; station::signal stop; @@ -4095,6 +4108,7 @@ 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_ticks_drawn.clear(); m_lengths.clear(); m_measured = false; m_numbered = false; @@ -4143,6 +4157,7 @@ void srk_tool::survey() nearest = distance; on = layout.paths[path].schematic[k]; mark.world = layout.paths[path].points[k]; + mark.drawn = m_plot.paths[path][k]; } mark.at = on; auto const entry{entry_circuit(mark.text, stem)}; @@ -4161,6 +4176,7 @@ void srk_tool::survey() glm::dvec3 point; glm::dvec3 way; int circuit; + glm::dvec2 drawn; }; std::vector ends; for (std::size_t index = 0; index < layout.paths.size(); ++index) @@ -4169,8 +4185,8 @@ void srk_tool::survey() if (points.size() < 2) continue; auto const circuit{m_circuit_of[index]}; - ends.push_back({points.front(), points[1] - points.front(), circuit}); - ends.push_back({points.back(), points.back() - points[points.size() - 2], circuit}); + ends.push_back({points.front(), points[1] - points.front(), circuit, m_plot.paths[index].front()}); + ends.push_back({points.back(), points.back() - points[points.size() - 2], circuit, m_plot.paths[index].back()}); } m_joints.clear(); for (std::size_t i = 0; i < ends.size(); ++i) @@ -4179,6 +4195,7 @@ void srk_tool::survey() std::none_of(m_joints.begin(), m_joints.end(), [&](joint_mark const &Joint) { return glm::distance(Joint.at, ends[i].point) < 0.05; })) { m_ticks.push_back(station::project(layout, ends[i].point)); + m_ticks_drawn.push_back(ends[i].drawn); glm::dvec2 const way{glm::normalize(glm::dvec2{ends[i].way.x, ends[i].way.z} + glm::dvec2{1e-12, 0.0})}; m_joints.push_back({ends[i].point, glm::dvec3{-way.y, 0.0, way.x}}); } @@ -4203,12 +4220,25 @@ void srk_tool::fit_view(float const Width, float const Height) from = std::min(from, row.chainage + row.outward * row.length); to = std::max(to, row.chainage + row.outward * row.length); } + from = m_plot.x(from); + to = m_plot.x(to); auto const span{std::max(50.0, to - from)}; view.from = from - span * 0.04 - 30.0; view.to = to + span * 0.04 + 30.0; - view.centre = (layout.low + layout.high) * 0.5; + auto low{std::numeric_limits::max()}; + auto high{std::numeric_limits::lowest()}; + for (std::size_t index = 0; index < layout.paths.size() && index < m_plot.paths.size(); ++index) + if (false == layout.paths[index].approach) + for (auto const &point : m_plot.paths[index]) + { + low = std::min(low, point.y); + high = std::max(high, point.y); + } + if (low > high) + low = high = 0.0; + view.centre = (low + high) * 0.5; auto const across{(view.to - view.from) / std::max(1.0f, Width)}; - auto const vertical{(layout.high - layout.low + 20.0) / std::max(1.0f, Height * 0.75f)}; + auto const vertical{(high - low + 2.0 * station::row_height) / std::max(1.0f, Height * 0.85f)}; view.exaggeration = static_cast(std::clamp(across / vertical, 1.0, 500.0)); } @@ -4245,9 +4275,16 @@ void srk_tool::render_canvas(float const Height) view.to -= io.MouseDelta.x * across; view.centre += io.MouseDelta.y * vertical; } - auto const x_of = [&](double const Chainage) { return corner.x + static_cast((Chainage - view.from) / across); }; - auto const y_of = [&](double const Offset) { return corner.y + size.y * 0.5f - static_cast((Offset - view.centre) / vertical); }; - auto const screen_of = [&](glm::dvec2 const &Point) { return ImVec2(x_of(Point.x), y_of(Point.y)); }; + // the plot draws the generalized schematic: its x shortens the chainage between the switches, its height puts the tracks on rows + auto const plot_x = [&](double const X) { return corner.x + static_cast((X - view.from) / across); }; + auto const plot_y = [&](double const Height) { return corner.y + size.y * 0.5f - static_cast((Height - view.centre) / vertical); }; + auto const x_of = [&](double const Chainage) { return plot_x(m_plot.x(Chainage)); }; + auto const screen_of = [&](glm::dvec2 const &Point) { return ImVec2(plot_x(Point.x), plot_y(Point.y)); }; + auto const chainage_at = [&](float const Mouse) { return m_plot.chainage(view.from + (Mouse - corner.x) * across); }; + auto const drawn_of = [&](std::size_t const Index) -> std::vector const & { + static std::vector const none; + return Index < m_plot.paths.size() ? m_plot.paths[Index] : none; + }; // the path under the mouse if (hovered && false == m_dragging) @@ -4257,7 +4294,7 @@ void srk_tool::render_canvas(float const Height) auto const &mouse{io.MousePos}; for (int index = 0; index < static_cast(layout.paths.size()); ++index) { - auto const &points{layout.paths[index].schematic}; + auto const &points{drawn_of(index)}; for (std::size_t i = 1; i < points.size(); ++i) { auto const a{screen_of(points[i - 1])}; @@ -4286,8 +4323,8 @@ void srk_tool::render_canvas(float const Height) for (std::size_t index = 0; index < m_lines.size() && index < m_line_ways.size() && line_hover < 0; ++index) { auto const &row{m_lines[index]}; - auto const distance{(view.from + (io.MousePos.x - corner.x) * across - row.chainage) * row.outward}; - if (std::abs(io.MousePos.y - y_of(row.offset)) < 6.0f && distance >= 0.0 && distance <= row.length) + auto const distance{(chainage_at(io.MousePos.x) - row.chainage) * row.outward}; + if (std::abs(io.MousePos.y - plot_y(m_plot.at({row.chainage, row.offset}).y)) < 6.0f && distance >= 0.0 && distance <= row.length) { line_hover = static_cast(index); line_at = distance; @@ -4318,37 +4355,44 @@ void srk_tool::render_canvas(float const Height) draw->PushClipRect(corner, corner_end, true); // the km, or the chainage, along the top { - auto const metres{across * 90.0}; + auto const first{chainage_at(corner.x)}; + auto const last{chainage_at(corner_end.x)}; + auto const metres{(last - first) / size.x * 90.0}; double step{10.0}; for (auto const candidate : {10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0}) if ((step = candidate) >= metres) break; - // the km of the station where it has them, else the chainage + // the km of the station where it has them, else the chainage; the shortened stretches skip the marks too near double km_from{0.0}; double km_to{0.0}; - auto const kilometrage{km_of_open(view.from, km_from) && km_of_open(view.to, km_to) && std::abs(km_to - km_from) > 1e-6}; - auto const low{kilometrage ? std::min(km_from, km_to) * 1000.0 : view.from}; - auto const high{kilometrage ? std::max(km_from, km_to) * 1000.0 : view.to}; + auto const kilometrage{km_of_open(first, km_from) && km_of_open(last, km_to) && std::abs(km_to - km_from) > 1e-6}; + auto const low{kilometrage ? std::min(km_from, km_to) * 1000.0 : first}; + auto const high{kilometrage ? std::max(km_from, km_to) * 1000.0 : last}; + auto shown{std::numeric_limits::lowest()}; for (auto mark{std::ceil(low / step) * step}; mark <= high; mark += step) { - auto const x{x_of(kilometrage ? view.from + (mark / 1000.0 - km_from) / (km_to - km_from) * (view.to - view.from) : mark)}; - draw->AddLine(ImVec2(x, corner.y), ImVec2(x, corner_end.y), IM_COL32(255, 255, 255, 16)); + auto const x{x_of(kilometrage ? first + (mark / 1000.0 - km_from) / (km_to - km_from) * (last - first) : mark)}; auto const label{kilometrage ? format("km %.3f", mark / 1000.0) : format("%.0f m", mark)}; + if (std::abs(x - shown) < ImGui::CalcTextSize(label.c_str()).x + 12.0f) + continue; + shown = x; + draw->AddLine(ImVec2(x, corner.y), ImVec2(x, corner_end.y), IM_COL32(255, 255, 255, 16)); draw->AddText(ImVec2(x + 3.0f, corner.y + 2.0f), IM_COL32(150, 155, 165, 255), label.c_str()); } } auto const *selected{m_selection.selected_track()}; - auto const draw_path = [&](station::path const &Path, ImU32 const Colour, float const Width) { - for (std::size_t i = 1; i < Path.schematic.size(); ++i) - draw->AddLine(screen_of(Path.schematic[i - 1]), screen_of(Path.schematic[i]), Colour, Width); + auto const draw_path = [&](std::size_t const Index, ImU32 const Colour, float const Width) { + auto const &points{drawn_of(Index)}; + for (std::size_t i = 1; i < points.size(); ++i) + draw->AddLine(screen_of(points[i - 1]), screen_of(points[i]), Colour, Width); }; - for (auto const &path : layout.paths) - if (path.approach) - draw_path(path, kApproach, 2.0f); + for (std::size_t index = 0; index < layout.paths.size(); ++index) + if (layout.paths[index].approach) + draw_path(index, kApproach, 2.0f); for (std::size_t index = 0; index < layout.paths.size(); ++index) if (auto const &path{layout.paths[index]}; false == path.approach || m_circuit_of[index] >= 0) - draw_path(path, m_circuit_of[index] >= 0 ? m_circuits[m_circuit_of[index]].colour : is_switch(*path.track) ? kSwitch : kPlain, 2.5f); - for (auto const &tick : m_ticks) + draw_path(index, m_circuit_of[index] >= 0 ? m_circuits[m_circuit_of[index]].colour : is_switch(*path.track) ? kSwitch : kPlain, 2.5f); + for (auto const &tick : m_ticks_drawn) { auto const at{screen_of(tick)}; draw->AddLine(ImVec2(at.x, at.y - 5.0f), ImVec2(at.x, at.y + 5.0f), IM_COL32(255, 255, 255, 220), 2.0f); @@ -4358,7 +4402,7 @@ void srk_tool::render_canvas(float const Height) for (std::size_t index = 0; index < m_lines.size(); ++index) { auto const &row{m_lines[index]}; - auto const y{y_of(row.offset)}; + auto const y{plot_y(m_plot.at({row.chainage, row.offset}).y)}; draw->AddLine(ImVec2(line_x(row, 0.0), y), ImVec2(line_x(row, row.length), y), kApproach, 2.0f); for (std::size_t k = 0; k < row.sections.size(); ++k) { @@ -4376,11 +4420,11 @@ void srk_tool::render_canvas(float const Height) draw->AddTriangleFilled(ImVec2(x - 4.0f * way, y - 4.0f), ImVec2(x - 4.0f * way, y + 4.0f), ImVec2(x + 4.0f * way, y), kSignal); } } - for (auto const &path : layout.paths) - if (path.track == selected) - draw_path(path, overlay_color::selected, 4.0f); + for (std::size_t index = 0; index < layout.paths.size(); ++index) + if (layout.paths[index].track == selected) + draw_path(index, overlay_color::selected, 4.0f); if (m_hover >= 0) - draw_path(layout.paths[m_hover], kHovered, 4.0f); + draw_path(static_cast(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; @@ -4396,7 +4440,7 @@ void srk_tool::render_canvas(float const Height) std::vector line; for (auto const &point : stretch_of(track, part.rear.along, part.head.along)) { - auto at{screen_of(station::project(layout, point))}; + auto at{screen_of(m_plot.at(station::project(layout, point)))}; at.y += shift; line.push_back(at); } @@ -4433,7 +4477,10 @@ void srk_tool::render_canvas(float const Height) for (std::size_t index = 0; index < layout.switches.size(); ++index) { auto const *track{layout.switches[index]}; - auto const at{screen_of(station::project(layout, track->m_paths.front().points[segment_data::point::start]))}; + auto at{screen_of(m_plot.at(station::project(layout, track->m_paths.front().points[segment_data::point::start])))}; + for (std::size_t path = 0; path < layout.paths.size(); ++path) + if (layout.paths[path].track == track && layout.paths[path].index == 0 && false == drawn_of(path).empty()) + at = screen_of(drawn_of(path).front()); draw->AddCircleFilled(at, 3.5f, kSwitch); auto text{track->name()}; if (false == stem.empty() && text.size() > stem.size() && text.compare(0, stem.size(), stem) == 0) @@ -4448,7 +4495,8 @@ void srk_tool::render_canvas(float const Height) // the signals: a flag pointing the way of the trains they face for (auto const &mark : m_signal_marks) { - auto const at{screen_of(mark.at)}; + auto at{screen_of(mark.drawn)}; + at.y -= mark.side * 6.0f; auto const way{mark.ahead ? 1.0f : -1.0f}; auto const colour{mark.unfit ? kUnfit : kSignal}; draw->AddTriangleFilled(ImVec2(at.x - 4.0f * way, at.y - 4.0f), ImVec2(at.x - 4.0f * way, at.y + 4.0f), ImVec2(at.x + 4.0f * way, at.y), colour); @@ -4458,7 +4506,7 @@ void srk_tool::render_canvas(float const Height) // track of a double-track one, under the lower for (auto const &row : m_lines) { - auto const y{y_of(row.offset)}; + auto const y{plot_y(m_plot.at({row.chainage, row.offset}).y)}; auto const up{row.partner < 0 || row.partner >= static_cast(m_lines.size()) || row.offset >= m_lines[row.partner].offset}; auto const outside = [&](float const Tall, float const Level) { return up ? y - 5.0f - (Tall + 3.0f) * Level + 3.0f : y + 5.0f + (Tall + 3.0f) * (Level - 1.0f); }; auto const tall{height * (row.km_known ? 2.0f : 1.0f)}; @@ -4483,14 +4531,14 @@ void srk_tool::render_canvas(float const Height) for (auto const index : order) { auto const &mark{m_circuits[index]}; - auto const at{screen_of(mark.at)}; + auto const at{screen_of(mark.drawn)}; label(mark.text, mark.colour, {ImVec2(at.x, at.y + 4.0f), ImVec2(at.x, at.y - height - 4.0f)}); } draw->PopClipRect(); if (hovered && m_hover >= 0 && false == m_dragging) { auto const &path{layout.paths[m_hover]}; - auto const along{view.from + (io.MousePos.x - corner.x) * across}; + auto const along{chainage_at(io.MousePos.x)}; ImGui::BeginTooltip(); ImGui::TextUnformatted(path.track->name().empty() ? STR_C("(no name)") : path.track->name().c_str()); ImGui::TextDisabled("%s, %.1f m", is_switch(*path.track) ? STR_C("switch") : path.approach ? STR_C("line out of the station") : STR_C("track"), path.length); @@ -5057,6 +5105,66 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog(format("SELFTEST srkfix: %zu problems before, status %s", before, m_status.c_str())); return editor_selftest_result::handled; } + if (Command == "srkdrawing") + { + // srkdrawing [track=]: the schematic as the plot draws it: the rows of the tracks, the shortened chainage, the points + // on the rows, on slants, the corners; the points of the paths of the circuit, offset > row + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkdrawing: no station open"); + return editor_selftest_result::handled; + } + auto const &layout{*m_layout}; + std::map rows; + for (auto const &run : m_plot.runs) + if (false == run.paths.empty()) + rows[run.row] += format(" %s@%.1f", layout.paths[run.paths.front()].track->name().c_str(), run.offset); + std::string list; + for (auto const &[row, names] : rows) + list += format(" |%d:", row) + names; + WriteLog(format("SELFTEST srkdrawing tracks %zu, rows %zu, spacing %.2f", m_plot.runs.size(), rows.size(), m_plot.spacing)); + WriteLog("SELFTEST srkdrawing rows" + list); + WriteLog(format("SELFTEST srkdrawing switches over %.0f..%.0f m drawn %.0f..%.0f, back %.1f..%.1f", layout.from, layout.to, m_plot.x(layout.from), m_plot.x(layout.to), m_plot.chainage(m_plot.x(layout.from)), m_plot.chainage(m_plot.x(layout.to)))); + std::size_t on_rows{0}; + std::size_t between{0}; + std::size_t bends{0}; + std::size_t backwards{0}; + for (std::size_t index = 0; index < layout.paths.size(); ++index) + { + if (layout.paths[index].approach) + continue; + auto const &points{m_plot.paths[index]}; + for (auto const &point : points) + { + auto const rows{point.y / station::row_height}; + if (std::abs(rows - std::round(rows)) < 1e-6) + ++on_rows; + else + ++between; + } + for (std::size_t k = 2; k < points.size(); ++k) + { + auto const a{points[k - 1] - points[k - 2]}; + auto const b{points[k] - points[k - 1]}; + if (std::abs(a.x) > 1e-6 && std::abs(b.x) > 1e-6 && (std::abs(a.y) > 1e-6 || std::abs(b.y) > 1e-6) && std::abs(a.y / a.x - b.y / b.x) > 1e-3) + ++bends; + } + for (std::size_t k = 1; k < points.size(); ++k) + if ((points[k].x - points[k - 1].x) * (layout.paths[index].schematic[k].x - layout.paths[index].schematic[k - 1].x) < 0.0) + ++backwards; + } + WriteLog(format("SELFTEST srkdrawing points on rows %zu, between %zu, bends %zu, x backwards %zu", on_rows, between, bends, backwards)); + if (auto const values{selftest_values(Words)}; values.count("track")) + for (std::size_t index = 0; index < layout.paths.size(); ++index) + if (auto const &path{layout.paths[index]}; false == path.track->Isolated.empty() && path.track->Isolated.front()->asName == values.at("track")) + { + std::string list; + for (std::size_t k = 0; k < path.schematic.size(); k += 4) + list += format(" %.0f/%.2f>%.2f", path.schematic[k].x, path.schematic[k].y, m_plot.paths[index][k].y / station::row_height); + WriteLog(format("SELFTEST srkdrawing %s/%d:", path.track->name().c_str(), path.index) + list); + } + 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 3c603b82..1134c12a 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -96,6 +96,7 @@ class srk_tool final : public editor_tool bool m_changed{false}; std::vector m_records; std::optional m_layout; + station::drawing m_plot; // the schematic of the layout as the plot draws it int m_record{-1}; // of the open station, -1 while it has none TTrack *m_anchor{nullptr}; // the switch the open station was found from bool m_reversed{false}; // the switches numbered from the right end of the schematic @@ -114,10 +115,12 @@ class srk_tool final : public editor_tool ImU32 colour{0}; double length{0.0}; bool labelled{true}; // the circuits of the switches go without, named after them + glm::dvec2 drawn{0.0}; // in the plot }; std::vector m_circuit_of; // of each path of the layout, an index into m_circuits, -1 for none std::vector m_circuits; std::vector m_ticks; // the joints between the circuits, in the schematic + std::vector m_ticks_drawn; // the same in the plot struct joint_mark { glm::dvec3 at{0.0}; @@ -131,6 +134,8 @@ class srk_tool final : public editor_tool std::string text; glm::dvec3 head{0.0}; // above its lamps, in the scenery bool unfit{false}; // its lights don't show what its routes need + glm::dvec2 drawn{0.0}; // on its track in the plot + float side{0.0f}; // 1: it stands to the left of the track in the plot, -1: to the right }; std::vector m_signal_marks; bool m_signals_known{false}; // the signals standing surveyed, once their templates are found diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 85b1f640..a252b91a 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -641,6 +641,435 @@ double km_of(layout const &Layout, double const Chainage) return Layout.km + Chainage / 1000.0; } +namespace +{ + +// a track rising more than this to the chainage runs across the rows +double constexpr kAcross{0.04}; +// m, two tracks this far apart lie side by side +double constexpr kSideBySide{1.0}; +// m of the chainage, points of two tracks compared side by side +double constexpr kAbreast{3.0}; +// m, a plain stretch between the switches keeps this much of its length in the plot, the rest shortened to kKept of it +double constexpr kWhole{60.0}; +double constexpr kKept{0.3}; +// m, how far a slant is followed over the tracks to the row it ends on +double constexpr kSlantSearch{400.0}; +// m, the plot puts a point of the schematic on a path no farther from it +double constexpr kNearPath{15.0}; + +double shortened(double const Length) +{ + return Length <= kWhole ? Length : kWhole + (Length - kWhole) * kKept; +} + +double lengthened(double const Length) +{ + return Length <= kWhole ? Length : kWhole + (Length - kWhole) / kKept; +} + +// rows, for the offset gone off a row: at most three rows further +double rows_off(double const Steps) +{ + auto const size{std::abs(Steps)}; + return std::copysign(size <= 1.0 ? size : 1.0 + 2.0 * (1.0 - std::exp((1.0 - size) * 0.5)), Steps); +} + +} // namespace + +double drawing::x(double const Chainage) const +{ + if (warp.empty()) + return Chainage; + if (Chainage <= warp.front().x) + return warp.front().y - shortened(warp.front().x - Chainage); + if (Chainage >= warp.back().x) + return warp.back().y + shortened(Chainage - warp.back().x); + auto const next{std::upper_bound(warp.begin(), warp.end(), Chainage, [](double const Value, glm::dvec2 const &Point) { return Value < Point.x; })}; + auto const &a{*(next - 1)}; + auto const &b{*next}; + return a.y + (b.y - a.y) * (Chainage - a.x) / std::max(1e-9, b.x - a.x); +} + +double drawing::chainage(double const X) const +{ + if (warp.empty()) + return X; + if (X <= warp.front().y) + return warp.front().x - lengthened(warp.front().y - X); + if (X >= warp.back().y) + return warp.back().x + lengthened(X - warp.back().y); + auto const next{std::upper_bound(warp.begin(), warp.end(), X, [](double const Value, glm::dvec2 const &Point) { return Value < Point.y; })}; + auto const &a{*(next - 1)}; + auto const &b{*next}; + return a.x + (b.x - a.x) * (X - a.y) / std::max(1e-9, b.y - a.y); +} + +glm::dvec2 drawing::at(glm::dvec2 const &Schematic) const +{ + auto best{kNearPath * kNearPath}; + glm::dvec2 result{x(Schematic.x), Schematic.y / spacing * row_height}; + if (false == runs.empty()) + { + auto const [low, high]{std::minmax_element(runs.begin(), runs.end(), [](run const &A, run const &B) { return A.row < B.row; })}; + result.y = std::clamp(result.y, (low->row - 3.0) * row_height, (high->row + 3.0) * row_height); + } + for (std::size_t p = 0; p < schematic.size() && p < paths.size(); ++p) + { + auto const &points{schematic[p]}; + if (points.empty() || std::max(points.front().x, points.back().x) + 2.0 * kNearPath < Schematic.x || std::min(points.front().x, points.back().x) - 2.0 * kNearPath > Schematic.x) + continue; + for (std::size_t i = 1; i < points.size(); ++i) + { + auto const &a{points[i - 1]}; + auto const along{points[i] - a}; + auto const length2{glm::dot(along, along)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(Schematic - a, along) / length2, 0.0, 1.0) : 0.0}; + auto const gap{a + along * t - Schematic}; + if (auto const distance2{glm::dot(gap, gap)}; distance2 < best) + { + best = distance2; + result = paths[p][i - 1] + (paths[p][i] - paths[p][i - 1]) * t; + } + } + } + return result; +} + +drawing generalize(layout const &Layout) +{ + drawing result; + auto const &paths{Layout.paths}; + auto const count{paths.size()}; + for (auto const &path : paths) + result.schematic.push_back(path.schematic); + auto const plain = [&](std::size_t const Path) { return paths[Path].track == nullptr || false == node(*paths[Path].track); }; + auto const usable = [&](std::size_t const Path) { return paths[Path].points.size() >= 2 && paths[Path].points.size() == paths[Path].schematic.size(); }; + auto const end_point = [&](std::size_t const Path, bool const Atend) { return Atend ? paths[Path].points.back() : paths[Path].points.front(); }; + // the ends of the paths which meet: of each path, at its start and at its end + std::vector>, 2>> met(count); + for (std::size_t p = 0; p < count; ++p) + for (std::size_t q = p + 1; q < count && usable(p); ++q) + for (auto const pend : {false, true}) + for (auto const qend : {false, true}) + if (usable(q) && plan_distance(end_point(p, pend), end_point(q, qend)) < 0.05) + { + met[p][pend].emplace_back(q, qend); + met[q][qend].emplace_back(p, pend); + } + // the points running along the axis rather than across it + std::vector> along(count); + for (std::size_t p = 0; p < count; ++p) + { + auto const &points{paths[p].schematic}; + for (std::size_t i = 0; i < points.size(); ++i) + { + auto const &a{points[i > 0 ? i - 1 : i]}; + auto const &b{points[i + 1 < points.size() ? i + 1 : i]}; + along[p].push_back(std::abs(b.x - a.x) > 1e-3 && std::abs(b.y - a.y) < std::abs(b.x - a.x) * kAcross); + } + } + // the station tracks: plain paths joined end to end where nothing else meets them, each on a row of its own + std::vector run_of(count, -1); + for (std::size_t p = 0; p < count; ++p) + { + if (run_of[p] >= 0 || false == plain(p) || false == usable(p)) + continue; + auto const index{static_cast(result.runs.size())}; + result.runs.emplace_back(); + run_of[p] = index; + std::vector stack{p}; + while (false == stack.empty()) + { + auto const current{stack.back()}; + stack.pop_back(); + result.runs[index].paths.push_back(current); + for (auto const atend : {false, true}) + { + if (met[current][atend].size() != 1) + continue; + auto const [other, otherend]{met[current][atend].front()}; + if (plain(other) && run_of[other] < 0 && met[other][otherend].size() == 1) + { + run_of[other] = index; + stack.push_back(other); + } + } + } + } + // of each run the points along the axis, by the chainage: the middle offset of them, which other runs lie beside it on the right + auto &runs{result.runs}; + std::vector> samples(runs.size()); + std::vector level(runs.size(), 0); + for (std::size_t r = 0; r < runs.size(); ++r) + { + for (auto const p : runs[r].paths) + for (std::size_t i = 0; i < paths[p].schematic.size(); ++i) + if (along[p][i]) + samples[r].push_back(paths[p].schematic[i]); + if (samples[r].empty()) + continue; + level[r] = 1; + std::sort(samples[r].begin(), samples[r].end(), [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x < B.x; }); + std::vector offsets; + for (auto const &sample : samples[r]) + offsets.push_back(sample.y); + std::nth_element(offsets.begin(), offsets.begin() + offsets.size() / 2, offsets.end()); + runs[r].offset = offsets[offsets.size() / 2]; + } + std::vector> below(runs.size()); + std::vector gaps; + for (std::size_t a = 0; a < runs.size(); ++a) + for (std::size_t b = a + 1; b < runs.size(); ++b) + { + if (0 == level[a] || 0 == level[b] || samples[a].back().x + kAbreast < samples[b].front().x || samples[b].back().x + kAbreast < samples[a].front().x) + continue; + auto votes{0}; + for (auto const &sample : samples[a]) + { + auto const found{std::lower_bound(samples[b].begin(), samples[b].end(), sample.x - kAbreast, [](glm::dvec2 const &Sample, double const Value) { return Sample.x < Value; })}; + if (found == samples[b].end() || found->x > sample.x + kAbreast || std::abs(sample.y - found->y) <= kSideBySide) + continue; + votes += sample.y > found->y ? 1 : -1; + // of the tracks next to each other + if (auto const gap{std::abs(sample.y - found->y)}; gap > 3.0 && gap < 8.0) + gaps.push_back(gap); + } + if (votes > 0) + below[a].push_back(b); + else if (votes < 0) + below[b].push_back(a); + } + if (false == gaps.empty()) + { + std::nth_element(gaps.begin(), gaps.begin() + gaps.size() / 2, gaps.end()); + result.spacing = std::clamp(gaps[gaps.size() / 2], 3.5, 12.0); + } + // the rows: by the offsets, then pushed apart away from the axis where a track lies beside another on the same row or beyond + for (std::size_t r = 0; r < runs.size(); ++r) + runs[r].row = static_cast(std::lround(runs[r].offset / result.spacing)); + for (std::size_t pass = 0; pass < runs.size() + 1; ++pass) + { + auto changed{false}; + for (std::size_t a = 0; a < runs.size(); ++a) + for (auto const b : below[a]) + if (runs[a].row <= runs[b].row) + { + if (runs[a].offset + runs[b].offset >= 0.0) + runs[a].row = runs[b].row + 1; + else + runs[b].row = runs[a].row - 1; + changed = true; + } + if (false == changed) + break; + } + // the main track through the station on the middle row + if (false == Layout.main.empty()) + for (std::size_t p = 0; p < count; ++p) + if (run_of[p] >= 0 && level[run_of[p]] && paths[p].track == Layout.main.front()) + { + auto const shift{runs[run_of[p]].row}; + for (auto &run : runs) + run.row -= shift; + break; + } + // a group of tracks far aside, as a yard, closer: no more than one row left empty before it + { + std::vector used; + for (std::size_t r = 0; r < runs.size(); ++r) + if (level[r]) + used.push_back(runs[r].row); + used.push_back(0); + std::sort(used.begin(), used.end()); + used.erase(std::unique(used.begin(), used.end()), used.end()); + std::map closer{{0, 0}}; + auto const middle{static_cast(std::find(used.begin(), used.end(), 0) - used.begin())}; + for (auto i{middle + 1}; i < used.size(); ++i) + closer[used[i]] = closer[used[i - 1]] + std::min(used[i] - used[i - 1], 2); + for (auto i{middle}; i > 0; --i) + closer[used[i - 1]] = closer[used[i]] - std::min(used[i] - used[i - 1], 2); + for (std::size_t r = 0; r < runs.size(); ++r) + if (level[r]) + runs[r].row = closer[runs[r].row]; + } + // the breakpoints at the ends of the switches, the stretches between them shortened + std::vector stops; + for (std::size_t p = 0; p < count; ++p) + if (false == paths[p].approach && false == plain(p) && false == paths[p].schematic.empty()) + { + stops.push_back(paths[p].schematic.front().x); + stops.push_back(paths[p].schematic.back().x); + } + if (stops.empty()) + stops = {Layout.from, Layout.to}; + std::sort(stops.begin(), stops.end()); + for (auto const stop : stops) + if (result.warp.empty()) + result.warp.emplace_back(stop, stop); + else if (stop - result.warp.back().x > 1.0) + result.warp.emplace_back(stop, result.warp.back().y + shortened(stop - result.warp.back().x)); + // the row of each point: the row of its track where it runs along it; a switch going straight on takes the row of the track it + // joins, its curved path keeps it at its ends only + auto constexpr none{std::numeric_limits::min()}; + std::vector> rows(count); + for (std::size_t p = 0; p < count; ++p) + { + rows[p].assign(paths[p].schematic.size(), none); + if (run_of[p] >= 0 && level[run_of[p]]) + for (std::size_t i = 0; i < rows[p].size(); ++i) + if (along[p][i]) + rows[p][i] = runs[run_of[p]].row; + } + for (int pass = 0; pass < 32; ++pass) + { + auto changed{false}; + for (std::size_t p = 0; p < count; ++p) + { + if (plain(p) || false == usable(p)) + continue; + auto const joined = [&](bool const Atend) { + for (auto const &[other, otherend] : met[p][Atend]) + if (auto const value{otherend ? rows[other].back() : rows[other].front()}; value != none) + return value; + return none; + }; + auto &row{rows[p]}; + auto const straight{std::all_of(along[p].begin(), along[p].end(), [](char const Along) { return Along != 0; })}; + auto const first{along[p].front() ? joined(false) : none}; + auto const last{along[p].back() ? joined(true) : none}; + std::vector wanted(row.size(), none); + if (straight && first != none && (last == none || last == first)) + std::fill(wanted.begin(), wanted.end(), first); + else if (straight && last != none && first == none) + std::fill(wanted.begin(), wanted.end(), last); + else + { + wanted.front() = first; + wanted.back() = last; + } + if (wanted != row) + { + row = std::move(wanted); + changed = true; + } + } + if (false == changed) + break; + } + auto const height = [&](int const Row) { return static_cast(Row) * row_height; }; + // the way along the path from its end, the chord of its first metres + auto const leaving = [&](std::size_t const Path, bool const Atend) { + auto const &points{paths[Path].points}; + auto const reach{std::min(5, points.size() - 1)}; + auto const way{Atend ? points[points.size() - 1 - reach] - points.back() : points[reach] - points.front()}; + return glm::normalize(glm::dvec2{way.x, way.z} + glm::dvec2{1e-12, 0.0}); + }; + struct on_row + { + glm::dvec2 drawn{0.0}; + double offset{0.0}; // in the schematic + }; + // the first point on a row along the tracks on from the end of the path, away from it: at a switch along the path going on the + // straightest, so a slant over a ladder of switches runs to its far end + auto const outward = [&](std::size_t const From, bool const Atend, on_row &Found) { + auto current{From}; + auto exit{Atend}; + auto walked{0.0}; + std::unordered_set seen{From}; + while (walked <= kSlantSearch) + { + auto const away{-leaving(current, exit)}; + auto best{0.0}; + auto next{count}; + auto entered{false}; + for (auto const &[other, otherend] : met[current][exit]) + if (seen.count(other) == 0) + if (auto const straightness{glm::dot(leaving(other, otherend), away)}; straightness > best) + { + best = straightness; + next = other; + entered = otherend; + } + if (next == count) + return false; + seen.insert(next); + auto const &row{rows[next]}; + auto const &schematic{paths[next].schematic}; + auto const size{row.size()}; + for (std::size_t k = 0; k < size; ++k) + { + auto const index{entered ? size - 1 - k : k}; + if (k > 0) + walked += glm::distance(schematic[index], schematic[entered ? index + 1 : index - 1]); + if (walked > kSlantSearch) + return false; + if (row[index] != none) + { + Found = {{result.x(schematic[index].x), height(row[index])}, schematic[index].y}; + return true; + } + } + current = next; + exit = false == entered; + } + return false; + }; + // the points in the plot: on their rows, between them on a straight slant from the last row to the next; far from any row a few + // rows beyond the outer ones at most + auto lowest{0}; + auto highest{0}; + for (std::size_t r = 0; r < runs.size(); ++r) + if (level[r]) + { + lowest = std::min(lowest, runs[r].row); + highest = std::max(highest, runs[r].row); + } + auto const loose = [&](double const Offset) { return std::clamp(Offset / result.spacing, lowest - 3.0, highest + 3.0) * row_height; }; + result.paths.resize(count); + for (std::size_t p = 0; p < count; ++p) + { + auto const &points{paths[p].schematic}; + auto const &row{rows[p]}; + auto &drawn{result.paths[p]}; + for (std::size_t i = 0; i < points.size(); ++i) + drawn.emplace_back(result.x(points[i].x), row[i] != none ? height(row[i]) : loose(points[i].y)); + if (false == usable(p)) + continue; + for (std::size_t i = 0; i < points.size();) + { + if (row[i] != none) + { + ++i; + continue; + } + auto end{i}; + while (end + 1 < points.size() && row[end + 1] == none) + ++end; + on_row before{i > 0 ? on_row{drawn[i - 1], points[i - 1].y} : on_row{}}; + on_row after{end + 1 < points.size() ? on_row{drawn[end + 1], points[end + 1].y} : on_row{}}; + auto const from{i > 0 || outward(p, false, before)}; + auto const to{end + 1 < points.size() || outward(p, true, after)}; + for (auto k{i}; k <= end; ++k) + { + if (from && to && std::abs(after.drawn.x - before.drawn.x) > 0.5) + { + auto const t{std::clamp((drawn[k].x - before.drawn.x) / (after.drawn.x - before.drawn.x), 0.0, 1.0)}; + drawn[k].y = before.drawn.y + (after.drawn.y - before.drawn.y) * t; + } + // off a row towards no other, a line out leaving the station aside + else if (from || to) + { + auto const &known{from ? before : after}; + drawn[k].y = known.drawn.y + row_height * rows_off((points[k].y - known.offset) / result.spacing); + } + } + i = end + 1; + } + } + return result; +} + std::vector stations(options const &Options) { std::vector result; diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index eb97b20e..4596424a 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -83,6 +83,30 @@ struct circuit double length{0.0}; }; +// the schematic generalized for the plot: each station track on a row of its own, level, the rows evenly apart in the order the +// tracks lie side by side, a path between them on a straight slant; the plain stretches between the switches shortened +struct drawing +{ + struct run + { + std::vector paths; // of the layout, joined end to end without a switch between + double offset{0.0}; // the middle one of its stretches along the axis + int row{0}; + }; + std::vector runs; + double spacing{4.75}; // m, of the tracks of the station + std::vector warp; // the chainage and the x of the plot at the breakpoints, by the chainage + std::vector> paths; // of each path of the layout, its points in the plot: the x and the height + std::vector> schematic; // the same in the schematic + double x(double const Chainage) const; + double chainage(double const X) const; + // the point of the schematic in the plot: on the path nearest to it, unless none runs near + glm::dvec2 at(glm::dvec2 const &Schematic) const; +}; +// m of the plot between two rows +double constexpr row_height{5.0}; +drawing generalize(layout const &Layout); + // the station of the switch, or of the plain track near one std::optional find(TTrack &From, options const &Options = {}); // the chainage along the axis and the offset to the left of it, as the map shows it