17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-31 05:20:52 +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

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