diff --git a/application/editormode.h b/application/editormode.h index d68ef2fd..dfcb1324 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -45,6 +45,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include #include @@ -349,8 +350,25 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector left; // tracks whose grade line is still to be fitted std::vector 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::vector> 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::vector> laid; + // the points of all the paths as they were laid, for the crossings + std::map, std::vector> 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> 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::vector> 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 diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index 5b46320e..dff1cf4b 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -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, std::vector>> 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 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::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::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::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)}; diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 94d5896a..e0adf534 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -30,6 +30,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include 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> 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::vector> 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(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(std::floor(end.point.x)), static_cast(std::floor(end.point.z))}].push_back(end); + } + } + std::vector> result; + std::set> 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_track_places(double const Step) const @@ -240,6 +330,27 @@ std::vector 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(std::floor(place.position.x / kPlanCell))}; + auto const cz{static_cast(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 const *Only) double constexpr step{5.0}; std::pair previous{nullptr, -1}; bool broken{false}; + std::vector 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 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 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 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 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 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::vector> 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(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(std::floor(point.x)), static_cast(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> states; std::vector changed; - std::set> 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 { + 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 { + 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::max(); double height{0.0}; - auto const cx{static_cast(std::floor(Point.x / kCell))}; - auto const cz{static_cast(std::floor(Point.z / kCell))}; + auto const cx{static_cast(std::floor(Point.x / kPlanCell))}; + auto const cz{static_cast(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 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 open{{exiting, 0.0, 0.0}}; + std::set> 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, 2> sides{std::pair{kNear, nullptr}, std::pair{kNear, nullptr}}; + auto const along{glm::normalize(Sample.direction)}; + auto const cx{static_cast(std::floor(Sample.position.x / kPlanCell))}; + auto const cz{static_cast(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 sampled(count), model(count, std::numeric_limits::quiet_NaN()); + std::vector plan(count); std::vector 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::vector> const &Points) { + std::vector 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(std::floor(sample.position.x / kPlanCell))}; + auto const cz{static_cast(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 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::max()}; + auto last{std::numeric_limits::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::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::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(std::floor(sample.position.x / kCell)), static_cast(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(std::floor(sample.position.x / kPlanCell)), static_cast(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(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 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{}}; + 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(std::floor(plan[i].x / kPlanCell)), static_cast(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(agreeing) / static_cast(std::max(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(std::ceil(chord * 1.1)))}; + for (int i = 0; i <= count; ++i) + { + auto const t{static_cast(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(std::floor(at.x / kPlanCell)), static_cast(std::floor(at.z / kPlanCell))}].push_back( + {at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast(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 const fitted(state.done.begin(), state.done.end()); + auto const touches = [&](TTrack *Track) { + for (int path = 0; path < static_cast(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); diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 24e495f6..d7bbab48 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -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) diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp index b838d13d..9bbabf9b 100644 --- a/editor/editorProfile.cpp +++ b/editor/editorProfile.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -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 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 const &Chainages, std::vector const & return result; } +std::vector humps(std::vector const &Chainages, std::vector const &Ground, context const &Context, double const Tolerance, std::vector const &Tolerances) +{ + std::vector 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::lround((end - origin) / spacing)) + 1}; + std::vector level(cells, 0.0); + std::vector 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::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::quiet_NaN(); + auto const at = [&](std::ptrdiff_t const K) { return K >= 0 && K < cells ? level[K] : std::numeric_limits::quiet_NaN(); }; + auto const cells_of = [](double const Distance) { return static_cast(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 { + 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(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(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 const &Chainages, std::vector const &Ground, context const &Context, line const &Settings, ground_fit const &Options, std::vector const &Tolerances, std::vector const &Reaches) { @@ -527,27 +650,60 @@ line fit_ground(std::vector const &Chainages, std::vector const part[i] = at - static_cast(cell[i]); } auto const node = [&](std::size_t const K) { return origin + step * static_cast(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 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 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 bounds; for (auto const &bound : Options.bounds) { auto const at{std::clamp((bound.chainage - origin) / step, 0.0, static_cast(nodes - 1))}; auto const cell{std::min(nodes - 2, static_cast(at))}; - bounds.push_back({cell, at - static_cast(cell), std::min(bound.low, bound.high), std::max(bound.low, bound.high)}); + bounds.push_back({cell, at - static_cast(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::max()}; + auto high{std::numeric_limits::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 const &Chainages, std::vector 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 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 x(nodes); { @@ -628,15 +794,10 @@ line fit_ground(std::vector const &Chainages, std::vector 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 const &Chainages, std::vector 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 const &Chainages, std::vector 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 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 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 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 const &Chainages, std::vector 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 const &Chainages, std::vector const result.join_end = false; result.points.push_back({origin, x[0]}); std::vector rooms{0.0}; + std::vector 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 const &Chainages, std::vector 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 const &Chainages, std::vector 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 const &Chainages, std::vector 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 departures(line const &Line, std::vector const &C return result; } +std::vector bridged(std::vector const &Chainages, std::vector const &Heights, std::vector const &Ground, double const Depth, double const Steepest) +{ + std::vector 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 breaks(line const &Line) { std::vector result; diff --git a/editor/editorProfile.hpp b/editor/editorProfile.hpp index b2314086..5f1c42f7 100644 --- a/editor/editorProfile.hpp +++ b/editor/editorProfile.hpp @@ -115,19 +115,47 @@ struct recognition // grade line of existing track, from its elevations and grades sampled along the chainage line recognize(std::vector const &Chainages, std::vector const &Elevations, std::vector 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 humps(std::vector const &Chainages, std::vector const &Ground, context const &Context, double const Tolerance, std::vector 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 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 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 departures(line const &Line, std::vector const &Chainages, std::vector 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 bridged(std::vector const &Chainages, std::vector const &Heights, std::vector 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 breaks(line const &Line); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index ba5a65be..01b33559 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -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(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 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> 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::sample_route(route &Route, double const Step) { std::vector samples; @@ -2448,6 +2481,20 @@ bool editor_track::adjoining_grade(route const &Route, bool const Atend, double return true; } +std::vector editor_track::continuations(route_span const &Span) +{ + std::vector 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::apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &Created) { double constexpr margin{0.25}; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 8fc41f2b..b32de786 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -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 continuations(route_span const &Span); // path off the route whose end no longer met the adjoining path in height struct height_gap { diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 5b6cb1e2..54968b17 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -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" diff --git a/scene/heightmapterrain.cpp b/scene/heightmapterrain.cpp index e7b8f491..036db219 100644 --- a/scene/heightmapterrain.cpp +++ b/scene/heightmapterrain.cpp @@ -514,7 +514,10 @@ std::optional 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(); if (auto const rounding{entry["rounding"]}) item.rounding = rounding.as(); + if (auto const open{entry["open_start"]}) + item.open_start = open.as(); + if (auto const open{entry["open_end"]}) + item.open_end = open.as(); 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; diff --git a/scene/heightmapterrain.h b/scene/heightmapterrain.h index 707af808..95ca6316 100644 --- a/scene/heightmapterrain.h +++ b/scene/heightmapterrain.h @@ -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 points; // rectangle (x, z) the modifier can change the ground in