17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-27 14:07:28 +01:00

feat(editor): the kilometrage tool sets the km of the railway lines, points where the km is known and jumps of the km, the rest along the axis of the line, between the tracks of a double-track one and along the longer track where they run apart, kept in the scenery as //$k lines

The track mode Kilometrage (X, under More) puts a point of the km where a track is clicked: the line, the km and, at a jump,
the km on from it. The axis of a line follows the main paths of the switches through the tracks of its points; on a
double-track line it runs between the tracks, and where they part on formations of their own and come together again it
runs along the longer one, the shorter one going by its own length with its last hectometre before they meet taking up
the difference (PKP GK-1 6.1.12). The km between the points are stretched to the length of the scenery evenly; the panel
lists the points with the stretch, the 3D view shows the axis, a tick at each hectometre, the points with their km and
the km of the lines by the mouse. Read the km from the posts turns the hectometre posts along the line into the points
of its km, a jump where two runs of numbers meet, and warns of a jump on a curve or at a station (Id-1 § 19). Each change
is a step of undo.
This commit is contained in:
maj00r
2026-10-22 13:27:37 +02:00
parent 8fde4dc843
commit 5ecb6ebe00
17 changed files with 2166 additions and 15 deletions

View File

@@ -191,6 +191,7 @@ set(SOURCES
"editor/editorGroundFollow.cpp"
"editor/editorAlignmentStore.cpp"
"editor/editorSwitchStore.cpp"
"editor/editorKilometrage.cpp"
"editor/editorStation.cpp"
"editor/editorSwitchDesign.cpp"
"editor/editorConnections.cpp"
@@ -232,6 +233,7 @@ set(SOURCES
"application/editormodeinfra.cpp"
"application/editormodespeed.cpp"
"application/editormodesrk.cpp"
"application/editormodekm.cpp"
"application/editormodejoints.cpp"
"application/editormodeparallel.cpp"
"application/editormodelineside.cpp"

View File

@@ -510,6 +510,7 @@ void editor_mode::editormode_input::poll()
editor_mode::editor_mode() {
alignment_store::reset();
switch_store::reset();
kilometrage::reset();
m_userinterface = std::make_shared<editor_ui>();
// the area fill settings live in the node bank window, in the tab of the fill mode
ui()->set_fill_options([this]() { render_area_fill(); });
@@ -1014,6 +1015,13 @@ void editor_mode::undo_one()
return;
}
if (snap.action == EditorSnapshot::Action::Kilometrage)
{
restore_kilometrage(snap);
g_redo.push_back(std::move(snap));
return;
}
if (snap.action == EditorSnapshot::Action::Bend)
{
restore_bend(snap, true);
@@ -1183,6 +1191,13 @@ void editor_mode::redo_one()
return;
}
if (snap.action == EditorSnapshot::Action::Kilometrage)
{
restore_kilometrage(snap);
m_history.push_back(std::move(snap));
return;
}
if (snap.action == EditorSnapshot::Action::Bend)
{
restore_bend(snap, false);
@@ -1506,6 +1521,8 @@ bool editor_mode::update()
m_speed.update();
m_joints.update();
m_srk.update();
kilometrage::follow(m_track_edits);
m_km.update();
draw_track_hints();
draw_empty_scenery_hint();
draw_profile_overlay();
@@ -1513,6 +1530,7 @@ bool editor_mode::update()
m_speed.draw_overlay();
m_joints.draw_overlay();
m_srk.draw_overlay();
m_km.draw_overlay();
}
else
{
@@ -6863,7 +6881,8 @@ void editor_mode::render_change_history(){
s.action == EditorSnapshot::Action::TrackEdit ? "TRK" :
s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" :
s.action == EditorSnapshot::Action::Array ? "ARR" :
s.action == EditorSnapshot::Action::Terrain ? "TER" : "OTH",
s.action == EditorSnapshot::Action::Terrain ? "TER" :
s.action == EditorSnapshot::Action::Kilometrage ? "KM" : "OTH",
s.node_name.empty() ? STR_C("(no name)") : s.node_name.c_str(),
s.position.x, s.position.y, s.position.z);

View File

@@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/editormodeparallel.h"
#include "application/editormodespeed.h"
#include "application/editormodesrk.h"
#include "application/editormodekm.h"
#include "application/editormodevehicle.h"
#include "application/editortool.h"
#include "scene/sceneeditor.h"
@@ -34,6 +35,7 @@ http://mozilla.org/MPL/2.0/.
#include "editor/editorAlignment.hpp"
#include "editor/editorAlignmentStore.hpp"
#include "editor/editorSwitchStore.hpp"
#include "editor/editorKilometrage.hpp"
#include "editor/editorSpeed.hpp"
#include "editor/editorRoad.hpp"
#include "editor/editorProfile.hpp"
@@ -123,7 +125,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri
struct stored_profile;
struct EditorSnapshot
{
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain };
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain, Kilometrage };
// copy of a model an array made
struct array_copy
{
@@ -161,6 +163,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri
std::vector<stored_profile> profile_library;
std::optional<std::vector<alignment_store::record>> alignments; // the kept horizontal alignments changed too, these were the ones before
std::optional<std::vector<switch_store::record>> switch_records; // the kept switches changed too, these were the ones before
std::optional<std::vector<kilometrage::mark>> kilometrage; // the marks of the kilometrage before the change (Action::Kilometrage)
std::vector<array_copy> copies; // models an array added to the one it was made of
bool joined{false}; // undone and redone together with the change before it
std::shared_ptr<terrain_streamer::change const> terrain; // the heightmap terrain from before the change (Action::Terrain)
@@ -185,6 +188,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri
void terrain_history_step();
// puts the terrain back as the step recorded it, the step left with the state it replaced
void restore_terrain(EditorSnapshot &Snapshot);
// swaps the marks of the kilometrage with the ones the snapshot keeps
void restore_kilometrage(EditorSnapshot &Snapshot);
bool m_history_busy{false}; // undo or redo under way
void handle_brush_mouse_hold(int Action, int Button);
void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value);
@@ -790,6 +795,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri
void commit_track_drag(bool const Force);
void cancel_track_tools();
void push_track_snapshot(std::vector<std::pair<TTrack *, editor_track::state>> States, std::vector<TTrack *> Created = {}, std::vector<TTrack *> Removed = {}) override;
void push_kilometrage_snapshot() override;
void set_current_node(scene::basic_node *Node) override;
alignment::limits const &route_norms() const override;
void clear_track_point() override;
@@ -1545,7 +1551,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, 12> const &track_modes();
static std::array<track_mode, 13> 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
@@ -1632,7 +1638,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri
void draw_track_hover() const;
struct track_intent
{
enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure, route_trace };
enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure, route_trace, kilometrage };
kind what{kind::none};
TTrack *track{nullptr};
int path{0};
@@ -2384,7 +2390,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri
fouling_tool m_fouling{*this, *this, *this, *this, *this};
parallel_tool m_parallel{*this, *this, *this, *this};
srk_tool m_srk{*this, *this, *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};
km_tool m_km{*this, *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, &m_km};
void infra_rebase();
void render_infra_search();

View File

@@ -0,0 +1,904 @@
/*
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/editormodekm.h"
#include "application/editorgestures.h"
#include "application/editormodeshared.h"
#include "application/editorprojection.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorTrack.hpp"
#include "simulation/simulation.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/utilities.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include "utilities/translation.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <limits>
namespace
{
using geometry::plan_distance;
using geometry::plan_of;
double constexpr kOverlayRange{3000.0};
double constexpr kTickRange{1500.0}; // m, of the hectometres shown in the scenery
double constexpr kLabelRange{700.0}; // m, of the km written there, farther ones crowd the horizon
double constexpr kDigitRange{250.0}; // m, of the hectometres written there
double constexpr kRailHead{0.18}; // m, above the axis of a path
double constexpr kPickReach{15.0}; // m from a point of the km a click on a track takes it
double constexpr kReadoutReach{30.0}; // m from the axis of a line its km is shown by the mouse
double constexpr kPostReach{60000.0}; // m along the line each way, of the posts read
double constexpr kPostCorridor{15.0}; // m from the track, posts farther away belong to another line
double constexpr kPostTolerance{10.0}; // m, of the distance between two posts against their numbers
double constexpr kCurve{10000.0}; // m, of the radius a jump of the km is on a curve under
double constexpr kStation{300.0}; // m from a switch a jump of the km is at a station
ImU32 constexpr kAxis{IM_COL32(110, 200, 255, 220)};
ImU32 constexpr kGuessed{IM_COL32(110, 200, 255, 90)};
ImU32 constexpr kTick{IM_COL32(235, 240, 250, 230)};
ImU32 constexpr kKm{IM_COL32(255, 215, 90, 255)};
ImU32 constexpr kJump{IM_COL32(255, 95, 215, 255)};
ImU32 constexpr kMark{IM_COL32(95, 220, 140, 255)};
ImU32 constexpr kAstray{IM_COL32(240, 90, 80, 255)};
// the digits after the rotation make the number of a hectometre post, the last one the hectometre
bool hectometres_of(std::vector<std::string> const &Values, int &Hectometres)
{
if (Values.size() < 6)
return false;
Hectometres = 0;
for (std::size_t i = 5; i < Values.size(); ++i)
{
double digit;
if (false == numeric(Values[i], digit) || digit < 0.0 || digit > 9.0)
return false;
Hectometres = Hectometres * 10 + static_cast<int>(digit);
}
return true;
}
// chainage of the point of the samples nearest to the point, and how far from them it is
double project(std::vector<editor_track::route_sample> const &Samples, glm::dvec3 const &Point, double &Distance)
{
Distance = std::numeric_limits<double>::max();
double chainage{0.0};
auto const point{plan_of(Point)};
for (std::size_t i = 1; i < Samples.size(); ++i)
{
auto const a{plan_of(Samples[i - 1].position)};
auto const ab{plan_of(Samples[i].position) - a};
auto const length2{glm::dot(ab, ab)};
auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0};
if (auto const distance{glm::length(point - (a + ab * t))}; distance < Distance)
{
Distance = distance;
chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t;
}
}
return chainage;
}
std::size_t sample_at(std::vector<editor_track::route_sample> const &Samples, double const Chainage)
{
auto const found{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })};
return std::min<std::size_t>(static_cast<std::size_t>(found - Samples.begin()), Samples.size() - 1);
}
// the number of a line: blanks taken out
std::string line_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;
}
// a km typed as PKP PLK write it, 100,234, or with a point
bool km_of(char const *Text, double &Km)
{
std::string text{Text};
std::replace(text.begin(), text.end(), ',', '.');
char *end{nullptr};
auto const value{std::strtod(text.c_str(), &end)};
if (end == text.c_str() || false == std::isfinite(value))
return false;
Km = value;
return true;
}
std::string mark_text(kilometrage::mark const &Mark)
{
return Mark.beyond.has_value() ? kilometrage::text(Mark.km) + " = " + kilometrage::text(*Mark.beyond) : kilometrage::text(Mark.km);
}
void draw_label(ImDrawList *List, ImVec2 const At, ImU32 const Ground, ImU32 const Colour, std::string const &Text)
{
auto const size{ImGui::CalcTextSize(Text.c_str())};
List->AddRectFilled(ImVec2(At.x - size.x * 0.5f - 4.0f, At.y - size.y - 4.0f), ImVec2(At.x + size.x * 0.5f + 4.0f, At.y), Ground, 3.0f);
List->AddText(ImVec2(At.x - size.x * 0.5f, At.y - size.y - 2.0f), Colour, Text.c_str());
}
void draw_arrow(screen_projection const &Projection, ImDrawList *List, glm::dvec3 const &From, glm::dvec2 const &Way, double const Length, ImU32 const Colour)
{
glm::dvec3 const along{Way.x, 0.0, Way.y};
glm::dvec3 const across{-Way.y, 0.0, Way.x};
auto const tip{From + along * Length};
Projection.line(List, From, tip, Colour, 3.0f);
Projection.line(List, tip, tip - along * (Length * 0.3) + across * (Length * 0.15), Colour, 3.0f);
Projection.line(List, tip, tip - along * (Length * 0.3) - across * (Length * 0.15), Colour, 3.0f);
}
} // namespace
void km_tool::refresh()
{
m_drawn.clear();
for (auto const &line : kilometrage::lines())
{
auto const *axis{kilometrage::axis_of(line)};
if (axis == nullptr || axis->points.size() < 2)
continue;
drawn entry;
entry.line = line;
entry.points = axis->points;
auto const low{axis->anchors.front().chainage - 1.0};
auto const high{axis->anchors.back().chainage + 1.0};
for (auto const chainage : axis->chainage)
entry.guessed.push_back(chainage < low || chainage > high ? 1 : 0);
std::vector<double> jumps;
for (auto const &anchor : axis->anchors)
{
if (std::abs(anchor.after - anchor.before) < 1e-6)
continue;
jumps.push_back(anchor.chainage);
auto const way{kilometrage::direction_on(*axis, anchor.chainage)};
entry.ticks.push_back({kilometrage::point_on(*axis, anchor.chainage), {-way.y, 0.0, way.x}, 0, true});
}
// a tick at each hectometre the km pass along the axis, none at a jump
auto const hectometres_along = [&](std::vector<glm::dvec3> const &Points, std::vector<double> const &Km, std::vector<double> const &Chainage, std::vector<tick> &Ticks) {
for (std::size_t i = 1; i < Points.size(); ++i)
{
auto const from{std::min(Chainage[i - 1], Chainage[i])};
auto const to{std::max(Chainage[i - 1], Chainage[i])};
if (std::any_of(jumps.begin(), jumps.end(), [&](double const Jump) { return Jump >= from - 1e-6 && Jump <= to + 1e-6; }))
continue;
auto const k0{Km[i - 1]};
auto const k1{Km[i]};
if (std::abs(k1 - k0) < 1e-9)
continue;
auto way{plan_of(Points[i] - Points[i - 1])};
way = glm::length(way) > 1e-9 ? glm::normalize(way) : glm::dvec2{0.0, 1.0};
auto const first{static_cast<int>(std::floor(std::min(k0, k1) * 10.0 + 1e-9)) + 1};
auto const last{static_cast<int>(std::floor(std::max(k0, k1) * 10.0 + 1e-9))};
for (auto hectometres = first; hectometres <= last && hectometres < first + 10; ++hectometres)
Ticks.push_back({glm::mix(Points[i - 1], Points[i], (hectometres / 10.0 - k0) / (k1 - k0)), {-way.y, 0.0, way.x}, hectometres, false});
}
};
std::vector<double> km;
for (auto const chainage : axis->chainage)
km.push_back(kilometrage::km_on(*axis, chainage));
hectometres_along(axis->points, km, axis->chainage, entry.ticks);
m_drawn.push_back(std::move(entry));
// the shorter track where the tracks run apart, with its irregular hectometre
for (auto const &side : axis->sides)
{
drawn apart;
apart.line = line;
apart.points = side.points;
apart.guessed.assign(side.points.size(), 0);
km.clear();
for (auto const along : side.along)
km.push_back(kilometrage::km_on(*axis, side, along));
hectometres_along(side.points, km, side.chainage, apart.ticks);
m_drawn.push_back(std::move(apart));
}
}
m_revision = kilometrage::revision();
m_drawn_once = true;
}
void km_tool::update()
{
if (false == m_open)
return;
auto const &marks{kilometrage::marks()};
// after an undo the point edited may be gone
if (m_edited >= 0 && (m_edited >= static_cast<int>(marks.size()) || false == m_pick.has_value() || plan_distance(marks[m_edited].point, m_pick->point) > 0.5))
m_edited = -1;
if (false == m_drawn_once || kilometrage::revision() != m_revision)
refresh();
}
void km_tool::forget(scene::basic_node const *Node)
{
if (m_pick.has_value() && m_pick->track == Node)
m_pick->track = nullptr;
}
glm::dvec2 km_tool::rising() const
{
if (false == m_pick.has_value())
return {0.0, 1.0};
return m_pick->along * (m_against ? -1.0 : 1.0);
}
std::string km_tool::jump_warning(glm::dvec3 const &Point) const
{
auto *track{kilometrage::track_near(Point, 6.0)};
if (track == nullptr)
return {};
auto route{editor_track::run_route(*track, kStation + 50.0)};
auto const samples{editor_track::sample_route(route, 2.0)};
if (samples.size() < 2)
return {};
double gap{0.0};
auto const at{project(samples, Point, gap)};
bool curve{false};
bool station{false};
for (auto const &sample : samples)
{
auto const apart{std::abs(sample.chainage - at)};
if (apart <= 30.0 && std::abs(sample.curvature) > 1.0 / kCurve)
curve = true;
if (apart <= kStation && sample.span < route.spans.size() && route.spans[sample.span].track->eType != tt_Normal)
station = true;
}
if (curve && station)
return STR("Id-1 § 19: the km shouldn't jump on a curve nor at a station, move the jump to a straight on the open line");
if (curve)
return STR("Id-1 § 19: the km shouldn't jump on a curve, move the jump to a straight");
if (station)
return STR("Id-1 § 19: the km shouldn't jump at a station, move the jump out to the open line");
return {};
}
void km_tool::pick(TTrack &Track, glm::dvec3 const &Point)
{
auto const t{editor_track::nearest_parameter(Track, Point)};
picked entry;
entry.track = &Track;
entry.point = editor_track::point_at(Track, t);
auto const ahead{plan_of(editor_track::point_at(Track, std::min(1.0, t + 0.01)) - editor_track::point_at(Track, std::max(0.0, t - 0.01)))};
entry.along = glm::length(ahead) > 1e-9 ? glm::normalize(ahead) : glm::dvec2{0.0, 1.0};
m_status.clear();
m_edited = -1;
auto const &marks{kilometrage::marks()};
auto nearest{kPickReach};
for (std::size_t index = 0; index < marks.size(); ++index)
if (auto const distance{plan_distance(marks[index].point, entry.point)}; distance < nearest)
{
nearest = distance;
m_edited = static_cast<int>(index);
}
if (m_edited >= 0)
{
auto const &mark{marks[m_edited]};
entry.point = mark.point;
m_line = mark.line;
m_km = mark.km;
m_jump = mark.beyond.has_value();
m_beyond = mark.beyond.value_or(mark.km);
m_against = glm::dot(mark.way, entry.along) < 0.0;
}
else
{
// the line the point lies on, and its km there
auto best{kReadoutReach};
for (auto const &line : kilometrage::lines())
if (auto const *axis{kilometrage::axis_of(line)}; axis != nullptr)
{
double gap{0.0};
kilometrage::chainage_at(*axis, entry.point, gap);
if (gap < best)
{
best = gap;
m_line = line;
}
}
double km{0.0};
if (false == m_line.empty() && kilometrage::km_at(m_line, entry.point, km, kReadoutReach))
{
m_km = std::round(km * 1000.0) / 1000.0;
auto const *axis{kilometrage::axis_of(m_line)};
double gap{0.0};
auto const along{kilometrage::chainage_at(*axis, entry.point, gap)};
m_against = glm::dot(kilometrage::direction_on(*axis, along) * static_cast<double>(axis->growth), entry.along) < 0.0;
}
m_jump = false;
m_beyond = m_km;
}
m_pick = entry;
}
std::string km_tool::readout(glm::dvec3 const &Point) const
{
std::string result;
for (auto const &line : kilometrage::lines())
{
double km{0.0};
if (false == kilometrage::km_at(line, Point, km, kReadoutReach))
continue;
if (false == result.empty())
result += '\n';
result += format(STR_C("line %s: km %s"), line.c_str(), kilometrage::text(km).c_str());
}
return result;
}
void km_tool::put()
{
m_status.clear();
if (false == m_pick.has_value())
return;
auto const line{line_of(m_line)};
if (line.empty())
{
m_status = STR("Give the number of the line first");
return;
}
if (m_jump && std::abs(m_beyond - m_km) < 0.0005)
{
m_status = STR("A jump needs another km on from the point");
return;
}
kilometrage::mark entry;
entry.line = line;
entry.point = m_pick->point;
entry.way = rising();
entry.km = m_km;
if (m_jump)
entry.beyond = m_beyond;
if (m_edited >= 0)
kilometrage::change(static_cast<std::size_t>(m_edited), entry);
else
{
kilometrage::put(entry);
m_edited = static_cast<int>(kilometrage::marks().size()) - 1;
}
m_line = line;
m_history.push_kilometrage_snapshot();
m_status = format(STR_C("Line %s: km %s at the point"), line.c_str(), mark_text(entry).c_str());
}
void km_tool::remove()
{
m_status.clear();
if (m_edited < 0)
return;
kilometrage::remove(static_cast<std::size_t>(m_edited));
m_edited = -1;
m_history.push_kilometrage_snapshot();
m_status = STR("The point of the km removed");
}
void km_tool::show(std::size_t const Index)
{
auto const &marks{kilometrage::marks()};
if (Index >= marks.size())
return;
auto const point{marks[Index].point};
if (auto *track{kilometrage::track_near(point, 6.0)}; track != nullptr)
pick(*track, point);
m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0);
}
void km_tool::read_posts()
{
m_status.clear();
auto const line{line_of(m_line)};
if (false == m_pick.has_value() || m_pick->track == nullptr)
{
m_status = STR("Click a track of the line first");
return;
}
if (line.empty())
{
m_status = STR("Give the number of the line first");
return;
}
auto route{editor_track::run_route(*m_pick->track, kPostReach)};
auto const samples{editor_track::sample_route(route, 2.0)};
if (samples.size() < 2)
{
m_status = STR("No line through this track");
return;
}
auto const length{samples.back().chainage};
struct post
{
double chainage{0.0};
int hectometres{0};
};
std::vector<post> posts;
for (auto const &standing : m_templates.standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; }))
{
post entry;
if (false == hectometres_of(standing.values, entry.hectometres))
continue;
double distance{0.0};
entry.chainage = project(samples, standing.location, distance);
if (distance > kPostCorridor || entry.chainage <= 0.0 || entry.chainage >= length)
continue;
posts.push_back(entry);
}
if (posts.size() < 2)
{
m_status = STR("Fewer than two hectometre posts stand along the line, nothing to read");
return;
}
std::sort(posts.begin(), posts.end(), [](post const &A, post const &B) { return A.chainage < B.chainage; });
// runs of posts numbered by the distance between them, a post of the same number by the other track taken once
auto const fits = [](post const &From, post const &To, int const Sign) {
auto const counted{(To.hectometres - From.hectometres) * 100.0};
auto const apart{To.chainage - From.chainage};
return counted != 0.0 && (Sign == 0 || (counted > 0.0 ? 1 : -1) == Sign) && std::abs(std::abs(counted) - apart) <= std::max(kPostTolerance, apart * 0.02);
};
auto const sign_of = [](std::vector<post> const &Run) { return Run.size() < 2 ? 0 : Run.back().hectometres > Run.front().hectometres ? 1 : -1; };
std::vector<std::vector<post>> runs;
std::size_t twins{0};
for (auto const &entry : posts)
{
if (false == runs.empty())
{
auto &run{runs.back()};
if (entry.hectometres == run.back().hectometres && entry.chainage - run.back().chainage < kPostTolerance * 3.0)
{
++twins;
continue;
}
if (fits(run.back(), entry, sign_of(run)))
{
run.push_back(entry);
continue;
}
}
runs.push_back({entry});
}
// a post alone between the runs is one out of place
std::size_t stray{0};
std::vector<std::vector<post>> kept;
for (auto &run : runs)
{
if (run.size() < 2)
{
++stray;
continue;
}
if (false == kept.empty() && fits(kept.back().back(), run.front(), sign_of(kept.back())) && sign_of(kept.back()) == sign_of(run))
kept.back().insert(kept.back().end(), run.begin(), run.end());
else
kept.push_back(std::move(run));
}
std::size_t rising{0};
std::size_t falling{0};
for (auto const &run : kept)
(sign_of(run) > 0 ? rising : falling) += run.size();
auto const growth{rising >= falling ? 1 : -1};
kept.erase(std::remove_if(kept.begin(), kept.end(), [&](std::vector<post> const &Run) {
if (sign_of(Run) == growth)
return false;
stray += Run.size();
return true;
}), kept.end());
if (kept.empty())
{
m_status = STR("The numbers of the posts don't go by 100 m anywhere along the line, nothing read");
return;
}
auto const mark_at = [&](double const Chainage, double const Km, std::optional<double> const Beyond) {
kilometrage::mark entry;
entry.line = line;
entry.point = editor_track::sampled_position(samples, Chainage);
entry.way = samples[sample_at(samples, Chainage)].direction * static_cast<double>(growth);
entry.km = Km;
entry.beyond = Beyond;
return entry;
};
std::vector<kilometrage::mark> made;
std::size_t jumps{0};
std::size_t misplaced{0};
for (std::size_t r = 0; r < kept.size(); ++r)
{
auto const &run{kept[r]};
auto const &first{run.front()};
auto const &last{run.back()};
// the jump to the next run at the post it starts the irregular hectometre at, along the rising km
auto const jump_before{r > 0 && growth > 0};
auto const jump_after{r + 1 < kept.size() && growth < 0};
if (jump_before)
{
auto const &previous{kept[r - 1].back()};
made.push_back(mark_at(first.chainage, previous.hectometres / 10.0 + (first.chainage - previous.chainage) / 1000.0, first.hectometres / 10.0));
}
else
made.push_back(mark_at(first.chainage, first.hectometres / 10.0, std::nullopt));
if (jump_after)
{
auto const &next{kept[r + 1].front()};
made.push_back(mark_at(last.chainage, next.hectometres / 10.0 + (next.chainage - last.chainage) / 1000.0, last.hectometres / 10.0));
}
else
made.push_back(mark_at(last.chainage, last.hectometres / 10.0, std::nullopt));
if (jump_before || jump_after)
{
++jumps;
if (false == jump_warning((jump_before ? made[made.size() - 2] : made.back()).point).empty())
++misplaced;
}
}
// the points of the line along the stretch read give way to the ones read
auto const &standing{kilometrage::marks()};
for (auto index{static_cast<int>(standing.size()) - 1}; index >= 0; --index)
{
if (standing[index].line != line)
continue;
double gap{0.0};
project(samples, standing[index].point, gap);
if (gap <= kPostCorridor)
kilometrage::remove(static_cast<std::size_t>(index));
}
for (auto &entry : made)
kilometrage::put(std::move(entry));
m_line = line;
m_edited = -1;
m_history.push_kilometrage_snapshot();
m_status = format(STR_C("%zu posts read: %zu points of the km, %zu jumps"), posts.size() - twins, made.size(), jumps);
if (stray > 0)
m_status += "\n" + format(STR_P("%zu post doesn't fit its neighbours and was left out", "%zu posts don't fit their neighbours and were left out", stray), stray);
if (misplaced > 0)
m_status += "\n" + format(STR_P("%zu jump lies on a curve or at a station, Id-1 § 19 wants none there", "%zu jumps lie on a curve or at a station, Id-1 § 19 wants none there", misplaced), misplaced);
}
void km_tool::render_options()
{
ImGui::TextDisabled("%s", STR_C("The km of a railway line: the points where the km is known, from a post or a document, and where it jumps. The km of the rest go from them along the axis of the line: between the tracks of a double-track one, along the longer track where the tracks run on formations of their own, the shorter one getting an irregular hectometre"));
ImGui::Spacing();
gesture_rows({{"LMB on a track", "a point of the km there, or the one standing by it"}});
auto const &marks{kilometrage::marks()};
auto const lines{kilometrage::lines()};
std::size_t jumps{0};
for (auto const &mark : marks)
if (mark.beyond.has_value())
++jumps;
metric_tiles({{format("%zu", lines.size()), STR_C("lines"), kAxis}, {format("%zu", marks.size()), STR_C("points"), kMark}, {format("%zu", jumps), STR_C("jumps"), kJump}});
step_heading(1, STR_C("A point of the km"));
if (false == m_pick.has_value())
ImGui::TextDisabled("%s", STR_C("Click a track where the km is known: by a post, a station or a bridge"));
else
{
if (begin_fields("##kmpoint"))
{
field_row("Line");
std::array<char, 32> buffer{};
m_line.copy(buffer.data(), buffer.size() - 1);
if (ImGui::InputTextWithHint("##kmline", "91", buffer.data(), buffer.size()))
m_line = line_of(buffer.data());
item_tooltip("The number of the railway line, as 91: a scenery can hold several lines, each with its own km");
auto const km_field = [&](char const *Label, char const *Id, double &Value) {
field_row(Label);
std::array<char, 32> text{};
kilometrage::text(Value).copy(text.data(), text.size() - 1);
if (ImGui::InputText(Id, text.data(), text.size()))
km_of(text.data(), Value);
};
km_field("Km", "##kmkm", m_km);
item_tooltip("The km of the point, as PKP PLK write it: 100,234 is km 100 and 234 m. At a jump: the km the point is reached at, coming along the rising km");
field_row("Jump");
ImGui::Checkbox("##kmjump", &m_jump);
item_tooltip("The km jumps at the point: a line built anew, shortened or lengthened keeps the km of the rest. The irregular hectometre is "
"marked by a black stripe under the digit of the post (Id-1 § 19)");
if (m_jump)
{
km_field("Km on", "##kmbeyond", m_beyond);
item_tooltip("The km on from the point, along the rising km");
}
ImGui::EndTable();
}
auto const *axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)};
auto const fixed{axis != nullptr && axis->anchors.size() > 1 && std::abs(axis->anchors.back().before - axis->anchors.front().before) > 1e-6};
if (false == fixed)
{
int way{m_against ? 1 : 0};
if (segmented("##kmway", way, {{0, STR_C("Rise this way"), "The km rise the way the arrow at the point shows"}, {1, STR_C("Rise the other way"), "The km rise against the arrow"}}))
m_against = way == 1;
}
if (m_jump)
if (auto const warning{jump_warning(m_pick->point)}; false == warning.empty())
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", warning.c_str());
if (main_button(m_edited >= 0 ? STR_C("Change the point") : STR_C("Put the point of the km"), ImVec2(-1.0f, tall_button_height())))
put();
item_tooltip("The point goes to the file of the scenery with the track, the simulation skips it. One step of undo");
if (m_edited >= 0)
{
if (ImGui::Button(STR_C("Remove the point"), ImVec2(-1.0f, 0.0f)))
remove();
item_tooltip("One step of undo");
}
}
step_heading(2, STR_C("From the posts"));
if (ImGui::Button(STR_C("Read the km from the posts"), ImVec2(-1.0f, 0.0f)))
read_posts();
item_tooltip("Reads the hectometre posts standing along the line through the track clicked: a point of the km at the first and the last "
"post of each run numbered by 100 m, a jump where two runs meet. The points of the line along it before give way. One step of undo");
if (false == m_status.empty())
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str());
step_heading(3, STR_C("The points of the lines"));
if (lines.empty())
{
ImGui::TextDisabled("%s", STR_C("None yet"));
return;
}
for (auto const &line : lines)
{
auto const *axis{kilometrage::axis_of(line)};
auto const count{static_cast<std::size_t>(std::count_if(marks.begin(), marks.end(), [&](kilometrage::mark const &Mark) { return Mark.line == line; }))};
if (false == ImGui::CollapsingHeader(format("%s %s (%zu)###kmline%s", STR_C("Line"), line.c_str(), count, line.c_str()).c_str(), ImGuiTreeNodeFlags_DefaultOpen))
continue;
if (axis == nullptr)
{
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", STR_C("No track by the points of the line"));
continue;
}
ImGui::TextDisabled("%s", format(STR_C("%.1f km of the axis, %s"), axis->length() / 1000.0, axis->between ? STR_C("between the tracks") : STR_C("along the track")).c_str());
if (false == axis->sides.empty())
{
ImGui::TextDisabled("%s", format(STR_P("%zu stretch with the tracks on formations of their own", "%zu stretches with the tracks on formations of their own", axis->sides.size()), axis->sides.size()).c_str());
item_tooltip("Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by "
"its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)");
}
auto const rows{axis->anchors.size() + axis->astray.size()};
if (false == ImGui::BeginTable(("##kmmarks" + line).c_str(), 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(260.0f, ImGui::GetTextLineHeightWithSpacing() * (rows + 1.5f)))))
continue;
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn(STR_C("Km"), ImGuiTableColumnFlags_WidthStretch, 0.4f);
ImGui::TableSetupColumn(STR_C("In the scenery"), ImGuiTableColumnFlags_WidthStretch, 0.22f);
ImGui::TableSetupColumn(STR_C("By the km"), ImGuiTableColumnFlags_WidthStretch, 0.22f);
ImGui::TableSetupColumn(STR_C("Off by"), ImGuiTableColumnFlags_WidthStretch, 0.16f);
ImGui::TableHeadersRow();
auto const growth{axis->growth};
for (std::size_t i = 0; i < axis->anchors.size(); ++i)
{
auto const &anchor{axis->anchors[i]};
auto const &mark{marks[anchor.mark]};
ImGui::TableNextRow();
ImGui::TableNextColumn();
if (ImGui::Selectable((mark_text(mark) + "##kmmark" + std::to_string(anchor.mark)).c_str(), static_cast<int>(anchor.mark) == m_edited, ImGuiSelectableFlags_SpanAllColumns))
show(anchor.mark);
if (i == 0)
continue;
auto const &previous{axis->anchors[i - 1]};
auto const scenery{anchor.chainage - previous.chainage};
auto const counted{std::abs((growth > 0 ? anchor.before - previous.after : previous.before - anchor.after) * 1000.0)};
ImGui::TableNextColumn();
ImGui::Text("%.0f m", scenery);
ImGui::TableNextColumn();
ImGui::Text("%.0f m", counted);
ImGui::TableNextColumn();
auto off{scenery > 1.0 ? (scenery - counted) / scenery * 100.0 : 0.0};
if (std::abs(off) < 0.05)
off = 0.0;
if (std::abs(off) > 2.0)
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%+.1f%%", off);
else
ImGui::Text("%+.1f%%", off);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("How much longer the stretch is in the scenery than by the km: the km between the points are stretched to it evenly"));
}
for (auto const index : axis->astray)
{
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.4f, 1.0f));
if (ImGui::Selectable((mark_text(marks[index]) + "##kmmark" + std::to_string(index)).c_str(), static_cast<int>(index) == m_edited, ImGuiSelectableFlags_SpanAllColumns))
show(index);
ImGui::PopStyleColor();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The point is away from the axis of the line the other points make: the main paths of the switches don't lead to it"));
}
ImGui::EndTable();
}
}
void km_tool::draw_overlay() const
{
if (false == m_open)
return;
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
glm::dvec3 const camera{Global.pCamera.Pos};
auto const near_enough = [&](glm::dvec3 const &Point, double const Range) { return Global.EditorOrtho || glm::distance(Point, camera) < Range; };
glm::dvec3 const lift{0.0, kRailHead, 0.0};
for (auto const &entry : m_drawn)
{
// the axis, faint where the km run on past the last point
for (std::size_t i = 1; i < entry.points.size(); i += 5)
{
auto const next{std::min(i + 4, entry.points.size() - 1)};
if (near_enough(entry.points[next], kOverlayRange))
projection.line(drawlist, entry.points[i - 1] + lift, entry.points[next] + lift, entry.guessed[next] != 0 ? kGuessed : kAxis, Global.EditorOrtho ? 4.0f : 2.5f);
}
for (auto const &tick : entry.ticks)
{
if (false == near_enough(tick.at, kTickRange))
continue;
auto const centre{tick.at + lift};
auto const whole{tick.hectometres % 10 == 0};
auto const reach{tick.jump ? 3.5 : whole ? 2.6 : 1.4};
projection.line(drawlist, centre - tick.across * reach, centre + tick.across * reach, tick.jump ? kJump : whole ? kKm : kTick, tick.jump || whole ? 3.5f : 2.0f);
if (tick.jump)
continue;
ImVec2 screen;
if (whole && near_enough(tick.at, kLabelRange) && projection.project(centre + tick.across * reach + glm::dvec3{0.0, 0.6, 0.0}, screen))
draw_label(drawlist, screen, IM_COL32(0, 0, 0, 170), kKm, format("km %d", tick.hectometres / 10));
else if (false == whole && near_enough(tick.at, kDigitRange) && projection.project(centre + tick.across * reach + glm::dvec3{0.0, 0.4, 0.0}, screen))
draw_label(drawlist, screen, IM_COL32(0, 0, 0, 140), kTick, std::to_string(std::abs(tick.hectometres) % 10));
}
}
// the points of the km: a pole with the km over it, the arrow the way the km rise
auto const &marks{kilometrage::marks()};
for (std::size_t index = 0; index < marks.size(); ++index)
{
auto const &mark{marks[index]};
if (false == near_enough(mark.point, kLabelRange * 2.0))
continue;
auto astray{true};
for (auto const &entry : m_drawn)
if (entry.line == mark.line)
if (auto const *axis{kilometrage::axis_of(mark.line)}; axis != nullptr)
astray = std::find(axis->astray.begin(), axis->astray.end(), index) != axis->astray.end();
auto const colour{astray ? kAstray : mark.beyond.has_value() ? kJump : kMark};
auto const foot{mark.point + lift};
auto const top{foot + glm::dvec3{0.0, 3.0, 0.0}};
projection.line(drawlist, foot, top, colour, static_cast<int>(index) == m_edited ? 4.0f : 2.5f);
draw_arrow(projection, drawlist, foot, mark.way, 6.0, colour);
ImVec2 screen;
if (projection.project(top, screen))
draw_label(drawlist, screen, colour, IM_COL32(16, 18, 22, 255), mark.line + ": " + mark_text(mark));
}
if (m_pick.has_value() && m_edited < 0)
{
auto const foot{m_pick->point + lift};
ImVec2 screen;
if (projection.project(foot, screen))
drawlist->AddCircle(screen, 9.0f, kKm, 20, 2.5f);
draw_arrow(projection, drawlist, foot, rising(), 8.0, kKm);
}
}
editor_selftest_result km_tool::selftest(std::string const &Command, std::istringstream &Words)
{
if (Command == "kmpick")
{
// kmpick track=<name> [at=<0..1>]: a click on the track
auto const values{selftest_values(Words)};
auto const found{values.find("track")};
auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr};
if (track == nullptr)
{
WriteLog("SELFTEST kmpick: no such track");
return editor_selftest_result::handled;
}
auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5};
pick(*track, editor_track::point_at(*track, at));
WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f", track->name().c_str(), m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_against ? 1 : 0, m_edited, m_pick->point.x, m_pick->point.z));
return editor_selftest_result::handled;
}
if (Command == "kmset")
{
// kmset line=<number> km=<km> [beyond=<km>] [against=1]: the fields set and the point put
auto const values{selftest_values(Words)};
if (auto const line{values.find("line")}; line != values.end())
m_line = line->second;
if (auto const km{values.find("km")}; km != values.end())
km_of(km->second.c_str(), m_km);
m_jump = values.count("beyond") > 0;
if (m_jump)
km_of(values.at("beyond").c_str(), m_beyond);
if (auto const against{values.find("against")}; against != values.end())
m_against = against->second == "1";
put();
WriteLog("SELFTEST kmset: " + m_status);
return editor_selftest_result::handled;
}
if (Command == "kmremove")
{
remove();
WriteLog("SELFTEST kmremove: " + m_status);
return editor_selftest_result::handled;
}
if (Command == "kmposts")
{
// kmposts line=<number>: the km read from the posts along the line through the track picked
auto const values{selftest_values(Words)};
if (auto const line{values.find("line")}; line != values.end())
m_line = line->second;
read_posts();
WriteLog("SELFTEST kmposts: " + m_status);
return editor_selftest_result::handled;
}
if (Command == "kmstate")
{
WriteLog(format("SELFTEST kmstate: picked %d line %s km %s jump %d edited %d", m_pick.has_value() ? 1 : 0, m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_edited));
return editor_selftest_result::handled;
}
if (Command == "kmshow")
{
// kmshow mark=<index>: the point of the km picked, the camera over it
auto const values{selftest_values(Words)};
auto const index{values.count("mark") ? std::atoi(values.at("mark").c_str()) : 0};
show(static_cast<std::size_t>(std::max(index, 0)));
WriteLog(format("SELFTEST kmshow %d: line %s km %s edited %d", index, m_line.c_str(), kilometrage::text(m_km).c_str(), m_edited));
return editor_selftest_result::handled;
}
if (Command == "kmlist")
{
refresh();
for (auto const &entry : m_drawn)
{
std::string list;
for (std::size_t i = 0; i < entry.ticks.size() && entry.points.size() < 2000; ++i)
list += format(" %d@%.0f,%.0f", entry.ticks[i].hectometres, entry.ticks[i].at.x, entry.ticks[i].at.z);
WriteLog(format("SELFTEST kmlist ticks %s: %zu points, %zu ticks", entry.line.c_str(), entry.points.size(), entry.ticks.size()) + list);
}
auto const &marks{kilometrage::marks()};
WriteLog(format("SELFTEST kmlist: %zu points", marks.size()));
for (auto const &line : kilometrage::lines())
{
auto const *axis{kilometrage::axis_of(line)};
if (axis == nullptr)
{
WriteLog("SELFTEST kmlist line " + line + ": no axis");
continue;
}
std::string list;
for (auto const &anchor : axis->anchors)
list += format(" %s@%.0f/%.1f", mark_text(marks[anchor.mark]).c_str(), anchor.chainage, anchor.gap);
for (auto const &side : axis->sides)
WriteLog(format("SELFTEST kmlist apart: %.1f m of track by %.1f m of axis (%.1f..%.1f), km %s..%s", side.length, side.to - side.from, side.from, side.to, kilometrage::text(kilometrage::km_on(*axis, side, 0.0)).c_str(),
kilometrage::text(kilometrage::km_on(*axis, side, side.length)).c_str()));
WriteLog(format("SELFTEST kmlist line %s: axis %.0f m %s growth %d, %zu astray, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.size(), kilometrage::text(kilometrage::km_on(*axis, 0.0)).c_str(),
kilometrage::text(kilometrage::km_on(*axis, axis->length())).c_str()) +
list);
}
return editor_selftest_result::handled;
}
if (Command == "kmat")
{
// kmat track=<name> [at=<0..1>]: the km of the lines there
auto const values{selftest_values(Words)};
auto const found{values.find("track")};
auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr};
if (track == nullptr)
{
WriteLog("SELFTEST kmat: no such track");
return editor_selftest_result::handled;
}
auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5};
auto text{readout(editor_track::point_at(*track, at))};
std::replace(text.begin(), text.end(), '\n', ';');
WriteLog(format("SELFTEST kmat %s %.2f: ", track->name().c_str(), at) + (text.empty() ? std::string{"-"} : text));
return editor_selftest_result::handled;
}
return editor_selftest_result::unhandled;
}

View File

@@ -0,0 +1,90 @@
/*
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/editorKilometrage.hpp"
#include <cstddef>
#include <optional>
#include <string>
#include <vector>
// the kilometrage of the lines: the points of a line where its km is known and where it jumps, the km of the rest from them
class km_tool final : public editor_tool
{
public:
km_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_templates &Templates)
: m_selection{Selection}, m_view{View}, m_history{History}, m_templates{Templates}
{
}
void update() override;
void draw_overlay() const override;
void render_options() 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 point of the track clicked: a new point of the km there, or the one standing by it
void pick(TTrack &Track, glm::dvec3 const &Point);
// the km of the lines at the point, a line of text each, empty where none reaches
std::string readout(glm::dvec3 const &Point) const;
private:
editor_selection &m_selection;
editor_view &m_view;
editor_history &m_history;
editor_templates &m_templates;
bool m_open{false};
struct picked
{
TTrack *track{nullptr};
glm::dvec3 point{0.0}; // on the axis of the track
glm::dvec2 along{0.0, 1.0}; // of the track there, in the plan
};
std::optional<picked> m_pick;
int m_edited{-1}; // of the marks, the one standing by the point picked, -1 for a new one
std::string m_line;
double m_km{0.0};
bool m_jump{false};
double m_beyond{0.0};
bool m_against{false}; // the km rise against the track picked
std::string m_status;
// what the overlay draws, made again when the axes change
struct tick
{
glm::dvec3 at{0.0};
glm::dvec3 across{0.0}; // in the plan, a unit
int hectometres{0};
bool jump{false};
};
struct drawn
{
std::string line;
std::vector<glm::dvec3> points;
std::vector<char> guessed; // of the points, beyond the last point of the km: the km run on from it
std::vector<tick> ticks;
};
std::vector<drawn> m_drawn;
std::size_t m_revision{0};
bool m_drawn_once{false};
void refresh();
void put();
void remove();
// the point of the km picked and the camera over it
void show(std::size_t const Index);
// the hectometre posts standing along the line through the track picked read into points of the km: the first and the last
// post of each run of them numbered by 100 m, a jump where the runs meet; one step of the history
void read_posts();
// the way the km rise at the point picked, in the plan
glm::dvec2 rising() const;
// warnings of a jump at the point: Id-1 § 19 wants none on a curve nor at a station
std::string jump_warning(glm::dvec3 const &Point) const;
};

View File

@@ -1054,6 +1054,8 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist
}
start(*track, track->m_paths.front().points[segment_data::point::start]);
auto &tool{m_hekto};
if (values.count("reach"))
tool.reach = std::stod(values["reach"]);
tool.dirty = true;
hekto_update();
if (values.count("km"))

View File

@@ -297,6 +297,28 @@ void editor_mode::push_track_snapshot(std::vector<std::pair<TTrack *, editor_tra
++m_track_edits;
}
void editor_mode::push_kilometrage_snapshot()
{
auto before{kilometrage::take_changes()};
if (false == before.has_value())
return;
trim_history();
EditorSnapshot snap;
snap.action = EditorSnapshot::Action::Kilometrage;
snap.kilometrage = std::move(before);
m_history.push_back(std::move(snap));
g_redo.clear();
}
void editor_mode::restore_kilometrage(EditorSnapshot &Snapshot)
{
if (false == Snapshot.kilometrage.has_value())
return;
auto current{kilometrage::marks()};
kilometrage::restore(std::move(*Snapshot.kilometrage));
Snapshot.kilometrage = std::move(current);
}
bool editor_mode::tracks_editable(std::vector<TTrack *> const &Tracks, std::string &Reason)
{
for (auto const *track : Tracks)
@@ -422,9 +444,9 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve
profile_restore();
}
std::array<editor_mode::track_mode, 12> const &editor_mode::track_modes()
std::array<editor_mode::track_mode, 13> const &editor_mode::track_modes()
{
static std::array<track_mode, 12> const modes{{
static std::array<track_mode, 13> 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},
@@ -433,6 +455,7 @@ std::array<editor_mode::track_mode, 12> const &editor_mode::track_modes()
{"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},
{"Kilometrage", "X", track_tab::kilometrage, "The km of the railway lines: LMB on a track puts a point of the km there, a jump of the km too", editor_icons::icon::kilometrage, 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},
@@ -578,6 +601,7 @@ std::string editor_mode::track_mode_name() const
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::kilometrage: return STR_C("Kilometrage");
case track_tab::turntable: return STR_C("Turntable");
default: return STR_C("Select");
}
@@ -591,6 +615,7 @@ void editor_mode::render_track_inspector()
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);
m_km.set_open(m_track_window_open && m_track_tab == track_tab::kilometrage);
if (false == m_track_window_open || m_track_tab != track_tab::lineside)
{
m_sweep.open = false;
@@ -675,6 +700,7 @@ void editor_mode::render_track_tool_options()
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::kilometrage: m_km.render_options(); break;
case track_tab::turntable: render_turntable_ui(); break;
default:
render_track_set_ui();
@@ -1694,6 +1720,10 @@ std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All)
hints = {{"LMB around it", "leads a track out"}, {"LMB on a free end", "fits that track to the turntable"}};
else if (m_track_window_open && m_track_tab == track_tab::turntable)
hints = {{"LMB", "places a new turntable"}};
else if (m_track_window_open && m_track_tab == track_tab::srk)
hints = {{"LMB on a switch", "opens its station"}};
else if (m_track_window_open && m_track_tab == track_tab::kilometrage)
hints = {{"LMB on a track", "a point of the km there"}, {"Mouse over a track", "its km by the cursor"}};
else if (m_track_window_open && m_track_tab == track_tab::joints)
hints = {{"LMB on a row", "show the place"}, {"Fix", "joins the ends, aligns them"}};
else if (m_track_window_open && m_track_tab == track_tab::infra)

View File

@@ -465,6 +465,16 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
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_tab == track_tab::kilometrage && m_hover.track != nullptr)
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
make(kind::kilometrage, "Click: a point of the km here");
if (auto const km{m_km.readout(m_hover.point)}; false == km.empty())
intent.action = km + "\n" + intent.action;
return intent;
}
if (m_track_style.picking && (m_track_tab == track_tab::path || m_track_tab == track_tab::turnout))
{
intent.track = m_hover.track;
@@ -796,6 +806,10 @@ void editor_mode::track_press(track_intent const &Intent)
handle_drag_start(Intent.position);
return;
case kind::switch_at: start_switch_placement(); return;
case kind::kilometrage:
if (Intent.track != nullptr)
m_km.pick(*Intent.track, Intent.position);
return;
case kind::fouling:
if (Intent.path == 1)
m_fouling.fix(Intent.index);

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::srk || 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::kilometrage || 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, signalling, 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, kilometrage, objects, trainset, profile, speed, joints, gauge"), more, [more]() {
EditorSettings.track_tools_more(false == more);
EditorSettings.save();
}, true});
@@ -189,6 +189,7 @@ void editor_mode::work_area_tools(std::vector<tool_button> &Buttons)
add(track_tab::turntable, true);
add(track_tab::signals, false);
add(track_tab::srk, false);
add(track_tab::kilometrage, 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, srk };
enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel, srk, kilometrage };
struct track_hover
{
@@ -167,6 +167,8 @@ class editor_history
public:
virtual std::size_t history_size() const = 0;
virtual void push_track_snapshot(std::vector<std::pair<TTrack *, editor_track::state>> States, std::vector<TTrack *> Created = {}, std::vector<TTrack *> Removed = {}) = 0;
// the changes of the kilometrage made since the last step: one step of the history
virtual void push_kilometrage_snapshot() = 0;
protected:
~editor_history() = default;

View File

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

View File

@@ -0,0 +1,779 @@
/*
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/editorKilometrage.hpp"
#include "editor/editorAlignmentStore.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorTrack.hpp"
#include "world/Track.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <map>
#include <set>
#include <sstream>
#include <unordered_set>
namespace kilometrage
{
namespace
{
using geometry::plan_distance;
using geometry::plan_of;
std::string const kMark{"//$k"};
double const kReach{60000.0}; // m, of the tracks followed each way from a mark
double const kStep{2.0}; // m between the points of an axis
double const kOnAxis{12.0}; // m, of a mark from the axis of its line
double const kNear{6.0}; // m, of a mark from the track it stands on
double const kBetweenLow{3.0}; // m between the axes of the tracks of a double-track line
double const kBetweenHigh{6.5};
double const kCell{10.0}; // m, of the squares the track beside is sorted into
double const kApart{15.0}; // m between the tracks, of a double-track line on two formations
double const kBlend{100.0}; // m, the axis takes to go over from between the tracks to one of them
double const kLonger{1.0}; // m, of the track beside longer than the track of the line, for the axis to follow it
std::vector<mark> g_marks;
std::vector<mark> g_base; // as they were before the changes not yet in the undo history
bool g_changed{false};
bool g_loaded{false};
std::size_t g_edits{0};
std::map<std::string, std::optional<axis>> g_axes;
std::size_t g_revision{0};
void drop_axes()
{
g_axes.clear();
++g_revision;
}
using span = editor_track::route_span;
std::string word(std::string const &Text)
{
return Text.empty() ? std::string{"-"} : Text;
}
// format of the lines, coordinates in the space of the file:
// mark <line> <x y z> way <x z> km <km> [beyond <km>]
void store(scene::layer_handle const Layer)
{
if (false == scene::Layers.valid(Layer))
return;
auto const &context{scene::Layers.layer(Layer).context_insert()};
std::vector<std::string> lines;
for (auto const &entry : g_marks)
{
if (entry.layer != Layer)
continue;
if (lines.empty())
lines.emplace_back("# kilometrage of the lines set in the track editor, the simulation skips these lines");
auto const point{context.to_local(entry.point)};
auto const ahead{context.to_local(entry.point + glm::dvec3{entry.way.x, 0.0, entry.way.y})};
auto way{plan_of(ahead - point)};
way = glm::length(way) > 1e-12 ? glm::normalize(way) : entry.way;
auto text{format("mark %s %.4f %.4f %.4f way %.9f %.9f km %.3f", word(entry.line).c_str(), point.x, point.y, point.z, way.x, way.y, entry.km)};
if (entry.beyond.has_value())
text += format(" beyond %.3f", *entry.beyond);
lines.push_back(text);
}
scene::Layers.mark(Layer, kMark, std::move(lines));
}
void changed(std::set<scene::layer_handle> const &Layers)
{
g_changed = true;
drop_axes();
for (auto const layer : Layers)
store(layer);
}
std::vector<span> flipped(std::vector<span> Spans)
{
std::reverse(Spans.begin(), Spans.end());
for (auto &entry : Spans)
entry.forward = false == entry.forward;
return Spans;
}
std::size_t index_of(std::vector<span> const &Spans, TTrack const *Track)
{
auto const found{std::find_if(Spans.begin(), Spans.end(), [&](span const &Entry) { return Entry.track == Track; })};
return found == Spans.end() ? Spans.size() : static_cast<std::size_t>(found - Spans.begin());
}
// the run on from the end of the span the way it goes, along the main paths of the switches
std::vector<span> onward(span const &Last)
{
auto const run{editor_track::run_route(*Last.track, kReach)};
auto const at{index_of(run.spans, Last.track)};
if (at == run.spans.size())
return {};
if (run.spans[at].forward == Last.forward)
return {run.spans.begin() + at + 1, run.spans.end()};
return flipped({run.spans.begin(), run.spans.begin() + at});
}
// the spans with each track once, the first time it comes
std::vector<span> once(std::vector<span> const &Spans)
{
std::vector<span> result;
std::unordered_set<TTrack const *> seen;
for (auto const &entry : Spans)
if (seen.insert(entry.track).second)
result.push_back(entry);
return result;
}
double plan_gap(std::vector<editor_track::route_sample> const &Samples, glm::dvec3 const &Point)
{
auto best{std::numeric_limits<double>::max()};
for (auto const &sample : Samples)
best = std::min(best, plan_distance(sample.position, Point));
return best;
}
// the track beside the line: of each sample of the line the sample of it beside, where they run side by side
struct pairing
{
std::vector<editor_track::route_sample> partner; // along the track beside, empty for none
std::vector<int> match; // of each sample, -1 where the track beside isn't there
std::vector<double> across; // to the sample beside, along the normal of the sample
};
pairing pair_with(std::vector<editor_track::route_sample> const &Samples, TTrack &Beside)
{
pairing result;
result.match.assign(Samples.size(), -1);
result.across.assign(Samples.size(), 0.0);
auto other{editor_track::run_route(Beside, kReach)};
result.partner = editor_track::sample_route(other, kStep);
auto const &partner{result.partner};
auto const cell = [](glm::dvec3 const &Point) { return std::make_pair(static_cast<long>(std::floor(Point.x / kCell)), static_cast<long>(std::floor(Point.z / kCell))); };
std::map<std::pair<long, long>, std::vector<std::size_t>> grid;
for (std::size_t j = 0; j < partner.size(); ++j)
grid[cell(partner[j].position)].push_back(j);
for (std::size_t i = 0; i < Samples.size(); ++i)
{
auto const &sample{Samples[i]};
glm::dvec2 const normal{-sample.direction.y, sample.direction.x};
auto const [cx, cz]{cell(sample.position)};
auto best{std::numeric_limits<double>::max()};
for (auto x = cx - 1; x <= cx + 1; ++x)
for (auto z = cz - 1; z <= cz + 1; ++z)
{
auto const found{grid.find({x, z})};
if (found == grid.end())
continue;
for (auto const j : found->second)
{
auto const offset{plan_of(partner[j].position) - plan_of(sample.position)};
auto const across{glm::dot(offset, normal)};
auto const along{glm::dot(offset, sample.direction)};
if (std::abs(across) < kBetweenLow || std::abs(across) > kBetweenHigh || std::abs(along) >= kStep * 1.5 || std::abs(glm::dot(sample.direction, partner[j].direction)) <= 0.97 || std::abs(along) >= best)
continue;
best = std::abs(along);
result.match[i] = static_cast<int>(j);
result.across[i] = across;
}
}
}
return result;
}
// the track beside the line, looked for by the first mark and then every 500 m along the line: a track the line doesn't run
// through, the spacing of a double-track line away, beside it for 100 m at least
pairing pair_up(std::vector<editor_track::route_sample> const &Samples, editor_track::route const &Route, std::size_t const Origin)
{
std::unordered_set<TTrack const *> own;
for (auto const &entry : Route.spans)
own.insert(entry.track);
std::unordered_set<TTrack const *> tried;
auto const step{static_cast<std::size_t>(500.0 / kStep)};
for (std::size_t k = 0; k * step < Samples.size() && tried.size() < 4; ++k)
for (auto const way : {1, -1})
{
if (k == 0 && way < 0)
continue;
auto const index{static_cast<long>(Origin) + way * static_cast<long>(k * step)};
if (index < 0 || index >= static_cast<long>(Samples.size()))
continue;
auto const &at{Samples[static_cast<std::size_t>(index)]};
glm::dvec2 const left{-at.direction.y, at.direction.x};
for (auto const side : {1.0, -1.0})
{
auto const probe{plan_of(at.position) + left * (side * (kBetweenLow + kBetweenHigh) * 0.5)};
auto *track{track_near({probe.x, at.position.y, probe.y}, 1.6)};
if (track == nullptr || own.count(track) > 0 || false == tried.insert(track).second)
continue;
auto result{pair_with(Samples, *track)};
if (std::count_if(result.match.begin(), result.match.end(), [](int const Match) { return Match >= 0; }) * kStep >= 100.0)
return result;
}
}
pairing none;
none.match.assign(Samples.size(), -1);
none.across.assign(Samples.size(), 0.0);
return none;
}
// the axis: between the tracks where they run side by side, along the longer one where they part and come together again
// (PKP GK-1 6.1.12), the other one getting the km by the share of its length, on the track of the line where the other one ends
void compose(axis &Result, std::vector<editor_track::route_sample> const &Samples, pairing const &Pairs)
{
using sample = editor_track::route_sample;
auto const emit = [&](glm::dvec3 const &Point, glm::dvec2 const &Direction) {
Result.chainage.push_back(Result.points.empty() ? 0.0 : Result.chainage.back() + plan_distance(Result.points.back(), Point));
Result.points.push_back(Point);
Result.directions.push_back(Direction);
};
auto const shifted = [](glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Offset) {
auto const planar{plan_of(Point) + glm::dvec2{-Direction.y, Direction.x} * Offset};
return glm::dvec3{planar.x, Point.y, planar.y};
};
std::vector<std::size_t> matched;
for (std::size_t i = 0; i < Samples.size(); ++i)
if (Pairs.match[i] >= 0)
matched.push_back(i);
if (matched.empty())
{
for (auto const &entry : Samples)
emit(entry.position, entry.direction);
return;
}
Result.between = true;
auto const half = [&](std::size_t const Index) { return Pairs.across[Index] * 0.5; };
// from between the tracks over to one of them, within kBlend of the ends of the stretch apart
auto const eased = [&](double const From, double const To, double const Low, double const High) {
return From < To ? Low * (1.0 - std::clamp(From / kBlend, 0.0, 1.0)) : High * (1.0 - std::clamp(To / kBlend, 0.0, 1.0));
};
auto const first{matched.front()};
for (std::size_t i = 0; i < first; ++i)
emit(shifted(Samples[i].position, Samples[i].direction, half(first) * std::clamp(1.0 - (Samples[first].chainage - Samples[i].chainage) / kBlend, 0.0, 1.0)), Samples[i].direction);
std::optional<axis::side> pending;
std::vector<double> shares;
for (std::size_t k = 0; k < matched.size(); ++k)
{
auto const a{matched[k]};
emit(shifted(Samples[a].position, Samples[a].direction, half(a)), Samples[a].direction);
if (pending)
{
pending->to = Result.chainage.back();
for (auto const share : shares)
pending->chainage.push_back(pending->from + share * (pending->to - pending->from));
Result.sides.push_back(std::move(*pending));
pending.reset();
}
if (k + 1 == matched.size())
break;
auto const b{matched[k + 1]};
if (b == a + 1)
continue;
// the tracks apart between a and b: the track beside from the sample by a to the one by b, turned along the line
auto const ja{Pairs.match[a]};
auto const jb{Pairs.match[b]};
std::vector<sample> beside;
for (auto j = ja; j != jb; j += ja < jb ? 1 : -1)
{
auto entry{Pairs.partner[static_cast<std::size_t>(j)]};
if (ja > jb)
entry.direction = -entry.direction;
beside.push_back(entry);
}
beside.push_back(Pairs.partner[static_cast<std::size_t>(jb)]);
if (ja > jb)
beside.back().direction = -beside.back().direction;
std::vector<double> along{0.0};
for (std::size_t q = 1; q < beside.size(); ++q)
along.push_back(along.back() + plan_distance(beside[q - 1].position, beside[q].position));
auto const own_length{Samples[b].chainage - Samples[a].chainage};
auto const beside_length{along.back()};
auto const at_share = [&](double const Share) {
auto const target{Share * beside_length};
auto const next{std::min<std::size_t>(beside.size() - 1, static_cast<std::size_t>(std::lower_bound(along.begin(), along.end(), target) - along.begin()))};
if (next == 0)
return beside.front().position;
auto const span{along[next] - along[next - 1]};
return glm::mix(beside[next - 1].position, beside[next].position, span > 1e-9 ? (target - along[next - 1]) / span : 0.0);
};
auto widest{0.0};
for (auto i = a + 1; i < b; ++i)
widest = std::max(widest, plan_distance(Samples[i].position, at_share((Samples[i].chainage - Samples[a].chainage) / own_length)));
if (widest <= kApart || own_length < 1e-6 || beside_length < 1e-6)
{
// spread a little, as through a station: between them still
for (auto i = a + 1; i < b; ++i)
{
auto const t{(Samples[i].chainage - Samples[a].chainage) / own_length};
emit(shifted(Samples[i].position, Samples[i].direction, half(a) + (half(b) - half(a)) * t), Samples[i].direction);
}
continue;
}
pending = axis::side{};
shares.clear();
pending->from = Result.chainage.back();
if (beside_length > own_length + kLonger)
{
for (std::size_t q = 1; q + 1 < beside.size(); ++q)
emit(shifted(beside[q].position, beside[q].direction, -eased(along[q], beside_length - along[q], half(a), half(b))), beside[q].direction);
pending->length = own_length;
for (auto i = a + 1; i < b; ++i)
{
pending->points.push_back(Samples[i].position);
pending->along.push_back(Samples[i].chainage - Samples[a].chainage);
shares.push_back(pending->along.back() / own_length);
}
}
else
{
for (auto i = a + 1; i < b; ++i)
emit(shifted(Samples[i].position, Samples[i].direction, eased(Samples[i].chainage - Samples[a].chainage, Samples[b].chainage - Samples[i].chainage, half(a), half(b))), Samples[i].direction);
pending->length = beside_length;
for (std::size_t q = 1; q + 1 < beside.size(); ++q)
{
pending->points.push_back(beside[q].position);
pending->along.push_back(along[q]);
shares.push_back(along[q] / beside_length);
}
}
}
auto const last{matched.back()};
for (auto i = last + 1; i < Samples.size(); ++i)
emit(shifted(Samples[i].position, Samples[i].direction, half(last) * std::clamp(1.0 - (Samples[i].chainage - Samples[last].chainage) / kBlend, 0.0, 1.0)), Samples[i].direction);
}
std::optional<axis> build(std::string const &Line)
{
std::vector<std::size_t> own;
for (std::size_t index = 0; index < g_marks.size(); ++index)
if (g_marks[index].line == Line)
own.push_back(index);
if (own.empty())
return std::nullopt;
std::vector<TTrack *> tracks;
for (auto const index : own)
tracks.push_back(track_near(g_marks[index].point, kNear));
auto const start{std::find_if(tracks.begin(), tracks.end(), [](TTrack const *Track) { return Track != nullptr; })};
if (start == tracks.end())
return std::nullopt;
auto *first{*start};
auto const first_mark{own[static_cast<std::size_t>(start - tracks.begin())]};
auto route{editor_track::run_route(*first, kReach)};
if (route.spans.empty())
return std::nullopt;
// the marks the main paths of the switches lead away from: the line goes their way
std::set<std::size_t> tried;
std::vector<editor_track::route_sample> samples;
pairing pairs;
auto const origin_of = [&](std::vector<editor_track::route_sample> const &Samples) {
std::size_t origin{0};
auto nearest{std::numeric_limits<double>::max()};
for (std::size_t i = 0; i < Samples.size(); ++i)
if (auto const distance{plan_distance(Samples[i].position, g_marks[first_mark].point)}; distance < nearest)
{
nearest = distance;
origin = i;
}
return origin;
};
for (std::size_t round = 0; round <= own.size(); ++round)
{
samples = editor_track::sample_route(route, kStep);
pairs = pair_up(samples, route, origin_of(samples));
std::size_t away{own.size()};
for (std::size_t k = 0; k < own.size() && away == own.size(); ++k)
if (tracks[k] != nullptr && tried.count(k) == 0 && plan_gap(samples, g_marks[own[k]].point) > kOnAxis && plan_gap(pairs.partner, g_marks[own[k]].point) > kOnAxis)
away = k;
if (away == own.size())
break;
tried.insert(away);
editor_track::route found;
std::string error;
if (false == editor_track::find_route(first, tracks[away], found, error) || found.spans.size() < 2)
continue;
auto const at{index_of(route.spans, first)};
if (at == route.spans.size())
break;
std::vector<span> spans;
if (found.spans.front().forward == route.spans[at].forward)
{
spans.assign(route.spans.begin(), route.spans.begin() + at);
spans.insert(spans.end(), found.spans.begin(), found.spans.end());
auto const more{onward(found.spans.back())};
spans.insert(spans.end(), more.begin(), more.end());
}
else
{
spans = flipped(onward(found.spans.back()));
auto const back{flipped(found.spans)};
spans.insert(spans.end(), back.begin(), back.end());
spans.insert(spans.end(), route.spans.begin() + at + 1, route.spans.end());
}
route.spans = once(spans);
}
if (samples.size() < 2)
return std::nullopt;
axis result;
result.line = Line;
compose(result, samples, pairs);
for (auto const index : own)
{
axis::anchor anchor;
anchor.mark = index;
anchor.chainage = chainage_at(result, g_marks[index].point, anchor.gap);
if (anchor.gap > kOnAxis)
{
result.astray.push_back(index);
continue;
}
anchor.before = g_marks[index].km;
anchor.after = g_marks[index].beyond.value_or(g_marks[index].km);
result.anchors.push_back(anchor);
}
if (result.anchors.empty())
return std::nullopt;
std::sort(result.anchors.begin(), result.anchors.end(), [](axis::anchor const &A, axis::anchor const &B) { return A.chainage < B.chainage; });
// the km rise the way the marks grow, the way the only one says
auto const &low{result.anchors.front()};
auto const &high{result.anchors.back()};
if (result.anchors.size() > 1 && std::abs(high.before - low.before) > 1e-6)
result.growth = high.before > low.before ? 1 : -1;
else
result.growth = glm::dot(g_marks[low.mark].way, direction_on(result, low.chainage)) >= 0.0 ? 1 : -1;
return result;
}
} // namespace
void reset()
{
g_marks.clear();
g_base.clear();
drop_axes();
g_changed = false;
g_loaded = false;
}
void load()
{
if (g_loaded || scene::Layers.empty())
return;
g_loaded = true;
g_marks.clear();
drop_axes();
for (auto const layer : scene::Layers.marked_layers(kMark))
{
auto const &context{scene::Layers.layer(layer).context_insert()};
for (auto const &text : scene::Layers.marked(layer, kMark))
{
std::istringstream line{text};
std::string keyword;
mark entry;
entry.layer = layer;
glm::dvec3 point{0.0};
glm::dvec2 way{0.0, 1.0};
line >> keyword;
if (keyword != "mark")
continue;
line >> entry.line >> point.x >> point.y >> point.z >> keyword >> way.x >> way.y >> keyword >> entry.km;
if (line.fail() || entry.line.empty() || false == std::isfinite(entry.km))
continue;
while (line >> keyword)
if (keyword == "beyond")
{
double beyond{0.0};
if (line >> beyond)
entry.beyond = beyond;
}
entry.point = context.to_world(point);
entry.way = plan_of(context.to_world(point + glm::dvec3{way.x, 0.0, way.y}) - entry.point);
entry.way = glm::length(entry.way) > 1e-12 ? glm::normalize(entry.way) : glm::dvec2{0.0, 1.0};
g_marks.push_back(std::move(entry));
}
}
g_base = g_marks;
g_changed = false;
if (false == g_marks.empty())
WriteLog("Editor: " + std::to_string(g_marks.size()) + " kilometrage mark(s) read from the scenery", logtype::generic);
}
std::vector<mark> const &marks()
{
load();
return g_marks;
}
void put(mark Entry)
{
load();
auto *track{track_near(Entry.point, kNear)};
Entry.layer = track != nullptr ? alignment_store::layer_of(*track) : scene::Layers.active();
if (false == scene::Layers.valid(Entry.layer))
Entry.layer = 1;
auto const layer{Entry.layer};
g_marks.push_back(std::move(Entry));
changed({layer});
}
void change(std::size_t const Index, mark Entry)
{
load();
if (Index >= g_marks.size())
return;
Entry.layer = g_marks[Index].layer;
g_marks[Index] = std::move(Entry);
changed({g_marks[Index].layer});
}
void remove(std::size_t const Index)
{
load();
if (Index >= g_marks.size())
return;
auto const layer{g_marks[Index].layer};
g_marks.erase(g_marks.begin() + static_cast<std::ptrdiff_t>(Index));
changed({layer});
}
std::optional<std::vector<mark>> take_changes()
{
if (false == g_changed)
return std::nullopt;
auto before{std::move(g_base)};
g_base = g_marks;
g_changed = false;
return before;
}
void settle()
{
g_base = g_marks;
g_changed = false;
}
void restore(std::vector<mark> Marks)
{
std::set<scene::layer_handle> layers;
for (auto const *list : {&g_marks, &Marks})
for (auto const &entry : *list)
layers.insert(entry.layer);
g_marks = std::move(Marks);
changed(layers);
settle();
}
void follow(std::size_t const Edits)
{
if (Edits == g_edits)
return;
g_edits = Edits;
drop_axes();
}
std::size_t revision()
{
return g_revision;
}
std::vector<std::string> lines()
{
load();
std::vector<std::string> result;
for (auto const &entry : g_marks)
if (std::find(result.begin(), result.end(), entry.line) == result.end())
result.push_back(entry.line);
std::sort(result.begin(), result.end());
return result;
}
axis const *axis_of(std::string const &Line)
{
load();
auto found{g_axes.find(Line)};
if (found == g_axes.end())
found = g_axes.emplace(Line, build(Line)).first;
return found->second.has_value() ? &*found->second : nullptr;
}
double km_on(axis const &Axis, double const Chainage)
{
auto const &anchors{Axis.anchors};
if (anchors.empty())
return 0.0;
auto const growth{static_cast<double>(Axis.growth)};
// the km of an anchor on the side of the lower chainage, and of the higher one
auto const lower = [&](axis::anchor const &Anchor) { return Axis.growth > 0 ? Anchor.before : Anchor.after; };
auto const higher = [&](axis::anchor const &Anchor) { return Axis.growth > 0 ? Anchor.after : Anchor.before; };
if (Chainage <= anchors.front().chainage)
return lower(anchors.front()) - growth * (anchors.front().chainage - Chainage) / 1000.0;
if (Chainage >= anchors.back().chainage)
return higher(anchors.back()) + growth * (Chainage - anchors.back().chainage) / 1000.0;
auto const next{std::upper_bound(anchors.begin(), anchors.end(), Chainage, [](double const Value, axis::anchor const &Anchor) { return Value < Anchor.chainage; })};
auto const &to{*next};
auto const &from{*std::prev(next)};
auto const span{to.chainage - from.chainage};
if (span < 1e-6)
return higher(from);
return higher(from) + (lower(to) - higher(from)) * (Chainage - from.chainage) / span;
}
double km_on(axis const &Axis, axis::side const &Side, double const Along)
{
auto const low{km_on(Axis, Side.from + 1e-6)};
auto const high{km_on(Axis, Side.to - 1e-6)};
auto const length{Side.length};
if (length < 1e-6 || Side.to - Side.from < 1e-6)
return low;
// along the rising km: from the km the stretch starts at, the irregular hectometre before the one it ends at
auto const rising{Axis.growth > 0};
auto const start{rising ? low : high};
auto const end{rising ? high : low};
auto const s{std::clamp(rising ? Along : length - Along, 0.0, length)};
auto const scale{std::abs(end - start) / ((Side.to - Side.from) / 1000.0)}; // km of the axis to a km of the scenery
auto const regular{start + scale * length / 1000.0};
auto const last{(std::ceil(std::min(regular, end) * 10.0 - 1e-9) - 1.0) / 10.0};
if (end <= start || last <= start || scale <= 1e-9)
return start + (end - start) * s / length;
auto const at{(last - start) / scale * 1000.0};
if (s <= at || at >= length)
return start + scale * s / 1000.0;
return last + (end - last) * (s - at) / (length - at);
}
double chainage_of(axis const &Axis, glm::dvec3 const &Point, double &Gap)
{
Gap = std::numeric_limits<double>::max();
auto result{0.0};
auto const point{plan_of(Point)};
for (std::size_t i = 1; i < Axis.points.size(); ++i)
{
auto const a{plan_of(Axis.points[i - 1])};
auto const along{plan_of(Axis.points[i]) - a};
auto const length2{glm::dot(along, along)};
auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, along) / length2, 0.0, 1.0) : 0.0};
if (auto const gap{glm::distance(point, a + along * t)}; gap < Gap)
{
Gap = gap;
result = Axis.chainage[i - 1] + (Axis.chainage[i] - Axis.chainage[i - 1]) * t;
}
}
return result;
}
double chainage_at(axis const &Axis, glm::dvec3 const &Point, double &Gap)
{
auto result{chainage_of(Axis, Point, Gap)};
auto const point{plan_of(Point)};
for (auto const &entry : Axis.sides)
for (std::size_t i = 1; i < entry.points.size(); ++i)
{
auto const a{plan_of(entry.points[i - 1])};
auto const along{plan_of(entry.points[i]) - a};
auto const length2{glm::dot(along, along)};
auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, along) / length2, 0.0, 1.0) : 0.0};
if (auto const gap{glm::distance(point, a + along * t)}; gap < Gap)
{
Gap = gap;
result = entry.chainage[i - 1] + (entry.chainage[i] - entry.chainage[i - 1]) * t;
}
}
return result;
}
glm::dvec3 point_on(axis const &Axis, double const Chainage)
{
if (Axis.points.empty())
return glm::dvec3{0.0};
auto const after{std::lower_bound(Axis.chainage.begin(), Axis.chainage.end(), Chainage)};
if (after == Axis.chainage.begin())
return Axis.points.front();
if (after == Axis.chainage.end())
return Axis.points.back();
auto const i{static_cast<std::size_t>(after - Axis.chainage.begin())};
auto const step{Axis.chainage[i] - Axis.chainage[i - 1]};
return glm::mix(Axis.points[i - 1], Axis.points[i], step > 1e-9 ? (Chainage - Axis.chainage[i - 1]) / step : 0.0);
}
glm::dvec2 direction_on(axis const &Axis, double const Chainage)
{
if (Axis.directions.empty())
return {0.0, 1.0};
auto const after{std::lower_bound(Axis.chainage.begin(), Axis.chainage.end(), Chainage)};
auto const i{std::min(Axis.directions.size() - 1, static_cast<std::size_t>(after - Axis.chainage.begin()))};
return Axis.directions[i];
}
bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double const Reach)
{
auto const *found{axis_of(Line)};
if (found == nullptr)
return false;
double gap{0.0};
auto const chainage{chainage_of(*found, Point, gap)};
auto const point{plan_of(Point)};
axis::side const *apart{nullptr};
auto along{0.0};
for (auto const &entry : found->sides)
for (std::size_t i = 1; i < entry.points.size(); ++i)
{
auto const a{plan_of(entry.points[i - 1])};
auto const ab{plan_of(entry.points[i]) - a};
auto const length2{glm::dot(ab, ab)};
auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0};
if (auto const distance{glm::distance(point, a + ab * t)}; distance < gap)
{
gap = distance;
apart = &entry;
along = entry.along[i - 1] + (entry.along[i] - entry.along[i - 1]) * t;
}
}
if (gap > Reach)
return false;
Km = apart != nullptr ? km_on(*found, *apart, along) : km_on(*found, chainage);
return true;
}
TTrack *track_near(glm::dvec3 const &Point, double const Reach)
{
TTrack *best{nullptr};
auto nearest{Reach};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0 || false == editor_track::is_supported(*track) || track->m_paths.empty())
continue;
auto const &ends{track->m_paths.front().points};
auto const chord{plan_distance(ends[segment_data::point::start], ends[segment_data::point::end])};
if (std::min(plan_distance(ends[segment_data::point::start], Point), plan_distance(ends[segment_data::point::end], Point)) > chord * 0.8 + nearest + 1.0)
continue;
auto const on{editor_track::point_at(*track, editor_track::nearest_parameter(*track, Point))};
if (auto const distance{plan_distance(on, Point)}; distance < nearest)
{
nearest = distance;
best = track;
}
}
return best;
}
std::string text(double const Km)
{
auto result{format("%.3f", Km)};
std::replace(result.begin(), result.end(), '.', ',');
return result;
}
} // namespace kilometrage

View File

@@ -0,0 +1,111 @@
/*
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 <cstddef>
#include <optional>
#include <string>
#include <vector>
#include <glm/glm.hpp>
#include "scene/scenelayers.h"
class TTrack;
// the kilometrage of the railway lines, set in the track editor and kept in the scenery files as "//$k" comment lines: the
// points of a line where its km is known, from a post or a document, and the points where it jumps, with the km on both
// sides. the km run along the axis of a single-track line and between the tracks of a double-track one, along the longer track
// where the tracks part on formations of their own (PKP GK-1 6.1.12), the other tracks get the km straight across from them
namespace kilometrage
{
struct mark
{
scene::layer_handle layer{null_handle};
std::string line; // the number of the line, as 91
glm::dvec3 point{0.0}; // on the axis of a track of the line
glm::dvec2 way{0.0, 1.0}; // the km rise this way along the line there, in the plan
double km{0.0}; // the km the point is reached at, coming along the rising km
std::optional<double> beyond; // of a jump: the km on from the point
};
void reset();
void load();
std::vector<mark> const &marks();
// a new mark, kept in the file of the layer of the track nearest to it
void put(mark Entry);
void change(std::size_t const Index, mark Entry);
void remove(std::size_t const Index);
// the marks as they were before the changes made since the last call, if there were any; the current ones become the base
std::optional<std::vector<mark>> take_changes();
// the current marks become the base, the changes made so far aren't a part of the undo history
void settle();
// puts the marks back as they were, for undo and redo
void restore(std::vector<mark> Marks);
// the axes found are dropped when the tracks change: Edits counts the changes of the tracks made in the editor
void follow(std::size_t const Edits);
// counts the times the axes were dropped: what is made from them is made again when it changes
std::size_t revision();
// the axis a line is measured along
struct axis
{
std::string line;
bool between{false}; // between the tracks of a double-track line, else along the track of the line
std::vector<glm::dvec3> points; // a couple of metres apart
std::vector<glm::dvec2> directions; // of the points, in the plan
std::vector<double> chainage; // of the points, along them
int growth{1}; // the km rise along the chainage, -1: against it
struct anchor
{
std::size_t mark{0}; // of the marks
double chainage{0.0};
double gap{0.0}; // m from the mark to the axis, in the plan
double before{0.0}; // the km the point is reached at, coming along the rising km
double after{0.0}; // the km on from it, other than before at a jump
};
// the shorter track of a double-track line where the tracks run on formations of their own, the axis along the longer one: its
// km go by its own length from the end of the stretch the km rise from, the irregular hectometre at the other end takes up the
// difference (PKP GK-1 6.1.12)
struct side
{
std::vector<glm::dvec3> points;
std::vector<double> along; // m from the start of the stretch, along the track
std::vector<double> chainage; // of the axis, by the share of the length: where a mark on the track anchors
double length{0.0}; // of the track between the ends of the stretch
double from{0.0}; // the chainage of the ends of the stretch
double to{0.0};
};
std::vector<side> sides;
std::vector<anchor> anchors; // by the chainage
std::vector<std::size_t> astray; // the marks of the line away from its axis
double length() const { return chainage.empty() ? 0.0 : chainage.back(); }
};
// the numbers of the lines with marks
std::vector<std::string> lines();
// the axis of the line through the tracks of its marks, nullptr while it has none; kept until the marks or the tracks change
axis const *axis_of(std::string const &Line);
double km_on(axis const &Axis, double const Chainage);
// where the point lies along the axis, Gap: how far from it in the plan
double chainage_of(axis const &Axis, glm::dvec3 const &Point, double &Gap);
// the same through the tracks running apart from the axis too, where they are nearer: the chainage the point takes the km of
double chainage_at(axis const &Axis, glm::dvec3 const &Point, double &Gap);
glm::dvec3 point_on(axis const &Axis, double const Chainage);
glm::dvec2 direction_on(axis const &Axis, double const Chainage);
// the km of a track running apart from the axis, Along: m from the start of the stretch
double km_on(axis const &Axis, axis::side const &Side, double const Along);
// the km of the line at the point, when it lies within the reach of the axis
bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double const Reach = 30.0);
// the track nearest to the point in the plan within the reach, nullptr for none
TTrack *track_near(glm::dvec3 const &Point, double const Reach);
// "100,234", as PKP PLK write the km
std::string text(double const Km);
} // namespace kilometrage

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -340,6 +340,16 @@ def station(d):
line(d, [(46, 36), (46, 56)], W - 10)
def kilometrage(d):
# a hectometre post by the track with its digits, and the way the km rise along the track
rect(d, 16, 4, 36, 40, outline=True, width=W - 6, radius=3)
line(d, [(22, 15), (30, 15)], W - 8)
line(d, [(22, 28), (30, 28)], W - 8)
line(d, [(26, 40), (26, 48)], W - 6)
line(d, [(4, 56), (48, 56)], W - 6)
arrowhead(d, 54, 56, 0, 12)
def more(d):
for x in (12, 32, 52):
circle(d, x, 32, 6)
@@ -390,7 +400,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, station, more, work_surroundings,
vehicle, profile, speed, joints, infra, gauge, turntable, parallel, station, kilometrage, 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, signalling, objects, trainset, profile, speed, joints, gauge"
msgstr "Obrotnica, sygnalizatory, SRK, obiekty, skład, profil, prędkość, styki, skrajnia"
msgid "Turntable, signals, signalling, kilometrage, objects, trainset, profile, speed, joints, gauge"
msgstr "Obrotnica, sygnalizatory, SRK, kilometraż, obiekty, skład, profil, prędkość, styki, skrajnia"
msgid "No switch drive models"
msgstr "Brak modeli napędów"
@@ -10542,3 +10542,181 @@ msgstr[0] "%zu obiekt scenerii idzie za nimi"
msgstr[1] "%zu obiekty scenerii idą za nimi"
msgstr[2] "%zu obiektów scenerii idzie za nimi"
# kilometrage tool
msgid "Kilometrage"
msgstr "Kilometraż"
msgid "The km of the railway lines: LMB on a track puts a point of the km there, a jump of the km too"
msgstr "Kilometraż linii kolejowych: LPM na torze wstawia tam punkt kilometra, także uskok kilometrażu"
msgid "Click: a point of the km here"
msgstr "Klik: punkt kilometra tutaj"
msgid "Id-1 § 19: the km shouldn't jump on a curve nor at a station, move the jump to a straight on the open line"
msgstr "Id-1 § 19: uskoku kilometrażu nie daje się na łuku ani na stacji, przesuń go na prostą na szlaku"
msgid "Id-1 § 19: the km shouldn't jump on a curve, move the jump to a straight"
msgstr "Id-1 § 19: uskoku kilometrażu nie daje się na łuku, przesuń go na prostą"
msgid "Id-1 § 19: the km shouldn't jump at a station, move the jump out to the open line"
msgstr "Id-1 § 19: uskoku kilometrażu nie daje się na stacji, przesuń go na szlak"
msgid "line %s: km %s"
msgstr "linia %s: km %s"
msgid "Give the number of the line first"
msgstr "Najpierw wpisz numer linii"
msgid "A jump needs another km on from the point"
msgstr "Uskok wymaga innego kilometra za punktem"
msgid "Line %s: km %s at the point"
msgstr "Linia %s: km %s w punkcie"
msgid "The point of the km removed"
msgstr "Punkt kilometra usunięty"
msgid "Click a track of the line first"
msgstr "Najpierw kliknij tor linii"
msgid "No line through this track"
msgstr "Przez ten tor nie biegnie żadna linia"
msgid "Fewer than two hectometre posts stand along the line, nothing to read"
msgstr "Wzdłuż linii stoją mniej niż dwa słupki hektometrowe, nie ma czego odczytać"
msgid "The numbers of the posts don't go by 100 m anywhere along the line, nothing read"
msgstr "Numery słupków nigdzie wzdłuż linii nie idą co 100 m, nic nie odczytano"
msgid "%zu posts read: %zu points of the km, %zu jumps"
msgstr "Odczytane słupki: %zu, punkty kilometra: %zu, uskoki: %zu"
msgid "The km of a railway line: the points where the km is known, from a post or a document, and where it jumps. The km of the rest go from them along the axis of the line: between the tracks of a double-track one, along the longer track where the tracks run on formations of their own, the shorter one getting an irregular hectometre"
msgstr "Kilometraż linii kolejowej: punkty, w których kilometr jest znany ze słupka albo z dokumentu, i miejsca uskoków. Kilometraż reszty liczy się od nich wzdłuż osi linii: na dwutorowej po osi międzytorza, a gdzie tory biegną osobnymi torowiskami, po torze dłuższym, krótszy dostaje hektometr o nieprawidłowej długości"
msgid "a point of the km there, or the one standing by it"
msgstr "punkt kilometra w tym miejscu albo ten, który już tam jest"
msgid "jumps"
msgstr "uskoki"
msgid "A point of the km"
msgstr "Punkt kilometra"
msgid "Click a track where the km is known: by a post, a station or a bridge"
msgstr "Kliknij tor tam, gdzie znasz kilometr: przy słupku, stacji albo moście"
msgid "The number of the railway line, as 91: a scenery can hold several lines, each with its own km"
msgstr "Numer linii kolejowej, np. 91: sceneria może mieć kilka linii, każda z własnym kilometrażem"
msgid "The km of the point, as PKP PLK write it: 100,234 is km 100 and 234 m. At a jump: the km the point is reached at, coming along the rising km"
msgstr "Kilometr punktu, zapisany jak w PKP PLK: 100,234 to km 100 i 234 m. Przy uskoku: kilometr, z jakim dojeżdża się do punktu, jadąc w stronę rosnącego kilometrażu"
msgid "Jump"
msgstr "Uskok"
msgid "The km jumps at the point: a line built anew, shortened or lengthened keeps the km of the rest. The irregular hectometre is marked by a black stripe under the digit of the post (Id-1 § 19)"
msgstr "W tym punkcie kilometraż skacze: przebudowana, skrócona albo wydłużona linia zachowuje kilometraż reszty. Nieregularny hektometr oznacza się czarnym paskiem pod cyfrą słupka (Id-1 § 19)"
msgid "Km on"
msgstr "Km dalej"
msgid "The km on from the point, along the rising km"
msgstr "Kilometr za punktem, w stronę rosnącego kilometrażu"
msgid "Rise this way"
msgstr "Rośnie w tę stronę"
msgid "The km rise the way the arrow at the point shows"
msgstr "Kilometraż rośnie tam, gdzie pokazuje strzałka w punkcie"
msgid "Rise the other way"
msgstr "Rośnie w drugą stronę"
msgid "The km rise against the arrow"
msgstr "Kilometraż rośnie przeciwnie do strzałki"
msgid "Change the point"
msgstr "Zmień punkt"
msgid "Put the point of the km"
msgstr "Wstaw punkt kilometra"
msgid "The point goes to the file of the scenery with the track, the simulation skips it. One step of undo"
msgstr "Punkt trafia do pliku scenerii z tym torem, symulacja go pomija. Jeden krok cofania"
msgid "Remove the point"
msgstr "Usuń punkt"
msgid "One step of undo"
msgstr "Jeden krok cofania"
msgid "From the posts"
msgstr "Ze słupków"
msgid "Read the km from the posts"
msgstr "Odczytaj kilometraż ze słupków"
msgid "Reads the hectometre posts standing along the line through the track clicked: a point of the km at the first and the last post of each run numbered by 100 m, a jump where two runs meet. The points of the line along it before give way. One step of undo"
msgstr "Odczytuje słupki hektometrowe stojące wzdłuż linii przez kliknięty tor: punkt kilometra na pierwszym i ostatnim słupku każdego ciągu numerowanego co 100 m, uskok tam, gdzie dwa ciągi się stykają. Wcześniejsze punkty tej linii na tym odcinku ustępują nowym. Jeden krok cofania"
msgid "The points of the lines"
msgstr "Punkty linii"
msgid "None yet"
msgstr "Jeszcze żadnych"
msgid "No track by the points of the line"
msgstr "Przy punktach linii nie ma toru"
msgid "%.1f km of the axis, %s"
msgstr "%.1f km osi, %s"
msgid "between the tracks"
msgstr "w osi międzytorza"
msgid "In the scenery"
msgstr "W scenerii"
msgid "By the km"
msgstr "Wg kilometrażu"
msgid "Off by"
msgstr "Różnica"
msgid "How much longer the stretch is in the scenery than by the km: the km between the points are stretched to it evenly"
msgstr "O ile odcinek w scenerii jest dłuższy niż wg kilometrażu: kilometry między punktami rozciąga się na nim równo"
msgid "The point is away from the axis of the line the other points make: the main paths of the switches don't lead to it"
msgstr "Punkt leży z dala od osi linii wyznaczonej przez pozostałe punkty: zasadnicze tory rozjazdów do niego nie prowadzą"
msgid "%zu post doesn't fit its neighbours and was left out"
msgid_plural "%zu posts don't fit their neighbours and were left out"
msgstr[0] "%zu słupek nie pasuje do sąsiednich i został pominięty"
msgstr[1] "%zu słupki nie pasują do sąsiednich i zostały pominięte"
msgstr[2] "%zu słupków nie pasuje do sąsiednich i zostało pominiętych"
msgid "%zu jump lies on a curve or at a station, Id-1 § 19 wants none there"
msgid_plural "%zu jumps lie on a curve or at a station, Id-1 § 19 wants none there"
msgstr[0] "%zu uskok leży na łuku albo na stacji, Id-1 § 19 tego nie dopuszcza"
msgstr[1] "%zu uskoki leżą na łuku albo na stacji, Id-1 § 19 tego nie dopuszcza"
msgstr[2] "%zu uskoków leży na łuku albo na stacji, Id-1 § 19 tego nie dopuszcza"
msgid "a point of the km there"
msgstr "punkt kilometra w tym miejscu"
msgid "Mouse over a track"
msgstr "Mysz nad torem"
msgid "its km by the cursor"
msgstr "jego kilometr przy kursorze"
msgid "%zu stretch with the tracks on formations of their own"
msgid_plural "%zu stretches with the tracks on formations of their own"
msgstr[0] "%zu odcinek z torami na osobnych torowiskach"
msgstr[1] "%zu odcinki z torami na osobnych torowiskach"
msgstr[2] "%zu odcinków z torami na osobnych torowiskach"
msgid "Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)"
msgstr "Gdzie tory rozchodzą się na więcej niż 15 m i znów schodzą, kilometraż biegnie po dłuższym, krótszy liczy się po własnej długości, a różnicę bierze jego ostatni hektometr przed zejściem, jak podaje PKP GK-1 (6.1.12)"

View File

@@ -843,17 +843,18 @@ bool node_layers::pending() const
namespace
{
// marks of the lines the editor keeps track of
std::array<char const *, 7> const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s"};
std::array<char const *, 8> const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s", "//$k"};
// 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>>, 4> const words{{
static std::array<std::pair<std::string_view, std::array<std::string_view, 9>>, 5> const words{{
{"//$h", {"#", "alignment", "source", "ends", "norms", "pieces", "vertex", "element", "end"}},
{"//$r", {"#", "switch", "slip", "crossing", "double", "connection"}},
{"//$o", {"on"}},
{"//$s", {"#", "station"}},
{"//$k", {"#", "mark"}},
}};
auto const lookup{std::find_if(words.begin(), words.end(), [&](auto const &Entry) { return Entry.first == Mark; })};
if (lookup == words.end())