17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-21 01:36:02 +02:00
Files
maszyna/application/editormodedigitize.cpp
maj00r ff1710d9ea feat(editor): labels for a layman, light at the start; whole sceneries fitted to the terrain model
Labels:
- every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the
  sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track,
  grade), sides told from directions, degrees and per mille as signs, messages built from one format string
- plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header
- lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped
- roads: the notes and errors naming a road translated; the build options and the layout fold the texture length,
  kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the
  lanes start folded

Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once:
- fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone
  past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way
  is one point of intersection, a curve with no room is as sharp as the room lets it be
- the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what
  the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run
- apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into
  pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the
  ends of the route are brought to its height
- Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm
2026-10-19 15:27:50 +02:00

424 lines
15 KiB
C++

/*
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/.
*/
// the digitization of a scenery made without the editor: its lines kept with their designs, its switches with their types out of
// the catalogue, its crossovers as connections and the objects along the tracks bound to them, at one go
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editoruilayer.h"
#include "editor/editorAlignmentStore.hpp"
#include "editor/editorConnections.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorSwitchStore.hpp"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "utilities/translation.h"
#include "world/Track.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <set>
namespace
{
using geometry::bezier;
using geometry::plan_of;
using geometry::signed_angle;
// how far the size of a switch may be from the template to be of it: the length of the main and the diverging track, the angle
double constexpr kMainTolerance{0.10};
double constexpr kBranchTolerance{0.15};
double constexpr kAngleTolerance{0.0025};
// a plain track this short between the ends of two switches is a part of a slip
double constexpr kSlipLink{6.0};
// the longest insert of a crossover
double constexpr kInsertLongest{150.0};
// the tracks a crossover joins lie this far apart
double constexpr kSpacingLeast{2.5};
double constexpr kSpacingMost{15.0};
// radii of the diverging track the switches are made with; a measured one this close to one of them is taken for it
double constexpr kNominalRadii[]{150.0, 190.0, 205.0, 230.0, 265.0, 300.0, 500.0, 760.0, 1200.0};
double constexpr kNominalNear{0.04};
double nominal_radius(double const Radius)
{
for (auto const nominal : kNominalRadii)
if (std::abs(Radius - nominal) <= kNominalNear * nominal)
return nominal;
return std::round(Radius);
}
std::vector<segment_data const *> parts_of(segment_data const &Path)
{
std::vector<segment_data const *> parts;
if (Path.pieces.empty())
parts.push_back(&Path);
else
for (auto const &piece : Path.pieces)
parts.push_back(&piece);
return parts;
}
double path_length(segment_data const &Path)
{
double length{0.0};
for (auto const *part : parts_of(Path))
length += bezier{*part}.plan_length();
return length;
}
glm::dvec2 heading(segment_data const &Path, bool const End)
{
auto const parts{parts_of(Path)};
bezier const curve{End ? *parts.back() : *parts.front()};
auto const direction{plan_of(curve.first(End ? 1.0 : 0.0))};
if (glm::length(direction) > 1e-9)
return glm::normalize(direction);
auto const chord{plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])};
return glm::length(chord) > 1e-9 ? glm::normalize(chord) : glm::dvec2{0.0, 1.0};
}
// in the plan, + to the left, at the middle of the longest part of the path
double curvature(segment_data const &Path)
{
segment_data const *longest{nullptr};
double length{-1.0};
for (auto const *part : parts_of(Path))
if (auto const l{bezier{*part}.plan_length()}; l > length)
{
length = l;
longest = part;
}
if (longest == nullptr || false == geometry::has_controls(*longest))
return 0.0;
bezier const curve{*longest};
auto const first{plan_of(curve.first(0.5))};
auto const second{plan_of(curve.second(0.5))};
auto const speed{glm::length(first)};
return speed > 1e-9 ? (first.x * second.y - first.y * second.x) / (speed * speed * speed) : 0.0;
}
struct switch_size
{
double main{0.0};
double branch{0.0};
double angle{0.0}; // between the ends of the two tracks
double radius{0.0}; // of the diverging track of the straight switch the switch is made of
};
switch_size size_of(segment_data const &Main, segment_data const &Branch)
{
switch_size size;
size.main = path_length(Main);
size.branch = path_length(Branch);
size.angle = std::abs(signed_angle(heading(Main, true), heading(Branch, true)));
auto const bend{std::abs(curvature(Branch) - curvature(Main))};
size.radius = bend > 1e-6 ? 1.0 / bend : 0.0;
return size;
}
// the template of the catalogue the size is of: the rail of the track and a movable nose decide between the same shapes
int matching(std::vector<editor_track::switch_template> const &Templates, std::vector<std::pair<int, switch_size>> const &Sizes, switch_size const &Size, std::string const &Rail, bool const Movable)
{
int best{-1};
auto score{std::numeric_limits<double>::max()};
for (auto const &[index, size] : Sizes)
{
auto const &shape{Templates[index]};
auto const main{std::abs(size.main - Size.main)};
auto const branch{std::abs(size.branch - Size.branch)};
auto const angle{std::abs(size.angle - Size.angle)};
if (main > kMainTolerance || branch > kBranchTolerance || angle > kAngleTolerance)
continue;
auto value{main / kMainTolerance + branch / kBranchTolerance + angle / kAngleTolerance};
if (false == Rail.empty() && shape.rail != Rail)
value += 10.0;
if (shape.movable != Movable)
value += 5.0;
if (value < score)
{
score = value;
best = index;
}
}
return best;
}
bool usable_switch(TTrack const *Track)
{
return Track != nullptr && false == Track->m_editorremoved && Track->eType == tt_Switch && Track->m_paths.size() == 2;
}
bool named(TTrack const &Track)
{
return false == Track.name().empty() && Track.name() != "none";
}
glm::dvec3 far_end(segment_data const &Path, editor_track::point_kind const Kind)
{
return Path.points[Kind == editor_track::point_kind::start ? segment_data::point::end : segment_data::point::start];
}
// the end of the path meets an end of another switch, right there or over a short plain track: the paths of a slip do
bool towards_switch(TTrack const &Track, glm::dvec3 const &Point)
{
for (auto const &[other, point] : editor_track::connected_points(Track, Point))
{
if (other == nullptr || other == &Track || other->m_editorremoved)
continue;
if (other->eType == tt_Switch && point.kind == editor_track::point_kind::end)
return true;
if (other->eType != tt_Normal || other->m_paths.size() != 1 || path_length(other->m_paths.front()) > kSlipLink)
continue;
for (auto const &[next, nextpoint] : editor_track::connected_points(*other, far_end(other->m_paths.front(), point.kind)))
if (next != nullptr && next != other && next != &Track && next->eType == tt_Switch && nextpoint.kind == editor_track::point_kind::end)
return true;
}
return false;
}
bool slip_part(TTrack const &Switch)
{
return towards_switch(Switch, Switch.m_paths[0].points[segment_data::point::end]) && towards_switch(Switch, Switch.m_paths[1].points[segment_data::point::end]);
}
} // namespace
void editor_mode::digitize_switches(digitize_report &Report, bool const Write)
{
auto const templates{editor_track::standard_switch_templates()};
std::vector<std::pair<int, switch_size>> sizes;
for (int i = 0; i < static_cast<int>(templates.size()); ++i)
{
auto const &shape{templates[i]};
if (shape.source == nullptr && false == shape.double_slip && false == shape.twofold())
sizes.emplace_back(i, size_of(shape.local[0], shape.local[1]));
}
for (auto *track : simulation::Paths.sequence())
{
if (false == usable_switch(track))
continue;
++Report.switches;
if (switch_store::named(*track).has_value())
{
++Report.known;
continue;
}
if (slip_part(*track))
{
++Report.slips;
continue;
}
auto const &main{track->m_paths[0]};
auto const &branch{track->m_paths[1]};
if (geometry::plan_distance(main.points[segment_data::point::start], branch.points[segment_data::point::start]) > geometry::same_point)
{
++Report.odd;
Report.notes.push_back(track->name() + ": " + STR("the tracks of the switch don't start at the same point"));
continue;
}
auto const size{size_of(main, branch)};
auto const movable{track->SwitchExtension != nullptr && track->SwitchExtension->m_movablenose};
auto const found{matching(templates, sizes, size, editor_track::rail_of(track), movable)};
editor_track::switch_template shape;
if (found >= 0)
{
shape = templates[found];
++Report.designed;
++Report.types[shape.label.substr(0, shape.label.find(" "))];
}
else
{
shape.source = track;
shape.radius = nominal_radius(size.radius);
shape.ratio = size.angle > 1e-6 ? 1.0 / std::tan(size.angle) : 0.0;
++Report.measured;
++Report.shapes[format("L %.2f / %.2f 1:%.2f R %.0f", size.main, size.branch, shape.ratio, shape.radius)];
}
if (false == named(*track))
{
// the record finds the switch by its name; the names of the scenery stay as they are
++Report.unnamed;
continue;
}
if (Write && found >= 0)
record_switch(*track, shape);
else if (Write)
{
switch_store::record entry;
entry.kind = "switch";
entry.name = track->name();
entry.parts = {entry.name};
entry.radius = shape.radius;
entry.ratio = shape.ratio;
entry.source = "measured";
auto const rail{editor_track::rail_of(track)};
entry.rail = rail.empty() ? std::string{"-"} : rail;
switch_store::place(entry, *track);
switch_store::classify(entry, *track);
switch_store::put(entry, alignment_store::layer_of(*track));
}
++Report.recorded;
}
}
void editor_mode::digitize_crossovers(digitize_report &Report, bool const Write)
{
std::set<std::pair<TTrack const *, TTrack const *>> seen;
for (auto *first : simulation::Paths.sequence())
{
if (false == usable_switch(first) || slip_part(*first))
continue;
auto const &branch{first->m_paths[1]};
auto const end{branch.points[segment_data::point::end]};
// the insert: plain tracks from the end of the diverging track up to the end of the diverging track of another switch
std::vector<TTrack *> insert;
TTrack *second{nullptr};
auto *from{static_cast<TTrack const *>(first)};
auto point{end};
double length{0.0};
bool direct{false};
while (second == nullptr && length <= kInsertLongest)
{
TTrack *next{nullptr};
editor_track::point_ref at;
for (auto const &[other, ref] : editor_track::connected_points(*from, point))
{
if (other == nullptr || other == from || other->m_editorremoved || std::find(insert.begin(), insert.end(), other) != insert.end())
continue;
if (usable_switch(other) && other != first && ref.path == 1 && ref.kind == editor_track::point_kind::end)
{
second = other;
direct = insert.empty();
break;
}
if (other->eType == tt_Normal && other->m_paths.size() == 1)
{
next = other;
at = ref;
}
}
if (second != nullptr || next == nullptr)
break;
insert.push_back(next);
length += path_length(next->m_paths.front());
point = far_end(next->m_paths.front(), at.kind);
from = next;
}
if (second == nullptr || slip_part(*second))
continue;
if (seen.count({second, first}) > 0)
continue;
seen.insert({first, second});
// the two switches face each other from two tracks side by side
auto const along{heading(first->m_paths[0], false)};
auto const back{heading(second->m_paths[0], false)};
if (glm::dot(along, back) > -0.9)
continue;
auto const spacing{std::abs(geometry::cross(along, plan_of(second->m_paths[0].points[segment_data::point::start] - first->m_paths[0].points[segment_data::point::start])))};
if (spacing < kSpacingLeast || spacing > kSpacingMost)
continue;
++Report.crossovers;
auto const name = [](TTrack const *Track) { return Track->name().empty() ? STR("(no name)") : Track->name(); };
auto const note = [&](std::string const &Why) { Report.notes.push_back(name(first) + " + " + name(second) + ": " + Why); };
if (false == switch_store::connections(*first).empty())
{
++Report.crossovers_known;
continue;
}
if (direct || insert.size() != 1)
{
++Report.crossovers_odd;
note(direct ? STR("the diverging tracks meet without an insert between them") : format(STR_C("the insert is made of %zu paths"), insert.size()));
continue;
}
auto const one{switch_store::named(*first)};
auto const other{switch_store::named(*second)};
if (Write && (false == one.has_value() || false == other.has_value()))
{
++Report.crossovers_odd;
note(STR("a switch of it has no type kept"));
continue;
}
if (one.has_value() && other.has_value() && (std::abs(one->radius - other->radius) > kNominalNear * one->radius || std::abs(one->ratio - other->ratio) > kNominalNear * one->ratio))
{
++Report.crossovers_odd;
note(format(STR_C("the switches differ: %s and %s"), switch_store::label(*one).c_str(), switch_store::label(*other).c_str()));
continue;
}
if (Write)
{
auto entry{*one};
entry.kind = "connection";
entry.name = first->name();
entry.parts = {first->name(), second->name()};
entry.number = "-";
switch_store::place(entry, *first);
switch_store::classify(entry, *first);
entry.type = connection::word(connection::kind::crossover);
entry.a = spacing;
entry.b = length;
switch_store::put(entry, alignment_store::layer_of(*first));
WriteLog(format("Editor: digitizing - crossover %s + %s kept, %s, spacing %.2f m, insert %.2f m", first->name().c_str(), second->name().c_str(), switch_store::label(entry).c_str(), spacing, length), logtype::generic);
}
++Report.crossovers_recorded;
}
}
std::string editor_mode::digitize_scenery(bool const Write)
{
digitize_report report;
if (Write)
digitize_all(&report.lines, &report.lines_failed);
digitize_switches(report, Write);
digitize_crossovers(report, Write);
if (Write)
{
switch_store::settle();
auto const scope{m_infra.scope};
m_infra.scope = 3;
infra_recognize();
report.found = static_cast<int>(m_infra.candidates.size());
auto const before{m_bindings.size()};
infra_bind_chosen();
report.bound = static_cast<int>(m_bindings.size() - before);
m_infra.scope = scope;
}
auto text{format(STR_C("Switches: %d, kept already %d, of the catalogue %d, measured %d, parts of slips %d, odd %d"), report.switches, report.known, report.designed, report.measured, report.slips, report.odd)};
if (report.unnamed > 0)
text += format(STR_C(", without a name, left out %d"), report.unnamed);
text += "\n" + format(STR_C("Crossovers: %d, kept already %d, recorded %d, left out %d"), report.crossovers, report.crossovers_known, report.crossovers_recorded, report.crossovers_odd);
if (Write)
{
text = format(STR_C("Lines digitized: %d, paths left out: %d"), report.lines, report.lines_failed) + "\n" + text;
text += "\n" + format(STR_C("Objects by the tracks: %d found, %d bound"), report.found, report.bound);
}
WriteLog("Editor: digitizing the scenery\n" + text, logtype::generic);
std::vector<std::pair<int, std::string>> types;
for (auto const &[label, count] : report.types)
types.emplace_back(count, label);
std::sort(types.rbegin(), types.rend());
for (auto const &[count, label] : types)
WriteLog(format("Editor: digitizing - %4d x %s", count, label.c_str()), logtype::generic);
std::vector<std::pair<int, std::string>> shapes;
for (auto const &[label, count] : report.shapes)
shapes.emplace_back(count, label);
std::sort(shapes.rbegin(), shapes.rend());
for (std::size_t i = 0; i < std::min<std::size_t>(shapes.size(), 20); ++i)
WriteLog(format("Editor: digitizing - measured %4d x %s", shapes[i].first, shapes[i].second.c_str()), logtype::generic);
for (std::size_t i = 0; i < std::min<std::size_t>(report.notes.size(), 30); ++i)
WriteLog("Editor: digitizing - " + report.notes[i], logtype::generic);
if (Write)
ui()->set_status(text, report.odd + report.crossovers_odd + report.lines_failed > 0);
return text;
}