diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d85d027..4e3430f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,7 @@ set(SOURCES "editor/editorGroundFollow.cpp" "editor/editorAlignmentStore.cpp" "editor/editorSwitchStore.cpp" +"editor/editorStation.cpp" "editor/editorSwitchDesign.cpp" "editor/editorConnections.cpp" "application/editormodeconnection.cpp" @@ -230,6 +231,7 @@ set(SOURCES "application/editormodegauge.cpp" "application/editormodeinfra.cpp" "application/editormodespeed.cpp" +"application/editormodesrk.cpp" "application/editormodejoints.cpp" "application/editormodeparallel.cpp" "application/editormodelineside.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 7ed6c292..3de5f4a5 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1505,12 +1505,14 @@ bool editor_mode::update() draw_tape(); m_speed.update(); m_joints.update(); + m_srk.update(); draw_track_hints(); draw_empty_scenery_hint(); draw_profile_overlay(); draw_infra_overlay(); m_speed.draw_overlay(); m_joints.draw_overlay(); + m_srk.draw_overlay(); } else { @@ -2978,6 +2980,7 @@ bool editor_mode::save() switch_store::tidy(); switch_store::settle(); infra_store(); + m_srk.store(); if (false == m_saved_camera.has_value() || glm::distance(m_saved_camera->first, glm::dvec3{Camera.Pos}) > 0.5 || glm::distance(m_saved_camera->second, glm::vec3{Camera.Angle}) > 0.01f) { m_saved_camera = std::make_pair(glm::dvec3{Camera.Pos}, glm::vec3{Camera.Angle}); diff --git a/application/editormode.h b/application/editormode.h index 3fb5bf59..e2692fc8 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodelineside.h" #include "application/editormodeparallel.h" #include "application/editormodespeed.h" +#include "application/editormodesrk.h" #include "application/editormodevehicle.h" #include "application/editortool.h" #include "scene/sceneeditor.h" @@ -1544,7 +1545,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri editor_icons::icon picture; int family; // 0 builds the track, 1 equips it, 2 checks it }; - static std::array const &track_modes(); + static std::array const &track_modes(); bool track_plan_tab() const { return m_track_tab == track_tab::straights || m_track_tab == track_tab::route; } track_tab plan_tab_for(TTrack const *Track) const; // the objects along the track: a tab for each tool @@ -2380,7 +2381,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri hekto_tool m_hekto{*this, *this, *this, *this, *this, *this}; fouling_tool m_fouling{*this, *this, *this, *this, *this}; parallel_tool m_parallel{*this, *this, *this, *this}; - std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel}; + srk_tool m_srk{*this, *this, *this}; + std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel, &m_srk}; void infra_rebase(); void render_infra_search(); diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp new file mode 100644 index 00000000..db2a8598 --- /dev/null +++ b/application/editormodesrk.cpp @@ -0,0 +1,642 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormodesrk.h" +#include "application/editorgestures.h" +#include "application/editormodeshared.h" +#include "application/editorprojection.h" +#include "application/editoruilayer.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorSwitchStore.hpp" + +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include + +namespace +{ + +char const *const kMark{"//$s"}; +double constexpr kOverlayRange{4000.0}; + +ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; +ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; +ImU32 constexpr kApproach{IM_COL32(95, 100, 112, 255)}; +ImU32 constexpr kHovered{IM_COL32(255, 255, 255, 255)}; + +bool is_switch(TTrack const &Track) +{ + return Track.eType != tt_Normal; +} + +// a word of the record line: blanks taken out, - for none +std::string word_of(std::string const &Text) +{ + std::string result; + for (auto const c : Text) + if (false == std::isspace(static_cast(c))) + result += c; + return result.empty() ? std::string{"-"} : result; +} + +std::string shown(std::string const &Word) +{ + return Word == "-" ? std::string{} : Word; +} + +} // namespace + +void srk_tool::load() +{ + if (m_loaded) + return; + m_loaded = true; + m_records.clear(); + if (scene::Layers.empty()) + return; + for (auto const layer : scene::Layers.marked_layers(kMark)) + for (auto const &text : scene::Layers.marked(layer, kMark)) + { + std::istringstream words{text}; + std::string word; + if ((words >> word).fail() || word != "station") + continue; + record entry; + entry.layer = layer; + words >> entry.code; + while (words >> word) + { + if (word == "line") + words >> entry.line; + else if (word == "anchor") + words >> entry.anchor; + else if (word == "name") + { + std::getline(words, entry.name); + entry.name.erase(0, entry.name.find_first_not_of(' ')); + break; + } + } + entry.code = shown(entry.code); + entry.line = shown(entry.line); + if (false == entry.anchor.empty()) + m_records.push_back(std::move(entry)); + } +} + +void srk_tool::store() +{ + if (false == m_changed || scene::Layers.empty()) + return; + m_changed = false; + std::vector layers{scene::Layers.marked_layers(kMark)}; + for (auto const &entry : m_records) + if (std::find(layers.begin(), layers.end(), entry.layer) == layers.end()) + layers.push_back(entry.layer); + for (auto const layer : layers) + { + std::vector lines; + for (auto const &entry : m_records) + { + if (entry.layer != layer) + continue; + if (lines.empty()) + lines.emplace_back("# stations described in the track editor, the simulation skips these lines"); + lines.push_back(format("station %s line %s anchor %s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), entry.anchor.c_str(), entry.name.c_str())); + } + scene::Layers.mark(layer, kMark, std::move(lines)); + } +} + +void srk_tool::open(TTrack &Track) +{ + m_status.clear(); + auto found{station::find(Track)}; + if (false == found.has_value()) + { + m_status = STR_C("No switch near this track: it isn't a part of a station"); + return; + } + m_layout = std::move(found); + m_hover = -1; + m_view_state.exaggeration = 0.0f; + m_history_size = m_history.history_size(); + // the record of the station, by any of its switches + m_record = -1; + for (int index = 0; index < static_cast(m_records.size()) && m_record < 0; ++index) + if (std::any_of(m_layout->switches.begin(), m_layout->switches.end(), [&](TTrack const *Switch) { return Switch->name() == m_records[index].anchor; })) + m_record = index; + m_anchor = m_record >= 0 ? m_records[m_record].anchor : m_layout->switches.front()->name(); +} + +void srk_tool::refresh() +{ + m_history_size = m_history.history_size(); + auto *anchor{m_anchor.empty() ? nullptr : simulation::Paths.find(m_anchor)}; + if (anchor == nullptr || anchor->m_editorremoved) + { + m_layout.reset(); + return; + } + auto const view{m_view_state}; + open(*anchor); + if (m_layout.has_value()) + m_view_state = view; +} + +srk_tool::record &srk_tool::record_of_open() +{ + if (m_record < 0) + { + record entry; + auto const *first{m_layout->switches.front()}; + entry.anchor = first->name(); + entry.layer = scene::Layers.resolve(first->layer()); + if (false == scene::Layers.valid(entry.layer) || false == scene::Layers.accepts(entry.layer)) + entry.layer = scene::Layers.active(); + if (false == scene::Layers.valid(entry.layer)) + entry.layer = 1; + m_records.push_back(std::move(entry)); + m_record = static_cast(m_records.size()) - 1; + m_anchor = m_records.back().anchor; + } + return m_records[m_record]; +} + +std::string srk_tool::title() const +{ + if (m_record >= 0) + { + auto const &entry{m_records[m_record]}; + if (false == entry.name.empty()) + return entry.code.empty() ? entry.name : entry.name + " (" + entry.code + ")"; + if (false == entry.code.empty()) + return entry.code; + } + return format(STR_C("Station of %s"), m_anchor.c_str()); +} + +void srk_tool::find_all() +{ + m_found.clear(); + for (auto *track : station::stations()) + m_found.emplace_back(track->name(), station::switches_of(*track).size()); + std::sort(m_found.begin(), m_found.end(), [](auto const &A, auto const &B) { return A.second > B.second; }); +} + +void srk_tool::update() +{ + if (false == m_open) + return; + load(); + if (m_layout.has_value() && m_history.history_size() != m_history_size) + refresh(); + else if (false == m_layout.has_value() && false == m_anchor.empty() && m_history.history_size() != m_history_size) + refresh(); + // a track of another station selected opens it + auto *track{m_selection.selected_track()}; + if (track == nullptr || track == m_picked) + return; + m_picked = track; + if (m_layout.has_value() && std::any_of(m_layout->paths.begin(), m_layout->paths.end(), [&](station::path const &Path) { return Path.track == track && false == Path.approach; })) + return; + open(*track); +} + +void srk_tool::forget(scene::basic_node const *Node) +{ + if (Node == m_picked) + m_picked = nullptr; + if (m_layout.has_value() && std::any_of(m_layout->paths.begin(), m_layout->paths.end(), [&](station::path const &Path) { return Path.track == Node; })) + { + // laid out again from its anchor at the next update + m_layout.reset(); + m_hover = -1; + m_history_size = 0; + } +} + +void srk_tool::draw_overlay() const +{ + if (false == m_open || false == m_layout.has_value()) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + if (m_hover >= 0 && m_hover < static_cast(m_layout->paths.size())) + { + auto const &points{m_layout->paths[m_hover].points}; + for (std::size_t i = 1; i < points.size(); ++i) + projection.line(drawlist, points[i - 1], points[i], kHovered, 4.0f); + } + for (auto const *track : m_layout->switches) + { + auto const start{track->m_paths.front().points[segment_data::point::start] + glm::dvec3{0.0, 1.0, 0.0}}; + ImVec2 screen; + if ((false == Global.EditorOrtho && glm::distance(start, camera) > kOverlayRange) || false == projection.project(start, screen)) + continue; + auto const text{track->name()}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + 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()); + } +} + +void srk_tool::render_found() +{ + if (main_button(STR_C("Find the stations of the scenery"), ImVec2(-1.0f, tall_button_height()))) + find_all(); + item_tooltip("Each group of switches joined by tracks shorter than 1.5 km is a station"); + if (m_found.empty()) + return; + ImGui::TextDisabled("%s", format(STR_P("%zu station", "%zu stations", m_found.size()), m_found.size()).c_str()); + if (false == ImGui::BeginChild("##srkfound", ImVec2(0.0f, std::min(300.0f, ImGui::GetTextLineHeightWithSpacing() * (m_found.size() + 1.0f))), true)) + { + ImGui::EndChild(); + return; + } + for (std::size_t index = 0; index < m_found.size(); ++index) + { + auto const &[name, count]{m_found[index]}; + auto const described{std::find_if(m_records.begin(), m_records.end(), [&](record const &Entry) { return Entry.anchor == name; })}; + auto label{format(STR_P("%s: %zu switch", "%s: %zu switches", count), name.c_str(), count)}; + if (described != m_records.end() && false == described->name.empty()) + label = described->name + " - " + label; + if (ImGui::Selectable((label + "##srkfound" + std::to_string(index)).c_str(), false)) + if (auto *track{simulation::Paths.find(name)}; track != nullptr) + { + open(*track); + m_selection.set_current_node(track); + m_picked = track; + m_view.focus_track(*track, 0, track->m_paths.front().points[segment_data::point::start]); + } + } + ImGui::EndChild(); +} + +void srk_tool::render_options() +{ + if (false == m_layout.has_value()) + { + ImGui::TextDisabled("%s", STR_C("The signalling of a station: its schematic, the numbers of the switches and the tracks, the track circuits and the signals")); + ImGui::Spacing(); + gesture_rows({{"LMB on a switch", "opens its station"}}); + if (false == m_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); + ImGui::Spacing(); + render_found(); + return; + } + auto const &layout{*m_layout}; + ImGui::TextUnformatted(title().c_str()); + std::unordered_set tracks; + for (auto const &path : layout.paths) + if (false == path.approach) + tracks.insert(path.track); + metric_tiles({{format("%zu", layout.switches.size()), STR_C("switches"), kSwitch}, + {format("%zu", tracks.size() - layout.switches.size()), STR_C("tracks"), kPlain}, + {format("%.0f m", layout.to - layout.from), STR_C("length"), ImGui::GetColorU32(ImGuiCol_Text)}}); + if (begin_fields("##srkstation")) + { + auto const text = [&](char const *Label, char const *Id, std::string const &Value, char const *Hint, auto const &Set) { + field_row(Label); + std::array buffer{}; + Value.copy(buffer.data(), buffer.size() - 1); + if (ImGui::InputTextWithHint(Id, Hint, buffer.data(), buffer.size())) + { + Set(std::string{buffer.data()}); + m_changed = true; + } + }; + auto const *entry{m_record >= 0 ? &m_records[m_record] : nullptr}; + text("Name", "##srkname", entry != nullptr ? entry->name : std::string{}, "Biała Podlaska", [&](std::string const &Value) { record_of_open().name = Value; }); + text("Code", "##srkcode", entry != nullptr ? entry->code : std::string{}, "BPd", [&](std::string const &Value) { record_of_open().code = word_of(Value) == "-" ? std::string{} : word_of(Value); }); + item_tooltip("The abbreviation of the station: the names of its switches, track circuits and signals begin with it"); + text("Line", "##srkline", entry != nullptr ? entry->line : std::string{}, "2", [&](std::string const &Value) { record_of_open().line = word_of(Value) == "-" ? std::string{} : word_of(Value); }); + item_tooltip("The number of the railway line the station lies on, as the block sections between the stations are named after it"); + ImGui::EndTable(); + } + 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))))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Switch"), ImGuiTableColumnFlags_WidthStretch, 0.35f); + ImGui::TableSetupColumn(STR_C("Type"), ImGuiTableColumnFlags_WidthStretch, 0.45f); + ImGui::TableSetupColumn(STR_C("km"), ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableHeadersRow(); + auto *selected{m_selection.selected_track()}; + for (std::size_t index = 0; index < layout.switches.size(); ++index) + { + auto *track{layout.switches[index]}; + auto const start{track->m_paths.front().points[segment_data::point::start]}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable((track->name() + "##srksw" + std::to_string(index)).c_str(), track == selected, ImGuiSelectableFlags_SpanAllColumns)) + { + m_selection.set_current_node(track); + m_picked = track; + m_view.focus_track(*track, 0, start); + } + ImGui::TableNextColumn(); + auto const record{switch_store::named(*track)}; + ImGui::TextUnformatted(record ? switch_store::label(*record).c_str() : track->eType == tt_Switch ? STR_C("switch") : track->eType == tt_Cross ? STR_C("crossing") : STR_C("turntable")); + ImGui::TableNextColumn(); + auto const along{station::project(layout, start).x}; + if (layout.kilometrage) + ImGui::Text("%.3f", station::km_of(layout, along)); + else + ImGui::Text("%.0f m", along); + } + ImGui::EndTable(); + } + } + ImGui::Spacing(); + if (false == m_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); + if (ImGui::Button(STR_C("Another station"), ImVec2(-1.0f, 0.0f))) + { + m_layout.reset(); + m_anchor.clear(); + m_record = -1; + m_hover = -1; + } + item_tooltip("Leaves this station; LMB on a switch opens another one"); +} + +void srk_tool::fit_view(float const Width, float const Height) +{ + auto const &layout{*m_layout}; + auto &view{m_view_state}; + auto const span{std::max(50.0, layout.to - layout.from)}; + view.from = layout.from - span * 0.08 - 30.0; + view.to = layout.to + span * 0.08 + 30.0; + view.centre = (layout.low + layout.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)}; + view.exaggeration = static_cast(std::clamp(across / vertical, 1.0, 500.0)); +} + +void srk_tool::render_canvas(float const Height) +{ + auto const &layout{*m_layout}; + auto &view{m_view_state}; + auto const &io{ImGui::GetIO()}; + ImVec2 const size{std::max(200.0f, ImGui::GetContentRegionAvail().x), std::max(80.0f, Height)}; + ImVec2 const corner{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##srkcanvas", size, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonMiddle); + bool const hovered{ImGui::IsItemHovered()}; + if (view.exaggeration <= 0.0f) + fit_view(size.x, size.y); + // the wheel zooms about the mouse, with Ctrl it exaggerates the offsets; a drag moves the plot + if (hovered && io.MouseWheel != 0.0f) + { + if (io.KeyCtrl) + view.exaggeration = std::clamp(view.exaggeration * std::pow(1.2f, io.MouseWheel), 1.0f, 500.0f); + else + { + auto const at{view.from + (io.MousePos.x - corner.x) / size.x * (view.to - view.from)}; + auto const factor{std::pow(0.8, static_cast(io.MouseWheel))}; + view.from = at - (at - view.from) * factor; + view.to = at + (view.to - at) * factor; + } + } + auto const across{(view.to - view.from) / size.x}; + auto const vertical{across / view.exaggeration}; + if (ImGui::IsItemActive() && (ImGui::IsMouseDragging(ImGuiMouseButton_Left, kDragPixels) || ImGui::IsMouseDragging(ImGuiMouseButton_Middle, kDragPixels))) + { + m_dragging = true; + view.from -= io.MouseDelta.x * across; + 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 path under the mouse + if (hovered && false == m_dragging) + { + m_hover = -1; + auto nearest{7.0f * 7.0f}; + auto const &mouse{io.MousePos}; + for (int index = 0; index < static_cast(layout.paths.size()); ++index) + { + auto const &points{layout.paths[index].schematic}; + for (std::size_t i = 1; i < points.size(); ++i) + { + auto const a{screen_of(points[i - 1])}; + auto const b{screen_of(points[i])}; + auto const dx{b.x - a.x}; + auto const dy{b.y - a.y}; + auto const length2{dx * dx + dy * dy}; + auto const t{length2 > 0.0f ? std::clamp(((mouse.x - a.x) * dx + (mouse.y - a.y) * dy) / length2, 0.0f, 1.0f) : 0.0f}; + auto const ex{a.x + t * dx - mouse.x}; + auto const ey{a.y + t * dy - mouse.y}; + if (ex * ex + ey * ey < nearest) + { + nearest = ex * ex + ey * ey; + m_hover = index; + } + } + } + } + else if (false == hovered && false == ImGui::IsItemActive()) + m_hover = -1; + if (ImGui::IsItemDeactivated()) + { + if (false == m_dragging && m_hover >= 0) + { + auto const &path{layout.paths[m_hover]}; + m_selection.set_current_node(path.track); + m_picked = path.track; + m_view.focus_track(*path.track, path.index, path.points[path.points.size() / 2]); + } + m_dragging = false; + } + + auto *draw{ImGui::GetWindowDrawList()}; + ImVec2 const corner_end{corner.x + size.x, corner.y + size.y}; + draw->AddRectFilled(corner, corner_end, IM_COL32(22, 24, 28, 255)); + draw->PushClipRect(corner, corner_end, true); + // the km, or the chainage, along the top + { + auto const metres{across * 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; + auto const origin{layout.kilometrage ? layout.km * 1000.0 : 0.0}; + for (auto mark{std::ceil((view.from + origin) / step) * step}; mark <= view.to + origin; mark += step) + { + auto const x{x_of(mark - origin)}; + draw->AddLine(ImVec2(x, corner.y), ImVec2(x, corner_end.y), IM_COL32(255, 255, 255, 16)); + auto const label{layout.kilometrage ? format("%.3f", mark / 1000.0) : format("%.0f m", mark)}; + 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); + }; + for (auto const &path : layout.paths) + if (path.approach) + draw_path(path, kApproach, 2.0f); + for (auto const &path : layout.paths) + if (false == path.approach) + draw_path(path, is_switch(*path.track) ? kSwitch : kPlain, 2.5f); + for (auto const &path : layout.paths) + if (path.track == selected) + draw_path(path, overlay_color::selected, 4.0f); + if (m_hover >= 0) + draw_path(layout.paths[m_hover], kHovered, 4.0f); + // the switches: a dot at the start, the name over or under it in turn + 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]))}; + draw->AddCircleFilled(at, 3.5f, kSwitch); + auto const text{track->name()}; + auto const extent{ImGui::CalcTextSize(text.c_str())}; + ImVec2 const place{at.x - extent.x * 0.5f, index % 2 == 0 ? at.y - extent.y - 6.0f : at.y + 6.0f}; + draw->AddRectFilled(ImVec2(place.x - 2.0f, place.y - 1.0f), ImVec2(place.x + extent.x + 2.0f, place.y + extent.y + 1.0f), IM_COL32(22, 24, 28, 200), 2.0f); + draw->AddText(place, kSwitch, text.c_str()); + } + 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}; + 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); + if (layout.kilometrage) + ImGui::TextDisabled("km %.3f", station::km_of(layout, along)); + ImGui::TextDisabled("%s", STR_C("LMB: select it")); + ImGui::EndTooltip(); + } +} + +void srk_tool::render_windows() +{ + if (false == m_open || false == m_layout.has_value()) + return; + auto const &io{ImGui::GetIO()}; + // along the bottom of the 3d view, as the vertical profile + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + auto const width{std::max(480.0f, high.x - low.x)}; + auto const height{std::clamp((high.y - low.y) * 0.4f, 220.0f, std::max(220.0f, high.y - low.y - 80.0f))}; + ImGui::SetNextWindowPos(ImVec2(low.x, high.y - height), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Appearing); + auto const name{std::string{STR_C("Station schematic")} + " " + title() + "###srkstrip"}; + if (ImGui::Begin(name.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + auto const position{ImGui::GetWindowPos()}; + auto const size{ImGui::GetWindowSize()}; + if (false == ImGui::IsWindowDocked() && (position.y + size.y > io.DisplaySize.y + 1.0f || position.x + 80.0f > io.DisplaySize.x || position.x + size.x < 80.0f)) + ImGui::SetWindowPos(ImVec2(std::clamp(position.x, 0.0f, std::max(0.0f, io.DisplaySize.x - size.x)), std::max(0.0f, std::min(position.y, io.DisplaySize.y - size.y)))); + if (ImGui::Button(STR_C("Fit"))) + m_view_state.exaggeration = 0.0f; + item_tooltip("Shows the whole station"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(140.0f); + ImGui::SliderFloat("##srkexaggeration", &m_view_state.exaggeration, 1.0f, 500.0f, STR_C("offsets x%.0f"), ImGuiSliderFlags_Logarithmic); + item_tooltip("How much the offsets of the tracks are stretched against the lengths, also Ctrl+wheel over the plot"); + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("Wheel: zoom, drag: move, LMB on a track: select it")); + render_canvas(ImGui::GetContentRegionAvail().y); + } + ImGui::End(); +} + +editor_selftest_result srk_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "srkopen") + { + // srkopen track=: the station of the track, as a click on it opens it + auto const values{selftest_values(Words)}; + auto const found{values.find("track")}; + auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr}; + m_open = true; + load(); + if (track == nullptr) + { + WriteLog("SELFTEST srkopen: no such track"); + return editor_selftest_result::handled; + } + open(*track); + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkopen: " + m_status); + return editor_selftest_result::handled; + } + auto const &layout{*m_layout}; + std::unordered_set tracks; + std::size_t approach{0}; + for (auto const &path : layout.paths) + if (path.approach) + ++approach; + else + tracks.insert(path.track); + WriteLog(format("SELFTEST srkopen %s: %zu switches, %zu tracks, %zu paths out, axis %.0f m, switches over %.0f..%.0f, offsets %.1f..%.1f, km %s, record %d", m_anchor.c_str(), layout.switches.size(), tracks.size(), approach, + layout.chainage.back(), layout.from, layout.to, layout.low, layout.high, layout.kilometrage ? format("%.3f", layout.km).c_str() : "-", m_record)); + for (auto const *track : layout.switches) + { + auto const at{station::project(layout, track->m_paths.front().points[segment_data::point::start])}; + WriteLog(format("SELFTEST srkopen switch %s at %.1f offset %.1f", track->name().c_str(), at.x, at.y)); + } + return editor_selftest_result::handled; + } + if (Command == "srkstations") + { + load(); + find_all(); + WriteLog(format("SELFTEST srkstations: %zu", m_found.size())); + for (auto const &[name, count] : m_found) + WriteLog(format("SELFTEST srkstations %s: %zu switches", name.c_str(), count)); + return editor_selftest_result::handled; + } + if (Command == "srkset") + { + // srkset code= line= name= + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkset: no station open"); + return editor_selftest_result::handled; + } + auto const values{selftest_values(Words)}; + auto &entry{record_of_open()}; + if (auto const code{values.find("code")}; code != values.end()) + entry.code = code->second; + if (auto const line{values.find("line")}; line != values.end()) + entry.line = line->second; + if (auto const name{values.find("name")}; name != values.end()) + { + entry.name = name->second; + std::replace(entry.name.begin(), entry.name.end(), '_', ' '); + } + m_changed = true; + WriteLog("SELFTEST srkset: " + title()); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodesrk.h b/application/editormodesrk.h new file mode 100644 index 00000000..2460bda5 --- /dev/null +++ b/application/editormodesrk.h @@ -0,0 +1,81 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editortool.h" +#include "editor/editorStation.hpp" +#include "scene/scenelayers.h" + +#include +#include +#include +#include + +// the signalling of a station (SRK): the station found from a switch of it, its schematic, its name and code +class srk_tool final : public editor_tool +{ + public: + srk_tool(editor_selection &Selection, editor_view &View, editor_history &History) : m_selection{Selection}, m_view{View}, m_history{History} {} + void update() override; + void draw_overlay() const override; + void render_options() override; + void render_windows() override; + void forget(scene::basic_node const *Node) override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + void set_open(bool const Open) { m_open = Open; } + // the stations described go to the files of their layers, as "//$s" lines + void store(); + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + // a station described: found again by a switch of it + struct record + { + scene::layer_handle layer{null_handle}; + std::string anchor; // the name of a switch of the station + std::string code; // the abbreviation of the station, as MMz + std::string name; + std::string line; // the number of the railway line + }; + // the schematic: the chainage across, the offsets upwards, exaggerated + struct view + { + double from{0.0}; + double to{1000.0}; + double centre{0.0}; + float exaggeration{0.0f}; // 0: fit at the next draw + }; + bool m_open{false}; + bool m_loaded{false}; + bool m_changed{false}; + std::vector m_records; + std::optional m_layout; + int m_record{-1}; // of the open station, -1 while it has none + std::string m_anchor; // the switch the open station was found from + std::size_t m_history_size{0}; + TTrack const *m_picked{nullptr}; // the track selected when the station was looked for + view m_view_state; + int m_hover{-1}; // path of the layout under the mouse in the schematic + bool m_dragging{false}; + std::vector> m_found; // the stations of the scenery: a switch and the count of them + std::string m_status; + + void load(); + void open(TTrack &Track); + void refresh(); + record &record_of_open(); + std::string title() const; + void render_found(); + void render_canvas(float const Height); + void fit_view(float const Width, float const Height); + void find_all(); +}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index bdb4d216..7da24b60 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -411,9 +411,9 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve profile_restore(); } -std::array const &editor_mode::track_modes() +std::array const &editor_mode::track_modes() { - static std::array const modes{{ + static std::array const modes{{ {"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters", editor_icons::icon::select, 0}, {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track", editor_icons::icon::lay_track, 0}, {"Plan", "G", track_tab::straights, "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too", editor_icons::icon::curve, 0}, @@ -421,6 +421,7 @@ std::array const &editor_mode::track_modes() {"Parallel", "N", track_tab::parallel, "A second track beside a track: LMB on it, then the side and the spacing", editor_icons::icon::parallel, 0}, {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it", editor_icons::icon::turntable, 1}, {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by", editor_icons::icon::signal, 1}, + {"Signalling", "I", track_tab::srk, "The signalling of a station: LMB on a switch lays its station out as a schematic", editor_icons::icon::station, 1}, {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, trainsets", editor_icons::icon::objects, 1}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line", editor_icons::icon::profile, 2}, {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows", editor_icons::icon::speed, 2}, @@ -565,6 +566,7 @@ std::string editor_mode::track_mode_name() const case track_tab::infra: return STR_C("Infrastructure"); case track_tab::parallel: return STR_C("Parallel track"); case track_tab::signals: return STR_C("Signal"); + case track_tab::srk: return STR_C("Signalling"); case track_tab::turntable: return STR_C("Turntable"); default: return STR_C("Select"); } @@ -577,6 +579,7 @@ void editor_mode::render_track_inspector() m_speed.set_open(m_track_window_open && m_track_tab == track_tab::speed); m_infra.open = m_track_window_open && m_track_tab == track_tab::infra; m_joints.set_open(m_track_window_open && m_track_tab == track_tab::joints); + m_srk.set_open(m_track_window_open && m_track_tab == track_tab::srk); if (false == m_track_window_open || m_track_tab != track_tab::lineside) { m_sweep.open = false; @@ -618,6 +621,7 @@ void editor_mode::render_track_inspector() m_profile.strip_top = -1.0f; if (m_track_tab == track_tab::profile && m_track_window_open) render_profile_strip(); + m_srk.render_windows(); } void editor_mode::render_track_tool_options() @@ -659,6 +663,7 @@ void editor_mode::render_track_tool_options() break; case track_tab::lineside: render_lineside_ui(); break; case track_tab::signals: render_signal_ui(); break; + case track_tab::srk: m_srk.render_options(); break; case track_tab::turntable: render_turntable_ui(); break; default: render_track_set_ui(); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index f40348d3..1c562ed1 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -458,6 +458,13 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) } return intent; } + if (m_track_tab == track_tab::srk && m_hover.track != nullptr) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + return make(kind::select, m_hover.track->eType != tt_Normal ? "Click: the station of this switch" : "Click: the station of the switch nearest along this track"); + } if (m_track_style.picking && (m_track_tab == track_tab::path || m_track_tab == track_tab::turnout)) { intent.track = m_hover.track; diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index 1bbed3fa..3f9887f7 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -177,10 +177,10 @@ void editor_mode::work_area_tools(std::vector &Buttons) add(track_tab::straights, false); add(track_tab::turnout, false); add(track_tab::parallel, false); - auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || + auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::srk || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_gauge.is_open()}; auto const more{EditorSettings.track_tools_more()}; - Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signals, objects, trainset, profile, speed, joints, gauge"), more, [more]() { + Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signals, signalling, objects, trainset, profile, speed, joints, gauge"), more, [more]() { EditorSettings.track_tools_more(false == more); EditorSettings.save(); }, true}); @@ -188,6 +188,7 @@ void editor_mode::work_area_tools(std::vector &Buttons) break; add(track_tab::turntable, true); add(track_tab::signals, false); + add(track_tab::srk, false); add(track_tab::lineside, false); Buttons.push_back({icon::vehicle, STR_C("Trainset"), nullptr, STR_C("A trainset on the selected track: written to the scenery on save, and to drive at once"), vehicle, [this, vehicle]() { show_track_tab(vehicle ? track_tab::path : track_tab::lineside); diff --git a/application/editortool.h b/application/editortool.h index f93503f8..744a7646 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -36,7 +36,7 @@ enum class editor_selftest_result enum class editor_first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; -enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel }; +enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel, srk }; struct track_hover { diff --git a/editor/editorIcons.hpp b/editor/editorIcons.hpp index eec9f4a8..8b583b40 100644 --- a/editor/editorIcons.hpp +++ b/editor/editorIcons.hpp @@ -51,6 +51,7 @@ enum class icon gauge, turntable, parallel, + station, more, // the fields of work work_surroundings, diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp new file mode 100644 index 00000000..c60aa4a0 --- /dev/null +++ b/editor/editorStation.cpp @@ -0,0 +1,399 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorStation.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorTrack.hpp" +#include "simulation/simulation.h" +#include "world/Track.h" + +#include +#include +#include +#include +#include + +namespace station +{ + +using geometry::bezier; +using geometry::plan_distance; +using geometry::same_point; + +namespace +{ + +bool rail(TTrack const &Track) +{ + return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && false == Track.m_paths.empty(); +} + +// switches, crossings and turntables: where the plain track between them ends +bool node(TTrack const &Track) +{ + return Track.eType != tt_Normal; +} + +glm::dvec3 end_of(TTrack const &Track, int const Path, bool const Atend) +{ + return Track.m_paths[Path].points[Atend ? segment_data::point::end : segment_data::point::start]; +} + +TTrack *neighbour(TTrack const &Track, int const Path, bool const Atend) +{ + if (Track.SwitchExtension != nullptr && (Track.eType == tt_Switch || Track.eType == tt_Cross)) + return Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]; // NOSONAR + return Atend ? Track.trNext : Track.trPrev; +} + +// the end of the track at the point +bool end_at(TTrack const &Track, glm::dvec3 const &Point, int &Path, bool &Atend) +{ + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + for (auto const atend : {false, true}) + if (glm::distance(end_of(Track, path, atend), Point) < same_point) + { + Path = path; + Atend = atend; + return true; + } + return false; +} + +// the ends of the paths of a switch, each place once: its start, the ends of its paths +std::vector> ends_of(TTrack const &Track) +{ + std::vector> result; + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + for (auto const atend : {false, true}) + { + auto const point{end_of(Track, path, atend)}; + if (std::none_of(result.begin(), result.end(), [&](auto const &End) { return glm::distance(end_of(Track, End.first, End.second), point) < same_point; })) + result.emplace_back(path, atend); + } + return result; +} + +double plan_length(TTrack const &Track, int const Path) +{ + return bezier{Track.m_paths[Path]}.plan_length(16); +} + +// plain track from an end of a switch on, up to the next switch, a free end or past the limit +struct chain +{ + std::vector tracks; + double length{0.0}; + TTrack *reached{nullptr}; // the switch met at its other end + bool ended{false}; // at a free end +}; + +chain follow(TTrack const &From, int const Path, bool const Atend, double const Limit) +{ + chain result; + auto point{end_of(From, Path, Atend)}; + auto *next{neighbour(From, Path, Atend)}; + while (next != nullptr && result.length <= Limit) + { + if (false == rail(*next)) + break; + if (node(*next)) + { + result.reached = next; + return result; + } + int path{0}; + bool atend{false}; + if (false == end_at(*next, point, path, atend) || std::find(result.tracks.begin(), result.tracks.end(), next) != result.tracks.end()) + break; + result.tracks.push_back(next); + result.length += plan_length(*next, 0); + point = end_of(*next, 0, false == atend); + next = neighbour(*next, 0, false == atend); + } + result.ended = next == nullptr || false == rail(*next); + return result; +} + +// the switches and the plain tracks of the station, and the lines leading out of it +struct area +{ + std::vector switches; + std::vector plain; + std::vector exits; +}; + +area collect(TTrack &From, options const &Options) +{ + area result; + if (false == rail(From)) + return result; + auto *first{node(From) ? &From : nullptr}; + if (first == nullptr) + { + auto nearest{Options.link}; + for (auto const atend : {false, true}) + if (auto const line{follow(From, 0, atend, Options.link)}; line.reached != nullptr && line.length < nearest) + { + nearest = line.length; + first = line.reached; + } + } + if (first == nullptr) + return result; + std::unordered_set known{first}; + result.switches.push_back(first); + for (std::size_t i = 0; i < result.switches.size(); ++i) + { + auto const &from{*result.switches[i]}; + for (auto const &[path, atend] : ends_of(from)) + { + auto line{follow(from, path, atend, Options.link)}; + if (line.reached == nullptr && false == line.ended) + { + result.exits.push_back(std::move(line)); + continue; + } + for (auto *track : line.tracks) + if (known.insert(track).second) + result.plain.push_back(track); + if (line.reached != nullptr && known.insert(line.reached).second) + result.switches.push_back(line.reached); + } + } + return result; +} + +std::vector points_of(TTrack const &Track, int const Path, double &Length) +{ + bezier const curve{Track.m_paths[Path]}; + Length = curve.plan_length(16); + auto const count{std::clamp(static_cast(Length / 2.0), 2, 64)}; + std::vector result; + for (int i = 0; i <= count; ++i) + result.push_back(curve.point(static_cast(i) / count)); + return result; +} + +// the map shows the east to the right and the north up: the x of the scenery runs to the west +glm::dvec2 map_of(glm::dvec3 const &Point) +{ + return {-Point.x, Point.z}; +} + +} // namespace + +std::optional find(TTrack &From, options const &Options) +{ + auto const found{collect(From, Options)}; + if (found.switches.empty()) + return std::nullopt; + std::unordered_set switches(found.switches.begin(), found.switches.end()); + // the axis: the run through the station along the main paths of its switches which passes the most of them + std::vector starts; + for (auto const &line : found.exits) + if (false == line.tracks.empty()) + starts.push_back(line.tracks.front()); + if (starts.empty()) + starts.push_back(found.switches.front()); + editor_track::route best; + std::size_t most{0}; + double longest{0.0}; + for (auto *start : starts) + { + auto route{editor_track::run_route(*start, 20000.0)}; + std::size_t passed{0}; + double length{0.0}; + for (auto const &span : route.spans) + { + passed += switches.count(span.track); + length += plan_length(*span.track, span.path); + } + if (passed > most || (passed == most && length > longest)) + { + most = passed; + longest = length; + best = std::move(route); + } + } + layout result; + for (auto const &span : best.spans) + { + double length{0.0}; + auto points{points_of(*span.track, span.path, length)}; + if (false == span.forward) + std::reverse(points.begin(), points.end()); + for (auto const &point : points) + if (result.axis.empty() || plan_distance(result.axis.back(), point) > 0.01) + result.axis.push_back(point); + } + if (result.axis.size() < 2) + return std::nullopt; + auto const measure = [&]() { + result.chainage.assign(1, 0.0); + for (std::size_t i = 1; i < result.axis.size(); ++i) + result.chainage.push_back(result.chainage.back() + plan_distance(result.axis[i - 1], result.axis[i])); + }; + measure(); + // the chainage runs with the km of the switches, when they have one and most of them agree on it + double constexpr kAgree{50.0}; // m + std::vector> marks; // the chainage and the km, in m + for (auto *track : found.switches) + { + auto const start{end_of(*track, 0, false)}; + std::optional km; + if (auto const record{switch_store::named(*track)}; record && record->km) + km = record->km; + else + km = alignment_store::chainage_at(start); + if (km) + marks.emplace_back(project(result, start).x, *km); + } + auto const length{result.chainage.back()}; + struct fit + { + std::size_t agree{0}; + double origin{0.0}; + }; + auto const fit_of = [&](bool const Reversed) { + std::vector origins; + for (auto const &[chainage, km] : marks) + origins.push_back(km - (Reversed ? length - chainage : chainage)); + std::nth_element(origins.begin(), origins.begin() + origins.size() / 2, origins.end()); + fit result{0, origins[origins.size() / 2]}; + for (auto const origin : origins) + result.agree += std::abs(origin - result.origin) <= kAgree ? 1 : 0; + return result; + }; + if (false == marks.empty()) + { + auto const along{fit_of(false)}; + auto const against{fit_of(true)}; + auto const reversed{against.agree > along.agree}; + auto const &best{reversed ? against : along}; + if (best.agree * 2 >= marks.size()) + { + if (reversed) + { + std::reverse(result.axis.begin(), result.axis.end()); + measure(); + } + result.kilometrage = true; + result.km = best.origin / 1000.0; + } + } + // the paths, laid out along the axis + auto const add = [&](TTrack &Track, bool const Approach) { + for (int index = 0; index < static_cast(Track.m_paths.size()); ++index) + { + path entry; + entry.track = &Track; + entry.index = index; + entry.approach = Approach; + entry.points = points_of(Track, index, entry.length); + for (auto const &point : entry.points) + entry.schematic.push_back(project(result, point)); + result.paths.push_back(std::move(entry)); + } + }; + for (auto *track : found.switches) + add(*track, false); + for (auto *track : found.plain) + add(*track, false); + for (auto const &line : found.exits) + { + double length{0.0}; + for (auto *track : line.tracks) + { + if (length >= Options.approach) + break; + add(*track, true); + length += plan_length(*track, 0); + } + } + result.from = std::numeric_limits::max(); + result.to = std::numeric_limits::lowest(); + result.low = std::numeric_limits::max(); + result.high = std::numeric_limits::lowest(); + for (auto const &entry : result.paths) + { + if (entry.approach) + continue; + for (auto const &point : entry.schematic) + { + if (switches.count(entry.track) > 0) + { + result.from = std::min(result.from, point.x); + result.to = std::max(result.to, point.x); + } + result.low = std::min(result.low, point.y); + result.high = std::max(result.high, point.y); + } + } + result.switches = found.switches; + std::sort(result.switches.begin(), result.switches.end(), [&](TTrack const *A, TTrack const *B) { return project(result, end_of(*A, 0, false)).x < project(result, end_of(*B, 0, false)).x; }); + return result; +} + +glm::dvec2 project(layout const &Layout, glm::dvec3 const &Point) +{ + auto const point{map_of(Point)}; + auto best{std::numeric_limits::max()}; + glm::dvec2 result{0.0}; + for (std::size_t i = 1; i < Layout.axis.size(); ++i) + { + auto const a{map_of(Layout.axis[i - 1])}; + auto const b{map_of(Layout.axis[i])}; + auto const along{b - a}; + auto const length{glm::length(along)}; + if (length < 1e-9) + continue; + auto const unit{along / length}; + auto const t{std::clamp(glm::dot(point - a, unit), 0.0, length)}; + auto const foot{a + unit * t}; + auto const distance{glm::distance(point, foot)}; + if (distance >= best) + continue; + best = distance; + auto const side{point - foot}; + result = {Layout.chainage[i - 1] + t, unit.x * side.y - unit.y * side.x}; + } + return result; +} + +double km_of(layout const &Layout, double const Chainage) +{ + return Layout.km + Chainage / 1000.0; +} + +std::vector stations(options const &Options) +{ + std::vector result; + std::unordered_set taken; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || false == node(*track) || taken.count(track) > 0) + continue; + auto const found{collect(*track, Options)}; + for (auto const *member : found.switches) + taken.insert(member); + result.push_back(track); + } + return result; +} + +std::vector switches_of(TTrack &Switch, options const &Options) +{ + return collect(Switch, Options).switches; +} + +} // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp new file mode 100644 index 00000000..96c283a0 --- /dev/null +++ b/editor/editorStation.hpp @@ -0,0 +1,65 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +#include + +class TTrack; + +// a station as the signalling sees it: its switches, the tracks between them and the lines leading out, and its schematic, +// the tracks laid out along the main track through it +namespace station +{ + +struct options +{ + double link{1500.0}; // m, switches joined by plain track no longer than this belong to the same station + double approach{400.0}; // m, of each line leading out, taken beyond the outermost switch +}; + +struct path +{ + TTrack *track{nullptr}; + int index{0}; // of the path of the track + std::vector points; // along the path, world space + std::vector schematic; // the chainage along the axis and the offset to the left of it, as the map shows it + double length{0.0}; // in the plan + bool approach{false}; // on a line leading out of the station +}; + +struct layout +{ + std::vector switches; // switches, crossings and turntables, by the chainage of their start + std::vector paths; + std::vector axis; // the main route through the station + std::vector chainage; // of the points of the axis + double from{0.0}; // the chainage over which the switches lie + double to{0.0}; + double low{0.0}; // the offsets of the outermost tracks of the station + double high{0.0}; + bool kilometrage{false}; // the chainage runs with the km of the switches + double km{0.0}; // at chainage 0, when it does +}; + +// 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 +glm::dvec2 project(layout const &Layout, glm::dvec3 const &Point); +// km at the chainage, when the station knows its kilometrage +double km_of(layout const &Layout, double const Chainage); +// one switch of each station of the scenery +std::vector stations(options const &Options = {}); +// the switches of the station of the switch, without laying it out +std::vector switches_of(TTrack &Switch, options const &Options = {}); + +} // namespace station diff --git a/editor/icons/editor_icons.png b/editor/icons/editor_icons.png index affe75b5..c7203149 100644 Binary files a/editor/icons/editor_icons.png and b/editor/icons/editor_icons.png differ diff --git a/editor/icons/editor_icons.py b/editor/icons/editor_icons.py index a1756b34..3015a1a0 100644 --- a/editor/icons/editor_icons.py +++ b/editor/icons/editor_icons.py @@ -331,6 +331,15 @@ def parallel(d): arrowhead(d, 40, 32, 0, 10) +def station(d): + # two tracks of a station with a crossover between them, and the insulated joints of its track circuits + line(d, [(4, 22), (60, 22)], W - 6) + line(d, [(4, 46), (60, 46)], W - 6) + line(d, [(14, 46), (32, 22)], W - 6) + line(d, [(46, 12), (46, 32)], W - 10) + line(d, [(46, 36), (46, 56)], W - 10) + + def more(d): for x in (12, 32, 52): circle(d, x, 32, 6) @@ -381,7 +390,7 @@ def work_terrain(d): ICONS = [select, insert, brush, area_fill, copy_to_bank, translate, rotate, scale, local_space, ortho, perspective, sculpt, smooth, chunks, orthophoto, road, place, lanes, lay_track, switch, straight, curve, signal, objects, - vehicle, profile, speed, joints, infra, gauge, turntable, parallel, more, work_surroundings, + vehicle, profile, speed, joints, infra, gauge, turntable, parallel, station, more, work_surroundings, work_tracks, work_roads, work_terrain] diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 4a2b9083..58b5fc97 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5981,8 +5981,8 @@ msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowani msgid "Crossover, double, scissors, ladder" msgstr "Przejście rozjazdowe, podwójne przejście rozjazdowe, nożyce, ulica zwrotnicowa" -msgid "Turntable, signals, objects, trainset, profile, speed, joints, gauge" -msgstr "Obrotnica, sygnalizatory, obiekty, skład, profil, prędkość, styki, skrajnia" +msgid "Turntable, signals, signalling, objects, trainset, profile, speed, joints, gauge" +msgstr "Obrotnica, sygnalizatory, SRK, obiekty, skład, profil, prędkość, styki, skrajnia" msgid "No switch drive models" msgstr "Brak modeli napędów" @@ -10270,3 +10270,98 @@ msgstr "Wyłączone: tor kładziony od innego toru albo dołączany do niego bie msgid "Off: a track laid on from another one, or joined to it, is designed for the speed of that track and takes it.\nOn: it's designed for the speed above and the new tracks get it.\nWhile laying: Ctrl+Shift+wheel" msgstr "Wyłączone: tor kładziony od innego toru albo dołączany do niego jest projektowany na prędkość tamtego toru i ją dostaje.\nWłączone: jest projektowany na prędkość z pola powyżej i nowe tory ją dostają.\nW trakcie kładzenia: Ctrl+Shift+kółko" + +# scenery editor: the signalling of a station (SRK) +msgid "Signalling" +msgstr "SRK" + +msgid "The signalling of a station: LMB on a switch lays its station out as a schematic" +msgstr "Sterowanie ruchem na stacji: LPM na rozjazd rozkłada jego stację w schemat" + +msgid "Click: the station of this switch" +msgstr "Klik: stacja tego rozjazdu" + +msgid "Click: the station of the switch nearest along this track" +msgstr "Klik: stacja najbliższego rozjazdu wzdłuż tego toru" + +msgid "No switch near this track: it isn't a part of a station" +msgstr "Przy tym torze nie ma rozjazdu: nie należy do żadnej stacji" + +msgid "Station of %s" +msgstr "Stacja rozjazdu %s" + +msgid "Find the stations of the scenery" +msgstr "Znajdź stacje w scenerii" + +msgid "Each group of switches joined by tracks shorter than 1.5 km is a station" +msgstr "Stacją jest każda grupa rozjazdów połączonych torami krótszymi niż 1.5 km" + +msgid "The signalling of a station: its schematic, the numbers of the switches and the tracks, the track circuits and the signals" +msgstr "SRK stacji: jej schemat, numery rozjazdów i torów, izolacje i semafory" + +msgid "LMB on a switch" +msgstr "LPM na rozjazd" + +msgid "opens its station" +msgstr "otwiera jego stację" + +msgid "switches" +msgstr "rozjazdy" + +msgid "Code" +msgstr "Skrót" + +msgid "The abbreviation of the station: the names of its switches, track circuits and signals begin with it" +msgstr "Skrót nazwy stacji: od niego zaczynają się nazwy jej rozjazdów, izolacji i semaforów" + +msgid "Line" +msgstr "Linia" + +msgid "The number of the railway line the station lies on, as the block sections between the stations are named after it" +msgstr "Numer linii kolejowej, na której leży stacja: od niego biorą nazwy odstępy blokady między stacjami" + +msgid "Type" +msgstr "Typ" + +msgid "km" +msgstr "km" + +msgid "Another station" +msgstr "Inna stacja" + +msgid "Leaves this station; LMB on a switch opens another one" +msgstr "Zamyka tę stację; LPM na rozjazd otwiera inną" + +msgid "Station schematic" +msgstr "Schemat stacji" + +msgid "Shows the whole station" +msgstr "Pokazuje całą stację" + +msgid "offsets x%.0f" +msgstr "rozstawy x%.0f" + +msgid "How much the offsets of the tracks are stretched against the lengths, also Ctrl+wheel over the plot" +msgstr "Ile razy rozstawy torów są rozciągnięte względem długości, także Ctrl+kółko nad schematem" + +msgid "Wheel: zoom, drag: move, LMB on a track: select it" +msgstr "Kółko: przybliżenie, przeciąganie: przesunięcie, LPM na tor: zaznacza go" + +msgid "line out of the station" +msgstr "szlak wychodzący ze stacji" + +msgid "LMB: select it" +msgstr "LPM: zaznacza go" + +msgid "%zu station" +msgid_plural "%zu stations" +msgstr[0] "%zu stacja" +msgstr[1] "%zu stacje" +msgstr[2] "%zu stacji" + +msgid "%s: %zu switch" +msgid_plural "%s: %zu switches" +msgstr[0] "%s: %zu rozjazd" +msgstr[1] "%s: %zu rozjazdy" +msgstr[2] "%s: %zu rozjazdów" + diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 561ccdd0..c7163477 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -843,16 +843,17 @@ bool node_layers::pending() const namespace { // marks of the lines the editor keeps track of -std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o"}; +std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s"}; // other tools put lines of their own after some of these marks: Rainsted its includes after //$r, the launcher the descriptions // of the trainsets after //$o. The editor's lines of them are told by their first word bool editor_line(char const *Mark, std::string const &Text) { - static std::array>, 3> const words{{ + static std::array>, 4> const words{{ {"//$h", {"#", "alignment", "source", "ends", "norms", "pieces", "vertex", "element", "end"}}, {"//$r", {"#", "switch", "slip", "crossing", "double", "connection"}}, {"//$o", {"on"}}, + {"//$s", {"#", "station"}}, }}; auto const lookup{std::find_if(words.begin(), words.end(), [&](auto const &Entry) { return Entry.first == Mark; })}; if (lookup == words.end())