mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-26 07:59:30 +01:00
feat(editor): the whole-scenery grade fit keeps the tracks a station shares at one height, lets the switches lie on the vertical curves the regulation allows, runs straight through the double slips and leaves the bridges out of the faults
A switch may lie on a concave vertical curve, and on a convex one no sharper than 2000 m (Dz.U. 2026 poz. 1225 § 58 ust. 16): the fit bends over the switches and their shared timbers where the ground does, a curve on a switch is a warning, and only a sharper convex one an error. A sharp crest of a hump keeps its curve off the switch zones. The zone of a crossing reaches only as far as the path crossed has rails, so the middle of a double slip keeps to itself. A line runs through a switch along its main path, and through the quarters of a double slip straight on, so the long lines go through the slips and the connections between them are fitted after. Rails of the lines fitted already nearer than a sleeper is long are taken at their height, as both lie on the same timbers. A short line between two lines whose end grades can't be joined within the norms keeps only the heights of its ends, and the grade breaks at the joints. The norms speed of a line is the one most of its length is laid for, so a few tracks set much faster don't stiffen the curves of all of it. The fault finder leaves out the places over the terrain model on a bridge or a viaduct, the ground falling away under them by the abutments. A formation modifier may be open at its start or end.
This commit is contained in:
@@ -45,6 +45,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -349,8 +350,25 @@ class editor_mode : public application_mode, private editor_track::observer, pri
|
||||
std::vector<TTrack *> left; // tracks whose grade line is still to be fitted
|
||||
std::vector<TTrack *> done; // tracks of the lines fitted already: the lines fitted later end on them and don't change them
|
||||
// the rail tops of the lines fitted already, in squares of the plan: the tracks of a station lie in one plane, a line laid
|
||||
// near them takes their height for its ground too, the more closely the nearer it is
|
||||
std::map<std::pair<int, int>, std::vector<glm::dvec3>> laid;
|
||||
// near them takes their height for its ground too, the more closely the nearer it is, and a line crossing one of them
|
||||
// on its level its height on the crossing
|
||||
struct laid_point
|
||||
{
|
||||
glm::dvec3 position{0.0};
|
||||
glm::dvec2 direction{0.0, 1.0};
|
||||
double grade{0.0};
|
||||
double was{0.0}; // the height before the line was fitted
|
||||
double rails{0.0}; // the height of the rails of the track
|
||||
TTrack const *track{nullptr};
|
||||
};
|
||||
std::map<std::pair<int, int>, std::vector<laid_point>> laid;
|
||||
// the points of all the paths as they were laid, for the crossings
|
||||
std::map<std::pair<int, int>, std::vector<laid_point>> paths;
|
||||
// ends which meet in the plan unlinked, laid apart in height, that the joining of the ends couldn't join as neither of
|
||||
// them can be changed alone, as of two switches: each with the other one, which a line ending there takes over
|
||||
std::map<std::tuple<TTrack const *, int, bool>, std::tuple<TTrack *, int, bool>> loose;
|
||||
// the points of the lines over a bridge or a viaduct, the model falling away under them: no formation is laid there
|
||||
std::map<std::pair<int, int>, std::vector<glm::dvec3>> bridged;
|
||||
std::size_t lines{0}; // grade lines fitted and applied
|
||||
std::size_t refused{0}; // grade lines the checks didn't let apply
|
||||
std::size_t departures{0}; // grade lines parting from the terrain model by much
|
||||
|
||||
@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "application/editorprojection.h"
|
||||
#include "editor/editorFormat.hpp"
|
||||
#include "editor/editorGeometry.hpp"
|
||||
#include "editor/editorProfile.hpp"
|
||||
|
||||
#include "scene/scene.h"
|
||||
#include "scene/scenelayers.h"
|
||||
@@ -45,6 +46,12 @@ double const kModelStep{5.0}; // m between the places of the tracks compared wit
|
||||
std::size_t const kModelRun{200}; // places of a path whose model is asked for at once
|
||||
double const kModelPoll{0.25}; // s between the looks whether the model is in
|
||||
double const kUnfitted{30.0}; // m; a scenery whose tracks lie this far from the model as a whole isn't fitted to it
|
||||
// a track over the model by more than the depth goes on a bridge or a viaduct the model leaves out where the model falls away by
|
||||
// the abutments, steeper than the banks, within the reach along the track both ways; as the grade lines fitted to the model
|
||||
// tell them. By the abutments the track is over the model by less
|
||||
double const kBridgeDepth{1.0}; // m
|
||||
double const kAbutment{100.0}; // per mille
|
||||
double const kBridgeReach{200.0}; // m
|
||||
|
||||
using kind = joints_tool::joint_issue::kind;
|
||||
|
||||
@@ -198,8 +205,8 @@ bool joints_tool::joints_model()
|
||||
for (auto i = Run.first; i < Run.second; ++i)
|
||||
{
|
||||
glm::dvec2 const at{state.places[i].position.x, state.places[i].position.z};
|
||||
Min = glm::min(Min, at - 2.0);
|
||||
Max = glm::max(Max, at + 2.0);
|
||||
Min = glm::min(Min, at - kBridgeReach);
|
||||
Max = glm::max(Max, at + kBridgeReach);
|
||||
}
|
||||
};
|
||||
glm::dvec2 low, high;
|
||||
@@ -235,6 +242,37 @@ bool joints_tool::joints_model()
|
||||
if (std::abs(known[known.size() / 2]) > kUnfitted)
|
||||
shift = known[known.size() / 2];
|
||||
}
|
||||
// a place on a bridge or a viaduct the model leaves out isn't off it: the route through its track sampled both ways, the
|
||||
// stretches over the model as on a bridge found along it
|
||||
std::map<TTrack const *, std::pair<std::vector<editor_track::route_sample>, std::vector<profile::departure>>> routes;
|
||||
auto const on_bridge = [&](nmt_place const &Place) {
|
||||
auto found{routes.find(Place.track)};
|
||||
if (found == routes.end())
|
||||
{
|
||||
auto route{editor_track::run_route(*Place.track, kBridgeReach)};
|
||||
auto samples{editor_track::sample_route(route, 1.0)};
|
||||
std::vector<double> chainages, heights, ground;
|
||||
for (auto const &sample : samples)
|
||||
{
|
||||
chainages.push_back(sample.chainage);
|
||||
heights.push_back(sample.position.y - route.spans[sample.span].track->rail_height());
|
||||
double height;
|
||||
ground.push_back(m_ground.terrain_model().height(sample.position.x, sample.position.z, height) ? height + m_ground.terrain_model_offset() + shift : std::numeric_limits<double>::quiet_NaN());
|
||||
}
|
||||
auto spans{profile::bridged(chainages, heights, ground, kBridgeDepth, kAbutment)};
|
||||
found = routes.emplace(Place.track, std::make_pair(std::move(samples), std::move(spans))).first;
|
||||
}
|
||||
auto const &[samples, spans]{found->second};
|
||||
auto nearest{std::numeric_limits<double>::max()};
|
||||
double at{0.0};
|
||||
for (auto const &sample : samples)
|
||||
if (auto const distance{geometry::plan_distance(sample.position, Place.position)}; distance < nearest)
|
||||
{
|
||||
nearest = distance;
|
||||
at = sample.chainage;
|
||||
}
|
||||
return nearest <= 1.0 && std::any_of(spans.begin(), spans.end(), [&](profile::departure const &Span) { return at >= Span.from - kModelStep && at <= Span.to + kModelStep; });
|
||||
};
|
||||
// the place of each path the farthest from the model
|
||||
std::map<std::pair<TTrack *, int>, std::size_t> worst;
|
||||
for (std::size_t i = 0; i < state.places.size(); ++i)
|
||||
@@ -242,7 +280,7 @@ bool joints_tool::joints_model()
|
||||
if (false == std::isfinite(differences[i]))
|
||||
continue;
|
||||
differences[i] -= shift;
|
||||
if (std::abs(differences[i]) <= state.model)
|
||||
if (std::abs(differences[i]) <= state.model || (differences[i] > 0.0 && on_bridge(state.places[i])))
|
||||
continue;
|
||||
auto const key{std::make_pair(state.places[i].track, state.places[i].path)};
|
||||
auto const found{worst.find(key)};
|
||||
|
||||
@@ -30,6 +30,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -140,12 +141,101 @@ bool ground_card(char const *Id, char const *Title, char const *Text, bool const
|
||||
return clicked && Enabled;
|
||||
}
|
||||
|
||||
// m, the squares of the plan the points of the lines are kept in
|
||||
double constexpr kPlanCell{50.0};
|
||||
|
||||
// a bridge or a tunnel keeps the model under it as it is
|
||||
bool shapes_ground(TTrack const &Track)
|
||||
{
|
||||
return Track.eEnvironment != e_bridge && Track.eEnvironment != e_tunnel;
|
||||
}
|
||||
|
||||
// the name the formation of a track is known by in the terrain, of a track with no name where it lies: two of the same name
|
||||
// would be one formation
|
||||
std::string formation_name(TTrack const &Track)
|
||||
{
|
||||
if (false == Track.name().empty() || Track.m_paths.empty())
|
||||
return Track.name();
|
||||
auto const &points{Track.m_paths.front().points};
|
||||
auto const &start{points[segment_data::point::start]};
|
||||
auto const &end{points[segment_data::point::end]};
|
||||
return format("@%.2f,%.2f>%.2f,%.2f", start.x, start.z, end.x, end.z);
|
||||
}
|
||||
|
||||
// an end of a path no other path is linked to
|
||||
struct loose_end
|
||||
{
|
||||
TTrack *track{nullptr};
|
||||
int path{0};
|
||||
bool atend{false};
|
||||
glm::dvec3 point{0.0};
|
||||
glm::dvec3 outward{0.0}; // off the path along it
|
||||
};
|
||||
|
||||
loose_end end_of(TTrack &Track, int const Path, bool const Atend)
|
||||
{
|
||||
auto const &points{Track.m_paths[Path].points};
|
||||
auto const &point{points[Atend ? segment_data::point::end : segment_data::point::start]};
|
||||
auto const &control{points[Atend ? segment_data::point::control2 : segment_data::point::control1]};
|
||||
auto const &other{points[Atend ? segment_data::point::start : segment_data::point::end]};
|
||||
return {&Track, Path, Atend, point, control != glm::dvec3{} ? -control : point - other};
|
||||
}
|
||||
|
||||
// ends which meet in the plan facing each other, laid apart in height by hand, so the game doesn't join them; farther apart
|
||||
// than a track over another can be, they lie on two levels
|
||||
std::vector<std::pair<loose_end, loose_end>> loose_pairs()
|
||||
{
|
||||
double constexpr kLevel{2.0}; // m
|
||||
auto const linked = [](TTrack const &Track, int const Path, bool const Atend) {
|
||||
if (Track.eType == tt_Switch && Track.SwitchExtension)
|
||||
return (Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]) != nullptr;
|
||||
return (Atend ? Track.trNext : Track.trPrev) != nullptr;
|
||||
};
|
||||
std::map<std::pair<int, int>, std::vector<loose_end>> cells;
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
{
|
||||
if (track == nullptr || false == rail(*track) || track->Length() < 0.01)
|
||||
continue;
|
||||
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
||||
for (auto const atend : {false, true})
|
||||
if (false == linked(*track, i, atend))
|
||||
{
|
||||
auto const end{end_of(*track, i, atend)};
|
||||
cells[{static_cast<int>(std::floor(end.point.x)), static_cast<int>(std::floor(end.point.z))}].push_back(end);
|
||||
}
|
||||
}
|
||||
std::vector<std::pair<loose_end, loose_end>> result;
|
||||
std::set<std::tuple<TTrack const *, int, bool>> taken;
|
||||
for (auto const &[cell, ends] : cells)
|
||||
for (auto const &a : ends)
|
||||
{
|
||||
if (taken.contains({a.track, a.path, a.atend}))
|
||||
continue;
|
||||
loose_end const *match{nullptr};
|
||||
for (int dx = -1; dx <= 1 && match == nullptr; ++dx)
|
||||
for (int dz = -1; dz <= 1 && match == nullptr; ++dz)
|
||||
{
|
||||
auto const found{cells.find({cell.first + dx, cell.second + dz})};
|
||||
if (found == cells.end())
|
||||
continue;
|
||||
auto const outward{geometry::plan_of(a.outward)};
|
||||
for (auto const &b : found->second)
|
||||
if (b.track != a.track && false == taken.contains({b.track, b.path, b.atend}) && geometry::plan_distance(a.point, b.point) <= geometry::same_point &&
|
||||
std::abs(a.point.y - b.point.y) <= kLevel && glm::dot(outward, geometry::plan_of(b.outward)) < -0.5 * glm::length(outward) * glm::length(geometry::plan_of(b.outward)))
|
||||
{
|
||||
match = &b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match == nullptr)
|
||||
continue;
|
||||
taken.insert({a.track, a.path, a.atend});
|
||||
taken.insert({match->track, match->path, match->atend});
|
||||
result.emplace_back(a, *match);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<editor_mode::nmt_place> editor_mode::nmt_track_places(double const Step) const
|
||||
@@ -240,6 +330,27 @@ std::vector<editor_mode::nmt_place> editor_mode::nmt_track_places(double const S
|
||||
for (std::size_t i = 0; i < result.size(); ++i)
|
||||
if (over[i] != 0)
|
||||
result[i].shapes = false;
|
||||
// nor where the grade lines went over the model falling away by the abutments of a bridge or a viaduct
|
||||
double constexpr bridged_near{1.0};
|
||||
double constexpr bridged_level{1.0};
|
||||
auto const &bridged{m_nmt_state.bridged};
|
||||
if (false == bridged.empty())
|
||||
for (auto &place : result)
|
||||
{
|
||||
if (false == place.shapes)
|
||||
continue;
|
||||
auto const cx{static_cast<int>(std::floor(place.position.x / kPlanCell))};
|
||||
auto const cz{static_cast<int>(std::floor(place.position.z / kPlanCell))};
|
||||
for (int dx = -1; dx <= 1 && place.shapes; ++dx)
|
||||
for (int dz = -1; dz <= 1 && place.shapes; ++dz)
|
||||
if (auto const cell{bridged.find({cx + dx, cz + dz})}; cell != bridged.end())
|
||||
for (auto const &point : cell->second)
|
||||
if (glm::length(glm::dvec2{point.x - place.position.x, point.z - place.position.z}) <= bridged_near && std::abs(point.y - place.position.y) <= bridged_level)
|
||||
{
|
||||
place.shapes = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -720,6 +831,7 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector<TTrack *> const *Only)
|
||||
double constexpr step{5.0};
|
||||
std::pair<TTrack *, int> previous{nullptr, -1};
|
||||
bool broken{false};
|
||||
std::vector<nmt_place> over;
|
||||
for (auto const &place : nmt_track_places(step))
|
||||
{
|
||||
if (false == wanted(place.track))
|
||||
@@ -731,6 +843,7 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector<TTrack *> const *Only)
|
||||
if (false == place.shapes)
|
||||
{
|
||||
broken = true;
|
||||
over.push_back(place);
|
||||
continue;
|
||||
}
|
||||
auto &piece{pieces[path]};
|
||||
@@ -742,21 +855,31 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector<TTrack *> const *Only)
|
||||
auto &modifier{modifiers[{place.track, place.path, piece}]};
|
||||
if (modifier.name.empty())
|
||||
{
|
||||
modifier.name = "nmt " + (place.track->name().empty() ? std::string{"track"} : place.track->name()) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{}) + (piece > 0 ? " #" + std::to_string(piece) : std::string{});
|
||||
modifier.name = "nmt " + formation_name(*place.track) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{}) + (piece > 0 ? " #" + std::to_string(piece) : std::string{});
|
||||
modifier.slope = state.slope;
|
||||
modifier.reach = state.reach;
|
||||
}
|
||||
modifier.points.push_back({{place.position.x, place.position.z}, place.formation, place.half_width});
|
||||
}
|
||||
// a formation which ends by a bridge or a tunnel ends square against its abutment, its portal, whichever path goes on over it
|
||||
auto const walled = [&](heightmap::corridor_point const &End) {
|
||||
return std::any_of(over.begin(), over.end(), [&](nmt_place const &Place) {
|
||||
return glm::distance(glm::dvec2{Place.position.x, Place.position.z}, End.position) <= step * 1.5 && std::abs(Place.formation - End.formation) <= 1.0;
|
||||
});
|
||||
};
|
||||
std::vector<heightmap::modifier> laid;
|
||||
for (auto &[path, modifier] : modifiers)
|
||||
if (modifier.points.size() >= 2)
|
||||
{
|
||||
modifier.open_start = walled(modifier.points.front());
|
||||
modifier.open_end = walled(modifier.points.back());
|
||||
laid.push_back(std::move(modifier));
|
||||
}
|
||||
if (Only != nullptr)
|
||||
{
|
||||
// the formations as they are already aren't led into the terrain again
|
||||
auto const same = [](heightmap::modifier const &A, heightmap::modifier const &B) {
|
||||
return A.slope == B.slope && A.reach == B.reach && A.rounding == B.rounding && A.points.size() == B.points.size() &&
|
||||
return A.slope == B.slope && A.reach == B.reach && A.rounding == B.rounding && A.open_start == B.open_start && A.open_end == B.open_end && A.points.size() == B.points.size() &&
|
||||
std::equal(A.points.begin(), A.points.end(), B.points.begin(), [](heightmap::corridor_point const &P, heightmap::corridor_point const &Q) {
|
||||
return glm::distance(P.position, Q.position) < 1e-6 && std::abs(P.formation - Q.formation) < 1e-6 && std::abs(P.half_width - Q.half_width) < 1e-6;
|
||||
});
|
||||
@@ -782,6 +905,17 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector<TTrack *> const *Only)
|
||||
for (auto const &name : gone)
|
||||
m_streamer.remove_modifier(name);
|
||||
}
|
||||
if (Only == nullptr)
|
||||
{
|
||||
// the formations of the tracks there are no more, or are on bridges now, and of the tracks with no name laid over each
|
||||
// other under one name before
|
||||
std::vector<std::string> gone;
|
||||
for (auto const &modifier : m_streamer.modifiers())
|
||||
if (modifier.name.starts_with("nmt ") && std::none_of(laid.begin(), laid.end(), [&](heightmap::modifier const &Laid) { return Laid.name == modifier.name; }))
|
||||
gone.push_back(modifier.name);
|
||||
for (auto const &name : gone)
|
||||
m_streamer.remove_modifier(name);
|
||||
}
|
||||
auto const count{laid.size()};
|
||||
m_streamer.modifiers(std::move(laid));
|
||||
forget_ground();
|
||||
@@ -978,6 +1112,8 @@ void editor_mode::nmt_stop()
|
||||
state.left.clear();
|
||||
state.done.clear();
|
||||
state.laid.clear();
|
||||
state.paths.clear();
|
||||
state.loose.clear();
|
||||
state.stage = nmt_state::automatic::idle;
|
||||
state.pending.clear();
|
||||
m_profile.open = false;
|
||||
@@ -1469,43 +1605,10 @@ void editor_mode::nmt_lift_scenery(double const Offset)
|
||||
|
||||
std::size_t editor_mode::nmt_join_ends()
|
||||
{
|
||||
// ends which meet in the plan, laid apart in height by hand, so the game doesn't join them, are one joint before the grade
|
||||
// lines run over them; farther apart than a track over another can be, they lie on two levels
|
||||
double constexpr kLevel{2.0}; // m
|
||||
struct end
|
||||
{
|
||||
TTrack *track;
|
||||
int path;
|
||||
bool atend;
|
||||
glm::dvec3 point;
|
||||
glm::dvec2 outward;
|
||||
};
|
||||
auto const linked = [](TTrack const &Track, int const Path, bool const Atend) {
|
||||
if (Track.eType == tt_Switch && Track.SwitchExtension)
|
||||
return (Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]) != nullptr;
|
||||
return (Atend ? Track.trNext : Track.trPrev) != nullptr;
|
||||
};
|
||||
std::map<std::pair<int, int>, std::vector<end>> cells;
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
{
|
||||
if (track == nullptr || false == rail(*track) || track->Length() < 0.01)
|
||||
continue;
|
||||
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
||||
for (auto const atend : {false, true})
|
||||
{
|
||||
if (linked(*track, i, atend))
|
||||
continue;
|
||||
auto const &path{track->m_paths[i]};
|
||||
auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]};
|
||||
auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]};
|
||||
auto const &other{path.points[atend ? segment_data::point::start : segment_data::point::end]};
|
||||
auto const outward{geometry::plan_of(control != glm::dvec3{} ? -control : point - other)};
|
||||
cells[{static_cast<int>(std::floor(point.x)), static_cast<int>(std::floor(point.z))}].push_back({track, i, atend, point, outward});
|
||||
}
|
||||
}
|
||||
// the loose ends are one joint before the grade lines run over them; of two switches, which can't be changed alone, the
|
||||
// lines over them take each other over as they're fitted
|
||||
std::vector<std::pair<TTrack *, editor_track::state>> states;
|
||||
std::vector<TTrack *> changed;
|
||||
std::set<std::tuple<TTrack const *, int, bool>> taken;
|
||||
auto const remember = [&](TTrack *Track) {
|
||||
if (std::find(changed.begin(), changed.end(), Track) != changed.end())
|
||||
return;
|
||||
@@ -1513,39 +1616,32 @@ std::size_t editor_mode::nmt_join_ends()
|
||||
changed.push_back(Track);
|
||||
};
|
||||
std::size_t joined{0};
|
||||
for (auto const &[cell, ends] : cells)
|
||||
for (auto const &a : ends)
|
||||
m_nmt_state.loose.clear();
|
||||
for (auto pair : loose_pairs())
|
||||
{
|
||||
if (pair.first.track->eType != tt_Normal)
|
||||
std::swap(pair.first, pair.second);
|
||||
auto const &a{pair.first};
|
||||
auto const &b{pair.second};
|
||||
if (a.track->eType != tt_Normal)
|
||||
{
|
||||
if (a.track->eType != tt_Normal || taken.contains({a.track, a.path, a.atend}))
|
||||
continue;
|
||||
end const *match{nullptr};
|
||||
for (int dx = -1; dx <= 1 && match == nullptr; ++dx)
|
||||
for (int dz = -1; dz <= 1 && match == nullptr; ++dz)
|
||||
{
|
||||
auto const found{cells.find({cell.first + dx, cell.second + dz})};
|
||||
if (found == cells.end())
|
||||
continue;
|
||||
for (auto const &b : found->second)
|
||||
if (b.track != a.track && false == taken.contains({b.track, b.path, b.atend}) && geometry::plan_distance(a.point, b.point) <= geometry::same_point &&
|
||||
std::abs(a.point.y - b.point.y) <= kLevel && glm::dot(a.outward, b.outward) < -0.5 * glm::length(a.outward) * glm::length(b.outward))
|
||||
{
|
||||
match = &b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match == nullptr || (match->track->eType != tt_Normal && false == editor_track::is_supported(*match->track)))
|
||||
continue;
|
||||
auto const height{match->track->eType == tt_Normal ? (a.point.y + match->point.y) * 0.5 : match->point.y};
|
||||
for (auto const *side : {&a, match})
|
||||
{
|
||||
taken.insert({side->track, side->path, side->atend});
|
||||
if (side->track->eType != tt_Normal)
|
||||
continue;
|
||||
remember(side->track);
|
||||
side->track->m_paths[side->path].points[side->atend ? segment_data::point::end : segment_data::point::start].y = height;
|
||||
}
|
||||
++joined;
|
||||
// two switches: the lines over them meet as they're fitted
|
||||
m_nmt_state.loose[{a.track, a.path, a.atend}] = {b.track, b.path, b.atend};
|
||||
m_nmt_state.loose[{b.track, b.path, b.atend}] = {a.track, a.path, a.atend};
|
||||
continue;
|
||||
}
|
||||
if (b.track->eType != tt_Normal && false == editor_track::is_supported(*b.track))
|
||||
continue;
|
||||
auto const height{b.track->eType == tt_Normal ? (a.point.y + b.point.y) * 0.5 : b.point.y};
|
||||
for (auto const *side : {&a, &b})
|
||||
{
|
||||
if (side->track->eType != tt_Normal)
|
||||
continue;
|
||||
remember(side->track);
|
||||
side->track->m_paths[side->path].points[side->atend ? segment_data::point::end : segment_data::point::start].y = height;
|
||||
}
|
||||
++joined;
|
||||
}
|
||||
if (changed.empty())
|
||||
return 0;
|
||||
editor_track::commit(changed);
|
||||
@@ -1621,22 +1717,52 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
};
|
||||
profile.context.start_joined = profile.context.start_joined && joint(false, profile.context.start_elevation);
|
||||
profile.context.end_joined = profile.context.end_joined && joint(true, profile.context.end_elevation);
|
||||
auto const on_route = [&](TTrack const *Other) { return std::any_of(profile.route.spans.begin(), profile.route.spans.end(), [&](auto const &Span) { return Span.track == Other; }); };
|
||||
// an end on a switch which meets a switch fitted already unlinked, laid apart in height, takes it over as if it went on into it
|
||||
auto const partner_of = [&](editor_track::route_span const &Span, bool const Atend) -> std::optional<loose_end> {
|
||||
auto const found{m_nmt_state.loose.find({Span.track, Span.path, Atend})};
|
||||
if (found == m_nmt_state.loose.end() || on_route(std::get<0>(found->second)))
|
||||
return std::nullopt;
|
||||
auto const end{end_of(*std::get<0>(found->second), std::get<1>(found->second), std::get<2>(found->second))};
|
||||
if (glm::length(geometry::plan_of(end.outward)) < 1e-6)
|
||||
return std::nullopt;
|
||||
return end;
|
||||
};
|
||||
auto const loose_into = [&](editor_track::route_span const &Span, bool const Atend) -> std::optional<loose_end> {
|
||||
auto const end{partner_of(Span, Atend)};
|
||||
return end.has_value() && fitted(end->track) ? end : std::nullopt;
|
||||
};
|
||||
// rising off the end met, into the route at its start and against it at its end
|
||||
auto const rising = [](loose_end const &End) { return End.outward.y / glm::length(geometry::plan_of(End.outward)); };
|
||||
if (false == profile.context.start_joined)
|
||||
if (auto const end{loose_into(profile.route.spans.front(), false == profile.route.spans.front().forward)})
|
||||
{
|
||||
profile.context.start_joined = true;
|
||||
profile.context.start_elevation = end->point.y;
|
||||
profile.context.start_grade = rising(*end);
|
||||
}
|
||||
if (false == profile.context.end_joined)
|
||||
if (auto const end{loose_into(profile.route.spans.back(), profile.route.spans.back().forward)})
|
||||
{
|
||||
profile.context.end_joined = true;
|
||||
profile.context.end_elevation = end->point.y;
|
||||
profile.context.end_grade = -rising(*end);
|
||||
}
|
||||
// the ground of the line: the terrain model on its axis, the top of the ballast with the rails on it, and the rail tops of
|
||||
// the lines fitted already beside it on the same level as the model here, which tell its height the more closely the
|
||||
// nearer they are. What doesn't agree with the line, a bridge over it, the ground under a viaduct, the fit lets go of by
|
||||
// itself
|
||||
double constexpr kModel{0.25}; // m, the tolerance of the model
|
||||
double constexpr kModel{0.15}; // m, the tolerance of the model
|
||||
double constexpr kRail{0.02}; // m, of a rail top next to the line, growing by kSpread a metre of the distance
|
||||
double constexpr kSpread{0.01};
|
||||
double constexpr kLevel{0.5}; // m, a rail top farther from the model or from the line lies on another level
|
||||
double constexpr kSameLevel{1.0}; // m, tracks authored closer in height lie on one level, farther one over the other
|
||||
double constexpr kReach{50.0};
|
||||
double constexpr kSteepest{40.0}; // per mille, the steepest grade the ground may give a line
|
||||
double constexpr kCell{50.0};
|
||||
auto const nearest = [&](glm::dvec3 const &Point, double &Distance) {
|
||||
Distance = std::numeric_limits<double>::max();
|
||||
double height{0.0};
|
||||
auto const cx{static_cast<int>(std::floor(Point.x / kCell))};
|
||||
auto const cz{static_cast<int>(std::floor(Point.z / kCell))};
|
||||
auto const cx{static_cast<int>(std::floor(Point.x / kPlanCell))};
|
||||
auto const cz{static_cast<int>(std::floor(Point.z / kPlanCell))};
|
||||
for (int dx = -1; dx <= 1; ++dx)
|
||||
for (int dz = -1; dz <= 1; ++dz)
|
||||
{
|
||||
@@ -1644,22 +1770,24 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
if (cell == m_nmt_state.laid.end())
|
||||
continue;
|
||||
for (auto const &laid : cell->second)
|
||||
if (auto const distance{glm::length(glm::dvec2{laid.x - Point.x, laid.z - Point.z})}; distance < Distance)
|
||||
if (auto const distance{glm::length(glm::dvec2{laid.position.x - Point.x, laid.position.z - Point.z})}; distance < Distance)
|
||||
{
|
||||
Distance = distance;
|
||||
height = laid.y;
|
||||
height = laid.position.y;
|
||||
}
|
||||
}
|
||||
return height;
|
||||
};
|
||||
// an end of the line, or the other path of a switch of it, which leads through the tracks not fitted yet into a line fitted
|
||||
// already, as at a crossover, ties the two where it leaves the line: there the line keeps to the height of the other, on
|
||||
// along its grade over the tracks between, give or take what these can climb with curves of the least radius at both ends
|
||||
// along its grade over the tracks between, give or take what the plain tracks among these can climb with curves of the least
|
||||
// radius. A switch lies in its plane and climbs nothing. Where they can climb more than a little, the curves of the norms of
|
||||
// the line come first: what it can't keep to, the tracks between take over
|
||||
double constexpr kJoin{0.002}; // m
|
||||
double constexpr kClimbing{0.05}; // m
|
||||
double constexpr kFarthest{200.0};
|
||||
profile::ground_fit options;
|
||||
auto const length_of = [](TTrack const &Track, int const Path) { return Track.eType == tt_Switch ? Track.SwitchExtension->Segments[Path]->GetLength() : Track.Segment->GetLength(); };
|
||||
auto const on_route = [&](TTrack const *Other) { return std::any_of(profile.route.spans.begin(), profile.route.spans.end(), [&](auto const &Span) { return Span.track == Other; }); };
|
||||
std::vector<editor_track::route_span> exits;
|
||||
exits.push_back({profile.route.spans.front().track, profile.route.spans.front().path, false == profile.route.spans.front().forward});
|
||||
exits.push_back({profile.route.spans.back().track, profile.route.spans.back().path, profile.route.spans.back().forward});
|
||||
@@ -1667,45 +1795,113 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
if (span.track->eType == tt_Switch && span.track->m_paths.size() >= 2)
|
||||
for (auto const outward : {true, false})
|
||||
exits.push_back({span.track, 1 - span.path, outward});
|
||||
// a switch entered at the far end of a path has the far end of its other path on its plane as high, when as far along it
|
||||
auto const frog_beside = [](TTrack const &Switch, int const Path) {
|
||||
if (Switch.m_paths.size() < 2)
|
||||
return false;
|
||||
auto const &entered{Switch.m_paths[Path].points};
|
||||
auto const &other{Switch.m_paths[1 - Path].points};
|
||||
auto const &origin{entered[segment_data::point::start]};
|
||||
auto const &control{entered[segment_data::point::control1]};
|
||||
auto const along{geometry::plan_of(control != glm::dvec3{} ? control : entered[segment_data::point::end] - origin)};
|
||||
if (glm::length(along) < 1e-6)
|
||||
return false;
|
||||
auto const ahead = [&](glm::dvec3 const &Point) { return glm::dot(geometry::plan_of(Point - origin), glm::normalize(along)); };
|
||||
return std::abs(ahead(entered[segment_data::point::end]) - ahead(other[segment_data::point::end])) <= 0.5;
|
||||
};
|
||||
for (auto const &exiting : exits)
|
||||
{
|
||||
auto const &exit{exiting.track->m_paths[exiting.path].points[exiting.forward ? segment_data::point::end : segment_data::point::start]};
|
||||
editor_track::route step;
|
||||
step.spans.push_back(exiting);
|
||||
double between{0.0};
|
||||
for (int hops = 0; hops < 50 && between < kFarthest; ++hops)
|
||||
auto const bind = [&](double const Height, double const Between, double const Bending) {
|
||||
std::size_t nearest{0};
|
||||
for (std::size_t i = 1; i < profile.samples.size(); ++i)
|
||||
if (geometry::plan_distance(profile.samples[i].position, exit) < geometry::plan_distance(profile.samples[nearest].position, exit))
|
||||
nearest = i;
|
||||
auto const climbing{Bending * Between / (4.0 * profile.line.norms.radius_min)};
|
||||
options.bounds.push_back({profile.samples[nearest].chainage, Height - kJoin - climbing, Height + kJoin + climbing, climbing >= kClimbing});
|
||||
};
|
||||
// the tracks it leads into, the nearest first, through both paths of a switch met at its start and across the frog of one
|
||||
// met at the end of a path, onto the end of its other path
|
||||
struct walk
|
||||
{
|
||||
double grade;
|
||||
TTrack *next{nullptr};
|
||||
int path{0};
|
||||
bool forward{true};
|
||||
if (false == editor_track::adjoining_grade(step, true, grade, &next, &path, &forward) || next == nullptr || on_route(next))
|
||||
break;
|
||||
if (fitted(next))
|
||||
editor_track::route_span span;
|
||||
double between{0.0}; // m, over the tracks not fitted yet
|
||||
double bending{0.0}; // m, of these the plain tracks
|
||||
};
|
||||
std::vector<walk> open{{exiting, 0.0, 0.0}};
|
||||
std::set<std::tuple<TTrack const *, int, bool>> seen;
|
||||
while (false == open.empty())
|
||||
{
|
||||
auto const nearest{std::min_element(open.begin(), open.end(), [](walk const &A, walk const &B) { return A.between < B.between; })};
|
||||
auto const step{*nearest};
|
||||
open.erase(nearest);
|
||||
if (step.between >= kFarthest || false == seen.insert({step.span.track, step.span.path, step.span.forward}).second)
|
||||
continue;
|
||||
auto onward{editor_track::continuations(step.span)};
|
||||
onward.erase(std::remove_if(onward.begin(), onward.end(), [&](editor_track::continuation const &Next) { return on_route(Next.span.track); }), onward.end());
|
||||
// met unlinked, laid apart in height: on as if linked
|
||||
if (onward.empty())
|
||||
if (auto const end{partner_of(step.span, step.span.forward)})
|
||||
onward.push_back({{end->track, end->path, false == end->atend}, -rising(*end)});
|
||||
for (auto const &[next, grade] : onward)
|
||||
{
|
||||
std::size_t nearest{0};
|
||||
for (std::size_t i = 1; i < profile.samples.size(); ++i)
|
||||
if (geometry::plan_distance(profile.samples[i].position, exit) < geometry::plan_distance(profile.samples[nearest].position, exit))
|
||||
nearest = i;
|
||||
auto const &points{next->m_paths[path].points};
|
||||
auto const height{points[forward ? segment_data::point::start : segment_data::point::end].y - grade * between};
|
||||
auto const leeway{kJoin + between * between / (4.0 * profile.line.norms.radius_min)};
|
||||
options.bounds.push_back({profile.samples[nearest].chainage, height - leeway, height + leeway});
|
||||
break;
|
||||
auto const &points{next.track->m_paths[next.path].points};
|
||||
auto const entry{points[next.forward ? segment_data::point::start : segment_data::point::end].y};
|
||||
if (fitted(next.track))
|
||||
{
|
||||
bind(entry - grade * step.between, step.between, step.bending);
|
||||
continue;
|
||||
}
|
||||
auto const length{length_of(*next.track, next.path)};
|
||||
open.push_back({next, step.between + length, step.bending + (next.track->eType == tt_Switch ? 0.0 : length)});
|
||||
if (false == next.forward && next.track->eType == tt_Switch && frog_beside(*next.track, next.path))
|
||||
open.push_back({{next.track, 1 - next.path, true}, step.between, step.bending});
|
||||
}
|
||||
step.spans = {{next, path, forward}};
|
||||
between += length_of(*next, path);
|
||||
}
|
||||
}
|
||||
// the lines fitted already close beside it on its level, as authored, are ground it never lets go of, however far they are
|
||||
// off: side by side the tracks lie on one formation, in one plane, as near as the curves of the norms let them. The nearest
|
||||
// on each side. Where they run so near that the timbers carry both, at a switch, a crossover, they are at one height, on
|
||||
// any vertical curve the timbers lie on
|
||||
double constexpr kNear{8.0}; // m between the axes
|
||||
double constexpr kHeld{5.0}; // m, the reach of their rail tops
|
||||
double constexpr kTimbers{2.65}; // m between the axes, about the length of a sleeper
|
||||
double constexpr kTimbered{0.005}; // m
|
||||
auto const alongside = [&](editor_track::route_sample const &Sample) {
|
||||
std::array<std::pair<double, nmt_state::laid_point const *>, 2> sides{std::pair{kNear, nullptr}, std::pair{kNear, nullptr}};
|
||||
auto const along{glm::normalize(Sample.direction)};
|
||||
auto const cx{static_cast<int>(std::floor(Sample.position.x / kPlanCell))};
|
||||
auto const cz{static_cast<int>(std::floor(Sample.position.z / kPlanCell))};
|
||||
for (int dx = -1; dx <= 1; ++dx)
|
||||
for (int dz = -1; dz <= 1; ++dz)
|
||||
{
|
||||
auto const cell{m_nmt_state.laid.find({cx + dx, cz + dz})};
|
||||
if (cell == m_nmt_state.laid.end())
|
||||
continue;
|
||||
for (auto const &point : cell->second)
|
||||
{
|
||||
glm::dvec2 const offset{point.position.x - Sample.position.x, point.position.z - Sample.position.z};
|
||||
auto const across{along.x * offset.y - along.y * offset.x};
|
||||
if (std::abs(glm::dot(offset, along)) > 1.0 || std::abs(across) >= kNear || std::abs(point.was - Sample.position.y) > kSameLevel)
|
||||
continue;
|
||||
auto &side{sides[across > 0.0 ? 0 : 1]};
|
||||
if (std::abs(across) < side.first)
|
||||
side = {std::abs(across), &point};
|
||||
}
|
||||
}
|
||||
return sides;
|
||||
};
|
||||
auto const count{profile.samples.size()};
|
||||
std::vector<double> sampled(count), model(count, std::numeric_limits<double>::quiet_NaN());
|
||||
std::vector<glm::dvec2> plan(count);
|
||||
std::vector<double> chainages, ground, tolerances, reaches;
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
auto const &sample{profile.samples[i]};
|
||||
auto const *track{sample.span < profile.route.spans.size() ? profile.route.spans[sample.span].track : nullptr};
|
||||
sampled[i] = sample.chainage;
|
||||
if (double height; track != nullptr && m_nmt.height(sample.position.x, sample.position.z, height))
|
||||
plan[i] = {sample.position.x, sample.position.z};
|
||||
if (double height; track != nullptr && shapes_ground(*track) && m_nmt.height(sample.position.x, sample.position.z, height))
|
||||
model[i] = height + m_nmt_state.offset + track->rail_height();
|
||||
chainages.push_back(sample.chainage);
|
||||
ground.push_back(model[i]);
|
||||
@@ -1720,11 +1916,108 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
tolerances.push_back(std::max(kRail, kSpread * distance));
|
||||
reaches.push_back(kLevel);
|
||||
}
|
||||
if (track != nullptr)
|
||||
for (auto const &[apart, point] : alongside(sample))
|
||||
if (point != nullptr)
|
||||
{
|
||||
chainages.push_back(sample.chainage);
|
||||
ground.push_back(point->position.y + point->rails - track->rail_height());
|
||||
tolerances.push_back(apart < kTimbers ? kTimbered : std::max(kRail, kSpread * apart));
|
||||
reaches.push_back(kHeld);
|
||||
}
|
||||
}
|
||||
if (std::none_of(ground.begin(), ground.end(), [](double const Height) { return std::isfinite(Height); }))
|
||||
return false;
|
||||
// a path which the line crosses on its level, as authored, makes a crossing with it: over it the two lie in one plane, the
|
||||
// grade doesn't break nor does a vertical curve reach; the line crosses a line fitted already at its height, the rail tops
|
||||
// of the two at one
|
||||
double constexpr kCrossing{0.75}; // m, the nearest the samples of the two come, a metre apart
|
||||
double constexpr kCrossed{0.035}; // the sine of the least angle the axes cross at
|
||||
double constexpr kGauge{1.435}; // m
|
||||
struct crossing
|
||||
{
|
||||
double chainage;
|
||||
double height;
|
||||
double sine;
|
||||
double distance;
|
||||
TTrack const *track;
|
||||
glm::dvec2 at; // where the axes meet
|
||||
glm::dvec2 along; // the way of the line there
|
||||
};
|
||||
auto const crossings_of = [&](std::map<std::pair<int, int>, std::vector<nmt_state::laid_point>> const &Points) {
|
||||
std::vector<crossing> found;
|
||||
auto const across = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; };
|
||||
for (auto const &sample : profile.samples)
|
||||
{
|
||||
auto const *track{profile.route.spans[sample.span].track};
|
||||
auto const cx{static_cast<int>(std::floor(sample.position.x / kPlanCell))};
|
||||
auto const cz{static_cast<int>(std::floor(sample.position.z / kPlanCell))};
|
||||
auto const along{glm::normalize(sample.direction)};
|
||||
for (int dx = -1; dx <= 1; ++dx)
|
||||
for (int dz = -1; dz <= 1; ++dz)
|
||||
{
|
||||
auto const cell{Points.find({cx + dx, cz + dz})};
|
||||
if (cell == Points.end())
|
||||
continue;
|
||||
for (auto const &point : cell->second)
|
||||
{
|
||||
glm::dvec2 const offset{point.position.x - sample.position.x, point.position.z - sample.position.z};
|
||||
auto const other{glm::normalize(point.direction)};
|
||||
auto const sine{across(along, other)};
|
||||
if (point.track == track || glm::length(offset) > kCrossing || std::abs(point.was - sample.position.y) > kSameLevel || std::abs(sine) < kCrossed)
|
||||
continue;
|
||||
// where the axes meet
|
||||
auto const here{across(offset, other) / sine};
|
||||
auto const there{across(offset, along) / sine};
|
||||
found.push_back({sample.chainage + here, point.position.y + there * point.grade + point.rails - track->rail_height(), std::abs(sine), glm::length(offset), point.track, glm::dvec2{sample.position.x, sample.position.z} + along * here, along});
|
||||
}
|
||||
}
|
||||
}
|
||||
// the nearest samples of each crossing tell it
|
||||
std::sort(found.begin(), found.end(), [](crossing const &A, crossing const &B) { return std::tie(A.track, A.chainage) < std::tie(B.track, B.chainage); });
|
||||
std::vector<crossing> result;
|
||||
for (auto const &one : found)
|
||||
if (one.chainage < 0.0 || one.chainage > profile.route.length || on_route(one.track))
|
||||
continue;
|
||||
else if (result.empty() || result.back().track != one.track || one.chainage - result.back().chainage > 5.0)
|
||||
result.push_back(one);
|
||||
else if (one.distance < result.back().distance)
|
||||
result.back() = one;
|
||||
return result;
|
||||
};
|
||||
// no farther than the path crossed has rails: the middle of a double slip, a few metres crossed at a low angle, keeps to
|
||||
// itself, the switches about it have zones of their own
|
||||
for (auto const &found : crossings_of(m_nmt_state.paths))
|
||||
{
|
||||
auto const half{std::min(30.0, kGauge / found.sine + 1.5)};
|
||||
auto first{std::numeric_limits<double>::max()};
|
||||
auto last{std::numeric_limits<double>::lowest()};
|
||||
for (auto const &path : found.track->m_paths)
|
||||
for (auto const end : {segment_data::point::start, segment_data::point::end})
|
||||
{
|
||||
auto const along{glm::dot(glm::dvec2{path.points[end].x, path.points[end].z} - found.at, found.along)};
|
||||
first = std::min(first, along);
|
||||
last = std::max(last, along);
|
||||
}
|
||||
auto const from{std::max(-half, first - 1.5)};
|
||||
auto const to{std::min(half, last + 1.5)};
|
||||
if (from < to)
|
||||
profile.context.switches.push_back({found.chainage + from, found.chainage + to, found.track->name()});
|
||||
}
|
||||
// the height of a crossing of a line fitted already is ground the line keeps to as closely as to the switches beside, though
|
||||
// not past the curves of the norms: where an end fitted another way is too near, as near as they let it
|
||||
double constexpr kCrossingHeight{0.005}; // m
|
||||
for (auto const &found : crossings_of(m_nmt_state.laid))
|
||||
{
|
||||
auto const at{static_cast<std::size_t>(std::distance(chainages.begin(), std::upper_bound(chainages.begin(), chainages.end(), found.chainage)))};
|
||||
chainages.insert(chainages.begin() + at, found.chainage);
|
||||
ground.insert(ground.begin() + at, found.height);
|
||||
tolerances.insert(tolerances.begin() + at, kCrossingHeight);
|
||||
reaches.insert(reaches.begin() + at, kLevel);
|
||||
}
|
||||
// the switches of the tracks beside, the ones of the crossovers above all, lie on bearers common with this track or close
|
||||
// to it: no break of the grade on them either, so the tracks of the station stay in one plane
|
||||
double constexpr kShared{7.0}; // m, about the spacing of the tracks
|
||||
for (auto *other : simulation::Paths.sequence())
|
||||
{
|
||||
if (other == nullptr || other->eType != tt_Switch || other->m_paths.empty() || false == rail(*other))
|
||||
@@ -1737,7 +2030,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
for (auto const end : {segment_data::point::start, segment_data::point::end})
|
||||
{
|
||||
auto const &point{points[end]};
|
||||
double best{25.0};
|
||||
double best{kShared};
|
||||
double chainage{0.0};
|
||||
for (auto const &sample : profile.samples)
|
||||
if (auto const distance{glm::length(glm::dvec2{sample.position.x - point.x, sample.position.z - point.z})}; distance < best)
|
||||
@@ -1745,7 +2038,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
best = distance;
|
||||
chainage = sample.chainage;
|
||||
}
|
||||
if (best < 25.0)
|
||||
if (best < kShared)
|
||||
{
|
||||
from = std::min(from, chainage);
|
||||
to = std::max(to, chainage);
|
||||
@@ -1758,13 +2051,16 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
auto settings{profile.line};
|
||||
settings.join_start = true;
|
||||
settings.join_end = true;
|
||||
settings.speed = 0.0;
|
||||
for (auto const &span : profile.route.spans)
|
||||
settings.speed = std::max(settings.speed, editor_track::velocity(*span.track));
|
||||
if (settings.speed <= 0.0)
|
||||
settings.speed = 100.0;
|
||||
settings.speed = editor_track::route_speed(profile.route);
|
||||
// the grades as steep as the ground has them; the vertical curves of the norms, sharper only on a hump of a marshalling yard
|
||||
options.tolerance = kModel;
|
||||
options.grade_max = kSteepest;
|
||||
options.grade_max = std::numeric_limits<double>::max();
|
||||
options.follow_length = 60.0;
|
||||
options.follow_height = 4.0;
|
||||
options.follow_steepest = 100.0;
|
||||
options.follow_tolerance = 0.05;
|
||||
options.humps = profile::humps(chainages, ground, profile.context, kModel, tolerances);
|
||||
options.hump_radius = 300.0;
|
||||
auto line{profile::fit_ground(chainages, ground, profile.context, settings, options, tolerances, reaches)};
|
||||
if (line.points.size() < 2)
|
||||
return false;
|
||||
@@ -1772,6 +2068,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
profile.selected = -1;
|
||||
profile_check();
|
||||
auto const fitted_line{profile.line};
|
||||
auto const authored{profile.samples};
|
||||
profile_apply();
|
||||
auto const applied{profile.error.empty()};
|
||||
// where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track
|
||||
@@ -1823,13 +2120,37 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
|
||||
}
|
||||
if (applied)
|
||||
for (auto const &sample : profile.samples)
|
||||
m_nmt_state.laid[{static_cast<int>(std::floor(sample.position.x / kCell)), static_cast<int>(std::floor(sample.position.z / kCell))}].push_back(sample.position);
|
||||
{
|
||||
auto const *track{sample.span < profile.route.spans.size() ? profile.route.spans[sample.span].track : nullptr};
|
||||
m_nmt_state.laid[{static_cast<int>(std::floor(sample.position.x / kPlanCell)), static_cast<int>(std::floor(sample.position.z / kPlanCell))}].push_back(
|
||||
{sample.position, sample.direction, sample.grade, editor_track::sampled_elevation(authored, sample.chainage), track != nullptr ? static_cast<double>(track->rail_height()) : 0.0, track});
|
||||
}
|
||||
// where the model falls away under the line by the abutments, the line goes over on a bridge or a viaduct: the formation
|
||||
// is laid up to it, the ground under it stays as the model has it
|
||||
double constexpr kBridged{1.0}; // m
|
||||
double constexpr kAbutment{100.0}; // per mille
|
||||
std::vector<double> heights(count);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
heights[i] = profile::elevation(fitted_line, sampled[i]);
|
||||
auto const spans{applied ? profile::bridged(sampled, heights, model, kBridged, kAbutment) : std::vector<profile::departure>{}};
|
||||
for (auto const &stretch : spans)
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (sampled[i] >= stretch.from && sampled[i] <= stretch.to)
|
||||
m_nmt_state.bridged[{static_cast<int>(std::floor(plan[i].x / kPlanCell)), static_cast<int>(std::floor(plan[i].y / kPlanCell))}].push_back({plan[i].x, heights[i], plan[i].y});
|
||||
// the line in the log: how much of it lies on the model, where it parts from it, its grades, why it wasn't applied, its
|
||||
// course in the plan
|
||||
std::string text{format("Editor: NMT line %s: %s .. %s, %.0f m, on the model within %.2f m over %.1f%%, parts from it", applied ? "applied" : "refused", from->name().c_str(), to->name().c_str(), profile.route.length, kModel,
|
||||
100.0 * static_cast<double>(agreeing) / static_cast<double>(std::max<std::size_t>(1, modelled)))};
|
||||
for (auto const &stretch : parted)
|
||||
text += format(" %s %.0f..%.0f %.1f m", stretch.depth < 0.0 ? "under" : "over", stretch.from, stretch.to, std::abs(stretch.depth));
|
||||
if (false == spans.empty())
|
||||
{
|
||||
text += ", bridged";
|
||||
for (auto const &stretch : spans)
|
||||
text += format(" %.0f..%.0f", stretch.from, stretch.to);
|
||||
}
|
||||
for (auto const &hump : options.humps)
|
||||
text += format(", a hump %.0f..%.0f, its crest at %.0f %.1f m high", hump.from, hump.to, hump.crest, hump.height);
|
||||
text += ", grades";
|
||||
for (std::size_t i = 1; i < fitted_line.points.size(); ++i)
|
||||
{
|
||||
@@ -1908,12 +2229,50 @@ void editor_mode::nmt_auto_step()
|
||||
state.split = 0;
|
||||
state.done.clear();
|
||||
state.laid.clear();
|
||||
state.bridged.clear();
|
||||
// every path sampled as laid, for the crossings of the lines
|
||||
state.paths.clear();
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
if (track != nullptr && rail(*track))
|
||||
for (auto const &path : track->m_paths)
|
||||
{
|
||||
geometry::bezier const curve{path};
|
||||
auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])};
|
||||
auto const count{std::max(1, static_cast<int>(std::ceil(chord * 1.1)))};
|
||||
for (int i = 0; i <= count; ++i)
|
||||
{
|
||||
auto const t{static_cast<double>(i) / count};
|
||||
auto const at{curve.point(t)};
|
||||
auto const tangent{curve.first(t)};
|
||||
auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})};
|
||||
if (run > 1e-9)
|
||||
state.paths[{static_cast<int>(std::floor(at.x / kPlanCell)), static_cast<int>(std::floor(at.z / kPlanCell))}].push_back(
|
||||
{at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast<double>(track->rail_height()), track});
|
||||
}
|
||||
}
|
||||
state.stage = nmt_state::automatic::fitting;
|
||||
return;
|
||||
}
|
||||
// a few lines a frame
|
||||
// a few lines a frame: the longest track next to the lines fitted already, so that a line meets the ones it leads into as
|
||||
// it's fitted, not when the lines between them come; the longest of all where none is next to them
|
||||
for (int line = 0; line < 2 && false == state.left.empty(); ++line)
|
||||
{
|
||||
std::unordered_set<TTrack const *> const fitted(state.done.begin(), state.done.end());
|
||||
auto const touches = [&](TTrack *Track) {
|
||||
for (int path = 0; path < static_cast<int>(Track->m_paths.size()); ++path)
|
||||
for (auto const forward : {true, false})
|
||||
{
|
||||
auto const onward{editor_track::continuations({Track, path, forward})};
|
||||
if (std::any_of(onward.begin(), onward.end(), [&](editor_track::continuation const &Next) { return fitted.contains(Next.span.track); }))
|
||||
return true;
|
||||
if (auto const loose{state.loose.find({Track, path, forward})}; loose != state.loose.end() && fitted.contains(std::get<0>(loose->second)))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
auto const next{std::find_if(state.left.begin(), state.left.end(), touches)};
|
||||
if (next != state.left.end())
|
||||
std::rotate(state.left.begin(), next, next + 1);
|
||||
auto *track{state.left.front()};
|
||||
auto const name{track->name()};
|
||||
auto const known{simulation::Paths.sequence().size()};
|
||||
@@ -1959,6 +2318,8 @@ void editor_mode::nmt_auto_step()
|
||||
m_profile.open = false;
|
||||
state.done.clear();
|
||||
state.laid.clear();
|
||||
state.paths.clear();
|
||||
state.loose.clear();
|
||||
state.stage = nmt_state::automatic::idle;
|
||||
WriteLog("Editor: NMT - " + state.status, logtype::generic);
|
||||
nmt_make(nmt_keys_along_tracks(), true);
|
||||
|
||||
@@ -82,14 +82,6 @@ double nice_step(double const Approximate)
|
||||
return power * 10.0;
|
||||
}
|
||||
|
||||
double route_speed(editor_track::route const &Route)
|
||||
{
|
||||
double speed{-1.0};
|
||||
for (auto const &span : Route.spans)
|
||||
speed = std::max(speed, editor_track::velocity(*span.track));
|
||||
return speed > 0.0 ? speed : 100.0;
|
||||
}
|
||||
|
||||
std::string const kProfileMark{"//$p"};
|
||||
|
||||
profile::line reversed_line(profile::line Line, double const Length)
|
||||
@@ -915,7 +907,7 @@ void editor_mode::profile_recognize()
|
||||
auto const norms{state.line.norms};
|
||||
state.line = profile::recognize(chainages, elevations, grades, {});
|
||||
state.line.norms = norms;
|
||||
state.line.speed = route_speed(state.route);
|
||||
state.line.speed = editor_track::route_speed(state.route);
|
||||
state.selected = -1;
|
||||
profile_check();
|
||||
state.status = format(STR_P("Grade line recognized from the track: %zu point, largest departure %.3f m", "Grade line recognized from the track: %zu points, largest departure %.3f m", state.line.points.size()), state.line.points.size(), state.departure);
|
||||
@@ -933,7 +925,7 @@ void editor_mode::profile_fit_ground()
|
||||
return;
|
||||
}
|
||||
auto settings{state.line};
|
||||
settings.speed = route_speed(state.route);
|
||||
settings.speed = editor_track::route_speed(state.route);
|
||||
// the grade line is the rail top: it's fitted so the formation under it, not the rails, follows the ground
|
||||
auto ground{state.terrain};
|
||||
for (std::size_t i = 0; i < ground.size() && i < state.beds.size(); ++i)
|
||||
|
||||
@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <tuple>
|
||||
|
||||
@@ -24,6 +25,9 @@ namespace profile
|
||||
namespace
|
||||
{
|
||||
|
||||
// a switch, and the timbers it shares, may lie on a vertical curve: a concave one, or a convex one no sharper than this
|
||||
double constexpr switch_crest{2000.0}; // m
|
||||
|
||||
double polyline(line const &Line, double const Chainage)
|
||||
{
|
||||
auto const &points{Line.points};
|
||||
@@ -256,9 +260,17 @@ std::vector<issue> check(line const &Line, context const &Context)
|
||||
add(true, point, points[c.point].chainage, format(STR_C("Point %zu: the vertical curve reaches past the end of the route"), c.point + 1));
|
||||
if (i + 1 < list.size() && c.end > list[i + 1].start + 1e-6)
|
||||
add(true, point, c.end, format(STR_C("Vertical curves of points %zu and %zu overlap by %.2f m"), c.point + 1, list[i + 1].point + 1, c.end - list[i + 1].start));
|
||||
// a switch may lie on a concave curve, and on a convex one no sharper than switch_crest, though better off them; within
|
||||
// what the fit rounds the radius to its room by
|
||||
for (auto const &zone : Context.switches)
|
||||
if (overlaps(c.start, c.end, zone))
|
||||
add(true, point, points[c.point].chainage, format(STR_C("Point %zu: the vertical curve lies on switch %s"), c.point + 1, zone.name.c_str()));
|
||||
{
|
||||
if (c.change < 0.0 && points[c.point].radius < switch_crest * 0.99)
|
||||
add(true, point, points[c.point].chainage, format(STR_C("Point %zu: the convex vertical curve on switch %s is sharper than %.0f m"), c.point + 1, zone.name.c_str(), switch_crest));
|
||||
else
|
||||
add(false, point, points[c.point].chainage, format(STR_C("Point %zu: the vertical curve lies on switch %s"), c.point + 1, zone.name.c_str()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (norms.element_min > 0.0)
|
||||
{
|
||||
@@ -499,6 +511,117 @@ line recognize(std::vector<double> const &Chainages, std::vector<double> const &
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<hump> humps(std::vector<double> const &Chainages, std::vector<double> const &Ground, context const &Context, double const Tolerance, std::vector<double> const &Tolerances)
|
||||
{
|
||||
std::vector<hump> result;
|
||||
auto const count{std::min(Chainages.size(), Ground.size())};
|
||||
if (count < 2)
|
||||
return result;
|
||||
// the ground every few metres, the mean of the model about each, past its noise
|
||||
double constexpr spacing{5.0};
|
||||
auto const origin{Chainages.front()};
|
||||
auto const end{Chainages[count - 1]};
|
||||
auto const cells{static_cast<std::ptrdiff_t>(std::lround((end - origin) / spacing)) + 1};
|
||||
std::vector<double> level(cells, 0.0);
|
||||
std::vector<int> taken(cells, 0);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (false == std::isfinite(Ground[i]) || (i < Tolerances.size() && Tolerances[i] > 0.0 && Tolerances[i] < Tolerance))
|
||||
continue;
|
||||
auto const k{std::clamp<std::ptrdiff_t>(std::lround((Chainages[i] - origin) / spacing), 0, cells - 1)};
|
||||
level[k] += Ground[i];
|
||||
++taken[k];
|
||||
}
|
||||
for (std::ptrdiff_t k = 0; k < cells; ++k)
|
||||
level[k] = taken[k] > 0 ? level[k] / taken[k] : std::numeric_limits<double>::quiet_NaN();
|
||||
auto const at = [&](std::ptrdiff_t const K) { return K >= 0 && K < cells ? level[K] : std::numeric_limits<double>::quiet_NaN(); };
|
||||
auto const cells_of = [](double const Distance) { return static_cast<std::ptrdiff_t>(std::lround(Distance / spacing)); };
|
||||
// the crest is the highest of the ground about it, the feet where the ground has fallen nearly as far as it falls within
|
||||
// the reach: some metres lower than the crest, not as many as of a hill or a bridge
|
||||
double constexpr around{60.0}; // m
|
||||
double constexpr farthest{300.0}; // m
|
||||
double constexpr lowest{1.0}; // m
|
||||
double constexpr highest{6.0}; // m
|
||||
// the grades just past the rounding of the crest fall away from it both ways, one of them more steeply than lines and
|
||||
// stations are laid, the two together a change of grade over some 50 m which no curve of 1250 m or more makes
|
||||
double constexpr inner{10.0}; // m
|
||||
double constexpr outer{40.0}; // m
|
||||
double constexpr falling{5.0}; // per mille
|
||||
double constexpr steep{25.0}; // per mille
|
||||
double constexpr sharp{40.0}; // per mille
|
||||
// nowhere about the crest as steep as the banks of a bridge, the walls of a cutting
|
||||
double constexpr wall{80.0}; // per mille
|
||||
// no switch on the crest; past it the fan, switches of their own length, not the shared timbers of the ones beside
|
||||
double constexpr clear{15.0}; // m
|
||||
double constexpr fan{150.0}; // m
|
||||
double constexpr body{15.0}; // m
|
||||
std::size_t constexpr fanning{2};
|
||||
auto const crest_of = [&](std::ptrdiff_t const C) -> std::optional<hump> {
|
||||
auto const top{at(C)};
|
||||
if (false == std::isfinite(top))
|
||||
return std::nullopt;
|
||||
for (auto k = C - cells_of(around); k <= C + cells_of(around); ++k)
|
||||
if (auto const other{at(k)}; std::isfinite(other) && (other > top || (other == top && k < C)))
|
||||
return std::nullopt;
|
||||
hump found;
|
||||
found.crest = origin + spacing * static_cast<double>(C);
|
||||
for (auto const &zone : Context.switches)
|
||||
if (zone.from < found.crest + clear && zone.to > found.crest - clear)
|
||||
return std::nullopt;
|
||||
double grades{0.0};
|
||||
double steepest{0.0};
|
||||
bool fanned{false};
|
||||
for (int const side : {-1, 1})
|
||||
{
|
||||
auto low{top};
|
||||
std::ptrdiff_t seen{0};
|
||||
auto const reach{cells_of(farthest)};
|
||||
for (std::ptrdiff_t d = 1; d <= reach; ++d)
|
||||
if (auto const height{at(C + side * d)}; std::isfinite(height))
|
||||
{
|
||||
low = std::min(low, height);
|
||||
++seen;
|
||||
}
|
||||
if (seen < cells_of(around) || top - low < lowest || top - low > highest)
|
||||
return std::nullopt;
|
||||
found.height = std::max(found.height, top - low);
|
||||
auto foot{reach};
|
||||
for (std::ptrdiff_t d = 1; d <= reach; ++d)
|
||||
if (auto const height{at(C + side * d)}; std::isfinite(height) && top - height >= 0.9 * (top - low))
|
||||
{
|
||||
foot = d;
|
||||
break;
|
||||
}
|
||||
// and on past the foot as far as the curve into the grade beyond
|
||||
(side < 0 ? found.from : found.to) = std::clamp(found.crest + side * spacing * static_cast<double>(foot + cells_of(25.0)), origin, end);
|
||||
auto const off{at(C + side * cells_of(inner))};
|
||||
auto const on{at(C + side * cells_of(outer))};
|
||||
if (false == std::isfinite(off) || false == std::isfinite(on))
|
||||
return std::nullopt;
|
||||
auto const grade{(off - on) / (outer - inner) * 1000.0};
|
||||
if (grade < falling)
|
||||
return std::nullopt;
|
||||
grades += grade;
|
||||
steepest = std::max(steepest, grade);
|
||||
for (std::ptrdiff_t d = 0; d + 2 <= cells_of(around); ++d)
|
||||
if (auto const a{at(C + side * d)}, b{at(C + side * (d + 2))}; std::isfinite(a) && std::isfinite(b) && std::abs(a - b) / (2.0 * spacing) * 1000.0 > wall)
|
||||
return std::nullopt;
|
||||
std::size_t past{0};
|
||||
for (auto const &zone : Context.switches)
|
||||
if (auto const distance{side > 0 ? zone.from - found.crest : found.crest - zone.to}; zone.to - zone.from >= body && distance >= clear && distance <= fan)
|
||||
++past;
|
||||
fanned = fanned || past >= fanning;
|
||||
}
|
||||
if (steepest < steep || grades < sharp || false == fanned)
|
||||
return std::nullopt;
|
||||
return found;
|
||||
};
|
||||
for (std::ptrdiff_t c = 0; c < cells; ++c)
|
||||
if (auto const found{crest_of(c)}; found && (result.empty() || found->crest > result.back().to))
|
||||
result.push_back(*found);
|
||||
return result;
|
||||
}
|
||||
|
||||
line fit_ground(std::vector<double> const &Chainages, std::vector<double> const &Ground, context const &Context, line const &Settings, ground_fit const &Options, std::vector<double> const &Tolerances,
|
||||
std::vector<double> const &Reaches)
|
||||
{
|
||||
@@ -527,27 +650,60 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
part[i] = at - static_cast<double>(cell[i]);
|
||||
}
|
||||
auto const node = [&](std::size_t const K) { return origin + step * static_cast<double>(K); };
|
||||
// a change of grade at a node is the curve of the required radius around it, which keeps off the switches: none on a
|
||||
// switch, and beside one no more than a curve reaching up to it
|
||||
// a change of grade at a node is the curve of the required radius around it. On a hump the least radius is the one of the
|
||||
// hump, though a convex curve over a switch is no sharper than switch_crest
|
||||
auto const required{std::ceil(required_radius(result) / 10.0) * 10.0};
|
||||
auto const on_hump = [&](double const Chainage) { return std::any_of(Options.humps.begin(), Options.humps.end(), [&](hump const &Hump) { return Chainage >= Hump.from && Chainage <= Hump.to; }); };
|
||||
auto const least = [&](double const Chainage) { return Options.hump_radius > 0.0 && on_hump(Chainage) ? std::min(required, std::ceil(Options.hump_radius / 10.0) * 10.0) : required; };
|
||||
auto const on_switch = [&](double const From, double const To) { return std::any_of(Context.switches.begin(), Context.switches.end(), [&](auto const &Zone) { return Zone.to > From && Zone.from < To; }); };
|
||||
auto const sharpest{step / (required * 1.001)};
|
||||
std::vector<double> bend_max(nodes, sharpest);
|
||||
for (std::size_t k = 1; k + 1 < nodes; ++k)
|
||||
for (auto const &zone : Context.switches)
|
||||
bend_max[k] = std::min(bend_max[k], std::max(0.0, 2.0 * std::max(zone.from - node(k), node(k) - zone.to) / (required * 1.001)));
|
||||
std::vector<double> sag_max(nodes), crest_max(nodes);
|
||||
for (std::size_t k = 0; k < nodes; ++k)
|
||||
{
|
||||
auto const radius{least(node(k))};
|
||||
sag_max[k] = crest_max[k] = step / (radius * 1.001);
|
||||
// a sharper one keeps its curve off the switches
|
||||
if (radius < switch_crest)
|
||||
for (auto const &zone : Context.switches)
|
||||
crest_max[k] = std::min(crest_max[k], std::max(step / (switch_crest * 1.001), 2.0 * std::max(zone.from - node(k), node(k) - zone.to) / (radius * 1.001)));
|
||||
}
|
||||
struct held
|
||||
{
|
||||
std::size_t cell;
|
||||
double part;
|
||||
double low;
|
||||
double high;
|
||||
bool yielding;
|
||||
};
|
||||
std::vector<held> bounds;
|
||||
for (auto const &bound : Options.bounds)
|
||||
{
|
||||
auto const at{std::clamp((bound.chainage - origin) / step, 0.0, static_cast<double>(nodes - 1))};
|
||||
auto const cell{std::min(nodes - 2, static_cast<std::size_t>(at))};
|
||||
bounds.push_back({cell, at - static_cast<double>(cell), std::min(bound.low, bound.high), std::max(bound.low, bound.high)});
|
||||
bounds.push_back({cell, at - static_cast<double>(cell), std::min(bound.low, bound.high), std::max(bound.low, bound.high), bound.yielding});
|
||||
}
|
||||
// the bounds which contradict each other about one place, as of two crossovers into lines fitted apart in height, hold as far
|
||||
// as both can: the line keeps between them. Held both, it would tear itself apart over them
|
||||
std::sort(bounds.begin(), bounds.end(), [](held const &A, held const &B) { return A.cell + A.part < B.cell + B.part; });
|
||||
for (std::size_t first = 0; first < bounds.size();)
|
||||
{
|
||||
auto last{first + 1};
|
||||
while (last < bounds.size() && bounds[last].cell + bounds[last].part - bounds[first].cell - bounds[first].part <= 1.0)
|
||||
++last;
|
||||
auto low{-std::numeric_limits<double>::max()};
|
||||
auto high{std::numeric_limits<double>::max()};
|
||||
for (auto i = first; i < last; ++i)
|
||||
{
|
||||
low = std::max(low, bounds[i].low);
|
||||
high = std::min(high, bounds[i].high);
|
||||
}
|
||||
if (low > high)
|
||||
for (auto i = first; i < last; ++i)
|
||||
{
|
||||
bounds[i].low = high;
|
||||
bounds[i].high = low;
|
||||
}
|
||||
first = last;
|
||||
}
|
||||
bool const start_fixed{Settings.join_start && Context.start_joined};
|
||||
bool const end_fixed{Settings.join_end && Context.end_joined};
|
||||
@@ -557,14 +713,24 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
// what parts from the line, a bridge, a viaduct, a wall, a track on another level, is gone past rather than followed. The
|
||||
// bends are no sharper than the radius of the norms and the grades no steeper than the limit
|
||||
auto const tolerance{std::max(0.01, Options.tolerance)};
|
||||
auto const own = [&](std::size_t const I) { return I < Tolerances.size() && Tolerances[I] > 0.0 ? std::max(0.005, Tolerances[I]) : tolerance; };
|
||||
auto const reach = [&](std::size_t const I) { return I < Reaches.size() && Reaches[I] > 0.0 ? std::max(0.005, Reaches[I]) : own(I); };
|
||||
auto const told = [&](std::size_t const I) { return I < Tolerances.size() && Tolerances[I] > 0.0 ? std::max(0.005, Tolerances[I]) : tolerance; };
|
||||
// the ground the line follows however far it parts, and as closely as asked, where it parted longer than a structure would
|
||||
std::vector<double> followed(count, 0.0);
|
||||
auto const close_to{Options.follow_tolerance > 0.0 ? std::max(0.005, Options.follow_tolerance) : tolerance};
|
||||
auto const own = [&](std::size_t const I) { return followed[I] > 0.0 ? close_to : told(I); };
|
||||
auto const reach = [&](std::size_t const I) { return followed[I] > 0.0 ? followed[I] : I < Reaches.size() && Reaches[I] > 0.0 ? std::max(0.005, Reaches[I]) : told(I); };
|
||||
for (auto const &hump : Options.humps)
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (std::isfinite(Ground[i]) && told(i) >= tolerance && Chainages[i] >= hump.from && Chainages[i] <= hump.to)
|
||||
followed[i] = hump.height + tolerance;
|
||||
auto const price{24000.0 * tolerance * tolerance};
|
||||
auto const grade_max{(Options.grade_max > 0.0 ? Options.grade_max : result.norms.grade_max) / 1000.0};
|
||||
// what has to hold, the heights of the joined ends and the switches in one plane, holds before the grades of the ends, and
|
||||
// these before the limits of the norms
|
||||
// what has to hold, the heights of the joined ends, holds before the grades of the ends, and these before the limits of the
|
||||
// norms. A yielding bound counts as much as a kilometre of the ground off as far, and gives
|
||||
// way to the norms
|
||||
double constexpr stiff{1e11};
|
||||
double constexpr firm{1e9};
|
||||
double constexpr leaning{1e3};
|
||||
|
||||
std::vector<double> x(nodes);
|
||||
{
|
||||
@@ -628,15 +794,10 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
{
|
||||
auto const terms = {std::pair{k - 1, 1.0 / step}, std::pair{k, -2.0 / step}, std::pair{k + 1, 1.0 / step}};
|
||||
auto const bend{(Line[k - 1] - 2.0 * Line[k] + Line[k + 1]) / step};
|
||||
if (bend_max[k] <= 0.0)
|
||||
{
|
||||
square(terms, bend, stiff);
|
||||
continue;
|
||||
}
|
||||
auto const root{std::sqrt(bend * bend + flat * flat)};
|
||||
add(terms, price * root, price * bend / root, price * flat * flat / (root * root * root));
|
||||
if (std::abs(bend) > bend_max[k])
|
||||
square(terms, bend - std::copysign(bend_max[k], bend), firm);
|
||||
if (auto const most{bend > 0.0 ? sag_max[k] : crest_max[k]}; std::abs(bend) > most)
|
||||
square(terms, bend - std::copysign(most, bend), firm);
|
||||
}
|
||||
for (std::size_t k = 0; k + 1 < nodes; ++k)
|
||||
{
|
||||
@@ -647,18 +808,20 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
if (start_fixed)
|
||||
{
|
||||
square({{0, 1.0}}, Line[0] - Context.start_elevation, stiff);
|
||||
square({{0, -1.0 / step}, {1, 1.0 / step}}, (Line[1] - Line[0]) / step - Context.start_grade, firm * 10.0);
|
||||
if (Options.hold_grades)
|
||||
square({{0, -1.0 / step}, {1, 1.0 / step}}, (Line[1] - Line[0]) / step - Context.start_grade, firm * 10.0);
|
||||
}
|
||||
if (end_fixed)
|
||||
{
|
||||
square({{nodes - 1, 1.0}}, Line[nodes - 1] - Context.end_elevation, stiff);
|
||||
square({{nodes - 2, -1.0 / step}, {nodes - 1, 1.0 / step}}, (Line[nodes - 1] - Line[nodes - 2]) / step - Context.end_grade, firm * 10.0);
|
||||
if (Options.hold_grades)
|
||||
square({{nodes - 2, -1.0 / step}, {nodes - 1, 1.0 / step}}, (Line[nodes - 1] - Line[nodes - 2]) / step - Context.end_grade, firm * 10.0);
|
||||
}
|
||||
for (auto const &bound : bounds)
|
||||
{
|
||||
auto const height{Line[bound.cell] * (1.0 - bound.part) + Line[bound.cell + 1] * bound.part};
|
||||
if (height < bound.low || height > bound.high)
|
||||
square({{bound.cell, 1.0 - bound.part}, {bound.cell + 1, bound.part}}, height - std::clamp(height, bound.low, bound.high), firm * 10.0);
|
||||
square({{bound.cell, 1.0 - bound.part}, {bound.cell + 1, bound.part}}, height - std::clamp(height, bound.low, bound.high), bound.yielding ? leaning : firm * 10.0);
|
||||
}
|
||||
return total;
|
||||
};
|
||||
@@ -683,36 +846,105 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
};
|
||||
// the spread and the flat narrow down level by level, each level going on from the line of the one before: from nearly
|
||||
// all the ground agreeing and changes of grade costing their square, to the tolerance and their size
|
||||
for (;;)
|
||||
{
|
||||
auto current{cost(x, true)};
|
||||
for (int pass = 0; pass < 100; ++pass)
|
||||
auto const solve = [&]() {
|
||||
spread = 256.0;
|
||||
flat = sharpest;
|
||||
for (;;)
|
||||
{
|
||||
auto const direction{newton()};
|
||||
auto descent{0.0};
|
||||
for (std::size_t k = 0; k < nodes; ++k)
|
||||
descent += gradient[k] * direction[k];
|
||||
std::vector<double> trial(nodes);
|
||||
auto length{1.0};
|
||||
for (;; length *= 0.5)
|
||||
auto current{cost(x, true)};
|
||||
for (int pass = 0; pass < 100; ++pass)
|
||||
{
|
||||
auto const direction{newton()};
|
||||
auto descent{0.0};
|
||||
for (std::size_t k = 0; k < nodes; ++k)
|
||||
trial[k] = x[k] + length * direction[k];
|
||||
if (cost(trial, false) <= current + 1e-4 * length * descent || length < 1e-6)
|
||||
descent += gradient[k] * direction[k];
|
||||
std::vector<double> trial(nodes);
|
||||
auto length{1.0};
|
||||
for (;; length *= 0.5)
|
||||
{
|
||||
for (std::size_t k = 0; k < nodes; ++k)
|
||||
trial[k] = x[k] + length * direction[k];
|
||||
if (cost(trial, false) <= current + 1e-4 * length * descent || length < 1e-6)
|
||||
break;
|
||||
}
|
||||
auto moved{0.0};
|
||||
for (std::size_t k = 0; k < nodes; ++k)
|
||||
moved = std::max(moved, std::abs(length * direction[k]));
|
||||
x = std::move(trial);
|
||||
current = cost(x, true);
|
||||
if (moved < 1e-5)
|
||||
break;
|
||||
}
|
||||
auto moved{0.0};
|
||||
for (std::size_t k = 0; k < nodes; ++k)
|
||||
moved = std::max(moved, std::abs(length * direction[k]));
|
||||
x = std::move(trial);
|
||||
current = cost(x, true);
|
||||
if (moved < 1e-5)
|
||||
if (spread <= 1.0 && flat <= 1e-6)
|
||||
break;
|
||||
spread = std::max(1.0, spread * 0.5);
|
||||
flat = std::max(1e-6, flat * 0.5);
|
||||
}
|
||||
if (spread <= 1.0 && flat <= 1e-6)
|
||||
};
|
||||
solve();
|
||||
// the ground parted from the line farther along than a structure is long, not as far as a viaduct or a hill over a tunnel
|
||||
// keep it, and nowhere as steep as the banks and the walls under a bridge, is none: a hump, a rise, a hollow the line goes
|
||||
// over. It agrees with the line however far it is, and the line is sought again. The ground which tells the height no more
|
||||
// closely than the tolerance counts, the model, not the rail tops beside; its steepness over a few metres, past the noise
|
||||
// of the model
|
||||
double constexpr chord{5.0};
|
||||
for (int round = 0; round < 3 && Options.follow_length > 0.0; ++round)
|
||||
{
|
||||
bool more{false};
|
||||
std::vector<std::size_t> run;
|
||||
std::size_t back{0};
|
||||
std::size_t previous{count};
|
||||
int side{0};
|
||||
double farthest{0.0};
|
||||
double steepest{0.0};
|
||||
auto const close = [&]() {
|
||||
if (side != 0 && Chainages[run.back()] - Chainages[run.front()] > Options.follow_length && farthest <= Options.follow_height && steepest <= Options.follow_steepest / 1000.0)
|
||||
{
|
||||
for (auto const i : run)
|
||||
if (followed[i] <= 0.0)
|
||||
followed[i] = farthest + tolerance;
|
||||
more = true;
|
||||
}
|
||||
side = 0;
|
||||
run.clear();
|
||||
};
|
||||
auto const extend = [&](std::size_t const I) {
|
||||
while (back + 1 < run.size() && Chainages[I] - Chainages[run[back + 1]] >= chord)
|
||||
++back;
|
||||
if (false == run.empty() && Chainages[I] - Chainages[run[back]] >= chord)
|
||||
steepest = std::max(steepest, std::abs(Ground[I] - Ground[run[back]]) / (Chainages[I] - Chainages[run[back]]));
|
||||
run.push_back(I);
|
||||
};
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (false == std::isfinite(Ground[i]) || told(i) < tolerance)
|
||||
continue;
|
||||
auto const off{x[cell[i]] * (1.0 - part[i]) + x[cell[i] + 1] * part[i] - Ground[i]};
|
||||
auto const now{followed[i] > 0.0 ? 0 : off > reach(i) ? 1 : off < -reach(i) ? -1 : 0};
|
||||
if (now != side)
|
||||
{
|
||||
close();
|
||||
if (now != 0)
|
||||
{
|
||||
side = now;
|
||||
back = 0;
|
||||
farthest = 0.0;
|
||||
steepest = 0.0;
|
||||
if (previous < count)
|
||||
run.push_back(previous);
|
||||
}
|
||||
}
|
||||
if (now != 0)
|
||||
{
|
||||
farthest = std::max(farthest, std::abs(off));
|
||||
extend(i);
|
||||
}
|
||||
previous = i;
|
||||
}
|
||||
close();
|
||||
if (false == more)
|
||||
break;
|
||||
spread = std::max(1.0, spread * 0.5);
|
||||
flat = std::max(1e-6, flat * 0.5);
|
||||
solve();
|
||||
}
|
||||
|
||||
// the design: the changes of grade along a bend, one after another the same way, are one point of intersection where
|
||||
@@ -720,10 +952,7 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
// radius stays within the nodes of its bend; where the ends held the line sharper, the curve is as sharp as the line
|
||||
auto const change = [&](std::size_t const K) { return (x[K - 1] - 2.0 * x[K] + x[K + 1]) / step; };
|
||||
auto const significant{sharpest * 0.01};
|
||||
auto const between = [&](std::size_t const K, std::size_t const L) {
|
||||
return std::any_of(Context.switches.begin(), Context.switches.end(), [&](auto const &Zone) { return Zone.to > node(K) && Zone.from < node(L); });
|
||||
};
|
||||
auto const bends = [&](std::size_t const K) { return bend_max[K] > 0.0 && std::abs(change(K)) >= significant; };
|
||||
auto const bends = [&](std::size_t const K) { return std::abs(change(K)) >= significant; };
|
||||
// an end whose grade the line couldn't take over keeps only its height, the grade breaks at the joint
|
||||
if (start_fixed && std::abs((x[1] - x[0]) / step - Context.start_grade) > significant)
|
||||
result.join_start = false;
|
||||
@@ -731,6 +960,7 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
result.join_end = false;
|
||||
result.points.push_back({origin, x[0]});
|
||||
std::vector<double> rooms{0.0};
|
||||
std::vector<double> lengths{0.0};
|
||||
for (std::size_t k = 1; k + 1 < nodes;)
|
||||
{
|
||||
if (false == bends(k))
|
||||
@@ -741,31 +971,50 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
auto const first{k};
|
||||
auto const sign{change(k) > 0.0};
|
||||
double sum{0.0}, moment{0.0};
|
||||
for (; k + 1 < nodes && bends(k) && (change(k) > 0.0) == sign && (k == first || false == between(k - 1, k)); ++k)
|
||||
for (; k + 1 < nodes && bends(k) && (change(k) > 0.0) == sign; ++k)
|
||||
{
|
||||
sum += change(k);
|
||||
moment += change(k) * node(k);
|
||||
}
|
||||
auto const at{moment / sum};
|
||||
pvi point{at, x[first] + (x[first] - x[first - 1]) / step * (at - node(first))};
|
||||
auto room{std::min(at - node(first) + step * 0.5, node(k - 1) + step * 0.5 - at)};
|
||||
for (auto const &zone : Context.switches)
|
||||
room = std::min(room, std::max(0.0, std::max(zone.from - at, at - zone.to)));
|
||||
auto const room{std::min(at - node(first) + step * 0.5, node(k - 1) + step * 0.5 - at)};
|
||||
result.points.push_back(point);
|
||||
rooms.push_back(room);
|
||||
lengths.push_back(node(k - 1) - node(first) + step);
|
||||
}
|
||||
result.points.push_back({node(nodes - 1), x[nodes - 1]});
|
||||
rooms.push_back(0.0);
|
||||
// the curve of the required radius where it has room, off the switches and the curves beside; where it hasn't, as sharp as
|
||||
// the room lets it be
|
||||
lengths.push_back(0.0);
|
||||
// the curve of the required radius where it has room, off the curves beside; where it hasn't, as sharp as the room lets it
|
||||
// be. On a hump as long as the bend of the line, no sharper than the radius of the hump, nor than a switch lets a convex one
|
||||
for (std::size_t i = 1; i + 1 < result.points.size(); ++i)
|
||||
{
|
||||
auto const turn{std::abs(grade_after(result, i) - grade_after(result, i - 1))};
|
||||
if (turn * 1000.0 > result.norms.curve_threshold + 1e-6 && required * turn * 0.5 > rooms[i] - 1e-4)
|
||||
if (turn * 1000.0 <= result.norms.curve_threshold + 1e-6)
|
||||
{
|
||||
// a change no curve needs breaks bare, but not on a switch
|
||||
if (auto const at{result.points[i].chainage}; turn > 1e-9 && on_switch(at, at))
|
||||
{
|
||||
result.points[i].radius = std::min(required, 2.0 * std::max(0.0, rooms[i] - 1e-4) / turn);
|
||||
result.points[i].automatic = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
auto const radius{least(result.points[i].chainage)};
|
||||
auto bent{radius < required ? std::clamp(std::ceil(lengths[i] / turn / 10.0) * 10.0, radius, required) : required};
|
||||
if (auto const at{result.points[i].chainage}; bent < switch_crest && grade_after(result, i) < grade_after(result, i - 1) && on_switch(at - bent * turn * 0.5, at + bent * turn * 0.5))
|
||||
bent = switch_crest;
|
||||
if (bent * turn * 0.5 > rooms[i] - 1e-4)
|
||||
{
|
||||
result.points[i].radius = 2.0 * std::max(0.0, rooms[i] - 1e-4) / turn;
|
||||
result.points[i].automatic = false;
|
||||
}
|
||||
else if (bent < required)
|
||||
{
|
||||
result.points[i].radius = bent;
|
||||
result.points[i].automatic = false;
|
||||
}
|
||||
}
|
||||
assign_radii(result);
|
||||
|
||||
@@ -793,6 +1042,24 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
return worst;
|
||||
};
|
||||
auto faults{errors(result)};
|
||||
// a convex curve over a switch which the room of its bend cut sharper than a switch may lie on takes the radius it may have,
|
||||
// as far as the curves beside leave it room
|
||||
for (std::size_t i = 1; faults > 0 && i + 1 < result.points.size(); ++i)
|
||||
{
|
||||
auto const turn{grade_after(result, i) - grade_after(result, i - 1)};
|
||||
auto const radius{result.points[i].radius};
|
||||
auto const at{result.points[i].chainage};
|
||||
if (radius <= 0.0 || radius >= switch_crest || turn >= 0.0 || false == on_switch(at + radius * turn * 0.5, at - radius * turn * 0.5))
|
||||
continue;
|
||||
auto wider{result};
|
||||
wider.points[i].radius = switch_crest;
|
||||
wider.points[i].automatic = false;
|
||||
if (auto const fewer{errors(wider)}; fewer < faults)
|
||||
{
|
||||
result = std::move(wider);
|
||||
faults = fewer;
|
||||
}
|
||||
}
|
||||
// an end whose grade the curve of the joint can't take over without a fault breaks at the joint
|
||||
for (auto *join : {&result.join_start, &result.join_end})
|
||||
if (faults > 0 && *join)
|
||||
@@ -803,6 +1070,15 @@ line fit_ground(std::vector<double> const &Chainages, std::vector<double> const
|
||||
else
|
||||
*join = true;
|
||||
}
|
||||
// where the line can't bend from the grades of its ends into the ground within the norms, as a short piece between two
|
||||
// lines, the grades break at the joints: the line keeps only the heights of the ends
|
||||
if (faults > 0 && Options.hold_grades && (start_fixed || end_fixed))
|
||||
{
|
||||
auto loose{Options};
|
||||
loose.hold_grades = false;
|
||||
if (auto again{fit_ground(Chainages, Ground, Context, Settings, loose, Tolerances, Reaches)}; errors(again) < faults)
|
||||
return again;
|
||||
}
|
||||
for (;;)
|
||||
{
|
||||
auto const &points{result.points};
|
||||
@@ -865,6 +1141,48 @@ std::vector<departure> departures(line const &Line, std::vector<double> const &C
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<departure> bridged(std::vector<double> const &Chainages, std::vector<double> const &Heights, std::vector<double> const &Ground, double const Depth, double const Steepest)
|
||||
{
|
||||
std::vector<departure> result;
|
||||
auto const count{std::min({Chainages.size(), Heights.size(), Ground.size()})};
|
||||
auto const under = [&](std::size_t const I) { return std::isfinite(Ground[I]) && Heights[I] - Ground[I] > Depth; };
|
||||
// the steepness over a few metres, past the noise of the model, in and about the stretch
|
||||
double constexpr chord{5.0};
|
||||
double constexpr about{10.0};
|
||||
auto const steepest = [&](double const From, double const To) {
|
||||
double most{0.0};
|
||||
for (std::size_t i = 0, j = 0; i < count; ++i)
|
||||
{
|
||||
if (Chainages[i] < From - about || Chainages[i] > To + about || false == std::isfinite(Ground[i]))
|
||||
continue;
|
||||
j = std::max(j, i);
|
||||
while (j + 1 < count && Chainages[j] - Chainages[i] < chord)
|
||||
++j;
|
||||
if (auto const run{Chainages[j] - Chainages[i]}; run >= chord && run <= 2.0 * chord && std::isfinite(Ground[j]))
|
||||
most = std::max(most, std::abs(Ground[j] - Ground[i]) / run);
|
||||
}
|
||||
return most * 1000.0;
|
||||
};
|
||||
for (std::size_t i = 0; i < count;)
|
||||
{
|
||||
if (false == under(i))
|
||||
{
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
departure stretch;
|
||||
stretch.from = Chainages[i];
|
||||
for (; i < count && under(i); ++i)
|
||||
{
|
||||
stretch.depth = std::min(stretch.depth, Ground[i] - Heights[i]);
|
||||
stretch.to = Chainages[i];
|
||||
}
|
||||
if (steepest(stretch.from, stretch.to) > Steepest)
|
||||
result.push_back(stretch);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<double> breaks(line const &Line)
|
||||
{
|
||||
std::vector<double> result;
|
||||
|
||||
@@ -115,19 +115,47 @@ struct recognition
|
||||
// grade line of existing track, from its elevations and grades sampled along the chainage
|
||||
line recognize(std::vector<double> const &Chainages, std::vector<double> const &Elevations, std::vector<double> const &Grades, recognition const &Options);
|
||||
|
||||
// the hump of a marshalling yard: a crest with the fan of the switches of the sorting tracks past it, the ground falling away from
|
||||
// it both ways, one of them more steeply than a line or a station is laid, and the grade changing across it more sharply than
|
||||
// any vertical curve of the norms bends. Nothing else has all of it
|
||||
struct hump
|
||||
{
|
||||
double from{0.0};
|
||||
double crest{0.0};
|
||||
double to{0.0};
|
||||
double height{0.0}; // m, of the crest over the lower of its feet
|
||||
};
|
||||
// the humps of the ground; the samples which tell the height more closely than Tolerance, the rail tops beside, don't count
|
||||
std::vector<hump> humps(std::vector<double> const &Chainages, std::vector<double> const &Ground, context const &Context, double const Tolerance, std::vector<double> const &Tolerances = {});
|
||||
|
||||
struct ground_fit
|
||||
{
|
||||
double tolerance{1.0}; // m, the ground this near the line agrees with it; farther, the line doesn't lie on it there
|
||||
double grade_max{0.0}; // per mille, the steepest grade the line may take; 0: the limit of the line
|
||||
// the ground parting from the line longer than this, by less than the height and nowhere steeper than the steepest, is no
|
||||
// structure: no bridge or viaduct with the banks and the walls under it, no hill over a tunnel, but a hump, a rise, a
|
||||
// hollow. The line follows it, as closely as the tolerance. 0: the line goes past whatever it parts from
|
||||
double follow_length{0.0}; // m
|
||||
double follow_height{0.0}; // m
|
||||
double follow_steepest{0.0}; // per mille
|
||||
double follow_tolerance{0.0}; // m, 0: the tolerance
|
||||
// on the humps the line follows the ground all along, as closely as the follow tolerance, and its vertical curves are as sharp
|
||||
// as the ground bends, down to the hump radius. Nowhere else is a curve sharper than the norms require
|
||||
std::vector<hump> humps;
|
||||
double hump_radius{0.0}; // m, 0: the required one
|
||||
// heights the line keeps between before it lies on the ground, as where the tracks off a switch of it lead into another line:
|
||||
// they can't climb more than their length lets them
|
||||
// they can't climb more than their length lets them. A yielding one, told over tracks which still bend, gives way to the
|
||||
// curves of the norms, if to no ground
|
||||
struct bound
|
||||
{
|
||||
double chainage{0.0};
|
||||
double low{0.0};
|
||||
double high{0.0};
|
||||
bool yielding{false};
|
||||
};
|
||||
std::vector<bound> bounds;
|
||||
// the joined ends keep the grade of the adjoining track; false: their height only, the grade breaks at the joint
|
||||
bool hold_grades{true};
|
||||
};
|
||||
// grade line which lies on the ground where it agrees, within the limits of Settings, with the fewest and least changes of
|
||||
// grade; where the ground parts from it, at a bridge, a viaduct, a cutting wall, the line goes on past it. Chainages ascend,
|
||||
@@ -145,6 +173,9 @@ struct departure
|
||||
double depth{0.0}; // m, the ground against the line where they part the most
|
||||
};
|
||||
std::vector<departure> departures(line const &Line, std::vector<double> const &Chainages, std::vector<double> const &Ground, double const Tolerance);
|
||||
// stretches a track goes over on a bridge or a viaduct the ground doesn't have: the ground under the heights of the track by over
|
||||
// Depth, with a bank about them steeper than Steepest (per mille), as it falls by the abutments into what the bridge spans
|
||||
std::vector<departure> bridged(std::vector<double> const &Chainages, std::vector<double> const &Heights, std::vector<double> const &Ground, double const Depth, double const Steepest);
|
||||
|
||||
// chainages where the paths have to end so that each of them carries a single grade or a single curve
|
||||
std::vector<double> breaks(line const &Line);
|
||||
|
||||
@@ -2192,6 +2192,20 @@ editor_track::route editor_track::run_route(TTrack &Track, double const Maximum)
|
||||
route result;
|
||||
if (false == is_supported(Track))
|
||||
return result;
|
||||
// through a switch the run goes along its first path, the main one; but where that one bends and the other runs straight,
|
||||
// as in the quarters of a double slip, straight on
|
||||
auto const bends = [](TTrack const &Of, int const Path) {
|
||||
bezier const curve{Of.m_paths[Path]};
|
||||
auto const from{plan_of(curve.first(0.0))};
|
||||
auto const to{plan_of(curve.first(1.0))};
|
||||
return glm::length(from) > 1e-9 && glm::length(to) > 1e-9 && std::abs(std::atan2(from.x * to.y - from.y * to.x, glm::dot(from, to))) > 0.002;
|
||||
};
|
||||
auto const main_of = [&](TTrack const &Of) {
|
||||
for (int i = 1; i < static_cast<int>(Of.m_paths.size()); ++i)
|
||||
if (bends(Of, 0) && false == bends(Of, i))
|
||||
return i;
|
||||
return 0;
|
||||
};
|
||||
auto const walk = [&](route_node Node) {
|
||||
std::vector<route_node> nodes;
|
||||
double length{0.0};
|
||||
@@ -2202,7 +2216,7 @@ editor_track::route editor_track::run_route(TTrack &Track, double const Maximum)
|
||||
break;
|
||||
auto chosen{candidates.front()};
|
||||
for (auto const &candidate : candidates)
|
||||
if (std::get<1>(candidate) == 0)
|
||||
if (std::get<1>(candidate) == main_of(*std::get<0>(candidate)))
|
||||
chosen = candidate;
|
||||
auto *track{std::get<0>(chosen)};
|
||||
if (track == &Track || std::any_of(nodes.begin(), nodes.end(), [&](route_node const &Other) { return std::get<0>(Other) == track; }))
|
||||
@@ -2213,12 +2227,13 @@ editor_track::route editor_track::run_route(TTrack &Track, double const Maximum)
|
||||
}
|
||||
return nodes;
|
||||
};
|
||||
auto const ahead{walk({&Track, 0, true})};
|
||||
auto const behind{walk({&Track, 0, false})};
|
||||
auto const own{main_of(Track)};
|
||||
auto const ahead{walk({&Track, own, true})};
|
||||
auto const behind{walk({&Track, own, false})};
|
||||
for (auto node = behind.rbegin(); node != behind.rend(); ++node)
|
||||
if (std::none_of(ahead.begin(), ahead.end(), [&](route_node const &Other) { return std::get<0>(Other) == std::get<0>(*node); }))
|
||||
result.spans.push_back({std::get<0>(*node), std::get<1>(*node), false == std::get<2>(*node)});
|
||||
result.spans.push_back({&Track, 0, true});
|
||||
result.spans.push_back({&Track, own, true});
|
||||
for (auto const &node : ahead)
|
||||
result.spans.push_back({std::get<0>(node), std::get<1>(node), std::get<2>(node)});
|
||||
return result;
|
||||
@@ -2232,6 +2247,24 @@ editor_track::route editor_track::route_of(chain const &Chain)
|
||||
return result;
|
||||
}
|
||||
|
||||
double editor_track::route_speed(route const &Route)
|
||||
{
|
||||
std::vector<std::pair<double, double>> speeds;
|
||||
auto total{0.0};
|
||||
for (auto const &span : Route.spans)
|
||||
if (auto const speed{velocity(*span.track)}; speed > 0.0)
|
||||
{
|
||||
speeds.emplace_back(speed, std::abs(span.to - span.from));
|
||||
total += speeds.back().second;
|
||||
}
|
||||
std::sort(speeds.begin(), speeds.end());
|
||||
auto passed{0.0};
|
||||
for (auto const &[speed, length] : speeds)
|
||||
if ((passed += length) >= total * 0.5)
|
||||
return speed;
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
std::vector<editor_track::route_sample> editor_track::sample_route(route &Route, double const Step)
|
||||
{
|
||||
std::vector<route_sample> samples;
|
||||
@@ -2448,6 +2481,20 @@ bool editor_track::adjoining_grade(route const &Route, bool const Atend, double
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<editor_track::continuation> editor_track::continuations(route_span const &Span)
|
||||
{
|
||||
std::vector<continuation> result;
|
||||
for (auto const &[track, path, forward] : route_next({Span.track, Span.path, Span.forward}))
|
||||
{
|
||||
auto const tangent{path_tangent(track->m_paths[path], false == forward)};
|
||||
auto const plan{std::hypot(tangent.x, tangent.z)};
|
||||
if (plan < 1e-9)
|
||||
continue;
|
||||
result.push_back({{track, path, forward}, (forward ? 1.0 : -1.0) * tangent.y / plan});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<editor_track::height_gap> editor_track::apply_profile(route &Route, std::function<double(double)> const &Elevation, std::function<double(double)> const &Grade, std::vector<double> const &Breaks, std::vector<std::pair<TTrack *, state>> &States, std::vector<TTrack *> &Created)
|
||||
{
|
||||
double constexpr margin{0.25};
|
||||
|
||||
@@ -377,6 +377,9 @@ class editor_track
|
||||
// continues both ways from the path, through switches along their path 0
|
||||
static route run_route(TTrack &Track, double const Maximum);
|
||||
static route route_of(chain const &Chain);
|
||||
// the speed most of the sampled route is laid for, by the length: a few tracks set faster or slower don't set the curves of
|
||||
// all of it. 100 where none is set
|
||||
static double route_speed(route const &Route);
|
||||
struct route_sample
|
||||
{
|
||||
double chainage{0.0};
|
||||
@@ -394,6 +397,14 @@ class editor_track
|
||||
// grade of the path which adjoins the end of the route, positive rising along the route
|
||||
// Adjoining, Path, Entered: the path met, and whether it's met at its start
|
||||
static bool adjoining_grade(route const &Route, bool const Atend, double &Grade, TTrack **Adjoining = nullptr, int *Path = nullptr, bool *Entered = nullptr);
|
||||
// the paths which go on from the end of the span the way it runs, both of a switch met at its start, with their grade there,
|
||||
// positive rising onward
|
||||
struct continuation
|
||||
{
|
||||
route_span span;
|
||||
double grade{0.0};
|
||||
};
|
||||
static std::vector<continuation> continuations(route_span const &Span);
|
||||
// path off the route whose end no longer met the adjoining path in height
|
||||
struct height_gap
|
||||
{
|
||||
|
||||
@@ -8846,6 +8846,9 @@ msgstr "Łuki pionowe punktów %zu i %zu nachodzą na siebie o %.2f m"
|
||||
msgid "Point %zu: the vertical curve lies on switch %s"
|
||||
msgstr "Punkt %zu: łuk pionowy leży na rozjeździe %s"
|
||||
|
||||
msgid "Point %zu: the convex vertical curve on switch %s is sharper than %.0f m"
|
||||
msgstr "Punkt %zu: wypukły łuk pionowy na rozjeździe %s jest ostrzejszy niż %.0f m"
|
||||
|
||||
msgid "Constant grade between points %zu and %zu is %.1f m long, less than %.0f m"
|
||||
msgstr "Stałe pochylenie między punktami %zu i %zu ma %.1f m, mniej niż %.0f m"
|
||||
|
||||
|
||||
@@ -514,7 +514,10 @@ std::optional<modifier_shape::effect> modifier_shape::evaluate(double const X, d
|
||||
auto const &b{points[i + 1].position};
|
||||
auto const run{b - a};
|
||||
auto const length{glm::dot(run, run)};
|
||||
auto const t{length > 1e-12 ? std::clamp(glm::dot(point - a, run) / length, 0.0, 1.0) : 0.0};
|
||||
auto const along{length > 1e-12 ? glm::dot(point - a, run) / length : 0.0};
|
||||
if ((m_modifier.open_start && i == 0 && along < 0.0) || (m_modifier.open_end && i + 2 == points.size() && along > 1.0))
|
||||
continue;
|
||||
auto const t{std::clamp(along, 0.0, 1.0)};
|
||||
auto const offset{glm::length(point - (a + run * t))};
|
||||
if (offset < distance)
|
||||
{
|
||||
@@ -608,6 +611,10 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err
|
||||
item.reach = reach.as<double>();
|
||||
if (auto const rounding{entry["rounding"]})
|
||||
item.rounding = rounding.as<double>();
|
||||
if (auto const open{entry["open_start"]})
|
||||
item.open_start = open.as<bool>();
|
||||
if (auto const open{entry["open_end"]})
|
||||
item.open_end = open.as<bool>();
|
||||
if (auto const points{entry["points"]}; points && points.IsSequence())
|
||||
for (auto const &point : points)
|
||||
if (point.IsSequence() && point.size() >= 4)
|
||||
@@ -681,6 +688,10 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin
|
||||
out << YAML::Key << "slope" << YAML::Value << item.slope;
|
||||
out << YAML::Key << "reach" << YAML::Value << item.reach;
|
||||
out << YAML::Key << "rounding" << YAML::Value << item.rounding;
|
||||
if (item.open_start)
|
||||
out << YAML::Key << "open_start" << YAML::Value << true;
|
||||
if (item.open_end)
|
||||
out << YAML::Key << "open_end" << YAML::Value << true;
|
||||
out << YAML::Key << "points" << YAML::Value << YAML::BeginSeq;
|
||||
for (auto const &point : item.points)
|
||||
out << YAML::Flow << YAML::BeginSeq << point.position.x << point.position.y << point.formation << point.half_width << YAML::EndSeq;
|
||||
|
||||
@@ -130,6 +130,10 @@ struct modifier
|
||||
double slope{1.5}; // horizontal run of the slopes per metre of height
|
||||
double reach{60.0}; // from the edge of the formation, where the slopes end even if they don't get to the ground
|
||||
double rounding{1.5}; // over which the slopes bend into the formation and into the ground
|
||||
// the formation and its slopes end square across the first or the last point, as against the abutment of a bridge or the
|
||||
// portal of a tunnel; otherwise they go round it
|
||||
bool open_start{false};
|
||||
bool open_end{false};
|
||||
std::vector<corridor_point> points;
|
||||
|
||||
// rectangle (x, z) the modifier can change the ground in
|
||||
|
||||
Reference in New Issue
Block a user