From 7cc8e5bb9aa7cfc1a7799cecab1b85457a8ec790 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 14:16:42 +0200 Subject: [PATCH] feat(editor): the kilometrage tool leads a line through a station along the tracks chosen, the straight paths of the switches by default, from the track chosen on along them until the line meets its tracks again or on and away at a junction, kept as //$k via lines; a track beside that comes back onto the line between the stretches it runs beside it or both ways is a track of a station, not the other track of a double-track line --- application/editormode.h | 4 +- application/editormodekm.cpp | 170 +++++++++++++++++++- application/editormodekm.h | 12 ++ application/editormodetrack.cpp | 2 +- editor/editorKilometrage.cpp | 272 +++++++++++++++++++++++++++++--- editor/editorKilometrage.hpp | 40 ++++- editor/editorTrack.cpp | 4 +- editor/editorTrack.hpp | 5 +- lang/editor_pl.po | 60 +++++++ scene/scenelayers.cpp | 2 +- 10 files changed, 533 insertions(+), 38 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 4c31165f..ccb9ea1d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -163,7 +163,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector profile_library; std::optional> alignments; // the kept horizontal alignments changed too, these were the ones before std::optional> switch_records; // the kept switches changed too, these were the ones before - std::optional> kilometrage; // the marks of the kilometrage before the change (Action::Kilometrage) + std::optional kilometrage; // the kilometrage before the change (Action::Kilometrage) std::vector copies; // models an array added to the one it was made of bool joined{false}; // undone and redone together with the change before it std::shared_ptr terrain; // the heightmap terrain from before the change (Action::Terrain) @@ -188,7 +188,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void terrain_history_step(); // puts the terrain back as the step recorded it, the step left with the state it replaced void restore_terrain(EditorSnapshot &Snapshot); - // swaps the marks of the kilometrage with the ones the snapshot keeps + // swaps the kilometrage with the one the snapshot keeps void restore_kilometrage(EditorSnapshot &Snapshot); bool m_history_busy{false}; // undo or redo under way void handle_brush_mouse_hold(int Action, int Button); diff --git a/application/editormodekm.cpp b/application/editormodekm.cpp index f28c2bf3..c851fb0d 100644 --- a/application/editormodekm.cpp +++ b/application/editormodekm.cpp @@ -278,7 +278,9 @@ void km_tool::pick(TTrack &Track, glm::dvec3 const &Point) entry.point = editor_track::point_at(Track, t); auto const ahead{plan_of(editor_track::point_at(Track, std::min(1.0, t + 0.01)) - editor_track::point_at(Track, std::max(0.0, t - 0.01)))}; entry.along = glm::length(ahead) > 1e-9 ? glm::normalize(ahead) : glm::dvec2{0.0, 1.0}; + entry.path = kilometrage::path_near(Track, Point, entry.on); m_status.clear(); + m_route_status.clear(); m_edited = -1; auto const &marks{kilometrage::marks()}; auto nearest{kPickReach}; @@ -400,6 +402,84 @@ void km_tool::show(std::size_t const Index) m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); } +void km_tool::show_via(std::size_t const Index) +{ + auto const &vias{kilometrage::vias()}; + if (Index >= vias.size()) + return; + auto const point{vias[Index].point}; + auto const line{vias[Index].line}; + int path{0}; + if (auto *track{kilometrage::track_near(point, 6.0, path)}; track != nullptr) + { + pick(*track, point); + m_line = line; + } + m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); +} + +int km_tool::via_here() const +{ + if (false == m_pick.has_value() || m_pick->track == nullptr) + return -1; + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + if (vias[index].line != m_line) + continue; + int path{0}; + if (kilometrage::track_near(vias[index].point, 6.0, path) == m_pick->track && path == m_pick->path) + return static_cast(index); + } + return -1; +} + +void km_tool::lead_along() +{ + m_route_status.clear(); + if (false == m_pick.has_value() || m_pick->track == nullptr) + return; + auto const line{line_of(m_line)}; + if (line.empty()) + { + m_route_status = STR("Give the number of the line first"); + return; + } + auto const astray_of = [&]() { + auto const *axis{kilometrage::axis_of(line)}; + return axis != nullptr ? axis->astray.size() : kilometrage::marks().size(); + }; + auto const before{kilometrage::current()}; + auto const astray{astray_of()}; + kilometrage::via entry; + entry.line = line; + entry.point = m_pick->on; + kilometrage::put(entry); + auto const *axis{kilometrage::axis_of(line)}; + if (axis == nullptr || false == axis->runs_along(m_pick->track, m_pick->path)) + { + kilometrage::restore(before); + m_route_status = STR("The line can't run along this track: no way leads from it onto the line"); + return; + } + m_line = line; + m_history.push_kilometrage_snapshot(); + m_route_status = format(STR_C("Line %s runs along this track now"), line.c_str()); + if (auto const lost{astray_of()}; lost > astray) + m_route_status += "\n" + format(STR_P("%zu point of the km is off the line now: the track leads it away from it", "%zu points of the km are off the line now: the track leads it away from them", lost - astray), lost - astray); +} + +void km_tool::drop_via() +{ + m_route_status.clear(); + auto const index{via_here()}; + if (index < 0) + return; + kilometrage::remove_via(static_cast(index)); + m_history.push_kilometrage_snapshot(); + m_route_status = format(STR_C("Line %s runs along the straight paths of the switches here again"), m_line.c_str()); +} + void km_tool::read_posts() { m_status.clear(); @@ -635,7 +715,36 @@ void km_tool::render_options() } } - step_heading(2, STR_C("From the posts")); + step_heading(2, STR_C("The way through a station")); + auto const *line_axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; + if (false == m_pick.has_value() || m_pick->track == nullptr) + ImGui::TextDisabled("%s", STR_C("Through a station the km run along the straight paths of the switches. Click a track of the station to lead them along it instead")); + else if (line_axis == nullptr) + ImGui::TextDisabled("%s", STR_C("Put a point of the km of the line first")); + else if (auto const via{via_here()}; via >= 0) + { + if (std::find(line_axis->blocked.begin(), line_axis->blocked.end(), static_cast(via)) != line_axis->blocked.end()) + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", STR_C("The line doesn't run along this track: another track chosen leads it elsewhere")); + else + ImGui::TextUnformatted(format(STR_C("Line %s runs along this track, as chosen"), m_line.c_str()).c_str()); + if (ImGui::Button(STR_C("Back onto the straight paths"), ImVec2(-1.0f, 0.0f))) + drop_via(); + item_tooltip("The track chosen is forgotten, the line runs along the straight paths of the switches here again. One step of undo"); + } + else if (line_axis->runs_along(m_pick->track, m_pick->path)) + ImGui::TextDisabled("%s", format(STR_C("Line %s runs along this track"), m_line.c_str()).c_str()); + else + { + ImGui::TextDisabled("%s", format(STR_C("Line %s doesn't run along this track"), m_line.c_str()).c_str()); + if (ImGui::Button(STR_C("Lead the km along this track"), ImVec2(-1.0f, 0.0f))) + lead_along(); + item_tooltip("The line runs along the track clicked, along the path of the switch clicked, and on along the straight paths of the switches " + "until it meets its tracks again; where it doesn't, as at a junction, it goes on that way. One step of undo"); + } + if (false == m_route_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_route_status.c_str()); + + step_heading(3, STR_C("From the posts")); if (ImGui::Button(STR_C("Read the km from the posts"), ImVec2(-1.0f, 0.0f))) read_posts(); item_tooltip("Reads the hectometre posts standing along the line through the track clicked: a point of the km at the first and the last " @@ -643,7 +752,7 @@ void km_tool::render_options() if (false == m_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); - step_heading(3, STR_C("The points of the lines")); + step_heading(4, STR_C("The points of the lines")); if (lines.empty()) { ImGui::TextDisabled("%s", STR_C("None yet")); @@ -667,6 +776,22 @@ void km_tool::render_options() item_tooltip("Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by " "its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)"); } + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + if (vias[index].line != line) + continue; + auto const blocked{std::find(axis->blocked.begin(), axis->blocked.end(), index) != axis->blocked.end()}; + double km{0.0}; + auto const label{kilometrage::km_at(line, vias[index].point, km, kReadoutReach) ? format(STR_C("Along the track chosen by km %s"), kilometrage::text(km).c_str()) : std::string{STR_C("Along the track chosen")}}; + if (blocked) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.4f, 1.0f)); + if (ImGui::Selectable((label + "##kmvia" + std::to_string(index)).c_str(), false)) + show_via(index); + if (blocked) + ImGui::PopStyleColor(); + item_tooltip(blocked ? "The line doesn't run along this track: another track chosen leads it elsewhere" : "Through the station the line runs along this track, not along the straight paths of the switches"); + } auto const rows{axis->anchors.size() + axis->astray.size()}; if (false == ImGui::BeginTable(("##kmmarks" + line).c_str(), 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(260.0f, ImGui::GetTextLineHeightWithSpacing() * (rows + 1.5f))))) continue; @@ -776,6 +901,23 @@ void km_tool::draw_overlay() const if (projection.project(top, screen)) draw_label(drawlist, screen, colour, IM_COL32(16, 18, 22, 255), mark.line + ": " + mark_text(mark)); } + // the tracks chosen for the way through a station: a rhombus on the track + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + auto const &via{vias[index]}; + ImVec2 screen; + if (false == near_enough(via.point, kLabelRange * 2.0) || false == projection.project(via.point + lift, screen)) + continue; + auto blocked{false}; + if (auto const *axis{kilometrage::axis_of(via.line)}; axis != nullptr) + blocked = std::find(axis->blocked.begin(), axis->blocked.end(), index) != axis->blocked.end(); + auto const colour{blocked ? kAstray : kAxis}; + auto const r{8.0f}; + drawlist->AddQuadFilled(ImVec2(screen.x, screen.y - r), ImVec2(screen.x + r, screen.y), ImVec2(screen.x, screen.y + r), ImVec2(screen.x - r, screen.y), colour); + drawlist->AddQuad(ImVec2(screen.x, screen.y - r), ImVec2(screen.x + r, screen.y), ImVec2(screen.x, screen.y + r), ImVec2(screen.x - r, screen.y), IM_COL32(16, 18, 22, 255), 1.5f); + draw_label(drawlist, ImVec2(screen.x, screen.y - r - 2.0f), colour, IM_COL32(16, 18, 22, 255), format(STR_C("%s: this way"), via.line.c_str())); + } if (m_pick.has_value() && m_edited < 0) { auto const foot{m_pick->point + lift}; @@ -800,8 +942,11 @@ editor_selftest_result km_tool::selftest(std::string const &Command, std::istrin return editor_selftest_result::handled; } auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5}; - pick(*track, editor_track::point_at(*track, at)); - WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f", track->name().c_str(), m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_against ? 1 : 0, m_edited, m_pick->point.x, m_pick->point.z)); + auto const path{values.count("path") ? std::atoi(values.at("path").c_str()) : 0}; + pick(*track, path > 0 && path < static_cast(track->m_paths.size()) ? geometry::bezier{track->m_paths[path]}.point(at) : editor_track::point_at(*track, at)); + auto const *axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; + WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f path %d along %d via %d", track->name().c_str(), m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_against ? 1 : 0, m_edited, m_pick->point.x, m_pick->point.z, + m_pick->path, axis != nullptr && axis->runs_along(m_pick->track, m_pick->path) ? 1 : 0, via_here())); return editor_selftest_result::handled; } if (Command == "kmset") @@ -821,6 +966,21 @@ editor_selftest_result km_tool::selftest(std::string const &Command, std::istrin WriteLog("SELFTEST kmset: " + m_status); return editor_selftest_result::handled; } + if (Command == "kmvia") + { + // kmvia [line=] [remove=1]: the line led along the track picked, or back onto the straight paths there + auto const values{selftest_values(Words)}; + if (auto const line{values.find("line")}; line != values.end()) + m_line = line->second; + if (values.count("remove") > 0) + drop_via(); + else + lead_along(); + auto text{m_route_status}; + std::replace(text.begin(), text.end(), '\n', ';'); + WriteLog("SELFTEST kmvia: " + text); + return editor_selftest_result::handled; + } if (Command == "kmremove") { remove(); @@ -877,7 +1037,7 @@ editor_selftest_result km_tool::selftest(std::string const &Command, std::istrin for (auto const &side : axis->sides) WriteLog(format("SELFTEST kmlist apart: %.1f m of track by %.1f m of axis (%.1f..%.1f), km %s..%s", side.length, side.to - side.from, side.from, side.to, kilometrage::text(kilometrage::km_on(*axis, side, 0.0)).c_str(), kilometrage::text(kilometrage::km_on(*axis, side, side.length)).c_str())); - WriteLog(format("SELFTEST kmlist line %s: axis %.0f m %s growth %d, %zu astray, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.size(), kilometrage::text(kilometrage::km_on(*axis, 0.0)).c_str(), + WriteLog(format("SELFTEST kmlist line %s: axis %.0f m %s growth %d, %zu astray, %zu blocked, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.size(), axis->blocked.size(), kilometrage::text(kilometrage::km_on(*axis, 0.0)).c_str(), kilometrage::text(kilometrage::km_on(*axis, axis->length())).c_str()) + list); } diff --git a/application/editormodekm.h b/application/editormodekm.h index 8b3c0b38..2e22bafb 100644 --- a/application/editormodekm.h +++ b/application/editormodekm.h @@ -47,6 +47,8 @@ class km_tool final : public editor_tool TTrack *track{nullptr}; glm::dvec3 point{0.0}; // on the axis of the track glm::dvec2 along{0.0, 1.0}; // of the track there, in the plan + int path{0}; // of the switch, the one nearest to the click + glm::dvec3 on{0.0}; // the point of it nearest to the click }; std::optional m_pick; int m_edited{-1}; // of the marks, the one standing by the point picked, -1 for a new one @@ -56,6 +58,7 @@ class km_tool final : public editor_tool double m_beyond{0.0}; bool m_against{false}; // the km rise against the track picked std::string m_status; + std::string m_route_status; // of the way through a station // what the overlay draws, made again when the axes change struct tick { @@ -80,6 +83,15 @@ class km_tool final : public editor_tool void remove(); // the point of the km picked and the camera over it void show(std::size_t const Index); + // the line of the field led along the track picked, along the path of the switch picked, instead of the straight paths of the + // switches; one step of the history + void lead_along(); + // the line back on the straight paths of the switches by the track picked; one step of the history + void drop_via(); + // of the vias of the line of the field, the one on the track picked, -1 for none + int via_here() const; + // the track of the via picked and the camera over it + void show_via(std::size_t const Index); // the hectometre posts standing along the line through the track picked read into points of the km: the first and the last // post of each run of them numbered by 100 m, a jump where the runs meet; one step of the history void read_posts(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index e8b4dec0..1470d8e5 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -314,7 +314,7 @@ void editor_mode::restore_kilometrage(EditorSnapshot &Snapshot) { if (false == Snapshot.kilometrage.has_value()) return; - auto current{kilometrage::marks()}; + auto current{kilometrage::current()}; kilometrage::restore(std::move(*Snapshot.kilometrage)); Snapshot.kilometrage = std::move(current); } diff --git a/editor/editorKilometrage.cpp b/editor/editorKilometrage.cpp index b6f354c7..860612ff 100644 --- a/editor/editorKilometrage.cpp +++ b/editor/editorKilometrage.cpp @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include #include @@ -46,9 +47,11 @@ double const kCell{10.0}; // m, of the squares the track beside is sorted into double const kApart{15.0}; // m between the tracks, of a double-track line on two formations double const kBlend{100.0}; // m, the axis takes to go over from between the tracks to one of them double const kLonger{1.0}; // m, of the track beside longer than the track of the line, for the axis to follow it +double const kDetour{20000.0}; // m, of the tracks followed each way from a via back onto the line std::vector g_marks; -std::vector g_base; // as they were before the changes not yet in the undo history +std::vector g_vias; +state g_base; // as it was before the changes not yet in the undo history bool g_changed{false}; bool g_loaded{false}; std::size_t g_edits{0}; @@ -70,18 +73,22 @@ std::string word(std::string const &Text) // format of the lines, coordinates in the space of the file: // mark way km [beyond ] +// via void store(scene::layer_handle const Layer) { if (false == scene::Layers.valid(Layer)) return; auto const &context{scene::Layers.layer(Layer).context_insert()}; std::vector lines; + auto const heading = [&]() { + if (lines.empty()) + lines.emplace_back("# kilometrage of the lines set in the track editor, the simulation skips these lines"); + }; for (auto const &entry : g_marks) { if (entry.layer != Layer) continue; - if (lines.empty()) - lines.emplace_back("# kilometrage of the lines set in the track editor, the simulation skips these lines"); + heading(); auto const point{context.to_local(entry.point)}; auto const ahead{context.to_local(entry.point + glm::dvec3{entry.way.x, 0.0, entry.way.y})}; auto way{plan_of(ahead - point)}; @@ -91,6 +98,14 @@ void store(scene::layer_handle const Layer) text += format(" beyond %.3f", *entry.beyond); lines.push_back(text); } + for (auto const &entry : g_vias) + { + if (entry.layer != Layer) + continue; + heading(); + auto const point{context.to_local(entry.point)}; + lines.push_back(format("via %s %.4f %.4f %.4f", word(entry.line).c_str(), point.x, point.y, point.z)); + } scene::Layers.mark(Layer, kMark, std::move(lines)); } @@ -139,6 +154,79 @@ std::vector once(std::vector const &Spans) return result; } +// the route runs along the track of the via, along the path of a switch it stands on +bool runs_by(editor_track::route const &Route, std::size_t const Via) +{ + int path{0}; + auto const *track{track_near(g_vias[Via].point, kNear, path)}; + return track != nullptr && std::any_of(Route.spans.begin(), Route.spans.end(), [&](span const &Entry) { return Entry.track == track && Entry.path == path; }); +} + +// the route led along the tracks of the vias: from the track of each the run along the straight paths of the switches both ways +// back onto the route takes the place of the stretch of the route between where it comes back; where it comes back one way only, +// as at a junction, the route goes on along it from there +void through(editor_track::route &Route, std::vector const &Vias) +{ + for (auto const index : Vias) + { + int path{0}; + auto *track{track_near(g_vias[index].point, kNear, path)}; + if (track == nullptr || runs_by(Route, index)) + continue; + auto const run{editor_track::run_route(*track, kDetour, path)}; + auto const at{index_of(run.spans, track)}; + if (at == run.spans.size()) + continue; + auto const none{Route.spans.size()}; + auto const on = [&](std::size_t const Index) { return index_of(Route.spans, run.spans[Index].track); }; + std::optional low; + for (auto i = at; i-- > 0;) + if (on(i) != none) + { + low = i; + break; + } + std::optional high; + for (auto i = at + 1; i < run.spans.size(); ++i) + if (on(i) != none) + { + high = i; + break; + } + auto const part = [&](std::size_t const From, std::size_t const To) { return std::vector{run.spans.begin() + From, run.spans.begin() + To}; }; + auto const head = [&](std::size_t const To) { return std::vector{Route.spans.begin(), Route.spans.begin() + To}; }; + auto const tail = [&](std::size_t const From) { return std::vector{Route.spans.begin() + From, Route.spans.end()}; }; + auto const joined = [](std::vector> const &Parts) { + std::vector result; + for (auto const &entry : Parts) + result.insert(result.end(), entry.begin(), entry.end()); + return result; + }; + std::vector spans; + if (low && high) + { + auto const from{on(*low)}; + auto const to{on(*high)}; + if (from == to) + continue; + spans = from < to ? joined({head(from), part(*low, *high + 1), tail(to + 1)}) : joined({head(to), flipped(part(*low, *high + 1)), tail(from + 1)}); + } + else if (low) + { + auto const from{on(*low)}; + spans = run.spans[*low].forward == Route.spans[from].forward ? joined({head(from), part(*low, run.spans.size())}) : joined({flipped(part(*low, run.spans.size())), tail(from + 1)}); + } + else if (high) + { + auto const to{on(*high)}; + spans = run.spans[*high].forward == Route.spans[to].forward ? joined({part(0, *high + 1), tail(to + 1)}) : joined({head(to), flipped(part(0, *high + 1))}); + } + else + continue; + Route.spans = once(spans); + } +} + double plan_gap(std::vector const &Samples, glm::dvec3 const &Point) { auto best{std::numeric_limits::max()}; @@ -150,18 +238,20 @@ double plan_gap(std::vector const &Samples, glm::dve // the track beside the line: of each sample of the line the sample of it beside, where they run side by side struct pairing { + editor_track::route route; // of the track beside std::vector partner; // along the track beside, empty for none std::vector match; // of each sample, -1 where the track beside isn't there std::vector across; // to the sample beside, along the normal of the sample }; -pairing pair_with(std::vector const &Samples, TTrack &Beside) +pairing pair_with(std::vector const &Samples, TTrack &Beside, std::vector const &Vias) { pairing result; result.match.assign(Samples.size(), -1); result.across.assign(Samples.size(), 0.0); - auto other{editor_track::run_route(Beside, kReach)}; - result.partner = editor_track::sample_route(other, kStep); + result.route = editor_track::run_route(Beside, kReach); + through(result.route, Vias); + result.partner = editor_track::sample_route(result.route, kStep); auto const &partner{result.partner}; auto const cell = [](glm::dvec3 const &Point) { return std::make_pair(static_cast(std::floor(Point.x / kCell)), static_cast(std::floor(Point.z / kCell))); }; std::map, std::vector> grid; @@ -195,9 +285,29 @@ pairing pair_with(std::vector const &Samples, TTrack return result; } +// the track beside comes back onto the line between the stretches it runs beside it, or both ways, as a track of a station or a +// passing loop does: not the other track +bool looped(pairing const &Pairs, std::unordered_set const &Own) +{ + auto low{Pairs.partner.size()}; + std::size_t high{0}; + for (auto const match : Pairs.match) + if (match >= 0) + { + low = std::min(low, static_cast(match)); + high = std::max(high, static_cast(match)); + } + auto const on_line = [&](editor_track::route_sample const &Sample) { return Sample.span < Pairs.route.spans.size() && Own.count(Pairs.route.spans[Sample.span].track) > 0; }; + if (low >= Pairs.partner.size()) + return false; + auto const first{Pairs.partner.begin() + static_cast(low)}; + auto const last{Pairs.partner.begin() + static_cast(high) + 1}; + return std::any_of(first, last, on_line) || (std::any_of(Pairs.partner.begin(), first, on_line) && std::any_of(last, Pairs.partner.end(), on_line)); +} + // the track beside the line, looked for by the first mark and then every 500 m along the line: a track the line doesn't run // through, the spacing of a double-track line away, beside it for 100 m at least -pairing pair_up(std::vector const &Samples, editor_track::route const &Route, std::size_t const Origin) +pairing pair_up(std::vector const &Samples, editor_track::route const &Route, std::size_t const Origin, std::vector const &Vias) { std::unordered_set own; for (auto const &entry : Route.spans) @@ -220,8 +330,8 @@ pairing pair_up(std::vector const &Samples, editor_t auto *track{track_near({probe.x, at.position.y, probe.y}, 1.6)}; if (track == nullptr || own.count(track) > 0 || false == tried.insert(track).second) continue; - auto result{pair_with(Samples, *track)}; - if (std::count_if(result.match.begin(), result.match.end(), [](int const Match) { return Match >= 0; }) * kStep >= 100.0) + auto result{pair_with(Samples, *track, Vias)}; + if (std::count_if(result.match.begin(), result.match.end(), [](int const Match) { return Match >= 0; }) * kStep >= 100.0 && false == looped(result, own)) return result; } } @@ -372,7 +482,12 @@ std::optional build(std::string const &Line) return std::nullopt; auto *first{*start}; auto const first_mark{own[static_cast(start - tracks.begin())]}; + std::vector passes; + for (std::size_t index = 0; index < g_vias.size(); ++index) + if (g_vias[index].line == Line) + passes.push_back(index); auto route{editor_track::run_route(*first, kReach)}; + through(route, passes); if (route.spans.empty()) return std::nullopt; // the marks the main paths of the switches lead away from: the line goes their way @@ -393,7 +508,10 @@ std::optional build(std::string const &Line) for (std::size_t round = 0; round <= own.size(); ++round) { samples = editor_track::sample_route(route, kStep); - pairs = pair_up(samples, route, origin_of(samples)); + // the track beside goes along the vias the line doesn't + std::vector beside; + std::copy_if(passes.begin(), passes.end(), std::back_inserter(beside), [&](std::size_t const Via) { return false == runs_by(route, Via); }); + pairs = pair_up(samples, route, origin_of(samples), beside); std::size_t away{own.size()}; for (std::size_t k = 0; k < own.size() && away == own.size(); ++k) if (tracks[k] != nullptr && tried.count(k) == 0 && plan_gap(samples, g_marks[own[k]].point) > kOnAxis && plan_gap(pairs.partner, g_marks[own[k]].point) > kOnAxis) @@ -424,12 +542,22 @@ std::optional build(std::string const &Line) spans.insert(spans.end(), route.spans.begin() + at + 1, route.spans.end()); } route.spans = once(spans); + through(route, passes); } if (samples.size() < 2) return std::nullopt; axis result; result.line = Line; compose(result, samples, pairs); + for (auto const &entry : route.spans) + result.paths.emplace_back(entry.track, entry.path); + auto const paired{std::any_of(pairs.match.begin(), pairs.match.end(), [](int const Match) { return Match >= 0; })}; + if (paired) + for (auto const &entry : pairs.route.spans) + result.paths.emplace_back(entry.track, entry.path); + for (auto const index : passes) + if (false == runs_by(route, index) && (false == paired || false == runs_by(pairs.route, index))) + result.blocked.push_back(index); for (auto const index : own) { axis::anchor anchor; @@ -462,7 +590,8 @@ std::optional build(std::string const &Line) void reset() { g_marks.clear(); - g_base.clear(); + g_vias.clear(); + g_base = {}; drop_axes(); g_changed = false; g_loaded = false; @@ -474,6 +603,7 @@ void load() return; g_loaded = true; g_marks.clear(); + g_vias.clear(); drop_axes(); for (auto const layer : scene::Layers.marked_layers(kMark)) { @@ -487,6 +617,18 @@ void load() glm::dvec3 point{0.0}; glm::dvec2 way{0.0, 1.0}; line >> keyword; + if (keyword == "via") + { + via entry; + entry.layer = layer; + glm::dvec3 point{0.0}; + line >> entry.line >> point.x >> point.y >> point.z; + if (line.fail() || entry.line.empty()) + continue; + entry.point = context.to_world(point); + g_vias.push_back(std::move(entry)); + continue; + } if (keyword != "mark") continue; line >> entry.line >> point.x >> point.y >> point.z >> keyword >> way.x >> way.y >> keyword >> entry.km; @@ -505,10 +647,10 @@ void load() g_marks.push_back(std::move(entry)); } } - g_base = g_marks; + g_base = current(); g_changed = false; if (false == g_marks.empty()) - WriteLog("Editor: " + std::to_string(g_marks.size()) + " kilometrage mark(s) read from the scenery", logtype::generic); + WriteLog("Editor: " + std::to_string(g_marks.size()) + " kilometrage mark(s) and " + std::to_string(g_vias.size()) + " via(s) read from the scenery", logtype::generic); } std::vector const &marks() @@ -517,6 +659,17 @@ std::vector const &marks() return g_marks; } +std::vector const &vias() +{ + load(); + return g_vias; +} + +state current() +{ + return {g_marks, g_vias}; +} + void put(mark Entry) { load(); @@ -549,29 +702,56 @@ void remove(std::size_t const Index) changed({layer}); } -std::optional> take_changes() +void put(via Entry) +{ + load(); + int path{0}; + auto *track{track_near(Entry.point, kNear, path)}; + Entry.layer = track != nullptr ? alignment_store::layer_of(*track) : scene::Layers.active(); + if (false == scene::Layers.valid(Entry.layer)) + Entry.layer = 1; + auto const layer{Entry.layer}; + g_vias.push_back(std::move(Entry)); + changed({layer}); +} + +void remove_via(std::size_t const Index) +{ + load(); + if (Index >= g_vias.size()) + return; + auto const layer{g_vias[Index].layer}; + g_vias.erase(g_vias.begin() + static_cast(Index)); + changed({layer}); +} + +std::optional take_changes() { if (false == g_changed) return std::nullopt; auto before{std::move(g_base)}; - g_base = g_marks; + g_base = current(); g_changed = false; return before; } void settle() { - g_base = g_marks; + g_base = current(); g_changed = false; } -void restore(std::vector Marks) +void restore(state State) { std::set layers; - for (auto const *list : {&g_marks, &Marks}) + for (auto const *list : {&g_marks, &State.marks}) for (auto const &entry : *list) layers.insert(entry.layer); - g_marks = std::move(Marks); + for (auto const *list : {&g_vias, &State.vias}) + for (auto const &entry : *list) + layers.insert(entry.layer); + g_marks = std::move(State.marks); + g_vias = std::move(State.vias); changed(layers); settle(); } @@ -769,6 +949,60 @@ TTrack *track_near(glm::dvec3 const &Point, double const Reach) return best; } +int path_near(TTrack const &Track, glm::dvec3 const &Point, glm::dvec3 &On) +{ + int result{0}; + auto nearest{std::numeric_limits::max()}; + On = Point; + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + { + geometry::bezier const curve{Track.m_paths[path]}; + auto previous{curve.point(0.0)}; + for (int i = 1; i <= 64; ++i) + { + auto const next{curve.point(i / 64.0)}; + auto const ab{plan_of(next - previous)}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(plan_of(Point - previous), ab) / length2, 0.0, 1.0) : 0.0}; + auto const at{glm::mix(previous, next, t)}; + // where the paths of a switch run together, the first one + if (auto const distance{plan_distance(at, Point)}; distance < nearest - 1e-3) + { + nearest = distance; + result = path; + On = at; + } + previous = next; + } + } + return result; +} + +TTrack *track_near(glm::dvec3 const &Point, double const Reach, int &Path) +{ + TTrack *best{nullptr}; + auto nearest{Reach}; + Path = 0; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0 || false == editor_track::is_supported(*track) || track->m_paths.empty()) + continue; + auto const &ends{track->m_paths.front().points}; + auto const chord{plan_distance(ends[segment_data::point::start], ends[segment_data::point::end])}; + if (std::min(plan_distance(ends[segment_data::point::start], Point), plan_distance(ends[segment_data::point::end], Point)) > chord * 0.8 + nearest + 1.0) + continue; + glm::dvec3 on{0.0}; + auto const path{path_near(*track, Point, on)}; + if (auto const distance{plan_distance(on, Point)}; distance < nearest) + { + nearest = distance; + best = track; + Path = path; + } + } + return best; +} + std::string text(double const Km) { auto result{format("%.3f", Km)}; diff --git a/editor/editorKilometrage.hpp b/editor/editorKilometrage.hpp index 50f61b1b..d266f6ee 100644 --- a/editor/editorKilometrage.hpp +++ b/editor/editorKilometrage.hpp @@ -9,9 +9,11 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include #include +#include #include #include @@ -22,7 +24,8 @@ class TTrack; // the kilometrage of the railway lines, set in the track editor and kept in the scenery files as "//$k" comment lines: the // points of a line where its km is known, from a post or a document, and the points where it jumps, with the km on both // sides. the km run along the axis of a single-track line and between the tracks of a double-track one, along the longer track -// where the tracks part on formations of their own (PKP GK-1 6.1.12), the other tracks get the km straight across from them +// where the tracks part on formations of their own (PKP GK-1 6.1.12), the other tracks get the km straight across from them. +// through a station the line runs along the straight paths of the switches, unless the tracks it runs along there are set namespace kilometrage { @@ -35,20 +38,38 @@ struct mark double km{0.0}; // the km the point is reached at, coming along the rising km std::optional beyond; // of a jump: the km on from the point }; +// a track a line runs along through a station, other than the one the straight paths of the switches lead it along: from it +// the line goes on along the straight paths until it meets the rest of it again, or on and away where it doesn't +struct via +{ + scene::layer_handle layer{null_handle}; + std::string line; + glm::dvec3 point{0.0}; // on the axis of the track, of the path of a switch the line takes +}; +// all of the kilometrage set, for the undo history +struct state +{ + std::vector marks; + std::vector vias; +}; void reset(); void load(); std::vector const &marks(); +std::vector const &vias(); +state current(); // a new mark, kept in the file of the layer of the track nearest to it void put(mark Entry); void change(std::size_t const Index, mark Entry); void remove(std::size_t const Index); -// the marks as they were before the changes made since the last call, if there were any; the current ones become the base -std::optional> take_changes(); -// the current marks become the base, the changes made so far aren't a part of the undo history +void put(via Entry); +void remove_via(std::size_t const Index); +// the kilometrage as it was before the changes made since the last call, if there were any; the current one becomes the base +std::optional take_changes(); +// the current kilometrage becomes the base, the changes made so far aren't a part of the undo history void settle(); -// puts the marks back as they were, for undo and redo -void restore(std::vector Marks); +// puts the kilometrage back as it was, for undo and redo +void restore(state State); // the axes found are dropped when the tracks change: Edits counts the changes of the tracks made in the editor void follow(std::size_t const Edits); // counts the times the axes were dropped: what is made from them is made again when it changes @@ -86,6 +107,9 @@ struct axis std::vector sides; std::vector anchors; // by the chainage std::vector astray; // the marks of the line away from its axis + std::vector blocked; // the vias of the line it doesn't run along: no track of theirs, or another via led it elsewhere + std::vector> paths; // the paths of the tracks the line runs along, of the track beside too + bool runs_along(TTrack const *Track, int const Path) const { return std::find(paths.begin(), paths.end(), std::make_pair(Track, Path)) != paths.end(); } double length() const { return chainage.empty() ? 0.0 : chainage.back(); } }; // the numbers of the lines with marks @@ -105,6 +129,10 @@ double km_on(axis const &Axis, axis::side const &Side, double const Along); bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double const Reach = 30.0); // the track nearest to the point in the plan within the reach, nullptr for none TTrack *track_near(glm::dvec3 const &Point, double const Reach); +// the path of the track nearest to the point in the plan, On: the point of it nearest +int path_near(TTrack const &Track, glm::dvec3 const &Point, glm::dvec3 &On); +// the track with a path nearest to the point within the reach, nullptr for none, Path: the path +TTrack *track_near(glm::dvec3 const &Point, double const Reach, int &Path); // "100,234", as PKP PLK write the km std::string text(double const Km); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index db98f6b2..853ed3c7 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -2188,7 +2188,7 @@ bool editor_track::find_route(TTrack *From, TTrack *To, route &Route, std::strin return true; } -editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) +editor_track::route editor_track::run_route(TTrack &Track, double const Maximum, int const Path) { route result; if (false == is_supported(Track)) @@ -2228,7 +2228,7 @@ editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) } return nodes; }; - auto const own{main_of(Track)}; + auto const own{Path >= 0 && Path < static_cast(Track.m_paths.size()) ? Path : main_of(Track)}; auto const ahead{walk({&Track, own, true})}; auto const behind{walk({&Track, own, false})}; for (auto node = behind.rbegin(); node != behind.rend(); ++node) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 7e69cc1c..acaf502a 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -375,8 +375,9 @@ class editor_track double length{0.0}; }; static bool find_route(TTrack *From, TTrack *To, route &Route, std::string &Error); - // continues both ways from the path, through switches along their path 0 - static route run_route(TTrack &Track, double const Maximum); + // continues both ways from the path, through switches along their path 0; Path: the path of the track to start along, -1 for its + // main one + static route run_route(TTrack &Track, double const Maximum, int const Path = -1); static route route_of(chain const &Chain); // the speed most of the sampled route is laid for, by the length: a few tracks set faster or slower don't set the curves of // all of it. 100 where none is set diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 842aa4b7..3a2baf9a 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10720,3 +10720,63 @@ msgstr[2] "%zu odcinków z torami na osobnych torowiskach" msgid "Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)" msgstr "Gdzie tory rozchodzą się na więcej niż 15 m i znów schodzą, kilometraż biegnie po dłuższym, krótszy liczy się po własnej długości, a różnicę bierze jego ostatni hektometr przed zejściem, jak podaje PKP GK-1 (6.1.12)" +msgid "The way through a station" +msgstr "Przebieg przez stację" + +msgid "Through a station the km run along the straight paths of the switches. Click a track of the station to lead them along it instead" +msgstr "Przez stację kilometraż biegnie po prostych torach rozjazdów. Kliknij tor stacji, żeby poprowadzić go tamtędy" + +msgid "Put a point of the km of the line first" +msgstr "Najpierw wstaw punkt kilometra linii" + +msgid "The line doesn't run along this track: another track chosen leads it elsewhere" +msgstr "Linia nie biegnie tym torem: inny wybrany tor prowadzi ją gdzie indziej" + +msgid "Line %s runs along this track, as chosen" +msgstr "Linia %s biegnie tym torem, tak jak wybrano" + +msgid "Back onto the straight paths" +msgstr "Wróć na proste tory rozjazdów" + +msgid "The track chosen is forgotten, the line runs along the straight paths of the switches here again. One step of undo" +msgstr "Wybór toru znika, linia znów biegnie tu po prostych torach rozjazdów. Jeden krok cofania" + +msgid "Line %s runs along this track" +msgstr "Linia %s biegnie tym torem" + +msgid "Line %s doesn't run along this track" +msgstr "Linia %s nie biegnie tym torem" + +msgid "Lead the km along this track" +msgstr "Prowadź kilometraż tym torem" + +msgid "The line runs along the track clicked, along the path of the switch clicked, and on along the straight paths of the switches until it meets its tracks again; where it doesn't, as at a junction, it goes on that way. One step of undo" +msgstr "Linia pobiegnie klikniętym torem, przez rozjazd tą jego drogą, którą kliknięto, i dalej po prostych torach rozjazdów, aż wróci na swoje tory; gdzie nie wraca, jak na odgałęzieniu, pobiegnie dalej tamtędy. Jeden krok cofania" + +msgid "The line can't run along this track: no way leads from it onto the line" +msgstr "Linia nie może biec tym torem: żadna droga nie prowadzi z niego na linię" + +msgid "Line %s runs along this track now" +msgstr "Linia %s biegnie teraz tym torem" + +msgid "%zu point of the km is off the line now: the track leads it away from it" +msgid_plural "%zu points of the km are off the line now: the track leads it away from them" +msgstr[0] "%zu punkt kilometra jest teraz poza linią: ten tor wyprowadza ją od niego" +msgstr[1] "%zu punkty kilometra są teraz poza linią: ten tor wyprowadza ją od nich" +msgstr[2] "%zu punktów kilometra jest teraz poza linią: ten tor wyprowadza ją od nich" + +msgid "Line %s runs along the straight paths of the switches here again" +msgstr "Linia %s znów biegnie tu po prostych torach rozjazdów" + +msgid "Along the track chosen by km %s" +msgstr "Wybrany tor przy km %s" + +msgid "Along the track chosen" +msgstr "Wybrany tor" + +msgid "Through the station the line runs along this track, not along the straight paths of the switches" +msgstr "Przez stację linia biegnie tym torem, a nie po prostych torach rozjazdów" + +msgid "%s: this way" +msgstr "%s: tędy" + diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 5731f748..18274767 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -854,7 +854,7 @@ bool editor_line(char const *Mark, std::string const &Text) {"//$r", {"#", "switch", "slip", "crossing", "double", "connection"}}, {"//$o", {"on"}}, {"//$s", {"#", "station"}}, - {"//$k", {"#", "mark"}}, + {"//$k", {"#", "mark", "via"}}, }}; auto const lookup{std::find_if(words.begin(), words.end(), [&](auto const &Entry) { return Entry.first == Mark; })}; if (lookup == words.end())