From 7371b189106c0891e9b265cc07cf76ac480da9f1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:14:03 +0200 Subject: [PATCH] feat(editor): vertical profile editor, grade lines kept in the scenery --- CMakeLists.txt | 2 + application/editormode.cpp | 4 + application/editormode.h | 68 ++ application/editormodeprofile.cpp | 1189 +++++++++++++++++++++++++++++ application/editormodetrack.cpp | 73 +- application/editorprojection.h | 63 ++ editor/editorAlignment.cpp | 14 - editor/editorProfile.cpp | 422 ++++++++++ editor/editorProfile.hpp | 119 +++ editor/editorTrack.cpp | 480 ++++++++++++ editor/editorTrack.hpp | 45 ++ scene/scenelayers.cpp | 116 ++- scene/scenelayers.h | 21 + scene/scenelayerssave.cpp | 51 +- 14 files changed, 2591 insertions(+), 76 deletions(-) create mode 100644 application/editormodeprofile.cpp create mode 100644 application/editorprojection.h create mode 100644 editor/editorProfile.cpp create mode 100644 editor/editorProfile.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fe5b1f2b..8900a0be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,10 +172,12 @@ set(SOURCES "editor/editorTerrain.cpp" "editor/editorTrack.cpp" "editor/editorAlignment.cpp" +"editor/editorProfile.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" +"application/editormodeprofile.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index dd8bafa2..3b83664d 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1021,6 +1021,8 @@ bool editor_mode::update() draw_build_overlay(); draw_track_hints(); render_track_window(); + draw_profile_overlay(); + render_profile_window(); } // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- @@ -1633,6 +1635,8 @@ void editor_mode::save() } } + if (m_profile.changed) + profile_store(); auto const result = scene::Layers.save(rootstatements); if (result.success && false == rootstatements.empty()) { diff --git a/application/editormode.h b/application/editormode.h index 19438feb..b517736c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorOrthophoto.hpp" #include "editor/editorTrack.hpp" #include "editor/editorAlignment.hpp" +#include "editor/editorProfile.hpp" #include #include @@ -477,6 +478,73 @@ class editor_mode : public application_mode void toggle_straight_set(TTrack &Track); void draw_track_hints(); bool m_route_tab{true}; + + // vertical profile (grade line) along a route + struct profile_state + { + bool open{false}; + TTrack *from{nullptr}; + TTrack *to{nullptr}; + TTrack *picked_from{nullptr}; + TTrack *picked_to{nullptr}; + double run_length{3000.0}; + editor_track::route route; + std::vector samples; + std::vector terrain; // NaN where there's no terrain + profile::line line; + profile::context context; + std::vector issues; + double origin{0.0}; // chainage of the start of the route + double departure{0.0}; // largest difference between the grade line and the track + double view_from{0.0}; + double view_to{100.0}; + double view_centre{0.0}; + float exaggeration{20.0f}; + bool show_terrain{true}; + bool show_track{true}; + bool show_plan{true}; + int selected{-1}; + int dragging{-1}; + int curve_grip{-1}; + bool panning{false}; + bool changed{false}; + double hover{-1.0}; + std::string status; + std::string error; + }; + profile_state m_profile; + // grade lines designed in the editor, kept in the scenery files as "//$p" comment lines + struct stored_profile + { + scene::layer_handle layer{null_handle}; + double length{0.0}; + glm::dvec3 start{0.0}, end{0.0}, middle{0.0}; // of the route, world space + double origin{0.0}; + profile::line line; + }; + std::vector m_profiles; + bool m_profiles_loaded{false}; + void profile_library_load(); + void profile_library_mark(std::vector Layers); + int profile_find(bool &Reversed) const; + bool profile_restore(); + void profile_store(); + void profile_forget(); + void profile_open_stored(std::size_t const Index); + void profile_edited(); + void render_profile_window(); + void profile_open(TTrack *From, TTrack *To); + void profile_open_run(TTrack &Track); + void profile_resample(); + void profile_recognize(); + void profile_check(); + void profile_apply(); + void profile_fit_view(); + void profile_after_undo(); + void render_profile_canvas(); + void render_profile_point(); + void draw_profile_overlay() const; + bool m_route_gizmo_using{false}; glm::mat4 m_route_gizmo{1.0f}; float m_track_snap_radius{1.0f}; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp new file mode 100644 index 00000000..8a6aa1e6 --- /dev/null +++ b/application/editormodeprofile.cpp @@ -0,0 +1,1189 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editorprojection.h" + +#include "world/Track.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + +ImU32 const kGradeLine{IM_COL32(90, 230, 110, 255)}; +ImU32 const kTangents{IM_COL32(255, 210, 60, 130)}; +ImU32 const kTrack{IM_COL32(190, 190, 190, 255)}; +ImU32 const kTerrain{IM_COL32(170, 120, 70, 255)}; +ImU32 const kGrid{IM_COL32(255, 255, 255, 22)}; +ImU32 const kLabel{IM_COL32(200, 200, 200, 200)}; +ImU32 const kSwitch{IM_COL32(160, 100, 220, 45)}; +ImU32 const kError{IM_COL32(255, 80, 70, 255)}; +ImU32 const kWarning{IM_COL32(255, 200, 60, 255)}; + +template +std::string format(char const *Format, Args... Arguments) +{ + char text[256]; + std::snprintf(text, sizeof(text), Format, Arguments...); + return text; +} + +double nice_step(double const Approximate) +{ + auto const power{std::pow(10.0, std::floor(std::log10(std::max(Approximate, 1e-6))))}; + for (auto const factor : {1.0, 2.0, 5.0, 10.0}) + if (power * factor >= Approximate) + return power * factor; + return power * 10.0; +} + +glm::dvec3 position_at(std::vector const &Samples, double const Chainage) +{ + auto const next{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + if (next == Samples.begin()) + return Samples.front().position; + if (next == Samples.end()) + return Samples.back().position; + auto const &before{*std::prev(next)}; + auto const run{next->chainage - before.chainage}; + return run > 1e-9 ? glm::mix(before.position, next->position, (Chainage - before.chainage) / run) : before.position; +} + +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"}; + +double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return std::hypot(A.x - B.x, A.z - B.z); +} + +profile::line reversed_line(profile::line Line, double const Length) +{ + std::reverse(Line.points.begin(), Line.points.end()); + for (auto &point : Line.points) + point.chainage = Length - point.chainage; + std::swap(Line.join_start, Line.join_end); + return Line; +} + +bool end_locked(profile::line const &Line, profile::context const &Context, int const Index) +{ + if (Index == 0) + return Line.join_start && Context.start_joined; + if (Index + 1 == static_cast(Line.points.size())) + return Line.join_end && Context.end_joined; + return false; +} + +bool has_errors(std::vector const &Issues) +{ + return std::any_of(Issues.begin(), Issues.end(), [](profile::issue const &Issue) { return Issue.error; }); +} + +} + +// format of the lines, coordinates in the space of the file: +// profile +// speed origin join +// norms = k*V^2> +// point +// end +void editor_mode::profile_library_load() +{ + if (m_profiles_loaded) + return; + m_profiles_loaded = true; + m_profiles.clear(); + for (auto const layer : scene::Layers.marked_layers(kProfileMark)) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const height = [&](double const Local) { return context.to_world({0.0, Local, 0.0}).y; }; + std::optional current; + for (auto const &text : scene::Layers.marked(layer, kProfileMark)) + { + std::istringstream line{text}; + std::string keyword; + line >> keyword; + if (keyword == "profile") + { + stored_profile entry; + entry.layer = layer; + glm::dvec3 start, end, middle; + line >> entry.length >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z >> middle.x >> middle.y >> middle.z; + if (line.fail()) + { + current.reset(); + continue; + } + entry.start = context.to_world(start); + entry.end = context.to_world(end); + entry.middle = context.to_world(middle); + current = entry; + continue; + } + if (false == current.has_value()) + continue; + auto &entry{*current}; + std::string word; + if (keyword == "speed") + { + int start{1}, end{1}; + line >> entry.line.speed >> word >> entry.origin >> word >> start >> end; + entry.line.join_start = (start != 0); + entry.line.join_end = (end != 0); + } + else if (keyword == "norms") + { + auto &norms{entry.line.norms}; + line >> norms.grade_max >> norms.grade_switch_max >> norms.radius_factor >> norms.radius_min >> norms.curve_threshold >> norms.element_min >> norms.fixed_tolerance; + } + else if (keyword == "point") + { + profile::pvi point; + int automatic{0}; + line >> point.chainage >> point.elevation >> point.radius >> automatic; + if (line.fail()) + continue; + point.elevation = height(point.elevation); + point.automatic = (automatic != 0); + entry.line.points.push_back(point); + } + else if (keyword == "end") + { + if (entry.line.points.size() >= 2 && entry.length > 0.0) + m_profiles.push_back(entry); + current.reset(); + } + } + } + if (false == m_profiles.empty()) + WriteLog("Editor: " + std::to_string(m_profiles.size()) + " vertical profile design(s) read from the scenery", logtype::generic); +} + +void editor_mode::profile_library_mark(std::vector Layers) +{ + std::sort(Layers.begin(), Layers.end()); + Layers.erase(std::unique(Layers.begin(), Layers.end()), Layers.end()); + for (auto const layer : Layers) + { + if (false == scene::Layers.valid(layer)) + continue; + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const local = [&](glm::dvec3 const &Point) { + auto const point{context.to_local(Point)}; + return format("%.4f %.4f %.4f", point.x, point.y, point.z); + }; + std::vector lines; + for (auto const &entry : m_profiles) + { + if (entry.layer != layer) + continue; + if (lines.empty()) + lines.emplace_back("# vertical profiles (grade lines) designed in the track editor, the simulation skips these lines"); + lines.push_back(format("profile %.4f ", entry.length) + local(entry.start) + ' ' + local(entry.end) + ' ' + local(entry.middle)); + lines.push_back(format("speed %.1f origin %.4f join %d %d", entry.line.speed, entry.origin, entry.line.join_start ? 1 : 0, entry.line.join_end ? 1 : 0)); + auto const &norms{entry.line.norms}; + lines.push_back(format("norms %.4f %.4f %.4f %.1f %.4f %.2f %.4f", norms.grade_max, norms.grade_switch_max, norms.radius_factor, norms.radius_min, norms.curve_threshold, norms.element_min, norms.fixed_tolerance)); + for (auto const &point : entry.line.points) + lines.push_back(format("point %.4f %.4f %.1f %d", point.chainage, context.to_local({0.0, point.elevation, 0.0}).y, point.radius, point.automatic ? 1 : 0)); + lines.emplace_back("end"); + } + scene::Layers.mark(layer, kProfileMark, std::move(lines)); + } +} + +int editor_mode::profile_find(bool &Reversed) const +{ + auto const &state{m_profile}; + Reversed = false; + if (state.samples.empty()) + return -1; + auto const start{state.samples.front().position}; + auto const end{state.samples.back().position}; + auto const middle{position_at(state.samples, state.route.length * 0.5)}; + for (std::size_t i = 0; i < m_profiles.size(); ++i) + { + auto const &entry{m_profiles[i]}; + if (std::abs(entry.length - state.route.length) > 0.5 || plan_distance(entry.middle, middle) > 0.5) + continue; + if (plan_distance(entry.start, start) <= 0.1 && plan_distance(entry.end, end) <= 0.1) + return static_cast(i); + if (plan_distance(entry.start, end) <= 0.1 && plan_distance(entry.end, start) <= 0.1) + { + Reversed = true; + return static_cast(i); + } + } + return -1; +} + +bool editor_mode::profile_restore() +{ + profile_library_load(); + auto &state{m_profile}; + bool reversed; + auto const index{profile_find(reversed)}; + if (index < 0) + return false; + auto const &entry{m_profiles[index]}; + state.line = reversed ? reversed_line(entry.line, entry.length) : entry.line; + state.origin = entry.origin; + state.selected = -1; + state.changed = false; + profile_check(); + state.status = "Design read from the scenery file " + scene::Layers.layer(entry.layer).name + format(", largest departure from the track %.3f m", state.departure); + return true; +} + +void editor_mode::profile_store() +{ + auto &state{m_profile}; + if (state.route.spans.empty() || state.samples.empty() || state.line.points.size() < 2) + return; + if (scene::Layers.empty()) + { + state.error = "The scenery wasn't opened for editing, the design can't be kept in its files"; + return; + } + profile_library_load(); + stored_profile entry; + entry.length = state.route.length; + entry.start = state.samples.front().position; + entry.end = state.samples.back().position; + entry.middle = position_at(state.samples, state.route.length * 0.5); + entry.origin = state.origin; + entry.line = state.line; + auto &points{entry.line.points}; + points.erase(std::remove_if(points.begin(), points.end(), [](profile::pvi const &Point) { return Point.joint; }), points.end()); + + bool reversed; + auto const index{profile_find(reversed)}; + if (index >= 0) + { + entry.layer = m_profiles[index].layer; + m_profiles[index] = entry; + } + else + { + auto layer{scene::Layers.resolve(state.route.spans.front().track->layer())}; + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + if (false == scene::Layers.valid(layer)) + layer = 1; + entry.layer = layer; + m_profiles.push_back(entry); + } + profile_library_mark({entry.layer}); + state.changed = false; +} + +void editor_mode::profile_forget() +{ + auto &state{m_profile}; + profile_library_load(); + bool reversed; + auto const index{profile_find(reversed)}; + if (index < 0) + return; + auto const layer{m_profiles[index].layer}; + m_profiles.erase(m_profiles.begin() + index); + profile_library_mark({layer}); + state.status = "The design will be removed from " + scene::Layers.layer(layer).name + " on save"; +} + +void editor_mode::profile_open_stored(std::size_t const Index) +{ + auto &state{m_profile}; + if (Index >= m_profiles.size()) + return; + auto const entry{m_profiles[Index]}; + auto const touching = [](glm::dvec3 const &Point) { + std::vector result; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == editor_track::is_supported(*track)) + continue; + for (auto const &path : track->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + if (plan_distance(path.points[index], Point) <= 0.1 && std::abs(path.points[index].y - Point.y) < 3.0) + { + if (std::find(result.begin(), result.end(), track) == result.end()) + result.push_back(track); + } + } + return result; + }; + for (auto *from : touching(entry.start)) + for (auto *to : touching(entry.end)) + { + editor_track::route route; + std::string error; + if (false == editor_track::find_route(from, to, route, error) || std::abs(route.length - entry.length) > 0.5) + continue; + auto const samples{editor_track::sample_route(route, 1.0)}; + if (samples.empty() || plan_distance(samples.front().position, entry.start) > 0.1 || plan_distance(position_at(samples, route.length * 0.5), entry.middle) > 0.5) + continue; + profile_open(from, to); + return; + } + state.open = true; + state.error = "The route of the stored design isn't in the scenery any more (paths moved or removed)"; +} + +void editor_mode::profile_edited() +{ + m_profile.changed = true; + profile_check(); +} + +void editor_mode::profile_open(TTrack *From, TTrack *To) +{ + auto &state{m_profile}; + state.open = true; + state.status.clear(); + editor_track::route route; + if (false == editor_track::find_route(From, To, route, state.error)) + return; + state.from = state.picked_from = From; + state.to = state.picked_to = To; + state.route = std::move(route); + state.selected = -1; + profile_resample(); + if (false == profile_restore()) + profile_recognize(); + profile_fit_view(); +} + +void editor_mode::profile_open_run(TTrack &Track) +{ + auto &state{m_profile}; + state.open = true; + state.status.clear(); + state.error.clear(); + auto route{editor_track::run_route(Track, state.run_length)}; + if (route.spans.empty()) + { + state.error = "The path can't carry a profile"; + return; + } + state.route = std::move(route); + state.from = state.picked_from = state.route.spans.front().track; + state.to = state.picked_to = state.route.spans.back().track; + state.selected = -1; + profile_resample(); + if (false == profile_restore()) + profile_recognize(); + profile_fit_view(); +} + +void editor_mode::profile_resample() +{ + auto &state{m_profile}; + state.samples = editor_track::sample_route(state.route, 1.0); + auto const &samples{state.samples}; + state.terrain.assign(samples.size(), std::numeric_limits::quiet_NaN()); + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const &position{samples[i].position}; + auto *patch{terrain_at(position.x, position.z)}; + if (patch != nullptr && patch->contains(position.x, position.z)) + state.terrain[i] = patch->height_at(position.x, position.z); + } + + auto &context{state.context}; + context = {}; + context.length = state.route.length; + if (samples.empty()) + return; + for (auto const &span : state.route.spans) + if (span.track->eType != tt_Normal) + context.switches.push_back({span.from, span.to, span.track->name()}); + context.start_elevation = samples.front().position.y; + context.end_elevation = samples.back().position.y; + context.start_joined = editor_track::adjoining_grade(state.route, false, context.start_grade); + context.end_joined = editor_track::adjoining_grade(state.route, true, context.end_grade); + profile_check(); +} + +void editor_mode::profile_recognize() +{ + auto &state{m_profile}; + std::vector chainages, elevations, grades; + for (auto const &sample : state.samples) + { + chainages.push_back(sample.chainage); + elevations.push_back(sample.position.y); + grades.push_back(sample.grade); + } + 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.selected = -1; + profile_check(); + state.status = format("Grade line recognized from the track: %zu points, largest departure %.3f m", state.line.points.size(), state.departure); +} + +void editor_mode::profile_check() +{ + auto &state{m_profile}; + auto &points{state.line.points}; + // joint points come and go, the selection follows the point it was made on + struct mark + { + int *index; + double chainage; + bool joint; + }; + std::vector marks; + for (auto *index : {&state.selected, &state.dragging, &state.curve_grip}) + if (*index >= 0 && *index < static_cast(points.size())) + marks.push_back({index, points[*index].chainage, points[*index].joint}); + profile::complete(state.line, state.context); + for (auto const &mark : marks) + { + int best{-1}; + double distance{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(points.size()); ++i) + if (points[i].joint == mark.joint && std::abs(points[i].chainage - mark.chainage) < distance) + { + distance = std::abs(points[i].chainage - mark.chainage); + best = i; + } + *mark.index = (mark.joint || distance < 1e-6) ? best : -1; + } + state.issues = profile::check(state.line, state.context); + state.departure = 0.0; + for (auto const &sample : state.samples) + state.departure = std::max(state.departure, std::abs(profile::elevation(state.line, sample.chainage) - sample.position.y)); +} + +void editor_mode::profile_fit_view() +{ + auto &state{m_profile}; + auto const length{std::max(1.0, state.route.length)}; + state.view_from = -0.02 * length; + state.view_to = length * 1.02; + state.exaggeration = 0.0f; +} + +void editor_mode::profile_apply() +{ + auto &state{m_profile}; + state.status.clear(); + state.error.clear(); + if (state.route.spans.empty() || state.line.points.size() < 2) + return; + if (has_errors(state.issues)) + { + state.error = "Correct the errors to apply the grade line"; + return; + } + auto const line{state.line}; + std::vector> states; + std::vector created; + auto const gaps{editor_track::apply_profile( + state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, profile::breaks(line), states, created)}; + auto const changed{states.size()}; + auto const added{created.size()}; + push_track_snapshot(std::move(states), std::move(created)); + state.from = state.route.spans.front().track; + state.to = state.route.spans.back().track; + profile_resample(); + profile_store(); + state.status = format("Grade line applied: %zu paths changed, %zu added, largest departure %.3f m", changed, added, state.departure); + if (false == scene::Layers.empty()) + state.status += "\nThe paths and the design (//$p lines) go to the scenery files on save"; + if (false == gaps.empty()) + state.status += "\nSwitch ends off the adjoining paths, to be corrected:"; + for (auto const &gap : gaps) + state.status += format("\n %s -> %s: %+.3f m", gap.track->name().c_str(), gap.neighbour->name().c_str(), gap.gap); + WriteLog("Editor: vertical profile - " + state.status, logtype::generic); +} + +void editor_mode::profile_after_undo() +{ + auto &state{m_profile}; + if (state.route.spans.empty()) + return; + auto const removed = [](TTrack const *Track) { return Track == nullptr || Track->m_editorremoved; }; + if (removed(state.from) || removed(state.to)) + { + state.from = state.picked_from; + state.to = state.picked_to; + } + std::string error; + if (removed(state.from) || removed(state.to) || false == editor_track::find_route(state.from, state.to, state.route, error)) + { + state.route = {}; + state.samples.clear(); + state.selected = state.dragging = state.curve_grip = -1; + return; + } + profile_resample(); +} + +void editor_mode::render_profile_window() +{ + auto &state{m_profile}; + if (false == state.open) + return; + ImGui::SetNextWindowSize(ImVec2(960.0f, 640.0f), ImGuiCond_FirstUseEver); + if (false == ImGui::Begin("Vertical profile###trackprofile", &state.open)) + { + ImGui::End(); + return; + } + auto *selected{selected_track()}; + if (ImGui::Button("Along the line from the selected path") && selected != nullptr) + profile_open_run(*selected); + ImGui::SameLine(); + ImGui::PushItemWidth(70.0f); + ImGui::InputDouble("m each way##profilerun", &state.run_length, 0.0, 0.0, "%.0f"); + ImGui::PopItemWidth(); + state.run_length = std::clamp(state.run_length, 10.0, 50000.0); + ImGui::SameLine(); + if (ImGui::Button("Start: selected path") && selected != nullptr) + state.picked_from = selected; + ImGui::SameLine(); + if (ImGui::Button("End: selected path") && selected != nullptr && state.picked_from != nullptr) + profile_open(state.picked_from, selected); + profile_library_load(); + if (false == m_profiles.empty() && ImGui::TreeNode("##profilestored", "Designs kept in the scenery (%zu)", m_profiles.size())) + { + for (std::size_t i = 0; i < m_profiles.size(); ++i) + { + auto const &entry{m_profiles[i]}; + ImGui::PushID(static_cast(i)); + if (ImGui::SmallButton("Open")) + profile_open_stored(i); + ImGui::SameLine(); + ImGui::Text("%s %.1f m, %zu points, from (%.1f, %.1f) to (%.1f, %.1f)", scene::Layers.layer(entry.layer).name.c_str(), entry.length, entry.line.points.size(), entry.start.x, entry.start.z, entry.end.x, entry.end.z); + ImGui::PopID(); + } + ImGui::TreePop(); + } + if (state.route.spans.empty()) + { + ImGui::TextDisabled("Select a path in the viewport, then build the route along the line, or set its start and end paths (switches are passed through)"); + if (state.picked_from != nullptr) + ImGui::Text("Start: %s", state.picked_from->name().c_str()); + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); + ImGui::End(); + return; + } + ImGui::Text("Route: %zu paths, %.2f m, %s ... %s", state.route.spans.size(), state.route.length, state.route.spans.front().track->name().c_str(), state.route.spans.back().track->name().c_str()); + { + bool reversed; + auto const stored{profile_find(reversed)}; + if (stored >= 0) + { + ImGui::TextDisabled("Design kept in %s%s", scene::Layers.layer(m_profiles[stored].layer).name.c_str(), state.changed ? ", changes go there on save" : ""); + ImGui::SameLine(); + if (ImGui::SmallButton("Forget the design")) + profile_forget(); + } + else if (state.changed) + ImGui::TextDisabled("The design goes to the scenery file of the route on save"); + } + + auto &line{state.line}; + bool changed{false}; + ImGui::PushItemWidth(90.0f); + ImGui::InputDouble("Chainage of the start (m)", &state.origin, 0.0, 0.0, "%.2f"); + ImGui::SameLine(); + changed |= ImGui::InputDouble("Design speed (km/h)", &line.speed, 0.0, 0.0, "%.0f"); + ImGui::SameLine(); + ImGui::SliderFloat("Exaggeration##profile", &state.exaggeration, 1.0f, 500.0f, "1:%.0f", 2.0f); + ImGui::PopItemWidth(); + line.speed = std::max(1.0, line.speed); + ImGui::Checkbox("Track", &state.show_track); + ImGui::SameLine(); + ImGui::Checkbox("Terrain", &state.show_terrain); + ImGui::SameLine(); + ImGui::Checkbox("Plan curvature", &state.show_plan); + ImGui::SameLine(); + if (ImGui::Button("Fit the view")) + profile_fit_view(); + ImGui::SameLine(); + if (ImGui::Button("Recognize from the track")) + { + profile_recognize(); + state.changed = true; + } + ImGui::SameLine(); + if (ImGui::Button("Apply to the scenery")) + profile_apply(); + if (ImGui::TreeNode("Limits##profile")) + { + auto &norms{line.norms}; + ImGui::PushItemWidth(90.0f); + changed |= ImGui::InputDouble("Maximum grade (per mille)", &norms.grade_max, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Maximum grade over switches (per mille)", &norms.grade_switch_max, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Vertical curve R >= k*V^2, k", &norms.radius_factor, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Vertical curve R min (m)", &norms.radius_min, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble("Vertical curve needed above a change of (per mille)", &norms.curve_threshold, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Constant grade between curves at least (m)", &norms.element_min, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble("Tolerance at fixed points (m)", &norms.fixed_tolerance, 0.0, 0.0, "%.3f"); + ImGui::PopItemWidth(); + if (ImGui::SmallButton("Automatic R at all points")) + { + for (auto &point : line.points) + point.automatic = true; + changed = true; + } + ImGui::TextDisabled("Required R for %.0f km/h: %.0f m", line.speed, profile::required_radius(line)); + ImGui::TreePop(); + } + if (state.context.start_joined) + changed |= ImGui::Checkbox("Join the adjoining track at the start", &line.join_start); + if (state.context.start_joined && state.context.end_joined) + ImGui::SameLine(); + if (state.context.end_joined) + changed |= ImGui::Checkbox("Join the adjoining track at the end", &line.join_end); + if ((state.context.start_joined || state.context.end_joined) && ImGui::IsItemHovered()) + ImGui::SetTooltip("Keeps the elevation of the joint and takes over the grade of the adjoining track,\n" + "with a vertical curve which starts right at the joint"); + if (changed) + profile_edited(); + + render_profile_canvas(); + + ImGui::Columns(2, "##profilecolumns", true); + render_profile_point(); + ImGui::NextColumn(); + ImGui::Text("Largest departure from the track: %.3f m", state.departure); + ImGui::BeginChild("##profileissues", ImVec2(0.0f, 0.0f), false); + if (state.issues.empty()) + ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "The grade line meets the limits"); + for (std::size_t i = 0; i < state.issues.size(); ++i) + { + auto const &issue{state.issues[i]}; + ImGui::PushID(static_cast(i)); + ImGui::PushStyleColor(ImGuiCol_Text, issue.error ? ImVec4(1.0f, 0.4f, 0.3f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f)); + if (ImGui::Selectable(issue.text.c_str())) + { + if (issue.point >= 0) + state.selected = issue.point; + auto const half{(state.view_to - state.view_from) * 0.5}; + state.view_from = issue.chainage - half; + state.view_to = issue.chainage + half; + } + ImGui::PopStyleColor(); + ImGui::PopID(); + } + if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); + ImGui::EndChild(); + ImGui::Columns(1); + ImGui::End(); +} + +void editor_mode::render_profile_point() +{ + auto &state{m_profile}; + auto &points{state.line.points}; + bool changed{false}; + auto const count{static_cast(points.size())}; + if (state.selected >= count) + state.selected = -1; + if (state.selected < 0) + { + ImGui::TextDisabled("LMB: select and drag a point (Ctrl: height only, Shift: keep the grade before)\n" + "Green grip on a curve: drag to set R Double click on the line: insert a point\n" + "Del: remove the point Wheel: zoom Ctrl+wheel: exaggeration RMB/MMB: pan"); + } + else if (points[state.selected].joint) + { + auto const index{state.selected}; + auto const &point{points[index]}; + auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0}; + ImGui::Text("Point %d: joint with the adjoining track (placed automatically)", index + 1); + ImGui::Text("km %s H %.3f R %.0f", profile::format_chainage(point.chainage + state.origin).c_str(), point.elevation, point.radius); + ImGui::Text("Grade %+.3f -> %+.3f per mille (%s)", profile::grade_after(state.line, index - 1) * 1000.0, profile::grade_after(state.line, index) * 1000.0, change > 0.0 ? "sag" : "crest"); + ImGui::TextDisabled("Moves with the neighbouring point; untick the joining to edit the end freely"); + } + else + { + auto const index{state.selected}; + auto &point{points[index]}; + bool const end{index == 0 || index == count - 1}; + bool const locked{end_locked(state.line, state.context, index)}; + ImGui::Text("Point %d%s", index + 1, locked ? " (end, joined to the adjoining track)" : end ? " (end of the route)" : ""); + ImGui::PushItemWidth(110.0f); + auto chainage{point.chainage + state.origin}; + if (ImGui::InputDouble("Chainage (m)", &chainage, 0.0, 0.0, "%.2f") && false == end) + { + point.chainage = std::clamp(chainage - state.origin, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); + changed = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("km %s", profile::format_chainage(point.chainage + state.origin).c_str()); + changed |= ImGui::InputDouble("Elevation (m)", &point.elevation, 0.0, 0.0, "%.3f", locked ? ImGuiInputTextFlags_ReadOnly : 0); + if (index > 0 && false == locked) + { + auto grade{profile::grade_after(state.line, index - 1) * 1000.0}; + if (ImGui::InputDouble("Grade from the previous (per mille)", &grade, 0.0, 0.0, "%.3f")) + { + point.elevation = points[index - 1].elevation + grade / 1000.0 * (point.chainage - points[index - 1].chainage); + changed = true; + } + } + if (index + 1 < count) + { + auto grade{profile::grade_after(state.line, index) * 1000.0}; + if (ImGui::InputDouble("Grade to the next (per mille)", &grade, 0.0, 0.0, "%.3f")) + { + points[index + 1].elevation = point.elevation + grade / 1000.0 * (points[index + 1].chainage - point.chainage); + changed = true; + } + } + if (false == end) + { + if (ImGui::InputDouble("Vertical curve R (m, 0: none)", &point.radius, 0.0, 0.0, "%.0f")) + { + point.automatic = false; + changed = true; + } + point.radius = std::max(0.0, point.radius); + ImGui::SameLine(); + changed |= ImGui::Checkbox("Auto", &point.automatic); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("R follows the limits: the required radius where the change of grade needs a curve"); + auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0}; + auto const tangent{point.radius * std::abs(change) / 2000.0}; + ImGui::Text("Change of grade %+.3f per mille, %s", change, change > 0.0 ? "sag" : "crest"); + if (point.radius > 0.0) + ImGui::Text("Tangent T %.2f m, sagitta f %.3f m", tangent, tangent * tangent / (2.0 * point.radius)); + } + ImGui::PopItemWidth(); + if (index + 1 < count && ImGui::SmallButton("Insert a point halfway to the next")) + { + profile::pvi inserted; + inserted.chainage = (point.chainage + points[index + 1].chainage) * 0.5; + inserted.elevation = (point.elevation + points[index + 1].elevation) * 0.5; + points.insert(points.begin() + index + 1, inserted); + state.selected = index + 1; + changed = true; + } + if (false == end) + { + ImGui::SameLine(); + if (ImGui::SmallButton("Remove the point")) + { + points.erase(points.begin() + index); + state.selected = -1; + changed = true; + } + } + } + ImGui::BeginChild("##profilepoints", ImVec2(0.0f, 0.0f), true); + for (int i = 0; i < static_cast(points.size()); ++i) + { + auto const &point{points[i]}; + auto const label{format("%2d km %s H %.3f %s##point%d", i + 1, profile::format_chainage(point.chainage + state.origin).c_str(), point.elevation, point.radius > 0.0 ? format("R %.0f%s", point.radius, point.joint ? " joint" : point.automatic ? " auto" : "").c_str() : "", i)}; + if (ImGui::Selectable(label.c_str(), state.selected == i)) + state.selected = (state.selected == i ? -1 : i); + } + ImGui::EndChild(); + if (changed) + profile_edited(); +} + +void editor_mode::render_profile_canvas() +{ + auto &state{m_profile}; + auto &line{state.line}; + auto &points{line.points}; + auto const &samples{state.samples}; + ImGuiIO const &io{ImGui::GetIO()}; + + auto const available{ImGui::GetContentRegionAvail()}; + ImVec2 const size{std::max(200.0f, available.x), std::max(240.0f, available.y * 0.6f)}; + ImVec2 const corner{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##profilecanvas", size); + bool const hovered{ImGui::IsItemHovered()}; + ImDrawList *draw{ImGui::GetWindowDrawList()}; + + float const strip{state.show_plan ? 44.0f : 0.0f}; + float const top{18.0f}; + float const height{size.y - strip - top}; + auto const width{std::max(1.0, state.view_to - state.view_from)}; + auto const across{width / size.x}; + if (state.exaggeration <= 0.0f) + { + double low{std::numeric_limits::max()}, high{std::numeric_limits::lowest()}; + for (auto const &sample : samples) + { + low = std::min(low, sample.position.y); + high = std::max(high, sample.position.y); + } + for (auto const &point : points) + { + low = std::min(low, point.elevation); + high = std::max(high, point.elevation); + } + if (low > high) + low = high = 0.0; + auto const range{std::max(1.0, (high - low) * 1.3)}; + state.exaggeration = static_cast(std::clamp(across / (range / height), 1.0, 500.0)); + state.view_centre = (low + high) * 0.5; + } + auto const vertical{across / state.exaggeration}; + auto const x_of = [&](double const Chainage) { return corner.x + static_cast((Chainage - state.view_from) / across); }; + auto const y_of = [&](double const Height) { return corner.y + top + height * 0.5f - static_cast((Height - state.view_centre) / vertical); }; + auto const chainage_of = [&](float const X) { return state.view_from + (X - corner.x) * across; }; + auto const height_of = [&](float const Y) { return state.view_centre - (Y - corner.y - top - height * 0.5f) * vertical; }; + ImVec2 const mouse{io.MousePos}; + + if (hovered && io.MouseWheel != 0.0f) + { + if (io.KeyCtrl) + state.exaggeration = std::clamp(state.exaggeration * std::pow(1.2f, io.MouseWheel), 1.0f, 500.0f); + else + { + auto const anchor{chainage_of(mouse.x)}; + auto const factor{std::pow(0.8, static_cast(io.MouseWheel))}; + state.view_from = anchor - (anchor - state.view_from) * factor; + state.view_to = anchor + (state.view_to - anchor) * factor; + } + } + if (hovered && (ImGui::IsMouseClicked(1) || ImGui::IsMouseClicked(2))) + state.panning = true; + if (state.panning) + { + if (ImGui::IsMouseDown(1) || ImGui::IsMouseDown(2)) + { + state.view_from -= io.MouseDelta.x * across; + state.view_to -= io.MouseDelta.x * across; + state.view_centre += io.MouseDelta.y * vertical; + } + else + state.panning = false; + } + + auto const curves{profile::curves(line)}; + auto const hit_point = [&]() { + int hit{-1}; + float best{64.0f}; + for (int i = 0; i < static_cast(points.size()); ++i) + { + if (points[i].joint) + continue; + auto const dx{x_of(points[i].chainage) - mouse.x}; + auto const dy{y_of(points[i].elevation) - mouse.y}; + if (dx * dx + dy * dy < best) + { + best = dx * dx + dy * dy; + hit = i; + } + } + return hit; + }; + auto const hit_grip = [&]() { + for (auto const &c : curves) + { + if (points[c.point].joint) + continue; + auto const chainage{points[c.point].chainage}; + auto const dx{x_of(chainage) - mouse.x}; + auto const dy{y_of(profile::elevation(line, chainage)) - mouse.y}; + if (dx * dx + dy * dy < 64.0f) + return static_cast(c.point); + } + return -1; + }; + bool changed{false}; + if (hovered && ImGui::IsMouseClicked(0)) + { + auto const point{hit_point()}; + auto const grip{point < 0 ? hit_grip() : -1}; + if (point >= 0) + state.selected = state.dragging = point; + else if (grip >= 0) + state.selected = state.curve_grip = grip; + else + state.selected = -1; + } + if (hovered && ImGui::IsMouseDoubleClicked(0) && state.dragging < 0 && state.curve_grip < 0 && points.size() >= 2) + { + auto const chainage{chainage_of(mouse.x)}; + if (chainage > points.front().chainage + 0.5 && chainage < points.back().chainage - 0.5 && std::abs(y_of(profile::elevation(line, chainage)) - mouse.y) < 12.0f) + { + std::size_t index{0}; + while (index + 2 < points.size() && points[index + 1].chainage < chainage) + ++index; + profile::pvi inserted; + inserted.chainage = std::round(chainage * 100.0) / 100.0; + inserted.elevation = points[index].elevation + profile::grade_after(line, index) * (inserted.chainage - points[index].chainage); + points.insert(points.begin() + index + 1, inserted); + state.selected = static_cast(index) + 1; + changed = true; + } + } + if (state.dragging >= 0 && state.dragging < static_cast(points.size())) + { + if (ImGui::IsMouseDown(0)) + { + auto const index{state.dragging}; + auto &point{points[index]}; + bool const end{index == 0 || index + 1 == static_cast(points.size())}; + auto const incoming{index > 0 ? profile::grade_after(line, index - 1) : 0.0}; + if (false == end && false == io.KeyCtrl) + point.chainage = std::clamp(std::round(chainage_of(mouse.x) * 100.0) / 100.0, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); + if (end_locked(line, state.context, index)) + ; + else if (io.KeyShift && index > 0) + point.elevation = points[index - 1].elevation + incoming * (point.chainage - points[index - 1].chainage); + else + point.elevation = std::round(height_of(mouse.y) * 1000.0) / 1000.0; + changed = true; + } + else + state.dragging = -1; + } + else + state.dragging = -1; + if (state.curve_grip > 0 && state.curve_grip + 1 < static_cast(points.size())) + { + if (ImGui::IsMouseDown(0)) + { + auto const sagitta{height_of(mouse.y) - points[state.curve_grip].elevation}; + points[state.curve_grip].radius = std::max(10.0, std::round(profile::radius_for_sagitta(line, state.curve_grip, sagitta) / 10.0) * 10.0); + points[state.curve_grip].automatic = false; + changed = true; + } + else + state.curve_grip = -1; + } + else + state.curve_grip = -1; + if ((hovered || ImGui::IsWindowFocused()) && state.selected > 0 && state.selected + 1 < static_cast(points.size()) && false == points[state.selected].joint && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete))) + { + points.erase(points.begin() + state.selected); + state.selected = -1; + changed = true; + } + if (changed) + profile_edited(); + state.hover = hovered ? chainage_of(mouse.x) : -1.0; + + ImVec2 const bottomright{corner.x + size.x, corner.y + size.y}; + draw->AddRectFilled(corner, bottomright, IM_COL32(22, 24, 28, 255)); + draw->PushClipRect(corner, bottomright, true); + auto const plotbottom{corner.y + top + height}; + + auto const step{nice_step(across * 110.0)}; + for (auto s = std::ceil((state.view_from + state.origin) / step) * step - state.origin; s <= state.view_to; s += step) + { + auto const x{x_of(s)}; + draw->AddLine(ImVec2(x, corner.y + top), ImVec2(x, plotbottom), kGrid); + draw->AddText(ImVec2(x + 2.0f, corner.y + 2.0f), kLabel, profile::format_chainage(s + state.origin).c_str()); + } + auto const heightstep{nice_step(vertical * 45.0)}; + for (auto h = std::floor(height_of(plotbottom) / heightstep) * heightstep; h <= height_of(corner.y + top); h += heightstep) + { + auto const y{y_of(h)}; + draw->AddLine(ImVec2(corner.x, y), ImVec2(bottomright.x, y), kGrid); + draw->AddText(ImVec2(corner.x + 2.0f, y - 14.0f), kLabel, format("%.1f", h).c_str()); + } + for (auto const &zone : state.context.switches) + { + draw->AddRectFilled(ImVec2(x_of(zone.from), corner.y + top), ImVec2(std::max(x_of(zone.to), x_of(zone.from) + 2.0f), plotbottom), kSwitch); + draw->AddText(ImVec2(x_of(zone.from) + 2.0f, corner.y + top + 2.0f), IM_COL32(200, 160, 255, 220), zone.name.c_str()); + } + + auto const polyline = [&](auto const &Height, ImU32 const Colour, float const Thickness) { + ImVec2 previous{}; + bool open{false}; + float lastx{-1e9f}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const x{x_of(samples[i].chainage)}; + auto const h{Height(i)}; + if (std::isnan(h) || x < corner.x - 50.0f || x > bottomright.x + 50.0f) + { + open = false; + continue; + } + if (open && x - lastx < 1.0f && i + 1 < samples.size()) + continue; + ImVec2 const next{x, y_of(h)}; + if (open) + draw->AddLine(previous, next, Colour, Thickness); + previous = next; + lastx = x; + open = true; + } + }; + if (state.show_terrain) + polyline([&](std::size_t const I) { return state.terrain[I]; }, kTerrain, 1.5f); + if (state.show_track) + polyline([&](std::size_t const I) { return samples[I].position.y; }, kTrack, 1.5f); + + for (std::size_t i = 0; i + 1 < points.size(); ++i) + draw->AddLine(ImVec2(x_of(points[i].chainage), y_of(points[i].elevation)), ImVec2(x_of(points[i + 1].chainage), y_of(points[i + 1].elevation)), kTangents, 1.0f); + { + auto const from{std::max(points.front().chainage, chainage_of(corner.x))}; + auto const to{std::min(points.back().chainage, chainage_of(bottomright.x))}; + ImVec2 previous{x_of(from), y_of(profile::elevation(line, from))}; + for (auto x = previous.x + 2.0f; x <= x_of(to) + 2.0f; x += 2.0f) + { + auto const chainage{std::min(to, chainage_of(x))}; + ImVec2 const next{x_of(chainage), y_of(profile::elevation(line, chainage))}; + draw->AddLine(previous, next, kGradeLine, 2.5f); + previous = next; + } + } + for (std::size_t i = 0; i + 1 < points.size(); ++i) + { + auto const middle{(points[i].chainage + points[i + 1].chainage) * 0.5}; + auto const x{x_of(middle)}; + if (x_of(points[i + 1].chainage) - x_of(points[i].chainage) < 70.0f) + continue; + auto const text{format("%+.2f o/oo L %.1f", profile::grade_after(line, i) * 1000.0, points[i + 1].chainage - points[i].chainage)}; + draw->AddText(ImVec2(x - 40.0f, y_of(profile::elevation(line, middle)) - 30.0f), kGradeLine, text.c_str()); + } + for (auto const &c : curves) + { + for (auto const at : {c.start, c.end}) + { + auto const y{y_of(profile::elevation(line, at))}; + draw->AddLine(ImVec2(x_of(at), y - 7.0f), ImVec2(x_of(at), y + 7.0f), kGradeLine, 1.5f); + } + auto const chainage{points[c.point].chainage}; + ImVec2 const grip{x_of(chainage), y_of(profile::elevation(line, chainage))}; + draw->AddCircleFilled(grip, 4.5f, IM_COL32(60, 230, 90, 255), 4); + if (static_cast(c.point) == state.curve_grip) + draw->AddCircle(grip, 9.0f, IM_COL32(255, 255, 255, 255), 12, 2.0f); + draw->AddText(ImVec2(grip.x + 7.0f, grip.y + 4.0f), IM_COL32(60, 230, 90, 255), format("R %.0f", points[c.point].radius).c_str()); + } + for (int i = 0; i < static_cast(points.size()); ++i) + { + ImVec2 const at{x_of(points[i].chainage), y_of(points[i].elevation)}; + if (i == 0 || i + 1 == static_cast(points.size())) + draw->AddRectFilled(ImVec2(at.x - 5.0f, at.y - 5.0f), ImVec2(at.x + 5.0f, at.y + 5.0f), IM_COL32(70, 140, 255, 255)); + else if (points[i].joint) + draw->AddQuadFilled(ImVec2(at.x, at.y - 6.0f), ImVec2(at.x + 6.0f, at.y), ImVec2(at.x, at.y + 6.0f), ImVec2(at.x - 6.0f, at.y), IM_COL32(90, 210, 230, 255)); + else + draw->AddCircleFilled(at, 5.5f, IM_COL32(255, 210, 60, 255)); + if (i == state.selected) + draw->AddCircle(at, 10.0f, IM_COL32(255, 255, 255, 255), 16, 2.0f); + draw->AddText(ImVec2(at.x + 7.0f, at.y - 18.0f), IM_COL32(255, 210, 60, 255), format("%d %.3f", i + 1, points[i].elevation).c_str()); + } + for (auto const &issue : state.issues) + { + auto const x{x_of(issue.chainage)}; + auto const colour{issue.error ? kError : kWarning}; + draw->AddTriangleFilled(ImVec2(x - 5.0f, corner.y + top), ImVec2(x + 5.0f, corner.y + top), ImVec2(x, corner.y + top + 8.0f), colour); + } + if (state.show_plan && false == samples.empty()) + { + auto const middle{plotbottom + strip * 0.5f}; + draw->AddLine(ImVec2(corner.x, plotbottom), ImVec2(bottomright.x, plotbottom), IM_COL32(255, 255, 255, 60)); + draw->AddLine(ImVec2(corner.x, middle), ImVec2(bottomright.x, middle), IM_COL32(255, 255, 255, 40)); + ImVec2 previous{}; + bool open{false}; + float lastx{-1e9f}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const x{x_of(samples[i].chainage)}; + if (x < corner.x - 50.0f || x > bottomright.x + 50.0f || (open && x - lastx < 1.0f)) + continue; + ImVec2 const next{x, middle - std::clamp(static_cast(samples[i].curvature * 300.0 * strip * 0.4), -strip * 0.45f, strip * 0.45f)}; + if (open) + draw->AddLine(previous, next, IM_COL32(120, 190, 255, 255), 1.5f); + previous = next; + lastx = x; + open = true; + } + draw->AddText(ImVec2(corner.x + 2.0f, plotbottom + 1.0f), kLabel, "plan: curvature (up: left)"); + } + if (hovered && state.hover >= 0.0 && state.hover <= state.route.length && false == samples.empty()) + { + auto const x{x_of(state.hover)}; + draw->AddLine(ImVec2(x, corner.y + top), ImVec2(x, bottomright.y), IM_COL32(255, 255, 255, 90)); + auto const designed{profile::elevation(line, state.hover)}; + auto const existing{editor_track::sampled_elevation(samples, state.hover)}; + ImGui::BeginTooltip(); + ImGui::Text("km %s", profile::format_chainage(state.hover + state.origin).c_str()); + ImGui::Text("Grade line %.3f m, %+.2f per mille", designed, profile::grade(line, state.hover) * 1000.0); + ImGui::Text("Track %.3f m (%+.3f), %+.2f per mille", existing, existing - designed, editor_track::sampled_grade(samples, state.hover) * 1000.0); + auto const next{std::lower_bound(samples.begin(), samples.end(), state.hover, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + auto const index{static_cast(std::distance(samples.begin(), std::min(next, std::prev(samples.end()))))}; + if (index < state.terrain.size() && false == std::isnan(state.terrain[index])) + ImGui::Text("Terrain %.3f m, %s %.2f m", state.terrain[index], designed > state.terrain[index] ? "fill" : "cut", std::abs(designed - state.terrain[index])); + auto const &span{state.route.spans[samples[index].span]}; + ImGui::TextDisabled("%s%s", span.track->name().c_str(), span.track->eType != tt_Normal ? " (switch, tilted as a whole with its branch)" : ""); + ImGui::EndTooltip(); + } + draw->PopClipRect(); +} + +void editor_mode::draw_profile_overlay() const +{ + auto const &state{m_profile}; + if (false == state.open || state.samples.size() < 2) + return; + screen_projection const projection; + ImDrawList *drawlist{ImGui::GetBackgroundDrawList()}; + auto const &samples{state.samples}; + glm::dvec3 previous{samples.front().position}; + glm::dvec3 designedprevious{previous.x, profile::elevation(state.line, 0.0), previous.z}; + for (std::size_t i = 1; i < samples.size(); ++i) + { + if (glm::distance(samples[i].position, previous) < 2.0 && i + 1 < samples.size()) + continue; + auto const &next{samples[i].position}; + projection.line(drawlist, previous, next, IM_COL32(255, 150, 40, 200), 2.5f); + glm::dvec3 const designed{next.x, profile::elevation(state.line, samples[i].chainage), next.z}; + if (std::abs(designed.y - next.y) > 0.01 || std::abs(designedprevious.y - previous.y) > 0.01) + projection.line(drawlist, designedprevious, designed, kGradeLine, 2.0f); + previous = next; + designedprevious = designed; + } + auto const &points{state.line.points}; + for (int i = 0; i < static_cast(points.size()); ++i) + { + auto position{position_at(samples, points[i].chainage)}; + position.y = profile::elevation(state.line, points[i].chainage); + ImVec2 screen; + if (false == projection.project(position, screen)) + continue; + drawlist->AddCircleFilled(screen, i == state.selected ? 7.0f : 5.0f, IM_COL32(255, 210, 60, 255)); + drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 16.0f), IM_COL32(255, 210, 60, 255), format("%d", i + 1).c_str()); + } + if (state.hover >= 0.0 && state.hover <= state.route.length) + { + ImVec2 screen; + if (projection.project(position_at(samples, state.hover), screen)) + drawlist->AddCircle(screen, 12.0f, IM_COL32(255, 255, 255, 230), 16, 2.5f); + } +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 8ebd64f9..ed5ecd30 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -11,6 +11,7 @@ http: #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" +#include "application/editorprojection.h" #include "utilities/Globals.h" #include "rendering/renderer.h" @@ -31,54 +32,6 @@ http: namespace { -class screen_projection -{ - public: - screen_projection() - { - ImGuiIO const &io = ImGui::GetIO(); - m_size = io.DisplaySize; - m_camera = GfxRenderer->Camera_Position(); - float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; - m_viewprojection = editor_mode::projection_matrix(aspect) * GfxRenderer->Camera_View_Matrix(); - } - - glm::vec4 clip(glm::dvec3 const &Point) const - { - return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); - } - ImVec2 screen(glm::vec4 const &Clip) const - { - return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); - } - bool project(glm::dvec3 const &Point, ImVec2 &Screen) const - { - auto const c = clip(Point); - if (c.w < kNear) - return false; - Screen = screen(c); - return true; - } - void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const - { - glm::vec4 a = clip(A); - glm::vec4 b = clip(B); - if (a.w < kNear && b.w < kNear) - return; - if (a.w < kNear) - a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); - else if (b.w < kNear) - b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); - Drawlist->AddLine(screen(a), screen(b), Color, Thickness); - } - - private: - static constexpr float kNear{0.1f}; - ImVec2 m_size; - glm::dvec3 m_camera; - glm::mat4 m_viewprojection; -}; - glm::dvec3 path_point(segment_data const &Path, double const T) { auto const &p0 = Path.points[segment_data::point::start]; @@ -452,6 +405,7 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve m_node = tracks.empty() || tracks.front()->m_editorremoved ? nullptr : tracks.front(); ui()->set_node(m_node); straight_refresh(); + profile_after_undo(); } void editor_mode::render_track_ui() @@ -459,6 +413,13 @@ void editor_mode::render_track_ui() ImGui::TextDisabled("LMB on a path opens its editor: straight, curve or switch"); if (ImGui::Button("Open the editor window")) m_track_window_open = true; + ImGui::SameLine(); + if (ImGui::Button("Vertical profile (grade line)")) + { + if (auto *track{selected_track()}; track != nullptr && m_profile.route.spans.empty()) + profile_open_run(*track); + m_profile.open = true; + } if (ImGui::CollapsingHeader("Straights in the scenery")) render_straights_ui(); render_switch_ui(); @@ -507,6 +468,8 @@ void editor_mode::render_track_window() if (ImGui::SmallButton("Edit points and parameters of the single path")) m_track_tab = track_tab::path; } + if (ImGui::SmallButton("Vertical profile along this line") && selected_track() != nullptr) + profile_open_run(*selected_track()); ImGui::End(); } @@ -990,6 +953,7 @@ void editor_mode::route_apply() if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) states.emplace_back(track, editor_track::capture(*track)); auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + editor_track::keep_heights(chain, pieces); auto relaid{editor_track::relay(chain, pieces)}; auto const added{relaid.size()}; created.insert(created.end(), relaid.begin(), relaid.end()); @@ -1232,11 +1196,6 @@ void editor_mode::render_route_gizmo() else vertex.position = point; } - if (std::abs(moved.y - position.y) > 1e-3) - { - vertex.auto_elevation = false; - vertex.elevation = moved.y; - } route_update(); } @@ -1308,8 +1267,6 @@ void editor_mode::render_route_ui() changed |= ImGui::InputDouble("Cant change rate (mm/s)", &norms.cant_rate, 0.0, 0.0, "%.0f"); changed |= ImGui::InputDouble("Cant ramp 1:(k*V), k", &norms.ramp_factor, 0.0, 0.0, "%.1f"); changed |= ImGui::InputDouble("Unbalanced acc. change rate (m/s3)", &norms.jerk, 0.0, 0.0, "%.2f"); - changed |= ImGui::InputDouble("Vertical curve R >= k*V^2, k", &norms.vertical_factor, 0.0, 0.0, "%.2f"); - changed |= ImGui::InputDouble("Vertical curve R min (m)", &norms.vertical_min, 0.0, 0.0, "%.0f"); changed |= ImGui::InputDouble("Straight between curves (s of travel)", &norms.tangent_min_time, 0.0, 0.0, "%.1f"); ImGui::PopItemWidth(); norms.gauge = std::max(100.0, norms.gauge); @@ -1401,16 +1358,11 @@ void editor_mode::render_route_ui() changed |= ImGui::InputDouble("Transition out (m)", &vertex.transition_out, 0.0, 0.0, "%.1f"); changed |= ImGui::InputDouble("Cant (mm)", &vertex.cant, 0.0, 0.0, "%.0f"); changed |= ImGui::Checkbox("Turn the longer way (over 180 deg)", &vertex.reverse_turn); - changed |= ImGui::Checkbox("Elevation from the ends", &vertex.auto_elevation); - if (false == vertex.auto_elevation) - changed |= ImGui::InputDouble("Elevation (m)", &vertex.elevation, 0.0, 0.0, "%.3f"); - changed |= ImGui::InputDouble("Vertical curve R (0: min)", &vertex.vertical_radius, 0.0, 0.0, "%.0f"); ImGui::PopItemWidth(); vertex.radius = std::max(1.0, vertex.radius); vertex.transition_in = std::max(0.0, vertex.transition_in); vertex.transition_out = std::max(0.0, vertex.transition_out); vertex.cant = std::max(0.0, vertex.cant); - vertex.vertical_radius = std::max(0.0, vertex.vertical_radius); if (ImGui::SmallButton("Apply recommended for V")) { route_recommend(vertex); @@ -2260,6 +2212,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co return; } auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; + editor_track::keep_heights(m_route.chain, pieces); auto created{editor_track::relay(m_route.chain, pieces)}; editor_track::commit(tracks); for (auto const &entry : splitstates) diff --git a/application/editorprojection.h b/application/editorprojection.h new file mode 100644 index 00000000..aa5cd45d --- /dev/null +++ b/application/editorprojection.h @@ -0,0 +1,63 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editormode.h" +#include "rendering/renderer.h" +#include "imgui/imgui.h" + +// world to screen projection of the current camera, for overlays drawn with ImGui +class screen_projection +{ + public: + screen_projection() + { + ImGuiIO const &io = ImGui::GetIO(); + m_size = io.DisplaySize; + m_camera = GfxRenderer->Camera_Position(); + float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; + m_viewprojection = editor_mode::projection_matrix(aspect) * GfxRenderer->Camera_View_Matrix(); + } + + glm::vec4 clip(glm::dvec3 const &Point) const + { + return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); + } + ImVec2 screen(glm::vec4 const &Clip) const + { + return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); + } + bool project(glm::dvec3 const &Point, ImVec2 &Screen) const + { + auto const c = clip(Point); + if (c.w < kNear) + return false; + Screen = screen(c); + return true; + } + void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const + { + glm::vec4 a = clip(A); + glm::vec4 b = clip(B); + if (a.w < kNear && b.w < kNear) + return; + if (a.w < kNear) + a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); + else if (b.w < kNear) + b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); + Drawlist->AddLine(screen(a), screen(b), Color, Thickness); + } + + private: + static constexpr float kNear{0.1f}; + ImVec2 m_size; + glm::dvec3 m_camera; + glm::mat4 m_viewprojection; +}; diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index b93046b9..c2940c2e 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -752,20 +752,6 @@ result fit_between(design const &Design) auto const gradeafter{(r.profile[i + 1].elevation - point.elevation) / (r.profile[i + 1].chainage - point.chainage)}; point.tangent = point.radius * std::abs(gradeafter - gradebefore) / 2.0; } - for (std::size_t i = 0; i + 1 < r.profile.size(); ++i) - { - if (r.profile[i].tangent + r.profile[i + 1].tangent > r.profile[i + 1].chainage - r.profile[i].chainage + 1e-6) - r.warnings.emplace_back(format("Vertical curves overlap between chainage %.0f m and %.0f m", r.profile[i].chainage, r.profile[i + 1].chainage)); - } - if (r.profile.size() >= 2) - { - auto const first{(r.profile[1].elevation - r.profile[0].elevation) / (r.profile[1].chainage - r.profile[0].chainage)}; - auto const last{(r.profile.back().elevation - r.profile[r.profile.size() - 2].elevation) / (r.profile.back().chainage - r.profile[r.profile.size() - 2].chainage)}; - if (std::abs(first - Design.start_grade) > 0.0005) - r.warnings.emplace_back(format("Grade break at the start: %.1f per mille against %.1f of the adjoining track", first * 1000.0, Design.start_grade * 1000.0)); - if (std::abs(last - Design.end_grade) > 0.0005) - r.warnings.emplace_back(format("Grade break at the end: %.1f per mille against %.1f of the adjoining track", last * 1000.0, Design.end_grade * 1000.0)); - } if (Design.start_radius > 0.0 && Design.start_radius < 10000.0) r.warnings.emplace_back(format("The adjoining track at the start is curved (R %.0f m), the curvature changes abruptly", Design.start_radius)); if (Design.end_radius > 0.0 && Design.end_radius < 10000.0) diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp new file mode 100644 index 00000000..f093dee4 --- /dev/null +++ b/editor/editorProfile.cpp @@ -0,0 +1,422 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorProfile.hpp" + +#include +#include +#include +#include + +namespace profile +{ + +namespace +{ + +template +std::string format(char const *Format, Args... Arguments) +{ + char text[256]; + std::snprintf(text, sizeof(text), Format, Arguments...); + return text; +} + +double polyline(line const &Line, double const Chainage) +{ + auto const &points{Line.points}; + if (points.empty()) + return 0.0; + if (points.size() == 1 || Chainage <= points.front().chainage) + return points.front().elevation + (points.size() > 1 ? grade_after(Line, 0) * (Chainage - points.front().chainage) : 0.0); + if (Chainage >= points.back().chainage) + return points.back().elevation + grade_after(Line, points.size() - 2) * (Chainage - points.back().chainage); + auto const next{std::upper_bound(points.begin(), points.end(), Chainage, [](double const Value, pvi const &Point) { return Value < Point.chainage; })}; + auto const index{static_cast(std::distance(points.begin(), next)) - 1}; + return points[index].elevation + grade_after(Line, index) * (Chainage - points[index].chainage); +} + +bool overlaps(double const From, double const To, context::zone const &Zone) +{ + return From < Zone.to - 1e-6 && To > Zone.from + 1e-6; +} + +} // namespace + +double grade_after(line const &Line, std::size_t const Index) +{ + auto const &points{Line.points}; + if (Index + 1 >= points.size()) + return 0.0; + auto const run{points[Index + 1].chainage - points[Index].chainage}; + return run > 1e-9 ? (points[Index + 1].elevation - points[Index].elevation) / run : 0.0; +} + +double required_radius(line const &Line) +{ + return std::max(Line.norms.radius_min, Line.norms.radius_factor * Line.speed * Line.speed); +} + +void assign_radii(line &Line) +{ + auto const required{std::ceil(required_radius(Line) / 10.0) * 10.0}; + for (std::size_t k = 1; k + 1 < Line.points.size(); ++k) + { + auto &point{Line.points[k]}; + if (false == point.automatic || point.joint) + continue; + auto const change{std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) * 1000.0}; + point.radius = change > Line.norms.curve_threshold + 1e-6 ? required : 0.0; + } +} + +void complete(line &Line, context const &Context) +{ + auto &points{Line.points}; + points.erase(std::remove_if(points.begin(), points.end(), [](pvi const &Point) { return Point.joint; }), points.end()); + if (points.size() < 2) + return; + bool const start{Line.join_start && Context.start_joined}; + bool const end{Line.join_end && Context.end_joined}; + if (start) + points.front().elevation = Context.start_elevation; + if (end) + points.back().elevation = Context.end_elevation; + + // the vertical curve of the joint starts right at the end of the route, off the adjoining track + auto const joint = [&](pvi const &End, pvi const &Neighbour, double const Grade, double const Direction) -> std::optional { + auto const run{(Neighbour.chainage - End.chainage) * Direction}; + if (run < 1.0) + return std::nullopt; + auto const offset{std::abs(Neighbour.elevation - (End.elevation + Grade * run * Direction))}; + if (offset / run * 1000.0 <= Line.norms.curve_threshold + 1e-6) + return std::nullopt; + auto radius{std::ceil(required_radius(Line) / 10.0) * 10.0}; + double tangent; + if (run * run >= 2.0 * radius * offset) + tangent = (run - std::sqrt(run * run - 2.0 * radius * offset)) * 0.5; + else + { + tangent = run * 0.5; + radius = run * run / (2.0 * offset); + } + pvi result; + result.chainage = End.chainage + tangent * Direction; + result.elevation = End.elevation + Grade * tangent * Direction; + result.radius = radius; + result.automatic = false; + result.joint = true; + return result; + }; + if (start) + if (auto const point{joint(points.front(), points[1], Context.start_grade, 1.0)}) + points.insert(points.begin() + 1, *point); + if (end) + if (auto const point{joint(points.back(), points[points.size() - 2], Context.end_grade, -1.0)}) + points.insert(points.end() - 1, *point); + assign_radii(Line); +} + +std::vector curves(line const &Line) +{ + std::vector result; + auto const &points{Line.points}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + auto const radius{points[k].radius}; + if (radius <= 0.0) + continue; + curve c; + c.point = k; + c.change = grade_after(Line, k) - grade_after(Line, k - 1); + c.tangent = radius * std::abs(c.change) * 0.5; + if (c.tangent < 1e-6) + continue; + c.sagitta = c.tangent * c.tangent / (2.0 * radius); + c.start = points[k].chainage - c.tangent; + c.end = points[k].chainage + c.tangent; + result.push_back(c); + } + return result; +} + +double elevation(line const &Line, double const Chainage) +{ + auto height{polyline(Line, Chainage)}; + for (auto const &c : curves(Line)) + { + auto const distance{std::abs(Chainage - Line.points[c.point].chainage)}; + if (distance >= c.tangent) + continue; + height += c.change * (c.tangent - distance) * (c.tangent - distance) / (4.0 * c.tangent); + } + return height; +} + +double grade(line const &Line, double const Chainage) +{ + auto const &points{Line.points}; + if (points.size() < 2) + return 0.0; + std::size_t index{0}; + while (index + 2 < points.size() && Chainage >= points[index + 1].chainage) + ++index; + auto slope{grade_after(Line, index)}; + for (auto const &c : curves(Line)) + { + auto const offset{Chainage - points[c.point].chainage}; + if (std::abs(offset) >= c.tangent) + continue; + slope -= c.change * (c.tangent - std::abs(offset)) / (2.0 * c.tangent) * (offset < 0.0 ? -1.0 : 1.0); + } + return slope; +} + +double radius_for_sagitta(line const &Line, std::size_t const Point, double const Sagitta) +{ + if (Point == 0 || Point + 1 >= Line.points.size()) + return 0.0; + auto const change{std::abs(grade_after(Line, Point) - grade_after(Line, Point - 1))}; + if (change < 1e-9) + return Line.points[Point].radius; + return std::max(0.0, 8.0 * std::abs(Sagitta) / (change * change)); +} + +std::vector check(line const &Line, context const &Context) +{ + std::vector result; + auto const &points{Line.points}; + auto const &norms{Line.norms}; + auto const add = [&](bool const Error, int const Point, double const Chainage, std::string Text) { + result.push_back({Error, Point, Chainage, std::move(Text)}); + }; + if (points.size() < 2) + { + add(true, -1, 0.0, "The grade line needs at least the two ends"); + return result; + } + for (std::size_t k = 0; k + 1 < points.size(); ++k) + { + if (points[k + 1].chainage - points[k].chainage < 0.5) + add(true, static_cast(k + 1), points[k + 1].chainage, format("Points %zu and %zu are closer than 0.5 m", k + 1, k + 2)); + auto const slope{grade_after(Line, k) * 1000.0}; + if (std::abs(slope) > norms.grade_max + 1e-6) + add(false, static_cast(k), points[k].chainage, format("Grade %.2f per mille between points %zu and %zu exceeds %.2f", slope, k + 1, k + 2, norms.grade_max)); + } + + auto const list{curves(Line)}; + auto const required{required_radius(Line)}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + auto const change{std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) * 1000.0}; + auto const radius{points[k].radius}; + if (radius <= 0.0 && change > norms.curve_threshold + 1e-6) + add(false, static_cast(k), points[k].chainage, format("Point %zu: change of grade %.2f per mille needs a vertical curve", k + 1, change)); + else if (radius > 0.0 && radius < required - 1e-6) + add(false, static_cast(k), points[k].chainage, format("Point %zu: R %.0f m is below the required %.0f m for %.0f km/h", k + 1, radius, required, Line.speed)); + if (radius <= 0.0 && change > 1e-3) + for (auto const &zone : Context.switches) + if (points[k].chainage > zone.from + 1e-3 && points[k].chainage < zone.to - 1e-3) + add(true, static_cast(k), points[k].chainage, format("Point %zu: the grade changes inside switch %s", k + 1, zone.name.c_str())); + } + for (std::size_t i = 0; i < list.size(); ++i) + { + auto const &c{list[i]}; + auto const point{static_cast(c.point)}; + if (c.start < points.front().chainage - 1e-6 || c.end > points.back().chainage + 1e-6) + add(true, point, points[c.point].chainage, format("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("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)); + for (auto const &zone : Context.switches) + if (overlaps(c.start, c.end, zone)) + add(true, point, points[c.point].chainage, format("Point %zu: the vertical curve lies on switch %s", c.point + 1, zone.name.c_str())); + } + if (norms.element_min > 0.0) + { + for (std::size_t k = 1; k + 2 < points.size(); ++k) + { + auto const tangent = [&](std::size_t const Point) { + for (auto const &c : list) + if (c.point == Point) + return c.tangent; + return 0.0; + }; + auto const free{(points[k + 1].chainage - tangent(k + 1)) - (points[k].chainage + tangent(k))}; + if (free < norms.element_min - 1e-6) + add(false, static_cast(k), points[k].chainage, format("Constant grade between points %zu and %zu is %.1f m long, less than %.0f m", k + 1, k + 2, std::max(0.0, free), norms.element_min)); + } + } + for (auto const &zone : Context.switches) + { + auto const slope{std::abs(grade(Line, (zone.from + zone.to) * 0.5)) * 1000.0}; + if (slope > norms.grade_switch_max + 1e-6) + add(false, -1, zone.from, format("Grade %.2f per mille over switch %s exceeds %.2f", slope, zone.name.c_str(), norms.grade_switch_max)); + } + for (auto const &fixed : Context.fixed) + { + auto const departure{elevation(Line, fixed.first) - fixed.second}; + if (std::abs(departure) > norms.fixed_tolerance) + { + add(true, -1, fixed.first, format("At %s the grade line departs %.3f m from a fixed point", format_chainage(fixed.first).c_str(), departure)); + break; + } + } + auto const ends = [&](bool const Atend) { + auto const joined{Atend ? Context.end_joined : Context.start_joined}; + if (false == joined) + return; + auto const chainage{Atend ? points.back().chainage : points.front().chainage}; + auto const departure{elevation(Line, chainage) - (Atend ? Context.end_elevation : Context.start_elevation)}; + if (std::abs(departure) > norms.fixed_tolerance) + add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format("The %s of the grade line is %.3f m off the adjoining track", Atend ? "end" : "start", departure)); + auto const slope{grade(Line, chainage)}; + auto const change{std::abs(slope - (Atend ? Context.end_grade : Context.start_grade)) * 1000.0}; + if (change > norms.curve_threshold + 1e-6) + add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format("Change of grade %.2f per mille against the adjoining track at the %s", change, Atend ? "end" : "start")); + }; + ends(false); + ends(true); + std::stable_sort(result.begin(), result.end(), [](issue const &A, issue const &B) { return A.error && false == B.error; }); + return result; +} + +line recognize(std::vector const &Chainages, std::vector const &Elevations, std::vector const &Grades, recognition const &Options) +{ + line result; + auto const count{std::min({Chainages.size(), Elevations.size(), Grades.size()})}; + if (count < 2) + return result; + + struct part + { + std::size_t first, last; + double slope{0.0}, offset{0.0}; + }; + auto const fit = [&](part &Part) { + double sx{0.0}, sy{0.0}, sxx{0.0}, sxy{0.0}; + auto const origin{Chainages[Part.first]}; + auto const n{static_cast(Part.last - Part.first + 1)}; + for (auto i = Part.first; i <= Part.last; ++i) + { + auto const x{Chainages[i] - origin}; + sx += x; + sy += Elevations[i]; + sxx += x * x; + sxy += x * Elevations[i]; + } + auto const denominator{n * sxx - sx * sx}; + Part.slope = std::abs(denominator) > 1e-12 ? (n * sxy - sx * sy) / denominator : 0.0; + Part.offset = (sy - Part.slope * sx) / n - Part.slope * origin; + }; + + std::vector parts; + for (std::size_t i = 0; i < count;) + { + auto j{i}; + auto low{Grades[i]}, high{Grades[i]}; + while (j + 1 < count && std::max(high, Grades[j + 1]) - std::min(low, Grades[j + 1]) <= Options.grade_tolerance) + { + ++j; + low = std::min(low, Grades[j]); + high = std::max(high, Grades[j]); + } + if (Chainages[j] - Chainages[i] >= Options.minimum_length) + { + part p{i, j}; + fit(p); + parts.push_back(p); + i = j + 1; + } + else + ++i; + } + for (std::size_t i = 0; i + 1 < parts.size();) + { + if (std::abs(parts[i].slope - parts[i + 1].slope) < Options.grade_tolerance * 0.5) + { + parts[i].last = parts[i + 1].last; + fit(parts[i]); + parts.erase(parts.begin() + i + 1); + } + else + ++i; + } + + result.points.push_back({Chainages.front(), Elevations.front(), 0.0}); + for (std::size_t i = 0; i + 1 < parts.size(); ++i) + { + auto const &a{parts[i]}; + auto const &b{parts[i + 1]}; + auto const from{Chainages[a.last]}; + auto const to{Chainages[b.first]}; + auto const change{b.slope - a.slope}; + auto chainage{std::abs(change) > 1e-9 ? (a.offset - b.offset) / change : (from + to) * 0.5}; + chainage = std::clamp(chainage, from, std::max(from, to)); + pvi point; + point.automatic = false; + point.chainage = chainage; + point.elevation = a.slope * chainage + a.offset; + auto const gap{to - from}; + if (gap >= Options.sharp_gap && std::abs(change) > 1e-9) + point.radius = std::round(gap / std::abs(change)); + if (point.chainage - result.points.back().chainage >= 0.5) + result.points.push_back(point); + } + if (Chainages[count - 1] - result.points.back().chainage < 0.5 && result.points.size() > 1) + result.points.pop_back(); + result.points.push_back({Chainages[count - 1], Elevations[count - 1], 0.0}); + + // the gap between constant grades is shorter than the curve, the sagitta gives the radius + for (std::size_t i = 1; i + 1 < result.points.size(); ++i) + { + auto &point{result.points[i]}; + if (point.radius <= 0.0) + continue; + auto const next{std::lower_bound(Chainages.begin(), Chainages.begin() + count, point.chainage)}; + if (next == Chainages.begin() || next == Chainages.begin() + count) + continue; + auto const index{static_cast(std::distance(Chainages.begin(), next))}; + auto const run{Chainages[index] - Chainages[index - 1]}; + auto const measured{run > 1e-9 ? Elevations[index - 1] + (Elevations[index] - Elevations[index - 1]) * (point.chainage - Chainages[index - 1]) / run : Elevations[index]}; + auto const radius{radius_for_sagitta(result, i, measured - point.elevation)}; + if (radius > 0.0) + point.radius = std::round(radius); + } + return result; +} + +std::vector breaks(line const &Line) +{ + std::vector result; + auto const &points{Line.points}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + if (points[k].radius > 0.0) + continue; + if (std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) > 1e-7) + result.push_back(points[k].chainage); + } + for (auto const &c : curves(Line)) + { + result.push_back(c.start); + result.push_back(c.end); + } + std::sort(result.begin(), result.end()); + return result; +} + +std::string format_chainage(double const Chainage) +{ + auto const value{std::abs(Chainage)}; + auto const kilometres{static_cast(value / 1000.0)}; + return format("%s%ld+%06.2f", Chainage < 0.0 ? "-" : "", kilometres, value - kilometres * 1000.0); +} + +} diff --git a/editor/editorProfile.hpp b/editor/editorProfile.hpp new file mode 100644 index 00000000..ebc1ca0b --- /dev/null +++ b/editor/editorProfile.hpp @@ -0,0 +1,119 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +// vertical alignment (grade line) of the rail top along the chainage of a route +namespace profile +{ + +// point of intersection of two grades, with a parabolic vertical curve of radius R (0: none) +struct pvi +{ + double chainage{0.0}; + double elevation{0.0}; + double radius{0.0}; + bool automatic{true}; // radius follows the limits + bool joint{false}; // placed by complete(), takes the grade over from the adjoining track +}; + +struct limits +{ + double grade_max{6.0}; // per mille + double grade_switch_max{2.5}; // per mille, over switches + double radius_factor{0.4}; // R >= k * V^2 + double radius_min{2000.0}; + double curve_threshold{2.0}; // per mille, change of grade which needs a vertical curve + double element_min{0.0}; // m, constant grade between vertical curves + double fixed_tolerance{0.005}; // m, allowed departure from fixed points +}; + +struct line +{ + std::vector points; // ordered by chainage; the first and the last are the ends of the route + double speed{100.0}; + limits norms; + // at the joined ends: keep the elevation and take over the grade of the adjoining track + bool join_start{true}; + bool join_end{true}; +}; + +double elevation(line const &Line, double const Chainage); +// dy/ds +double grade(line const &Line, double const Chainage); +// grade of the element between points Index and Index + 1 +double grade_after(line const &Line, std::size_t const Index); +double required_radius(line const &Line); +// radii of the automatic points: the required one where the change of grade needs a curve, none elsewhere +void assign_radii(line &Line); + +struct curve +{ + std::size_t point{0}; + double start{0.0}; + double end{0.0}; + double tangent{0.0}; + double sagitta{0.0}; + double change{0.0}; // grade after - grade before +}; +std::vector curves(line const &Line); + +// radius which gives specified elevation of the curve at the point of intersection +double radius_for_sagitta(line const &Line, std::size_t const Point, double const Sagitta); + +// facts about the route which the checks take into account +struct context +{ + double length{0.0}; + struct zone + { + double from{0.0}; + double to{0.0}; + std::string name; + }; + std::vector switches; + // elevations which the grade line has to keep (chainage, elevation) + std::vector> fixed; + // adjoining track at the ends; grades rise along the route + double start_elevation{0.0}, end_elevation{0.0}; + double start_grade{0.0}, end_grade{0.0}; + bool start_joined{false}, end_joined{false}; +}; + +// locks the joined ends, places the joint points and sets the automatic radii +void complete(line &Line, context const &Context); + +struct issue +{ + bool error{false}; + int point{-1}; + double chainage{0.0}; + std::string text; +}; +std::vector check(line const &Line, context const &Context); + +struct recognition +{ + double grade_tolerance{0.0004}; + double minimum_length{15.0}; + double sharp_gap{2.0}; +}; +// 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); + +// 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); + +std::string format_chainage(double const Chainage); + +} diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 6df8d41b..a579defe 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1295,6 +1295,486 @@ TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm:: return edge; } +namespace +{ + +struct bezier +{ + glm::dvec3 p0, p1, p2, p3; + explicit bezier(segment_data const &Path) + { + p0 = Path.points[segment_data::point::start]; + p3 = Path.points[segment_data::point::end]; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + p1 = glm::mix(p0, p3, 1.0 / 3.0); + p2 = glm::mix(p0, p3, 2.0 / 3.0); + } + else + { + p1 = p0 + control1; + p2 = p3 + control2; + } + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + } + glm::dvec3 first(double const T) const + { + auto const u{1.0 - T}; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); + } + glm::dvec3 second(double const T) const + { + return 6.0 * (1.0 - T) * (p2 - 2.0 * p1 + p0) + 6.0 * T * (p3 - 2.0 * p2 + p1); + } + double plan_length(int const Steps = 32) const + { + double length{0.0}; + auto previous{plan_of(point(0.0))}; + for (int i = 1; i <= Steps; ++i) + { + auto const next{plan_of(point(static_cast(i) / Steps))}; + length += glm::distance(previous, next); + previous = next; + } + return length; + } + // parameter at specified plan distance from the start + double parameter(double const Distance) const + { + int constexpr steps{256}; + double length{0.0}; + auto previous{plan_of(point(0.0))}; + for (int i = 1; i <= steps; ++i) + { + auto const next{plan_of(point(static_cast(i) / steps))}; + auto const step{glm::distance(previous, next)}; + if (length + step >= Distance) + return (i - 1 + (step > 1e-12 ? (Distance - length) / step : 0.0)) / steps; + length += step; + previous = next; + } + return 1.0; + } +}; + +using route_node = std::tuple; + +std::vector route_next(route_node const &Node) +{ + std::vector result; + auto *track{std::get<0>(Node)}; + auto const path{std::get<1>(Node)}; + auto const forward{std::get<2>(Node)}; + auto const &exit{track->m_paths[path].points[forward ? segment_data::point::end : segment_data::point::start]}; + auto *next{neighbour_at(*track, path, forward)}; + if (next == nullptr || next == track || next->m_editorremoved || false == editor_track::is_supported(*next) || (next->iCategoryFlag & 15) != (track->iCategoryFlag & 15)) + return result; + for (int i = 0; i < static_cast(next->m_paths.size()); ++i) + { + auto const &candidate{next->m_paths[i]}; + if (glm::distance(candidate.points[segment_data::point::start], exit) <= kSamePoint) + result.emplace_back(next, i, true); + else if (glm::distance(candidate.points[segment_data::point::end], exit) <= kSamePoint) + result.emplace_back(next, i, false); + } + return result; +} + +} // namespace + +bool editor_track::find_route(TTrack *From, TTrack *To, route &Route, std::string &Error) +{ + Route = {}; + if (From == nullptr || To == nullptr || false == is_supported(*From) || false == is_supported(*To)) + { + Error = "Select the first and the last path of the route"; + return false; + } + if (From == To) + { + Route.spans.push_back({From, 0, true}); + Error.clear(); + return true; + } + std::vector nodes; + std::vector parents; + std::map visited; + for (int i = 0; i < static_cast(From->m_paths.size()); ++i) + for (auto const forward : {true, false}) + { + route_node const node{From, i, forward}; + visited.emplace(node, static_cast(nodes.size())); + nodes.push_back(node); + parents.push_back(-1); + } + int goal{-1}; + for (std::size_t index = 0; index < nodes.size() && goal < 0 && nodes.size() < 200000; ++index) + { + for (auto const &next : route_next(nodes[index])) + { + if (std::get<0>(next) == From || visited.count(next) > 0) + continue; + visited.emplace(next, static_cast(nodes.size())); + nodes.push_back(next); + parents.push_back(static_cast(index)); + if (std::get<0>(next) == To) + { + goal = static_cast(nodes.size()) - 1; + break; + } + } + } + if (goal < 0) + { + Error = "No route connects the selected paths"; + return false; + } + for (int index = goal; index >= 0; index = parents[index]) + Route.spans.push_back({std::get<0>(nodes[index]), std::get<1>(nodes[index]), std::get<2>(nodes[index])}); + std::reverse(Route.spans.begin(), Route.spans.end()); + Error.clear(); + return true; +} + +editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) +{ + route result; + if (false == is_supported(Track)) + return result; + auto const walk = [&](route_node Node) { + std::vector nodes; + double length{0.0}; + while (nodes.size() < 5000 && length < Maximum) + { + auto candidates{route_next(Node)}; + if (candidates.empty()) + break; + auto chosen{candidates.front()}; + for (auto const &candidate : candidates) + if (std::get<1>(candidate) == 0) + 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; })) + break; + nodes.push_back(chosen); + length += bezier(track->m_paths[std::get<1>(chosen)]).plan_length(8); + Node = chosen; + } + return nodes; + }; + auto const ahead{walk({&Track, 0, true})}; + auto const behind{walk({&Track, 0, 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}); + for (auto const &node : ahead) + result.spans.push_back({std::get<0>(node), std::get<1>(node), std::get<2>(node)}); + return result; +} + +editor_track::route editor_track::route_of(chain const &Chain) +{ + route result; + for (std::size_t i = 0; i < Chain.tracks.size(); ++i) + result.spans.push_back({Chain.tracks[i], 0, Chain.forward[i]}); + return result; +} + +std::vector editor_track::sample_route(route &Route, double const Step) +{ + std::vector samples; + double chainage{0.0}; + for (std::size_t i = 0; i < Route.spans.size(); ++i) + { + auto &span{Route.spans[i]}; + bezier const curve{span.track->m_paths[span.path]}; + auto const count{std::max(2, static_cast(std::ceil(curve.plan_length(16) / std::max(0.1, Step))))}; + span.from = chainage; + auto previous{plan_of(curve.point(span.forward ? 0.0 : 1.0))}; + for (int j = (samples.empty() ? 0 : 1); j <= count; ++j) + { + auto const t{span.forward ? static_cast(j) / count : 1.0 - static_cast(j) / count}; + auto const position{curve.point(t)}; + chainage += glm::distance(previous, plan_of(position)); + previous = plan_of(position); + auto derivative{curve.first(t)}; + if (false == span.forward) + derivative = -derivative; + auto const second{curve.second(t)}; + auto const planar{std::hypot(derivative.x, derivative.z)}; + route_sample sample; + sample.chainage = chainage; + sample.position = position; + sample.span = i; + if (planar > 1e-9) + { + sample.direction = glm::dvec2{derivative.x, derivative.z} / planar; + sample.grade = derivative.y / planar; + sample.curvature = (derivative.x * second.z - derivative.z * second.x) / (planar * planar * planar); + } + samples.push_back(sample); + } + span.to = chainage; + } + Route.length = chainage; + return samples; +} + +namespace +{ + +template +double interpolate(std::vector const &Samples, double const Chainage, Getter const &Get) +{ + if (Samples.empty()) + return 0.0; + auto const next{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + if (next == Samples.begin()) + return Get(Samples.front()); + if (next == Samples.end()) + return Get(Samples.back()); + auto const &before{*std::prev(next)}; + auto const run{next->chainage - before.chainage}; + return run > 1e-9 ? Get(before) + (Get(*next) - Get(before)) * (Chainage - before.chainage) / run : Get(before); +} + +void set_heights(segment_data &Path, double const Startheight, double const Endheight, double const Startgrade, double const Endgrade) +{ + auto &start{Path.points[segment_data::point::start]}; + auto &end{Path.points[segment_data::point::end]}; + auto &control1{Path.points[segment_data::point::control1]}; + auto &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + start.y = Startheight; + end.y = Endheight; + control1.y = Startgrade * std::hypot(control1.x, control1.z); + control2.y = -Endgrade * std::hypot(control2.x, control2.z); +} + +// the path lies in the plane through Origin which rises with Grade along Direction (unit, in plan) and is level across it +void plane_heights(segment_data &Path, glm::dvec3 const &Origin, glm::dvec2 const &Direction, double const Grade) +{ + auto &start{Path.points[segment_data::point::start]}; + auto &end{Path.points[segment_data::point::end]}; + auto &control1{Path.points[segment_data::point::control1]}; + auto &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + auto const height = [&](glm::dvec3 const &Point) { return Origin.y + Grade * glm::dot(glm::dvec2{Point.x - Origin.x, Point.z - Origin.z}, Direction); }; + auto const first{start + control1}; + auto const second{end + control2}; + start.y = height(start); + end.y = height(end); + control1.y = height(first) - start.y; + control2.y = height(second) - end.y; +} + +glm::dvec2 start_direction(segment_data const &Path) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &control{Path.points[segment_data::point::control1]}; + glm::dvec2 direction{control.x, control.z}; + if (glm::length(direction) < 1e-6) + direction = {Path.points[segment_data::point::end].x - start.x, Path.points[segment_data::point::end].z - start.z}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{1.0, 0.0}; +} + +} // namespace + +double editor_track::sampled_elevation(std::vector const &Samples, double const Chainage) +{ + return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position.y; }); +} + +double editor_track::sampled_grade(std::vector const &Samples, double const Chainage) +{ + return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.grade; }); +} + +bool editor_track::adjoining_grade(route const &Route, bool const Atend, double &Grade) +{ + if (Route.spans.empty()) + return false; + auto const &span{Atend ? Route.spans.back() : Route.spans.front()}; + auto const outward{Atend ? span.forward : false == span.forward}; + auto const next{route_next({span.track, span.path, outward})}; + if (next.empty()) + return false; + auto const &[track, path, forward]{next.front()}; + auto const tangent{path_tangent(track->m_paths[path], false == forward)}; + auto const plan{std::hypot(tangent.x, tangent.z)}; + if (plan < 1e-9) + return false; + auto const along{(forward ? 1.0 : -1.0) * tangent.y / plan}; + Grade = Atend ? along : -along; + return true; +} + +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}; + auto const remember = [&](TTrack *Track) { + if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; }) && std::find(Created.begin(), Created.end(), Track) == Created.end()) + States.emplace_back(Track, capture(*Track)); + }; + sample_route(Route, 1.0); + std::vector spans; + for (auto const &span : Route.spans) + { + if (span.track->eType != tt_Normal) + { + spans.push_back(span); + continue; + } + std::vector cuts; + for (auto const chainage : Breaks) + if (chainage > span.from + margin && chainage < span.to - margin) + cuts.push_back(chainage); + if (cuts.empty()) + { + spans.push_back(span); + continue; + } + remember(span.track); + if (span.forward) + { + auto *piece{span.track}; + auto origin{span.from}; + spans.push_back({piece, 0, true}); + for (auto const cut : cuts) + { + auto const t{bezier(piece->m_paths.front()).parameter(cut - origin)}; + auto *created{split_path(*piece, t)}; + if (created == nullptr) + continue; + Created.push_back(created); + spans.push_back({created, 0, true}); + piece = created; + origin = cut; + } + } + else + { + std::vector order; + for (auto cut = cuts.begin(); cut != cuts.end(); ++cut) + { + auto const t{bezier(span.track->m_paths.front()).parameter(span.to - *cut)}; + auto *created{split_path(*span.track, t)}; + if (created == nullptr) + continue; + Created.push_back(created); + order.push_back(created); + } + for (auto *piece : order) + spans.push_back({piece, 0, false}); + spans.push_back({span.track, 0, false}); + } + } + Route.spans = spans; + sample_route(Route, 1.0); + + auto const on_route = [&](TTrack const *Track) { + return std::any_of(Route.spans.begin(), Route.spans.end(), [&](route_span const &Span) { return Span.track == Track; }); + }; + struct joint + { + TTrack *track; + int path; + bool atend; + TTrack *neighbour; + }; + std::vector joints; + std::vector changed; + for (auto const &span : Route.spans) + { + remember(span.track); + auto const startchainage{span.forward ? span.from : span.to}; + auto const endchainage{span.forward ? span.to : span.from}; + auto const sign{span.forward ? 1.0 : -1.0}; + auto &path{span.track->m_paths[span.path]}; + set_heights(path, Elevation(startchainage), Elevation(endchainage), Grade(startchainage) * sign, Grade(endchainage) * sign); + add_unique(changed, span.track); + if (span.track->eType == tt_Normal) + continue; + auto const grade{Grade((span.from + span.to) * 0.5) * sign}; + for (int i = 0; i < static_cast(span.track->m_paths.size()); ++i) + { + for (auto const atend : {false, true}) + { + auto *neighbour{neighbour_at(*span.track, i, atend)}; + if (neighbour != nullptr && false == on_route(neighbour) && std::none_of(joints.begin(), joints.end(), [&](joint const &Joint) { return Joint.track == span.track && Joint.neighbour == neighbour; })) + joints.push_back({span.track, i, atend, neighbour}); + } + if (i != span.path) + plane_heights(span.track->m_paths[i], path.points[segment_data::point::start], start_direction(path), grade); + } + } + commit(changed); + sample_route(Route, 1.0); + + std::vector gaps; + for (auto const &joint : joints) + { + auto const &end{joint.track->m_paths[joint.path].points[joint.atend ? segment_data::point::end : segment_data::point::start]}; + double nearest{std::numeric_limits::max()}; + double gap{0.0}; + for (auto const &path : joint.neighbour->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const &point{path.points[index]}; + auto const distance{std::hypot(point.x - end.x, point.z - end.z)}; + if (distance < nearest) + { + nearest = distance; + gap = point.y - end.y; + } + } + if (nearest <= kSamePoint && std::abs(gap) > 0.001) + gaps.push_back({joint.track, joint.neighbour, gap}); + } + return gaps; +} + +void editor_track::keep_heights(chain const &Chain, std::vector &Pieces) +{ + auto existing{route_of(Chain)}; + auto const samples{sample_route(existing, 1.0)}; + if (samples.empty() || existing.length <= 1e-6) + return; + std::vector lengths; + double total{0.0}; + for (auto const &piece : Pieces) + { + lengths.push_back(bezier(piece).plan_length()); + total += lengths.back(); + } + if (total <= 1e-6) + return; + auto const scale{existing.length / total}; + double chainage{0.0}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + { + auto const from{chainage * scale}; + auto const to{(chainage + lengths[i]) * scale}; + set_heights(Pieces[i], sampled_elevation(samples, from), sampled_elevation(samples, to), sampled_grade(samples, from) * scale, sampled_grade(samples, to) * scale); + chainage += lengths[i]; + } +} + std::vector editor_track::find_switch_templates() { std::vector result; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 13800f43..396a66c5 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -147,6 +147,51 @@ class editor_track static straight_run straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance); static double run_reserve(straight_run const &Run); static TTrack *take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector> &States, std::vector &Created); + // sequence of paths, switches included, which a train can run along + struct route_span + { + TTrack *track{nullptr}; + int path{0}; + bool forward{true}; + double from{0.0}; // chainage, measured in the plan + double to{0.0}; + }; + struct route + { + std::vector spans; + double length{0.0}; + }; + static bool find_route(TTrack *From, TTrack *To, route &Route, std::string &Error); + // 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); + struct route_sample + { + double chainage{0.0}; + glm::dvec3 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; + double curvature{0.0}; // signed, 1/m, positive turning left + std::size_t span{0}; + }; + static std::vector sample_route(route &Route, double const Step); + static double sampled_elevation(std::vector const &Samples, double const Chainage); + static double sampled_grade(std::vector const &Samples, double const Chainage); + // grade of the path which adjoins the end of the route, positive rising along the route + static bool adjoining_grade(route const &Route, bool const Atend, double &Grade); + // path off the route whose end no longer meets the adjoining path in height + struct height_gap + { + TTrack *track{nullptr}; + TTrack *neighbour{nullptr}; + double gap{0.0}; // neighbour - track + }; + // sets elevations of the paths of the route; regular paths are split at the chainages in Breaks, + // switches are tilted as a whole in the plane of their grade, branches included + static std::vector apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &Created); + // carries elevations of the chain over to the new pieces, in proportion of the length + static void keep_heights(chain const &Chain, std::vector &Pieces); + struct switch_template { TTrack *source{nullptr}; diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 0c9a1c5d..2b8436e1 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -17,7 +17,10 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "utilities/utilities.h" +#include +#include #include +#include namespace scene { @@ -37,6 +40,17 @@ glm::dvec3 layer_context::to_local(glm::dvec3 Location) const return Location; } +glm::dvec3 layer_context::to_world(glm::dvec3 Location) const +{ + if (rotation != glm::vec3{0.f}) + { + Location = glm::rotateY(Location, glm::radians(static_cast(rotation.y))); + } + Location *= glm::dvec3{scale}; + Location += offset; + return Location; +} + bool layer_context::matches(layer_context const &Other) const { return glm::all(glm::epsilonEqual(offset, Other.offset, 1e-4)) && glm::all(glm::epsilonEqual(rotation, Other.rotation, 1e-4f)) && glm::all(glm::epsilonEqual(scale, Other.scale, 1e-5f)); @@ -62,6 +76,7 @@ layer_handle node_layers::open(std::string const &Name) m_layers.back().parent = handle(); m_layers.back().late = m_initialized; stat(layerhandle); + scan(layerhandle); } else { @@ -119,6 +134,8 @@ void node_layers::clear() m_instance = 0; m_initialized = false; m_terraindirective = false; + m_marked.clear(); + m_marking.clear(); } // indicates the scenario initialization (FirstInit) is being performed @@ -767,7 +784,104 @@ bool node_layers::merge(layer_handle Source, layer_handle Target) // true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes bool node_layers::pending() const { - return std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); }); + return false == m_marking.empty() || + std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); }); +} + +namespace +{ +// marks of the lines the editor keeps track of +std::array const editormarks{"//$p", "//$b"}; +} // namespace + +void node_layers::scan(layer_handle const Layer) +{ + m_marked.erase(Layer); + std::ifstream file{path(Layer), std::ios_base::binary}; + if (false == file.is_open()) + { + return; + } + std::string content{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + if (content.find("//$") == std::string::npos) + { + return; + } + std::size_t begin{0}; + while (begin < content.size()) + { + auto end{content.find('\n', begin)}; + end = (end == std::string::npos ? content.size() : end + 1); + auto const first{content.find_first_not_of(" \t", begin)}; + if (first < end) + { + for (auto const *mark : editormarks) + { + auto const length{std::char_traits::length(mark)}; + if (content.compare(first, length, mark) != 0 || (first + length < end && content[first + length] != ' ' && content[first + length] != '\t' && content[first + length] != '\r' && content[first + length] != '\n')) + { + continue; + } + auto text{content.substr(first + length, end - first - length)}; + while (false == text.empty() && (text.back() == '\n' || text.back() == '\r')) + { + text.pop_back(); + } + text.erase(0, text.find_first_not_of(" \t")); + m_marked[Layer].push_back({mark, {static_cast(begin), static_cast(end)}, std::move(text)}); + break; + } + } + begin = end; + } +} + +std::vector node_layers::marked(layer_handle const Layer, std::string const &Mark) const +{ + std::vector result; + if (auto const pending{m_marking.find({Layer, Mark})}; pending != m_marking.end()) + { + return pending->second; + } + if (auto const lookup{m_marked.find(Layer)}; lookup != m_marked.end()) + { + for (auto const &line : lookup->second) + { + if (line.mark == Mark) + { + result.push_back(line.text); + } + } + } + return result; +} + +std::vector node_layers::marked_layers(std::string const &Mark) const +{ + std::vector result; + for (auto const &entry : m_marked) + { + if (listed(entry.first) && std::any_of(entry.second.begin(), entry.second.end(), [&](marked_line const &Line) { return Line.mark == Mark; })) + { + result.push_back(entry.first); + } + } + for (auto const &entry : m_marking) + { + if (entry.first.second == Mark && std::find(result.begin(), result.end(), entry.first.first) == result.end()) + { + result.push_back(entry.first.first); + } + } + return result; +} + +void node_layers::mark(layer_handle const Layer, std::string const &Mark, std::vector Lines) +{ + if (valid(Layer)) + { + m_marking[{Layer, Mark}] = std::move(Lines); + } } // indicates start of content of a template, included by directive at specified location of the layer file being loaded diff --git a/scene/scenelayers.h b/scene/scenelayers.h index 211de91f..523e2da3 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include namespace scene @@ -44,6 +45,8 @@ struct layer_context // converts world space location to the one which produces it when loaded with this context in effect glm::dvec3 to_local(glm::dvec3 Location) const; + // reverse of to_local() + glm::dvec3 to_world(glm::dvec3 Location) const; bool matches(layer_context const &Other) const; }; @@ -261,6 +264,14 @@ class node_layers return m_terraindirective; } + // editor data kept in the scenery files as comment lines starting with a mark, e.g. "//$p ...", which the simulation skips + // text of the lines with specified mark in specified layer, with the mark taken off + std::vector marked(layer_handle Layer, std::string const &Mark) const; + // layers whose files hold lines with specified mark + std::vector marked_layers(std::string const &Mark) const; + // lines to take the place of the ones with specified mark in the layer file, on save. the mark is added to each + void mark(layer_handle Layer, std::string const &Mark, std::vector Lines); + // layer management. changes are applied to the scenery files by save() // creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason layer_handle create(std::string Name, layer_handle Parent, std::string &Reason); @@ -319,9 +330,17 @@ class node_layers struct file_patch; struct composition; struct save_state; + struct marked_line + { + std::string mark; + source_span span; // whole line, its end included + std::string text; + }; // methods std::string path(layer_handle const Layer) const; void stat(layer_handle const Layer); + // finds the marked lines in the layer file + void scan(layer_handle const Layer); bool compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const; // members std::vector m_layers; // layer handle is index in the vector + 1 @@ -335,6 +354,8 @@ class node_layers instance_handle m_instance{0}; // helper, include whose template is being loaded bool m_initialized{false}; // helper, the scenario initialization was performed already bool m_terraindirective{false}; + std::map> m_marked; + std::map, std::vector> m_marking; // changes awaiting save }; extern node_layers Layers; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index df077aa7..9d5f9297 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -801,6 +801,37 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[erased.first].edits.emplace_back(std::move(edit)); } + // editor data in the comment lines: the old lines go, the new ones are added to the file like new definitions + std::map> markedtext; + for (auto const &marking : m_marking) + { + auto const markedlayer{marking.first.first}; + auto const &mark{marking.first.second}; + if (auto const lookup{m_marked.find(markedlayer)}; lookup != m_marked.end()) + { + for (auto const &line : lookup->second) + { + if (line.mark != mark) + { + continue; + } + file_patch::edit edit; + edit.begin = line.span.begin; + edit.end = line.span.end; + state.patches[markedlayer].edits.emplace_back(std::move(edit)); + } + } + auto const target{resolve(markedlayer)}; + if (marking.second.empty() || false == is_output(target)) + { + continue; + } + for (auto const &text : marking.second) + { + markedtext[target].push_back(mark + ' ' + text); + } + } + // includes of templates std::map> placed; // directives made in the editor, by target layer for (std::size_t idx = 0; idx < m_instances.size(); ++idx) @@ -940,7 +971,7 @@ save_result node_layers::save(std::vector const &Rootstatements) continue; } if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 || placed.count(candidate) != 0 || - (candidate == root && false == Rootstatements.empty())) + markedtext.count(candidate) != 0 || (candidate == root && false == Rootstatements.empty())) { outputs.emplace_back(candidate); } @@ -1034,6 +1065,14 @@ save_result node_layers::save(std::vector const &Rootstatements) added.text += directive + eol; } } + if (auto const lookup{markedtext.find(output)}; lookup != markedtext.end()) + { + for (auto const &line : lookup->second) + { + ensure_newline(added.text, eol); + added.text += line + eol; + } + } if (output == root) { for (auto const &statement : Rootstatements) @@ -1221,7 +1260,17 @@ save_result node_layers::save(std::vector const &Rootstatements) for (auto const output : written) { stat(output); + scan(output); } + for (auto const &marking : m_marking) + { + if (resolve(marking.first.first) != marking.first.first) + { + // the lines of the merged layer are now in the file of its target + m_marked.erase(marking.first.first); + } + } + m_marking.clear(); for (auto &included : m_instances) { // removed includes are gone from the files now