17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-26 19:24:54 +01:00

feat(editor): the signalling mode finds the station of a switch, lays it out as a schematic along its main track and keeps its name, code and line in the scenery

This commit is contained in:
maj00r
2026-10-22 07:35:31 +02:00
parent 7fb9d2d458
commit 8c5795348f
16 changed files with 1325 additions and 12 deletions

View File

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

View File

@@ -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});

View File

@@ -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<track_mode, 11> const &track_modes();
static std::array<track_mode, 12> 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<editor_tool *> 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<editor_tool *> 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();

View File

@@ -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 <algorithm>
#include <array>
#include <cmath>
#include <unordered_set>
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<unsigned char>(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<scene::layer_handle> 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<std::string> 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<int>(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<int>(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<int>(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<TTrack const *> 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<char, 128> 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<float>(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<double>(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<float>((Chainage - view.from) / across); };
auto const y_of = [&](double const Offset) { return corner.y + size.y * 0.5f - static_cast<float>((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<int>(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=<name>: 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<TTrack const *> 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=<code> line=<line> name=<words joined by _>
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;
}

View File

@@ -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 <optional>
#include <string>
#include <utility>
#include <vector>
// 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<record> m_records;
std::optional<station::layout> 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<std::pair<std::string, std::size_t>> 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();
};

View File

@@ -411,9 +411,9 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve
profile_restore();
}
std::array<editor_mode::track_mode, 11> const &editor_mode::track_modes()
std::array<editor_mode::track_mode, 12> const &editor_mode::track_modes()
{
static std::array<track_mode, 11> const modes{{
static std::array<track_mode, 12> 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<editor_mode::track_mode, 11> 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();

View File

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

View File

@@ -177,10 +177,10 @@ void editor_mode::work_area_tools(std::vector<tool_button> &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<tool_button> &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);

View File

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

View File

@@ -51,6 +51,7 @@ enum class icon
gauge,
turntable,
parallel,
station,
more,
// the fields of work
work_surroundings,

399
editor/editorStation.cpp Normal file
View File

@@ -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 <algorithm>
#include <cmath>
#include <limits>
#include <unordered_set>
#include <utility>
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<int>(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<std::pair<int, bool>> ends_of(TTrack const &Track)
{
std::vector<std::pair<int, bool>> result;
for (int path = 0; path < static_cast<int>(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<TTrack *> 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<TTrack *> switches;
std::vector<TTrack *> plain;
std::vector<chain> 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<TTrack const *> 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<glm::dvec3> 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<int>(Length / 2.0), 2, 64)};
std::vector<glm::dvec3> result;
for (int i = 0; i <= count; ++i)
result.push_back(curve.point(static_cast<double>(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<layout> find(TTrack &From, options const &Options)
{
auto const found{collect(From, Options)};
if (found.switches.empty())
return std::nullopt;
std::unordered_set<TTrack const *> 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<TTrack *> 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<std::pair<double, double>> marks; // the chainage and the km, in m
for (auto *track : found.switches)
{
auto const start{end_of(*track, 0, false)};
std::optional<double> 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<double> 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<int>(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<double>::max();
result.to = std::numeric_limits<double>::lowest();
result.low = std::numeric_limits<double>::max();
result.high = std::numeric_limits<double>::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<double>::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<TTrack *> stations(options const &Options)
{
std::vector<TTrack *> result;
std::unordered_set<TTrack const *> 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<TTrack *> switches_of(TTrack &Switch, options const &Options)
{
return collect(Switch, Options).switches;
}
} // namespace station

65
editor/editorStation.hpp Normal file
View File

@@ -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 <optional>
#include <vector>
#include <glm/glm.hpp>
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<glm::dvec3> points; // along the path, world space
std::vector<glm::dvec2> 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<TTrack *> switches; // switches, crossings and turntables, by the chainage of their start
std::vector<path> paths;
std::vector<glm::dvec3> axis; // the main route through the station
std::vector<double> 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<layout> 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<TTrack *> stations(options const &Options = {});
// the switches of the station of the switch, without laying it out
std::vector<TTrack *> switches_of(TTrack &Switch, options const &Options = {});
} // namespace station

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

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

View File

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

View File

@@ -843,16 +843,17 @@ bool node_layers::pending() const
namespace
{
// marks of the lines the editor keeps track of
std::array<char const *, 6> const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o"};
std::array<char const *, 7> 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<std::pair<std::string_view, std::array<std::string_view, 9>>, 3> const words{{
static std::array<std::pair<std::string_view, std::array<std::string_view, 9>>, 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())