/* 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/editormodekm.h" #include "application/editorgestures.h" #include "application/editormodeshared.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorTrack.hpp" #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/utilities.h" #include "world/Track.h" #include "imgui/imgui.h" #include "utilities/translation.h" #include #include #include #include #include #include namespace { using geometry::plan_distance; using geometry::plan_of; double constexpr kOverlayRange{3000.0}; double constexpr kTickRange{1500.0}; // m, of the hectometres shown in the scenery double constexpr kLabelRange{700.0}; // m, of the km written there, farther ones crowd the horizon double constexpr kDigitRange{250.0}; // m, of the hectometres written there double constexpr kRailHead{0.18}; // m, above the axis of a path double constexpr kPickReach{15.0}; // m from a point of the km a click on a track takes it double constexpr kReadoutReach{30.0}; // m from the axis of a line its km is shown by the mouse double constexpr kPostReach{60000.0}; // m along the line each way, of the posts read double constexpr kPostCorridor{15.0}; // m from the track, posts farther away belong to another line double constexpr kPostTolerance{10.0}; // m, of the distance between two posts against their numbers double constexpr kCurve{10000.0}; // m, of the radius a jump of the km is on a curve under double constexpr kStation{300.0}; // m from a switch a jump of the km is at a station ImU32 constexpr kAxis{IM_COL32(110, 200, 255, 220)}; ImU32 constexpr kGuessed{IM_COL32(110, 200, 255, 90)}; ImU32 constexpr kTick{IM_COL32(235, 240, 250, 230)}; ImU32 constexpr kKm{IM_COL32(255, 215, 90, 255)}; ImU32 constexpr kJump{IM_COL32(255, 95, 215, 255)}; ImU32 constexpr kMark{IM_COL32(95, 220, 140, 255)}; ImU32 constexpr kAstray{IM_COL32(240, 90, 80, 255)}; // the digits after the rotation make the number of a hectometre post, the last one the hectometre bool hectometres_of(std::vector const &Values, int &Hectometres) { if (Values.size() < 6) return false; Hectometres = 0; for (std::size_t i = 5; i < Values.size(); ++i) { double digit; if (false == numeric(Values[i], digit) || digit < 0.0 || digit > 9.0) return false; Hectometres = Hectometres * 10 + static_cast(digit); } return true; } // chainage of the point of the samples nearest to the point, and how far from them it is double project(std::vector const &Samples, glm::dvec3 const &Point, double &Distance) { Distance = std::numeric_limits::max(); double chainage{0.0}; auto const point{plan_of(Point)}; for (std::size_t i = 1; i < Samples.size(); ++i) { auto const a{plan_of(Samples[i - 1].position)}; auto const ab{plan_of(Samples[i].position) - a}; auto const length2{glm::dot(ab, ab)}; auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0}; if (auto const distance{glm::length(point - (a + ab * t))}; distance < Distance) { Distance = distance; chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t; } } return chainage; } std::size_t sample_at(std::vector const &Samples, double const Chainage) { auto const found{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; return std::min(static_cast(found - Samples.begin()), Samples.size() - 1); } // the number of a line: blanks taken out std::string line_of(std::string const &Text) { std::string result; for (auto const c : Text) if (false == std::isspace(static_cast(c))) result += c; return result; } // a km typed as PKP PLK write it, 100,234, or with a point bool km_of(char const *Text, double &Km) { std::string text{Text}; std::replace(text.begin(), text.end(), ',', '.'); char *end{nullptr}; auto const value{std::strtod(text.c_str(), &end)}; if (end == text.c_str() || false == std::isfinite(value)) return false; Km = value; return true; } std::string mark_text(kilometrage::mark const &Mark) { return Mark.beyond.has_value() ? kilometrage::text(Mark.km) + " = " + kilometrage::text(*Mark.beyond) : kilometrage::text(Mark.km); } void draw_label(ImDrawList *List, ImVec2 const At, ImU32 const Ground, ImU32 const Colour, std::string const &Text) { auto const size{ImGui::CalcTextSize(Text.c_str())}; List->AddRectFilled(ImVec2(At.x - size.x * 0.5f - 4.0f, At.y - size.y - 4.0f), ImVec2(At.x + size.x * 0.5f + 4.0f, At.y), Ground, 3.0f); List->AddText(ImVec2(At.x - size.x * 0.5f, At.y - size.y - 2.0f), Colour, Text.c_str()); } void draw_arrow(screen_projection const &Projection, ImDrawList *List, glm::dvec3 const &From, glm::dvec2 const &Way, double const Length, ImU32 const Colour) { glm::dvec3 const along{Way.x, 0.0, Way.y}; glm::dvec3 const across{-Way.y, 0.0, Way.x}; auto const tip{From + along * Length}; Projection.line(List, From, tip, Colour, 3.0f); Projection.line(List, tip, tip - along * (Length * 0.3) + across * (Length * 0.15), Colour, 3.0f); Projection.line(List, tip, tip - along * (Length * 0.3) - across * (Length * 0.15), Colour, 3.0f); } } // namespace void km_tool::refresh() { m_drawn.clear(); for (auto const &line : kilometrage::lines()) { auto const *axis{kilometrage::axis_of(line)}; if (axis == nullptr || axis->points.size() < 2) continue; drawn entry; entry.line = line; entry.points = axis->points; auto const low{axis->anchors.front().chainage - 1.0}; auto const high{axis->anchors.back().chainage + 1.0}; for (auto const chainage : axis->chainage) entry.guessed.push_back(chainage < low || chainage > high ? 1 : 0); for (auto const &anchor : axis->anchors) if (std::abs(anchor.after - anchor.before) >= 1e-6) { auto const way{kilometrage::direction_on(*axis, anchor.chainage)}; entry.ticks.push_back({kilometrage::point_on(*axis, anchor.chainage), {-way.y, 0.0, way.x}, 0, true}); } for (auto const &hectometre : kilometrage::hectometres(*axis)) entry.ticks.push_back({hectometre.at, {-hectometre.rising.y, 0.0, hectometre.rising.x}, hectometre.number, false}); m_drawn.push_back(std::move(entry)); // the shorter track where the tracks run apart for (auto const &side : axis->sides) { drawn apart; apart.line = line; apart.points = side.points; apart.guessed.assign(side.points.size(), 0); m_drawn.push_back(std::move(apart)); } } m_revision = kilometrage::revision(); m_drawn_once = true; } void km_tool::update() { if (false == m_open) return; auto const &marks{kilometrage::marks()}; // after an undo the point edited may be gone if (m_edited >= 0 && (m_edited >= static_cast(marks.size()) || false == m_pick.has_value() || plan_distance(marks[m_edited].point, m_pick->point) > 0.5)) m_edited = -1; if (false == m_drawn_once || kilometrage::revision() != m_revision) refresh(); } void km_tool::forget(scene::basic_node const *Node) { if (m_pick.has_value() && m_pick->track == Node) m_pick->track = nullptr; } glm::dvec2 km_tool::rising() const { if (false == m_pick.has_value()) return {0.0, 1.0}; return m_pick->along * (m_against ? -1.0 : 1.0); } std::string km_tool::jump_warning(glm::dvec3 const &Point) const { auto *track{kilometrage::track_near(Point, 6.0)}; if (track == nullptr) return {}; auto route{editor_track::run_route(*track, kStation + 50.0)}; auto const samples{editor_track::sample_route(route, 2.0)}; if (samples.size() < 2) return {}; double gap{0.0}; auto const at{project(samples, Point, gap)}; bool curve{false}; bool station{false}; for (auto const &sample : samples) { auto const apart{std::abs(sample.chainage - at)}; if (apart <= 30.0 && std::abs(sample.curvature) > 1.0 / kCurve) curve = true; if (apart <= kStation && sample.span < route.spans.size() && route.spans[sample.span].track->eType != tt_Normal) station = true; } if (curve && station) return STR("Id-1 § 19: the km shouldn't jump on a curve nor at a station, move the jump to a straight on the open line"); if (curve) return STR("Id-1 § 19: the km shouldn't jump on a curve, move the jump to a straight"); if (station) return STR("Id-1 § 19: the km shouldn't jump at a station, move the jump out to the open line"); return {}; } void km_tool::pick(TTrack &Track, glm::dvec3 const &Point) { auto const t{editor_track::nearest_parameter(Track, Point)}; picked entry; entry.track = &Track; entry.point = editor_track::point_at(Track, t); auto const ahead{plan_of(editor_track::point_at(Track, std::min(1.0, t + 0.01)) - editor_track::point_at(Track, std::max(0.0, t - 0.01)))}; entry.along = glm::length(ahead) > 1e-9 ? glm::normalize(ahead) : glm::dvec2{0.0, 1.0}; entry.path = kilometrage::path_near(Track, Point, entry.on); m_status.clear(); m_route_status.clear(); m_edited = -1; auto const &marks{kilometrage::marks()}; auto nearest{kPickReach}; for (std::size_t index = 0; index < marks.size(); ++index) if (auto const distance{plan_distance(marks[index].point, entry.point)}; distance < nearest) { nearest = distance; m_edited = static_cast(index); } if (m_edited >= 0) { auto const &mark{marks[m_edited]}; entry.point = mark.point; m_line = mark.line; m_km = mark.km; m_jump = mark.beyond.has_value(); m_beyond = mark.beyond.value_or(mark.km); m_against = glm::dot(mark.way, entry.along) < 0.0; } else { // the line the point lies on, and its km there if (auto const line{kilometrage::line_at(entry.point, kReadoutReach)}; false == line.empty()) m_line = line; double km{0.0}; if (false == m_line.empty() && kilometrage::km_at(m_line, entry.point, km, kReadoutReach)) { m_km = std::round(km * 1000.0) / 1000.0; auto const *axis{kilometrage::axis_of(m_line)}; double gap{0.0}; auto const along{kilometrage::chainage_at(*axis, entry.point, gap)}; m_against = glm::dot(kilometrage::direction_on(*axis, along) * static_cast(axis->growth), entry.along) < 0.0; } m_jump = false; m_beyond = m_km; } m_pick = entry; } std::string km_tool::readout(glm::dvec3 const &Point) const { std::string result; for (auto const &line : kilometrage::lines()) { double km{0.0}; if (false == kilometrage::km_at(line, Point, km, kReadoutReach)) continue; if (false == result.empty()) result += '\n'; result += format(STR_C("line %s: km %s"), line.c_str(), kilometrage::text(km).c_str()); } return result; } void km_tool::put() { m_status.clear(); if (false == m_pick.has_value()) return; auto const line{line_of(m_line)}; if (line.empty()) { m_status = STR("Give the number of the line first"); return; } if (m_jump && std::abs(m_beyond - m_km) < 0.0005) { m_status = STR("A jump needs another km on from the point"); return; } kilometrage::mark entry; entry.line = line; entry.point = m_pick->point; entry.way = rising(); entry.km = m_km; if (m_jump) entry.beyond = m_beyond; if (m_edited >= 0) kilometrage::change(static_cast(m_edited), entry); else { kilometrage::put(entry); m_edited = static_cast(kilometrage::marks().size()) - 1; } m_line = line; m_history.push_kilometrage_snapshot(); m_status = format(STR_C("Line %s: km %s at the point"), line.c_str(), mark_text(entry).c_str()); } void km_tool::remove() { m_status.clear(); if (m_edited < 0) return; kilometrage::remove(static_cast(m_edited)); m_edited = -1; m_history.push_kilometrage_snapshot(); m_status = STR("The point of the km removed"); } void km_tool::show(std::size_t const Index) { auto const &marks{kilometrage::marks()}; if (Index >= marks.size()) return; auto const point{marks[Index].point}; if (auto *track{kilometrage::track_near(point, 6.0)}; track != nullptr) pick(*track, point); m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); } void km_tool::show_via(std::size_t const Index) { auto const &vias{kilometrage::vias()}; if (Index >= vias.size()) return; auto const point{vias[Index].point}; auto const line{vias[Index].line}; int path{0}; if (auto *track{kilometrage::track_near(point, 6.0, path)}; track != nullptr) { pick(*track, point); m_line = line; } m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); } int km_tool::via_here() const { if (false == m_pick.has_value() || m_pick->track == nullptr) return -1; auto const &vias{kilometrage::vias()}; for (std::size_t index = 0; index < vias.size(); ++index) { if (vias[index].line != m_line) continue; int path{0}; if (kilometrage::track_near(vias[index].point, 6.0, path) == m_pick->track && path == m_pick->path) return static_cast(index); } return -1; } void km_tool::lead_along() { m_route_status.clear(); if (false == m_pick.has_value() || m_pick->track == nullptr) return; auto const line{line_of(m_line)}; if (line.empty()) { m_route_status = STR("Give the number of the line first"); return; } auto const astray_of = [&]() { auto const *axis{kilometrage::axis_of(line)}; return axis != nullptr ? axis->astray.size() : kilometrage::marks().size(); }; auto const before{kilometrage::current()}; auto const astray{astray_of()}; kilometrage::via entry; entry.line = line; entry.point = m_pick->on; kilometrage::put(entry); auto const *axis{kilometrage::axis_of(line)}; if (axis == nullptr || false == axis->runs_along(m_pick->track, m_pick->path)) { kilometrage::restore(before); m_route_status = STR("The line can't run along this track: no way leads from it onto the line"); return; } m_line = line; m_history.push_kilometrage_snapshot(); m_route_status = format(STR_C("Line %s runs along this track now"), line.c_str()); if (auto const lost{astray_of()}; lost > astray) m_route_status += "\n" + format(STR_P("%zu point of the km is off the line now: the track leads it away from it", "%zu points of the km are off the line now: the track leads it away from them", lost - astray), lost - astray); } void km_tool::drop_via() { m_route_status.clear(); auto const index{via_here()}; if (index < 0) return; kilometrage::remove_via(static_cast(index)); m_history.push_kilometrage_snapshot(); m_route_status = format(STR_C("Line %s runs along the straight paths of the switches here again"), m_line.c_str()); } void km_tool::read_posts() { m_status.clear(); auto const line{line_of(m_line)}; if (false == m_pick.has_value() || m_pick->track == nullptr) { m_status = STR("Click a track of the line first"); return; } if (line.empty()) { m_status = STR("Give the number of the line first"); return; } auto route{editor_track::run_route(*m_pick->track, kPostReach)}; auto const samples{editor_track::sample_route(route, 2.0)}; if (samples.size() < 2) { m_status = STR("No line through this track"); return; } auto const length{samples.back().chainage}; struct post { double chainage{0.0}; int hectometres{0}; }; std::vector posts; for (auto const &standing : m_templates.standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) { post entry; if (false == hectometres_of(standing.values, entry.hectometres)) continue; double distance{0.0}; entry.chainage = project(samples, standing.location, distance); if (distance > kPostCorridor || entry.chainage <= 0.0 || entry.chainage >= length) continue; posts.push_back(entry); } if (posts.size() < 2) { m_status = STR("Fewer than two hectometre posts stand along the line, nothing to read"); return; } std::sort(posts.begin(), posts.end(), [](post const &A, post const &B) { return A.chainage < B.chainage; }); // runs of posts numbered by the distance between them, a post of the same number by the other track taken once auto const fits = [](post const &From, post const &To, int const Sign) { auto const counted{(To.hectometres - From.hectometres) * 100.0}; auto const apart{To.chainage - From.chainage}; return counted != 0.0 && (Sign == 0 || (counted > 0.0 ? 1 : -1) == Sign) && std::abs(std::abs(counted) - apart) <= std::max(kPostTolerance, apart * 0.02); }; auto const sign_of = [](std::vector const &Run) { return Run.size() < 2 ? 0 : Run.back().hectometres > Run.front().hectometres ? 1 : -1; }; std::vector> runs; std::size_t twins{0}; for (auto const &entry : posts) { if (false == runs.empty()) { auto &run{runs.back()}; if (entry.hectometres == run.back().hectometres && entry.chainage - run.back().chainage < kPostTolerance * 3.0) { ++twins; continue; } if (fits(run.back(), entry, sign_of(run))) { run.push_back(entry); continue; } } runs.push_back({entry}); } // a post alone between the runs is one out of place std::size_t stray{0}; std::vector> kept; for (auto &run : runs) { if (run.size() < 2) { ++stray; continue; } if (false == kept.empty() && fits(kept.back().back(), run.front(), sign_of(kept.back())) && sign_of(kept.back()) == sign_of(run)) kept.back().insert(kept.back().end(), run.begin(), run.end()); else kept.push_back(std::move(run)); } std::size_t rising{0}; std::size_t falling{0}; for (auto const &run : kept) (sign_of(run) > 0 ? rising : falling) += run.size(); auto const growth{rising >= falling ? 1 : -1}; kept.erase(std::remove_if(kept.begin(), kept.end(), [&](std::vector const &Run) { if (sign_of(Run) == growth) return false; stray += Run.size(); return true; }), kept.end()); if (kept.empty()) { m_status = STR("The numbers of the posts don't go by 100 m anywhere along the line, nothing read"); return; } auto const mark_at = [&](double const Chainage, double const Km, std::optional const Beyond) { kilometrage::mark entry; entry.line = line; entry.point = editor_track::sampled_position(samples, Chainage); entry.way = samples[sample_at(samples, Chainage)].direction * static_cast(growth); entry.km = Km; entry.beyond = Beyond; return entry; }; std::vector made; std::size_t jumps{0}; std::size_t misplaced{0}; for (std::size_t r = 0; r < kept.size(); ++r) { auto const &run{kept[r]}; auto const &first{run.front()}; auto const &last{run.back()}; // the jump to the next run at the post it starts the irregular hectometre at, along the rising km auto const jump_before{r > 0 && growth > 0}; auto const jump_after{r + 1 < kept.size() && growth < 0}; if (jump_before) { auto const &previous{kept[r - 1].back()}; made.push_back(mark_at(first.chainage, previous.hectometres / 10.0 + (first.chainage - previous.chainage) / 1000.0, first.hectometres / 10.0)); } else made.push_back(mark_at(first.chainage, first.hectometres / 10.0, std::nullopt)); if (jump_after) { auto const &next{kept[r + 1].front()}; made.push_back(mark_at(last.chainage, next.hectometres / 10.0 + (next.chainage - last.chainage) / 1000.0, last.hectometres / 10.0)); } else made.push_back(mark_at(last.chainage, last.hectometres / 10.0, std::nullopt)); if (jump_before || jump_after) { ++jumps; if (false == jump_warning((jump_before ? made[made.size() - 2] : made.back()).point).empty()) ++misplaced; } } // the points of the line along the stretch read give way to the ones read auto const &standing{kilometrage::marks()}; for (auto index{static_cast(standing.size()) - 1}; index >= 0; --index) { if (standing[index].line != line) continue; double gap{0.0}; project(samples, standing[index].point, gap); if (gap <= kPostCorridor) kilometrage::remove(static_cast(index)); } for (auto &entry : made) kilometrage::put(std::move(entry)); m_line = line; m_edited = -1; m_history.push_kilometrage_snapshot(); m_status = format(STR_C("%zu posts read: %zu points of the km, %zu jumps"), posts.size() - twins, made.size(), jumps); if (stray > 0) m_status += "\n" + format(STR_P("%zu post doesn't fit its neighbours and was left out", "%zu posts don't fit their neighbours and were left out", stray), stray); if (misplaced > 0) m_status += "\n" + format(STR_P("%zu jump lies on a curve or at a station, Id-1 § 19 wants none there", "%zu jumps lie on a curve or at a station, Id-1 § 19 wants none there", misplaced), misplaced); } void km_tool::render_options() { ImGui::TextDisabled("%s", STR_C("The km of a railway line: the points where the km is known, from a post or a document, and where it jumps. The km of the rest go from them along the axis of the line: between the tracks of a double-track one, along the longer track where the tracks run on formations of their own, the shorter one getting an irregular hectometre")); ImGui::Spacing(); gesture_rows({{"LMB on a track", "a point of the km there, or the one standing by it"}}); auto const &marks{kilometrage::marks()}; auto const lines{kilometrage::lines()}; std::size_t jumps{0}; for (auto const &mark : marks) if (mark.beyond.has_value()) ++jumps; metric_tiles({{format("%zu", lines.size()), STR_C("lines"), kAxis}, {format("%zu", marks.size()), STR_C("points"), kMark}, {format("%zu", jumps), STR_C("jumps"), kJump}}); step_heading(1, STR_C("A point of the km")); if (false == m_pick.has_value()) ImGui::TextDisabled("%s", STR_C("Click a track where the km is known: by a post, a station or a bridge")); else { if (begin_fields("##kmpoint")) { field_row("Line"); std::array buffer{}; m_line.copy(buffer.data(), buffer.size() - 1); if (ImGui::InputTextWithHint("##kmline", "91", buffer.data(), buffer.size())) m_line = line_of(buffer.data()); item_tooltip("The number of the railway line, as 91: a scenery can hold several lines, each with its own km"); auto const km_field = [&](char const *Label, char const *Id, double &Value) { field_row(Label); std::array text{}; kilometrage::text(Value).copy(text.data(), text.size() - 1); if (ImGui::InputText(Id, text.data(), text.size())) km_of(text.data(), Value); }; km_field("Km", "##kmkm", m_km); item_tooltip("The km of the point, as PKP PLK write it: 100,234 is km 100 and 234 m. At a jump: the km the point is reached at, coming along the rising km"); field_row("Jump"); ImGui::Checkbox("##kmjump", &m_jump); item_tooltip("The km jumps at the point: a line built anew, shortened or lengthened keeps the km of the rest. The irregular hectometre is " "marked by a black stripe under the digit of the post (Id-1 § 19)"); if (m_jump) { km_field("Km on", "##kmbeyond", m_beyond); item_tooltip("The km on from the point, along the rising km"); } ImGui::EndTable(); } auto const *axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; auto const fixed{axis != nullptr && axis->anchors.size() > 1 && std::abs(axis->anchors.back().before - axis->anchors.front().before) > 1e-6}; if (false == fixed) { int way{m_against ? 1 : 0}; if (segmented("##kmway", way, {{0, STR_C("Rise this way"), "The km rise the way the arrow at the point shows"}, {1, STR_C("Rise the other way"), "The km rise against the arrow"}})) m_against = way == 1; } if (m_jump) if (auto const warning{jump_warning(m_pick->point)}; false == warning.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", warning.c_str()); if (main_button(m_edited >= 0 ? STR_C("Change the point") : STR_C("Put the point of the km"), ImVec2(-1.0f, tall_button_height()))) put(); item_tooltip("The point goes to the file of the scenery with the track, the simulation skips it. One step of undo"); if (m_edited >= 0) { if (ImGui::Button(STR_C("Remove the point"), ImVec2(-1.0f, 0.0f))) remove(); item_tooltip("One step of undo"); } } step_heading(2, STR_C("The way through a station")); auto const *line_axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; if (false == m_pick.has_value() || m_pick->track == nullptr) ImGui::TextDisabled("%s", STR_C("Through a station the km run along the straight paths of the switches. Click a track of the station to lead them along it instead")); else if (line_axis == nullptr) ImGui::TextDisabled("%s", STR_C("Put a point of the km of the line first")); else if (auto const via{via_here()}; via >= 0) { if (std::find(line_axis->blocked.begin(), line_axis->blocked.end(), static_cast(via)) != line_axis->blocked.end()) ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", STR_C("The line doesn't run along this track: another track chosen leads it elsewhere")); else ImGui::TextUnformatted(format(STR_C("Line %s runs along this track, as chosen"), m_line.c_str()).c_str()); if (ImGui::Button(STR_C("Back onto the straight paths"), ImVec2(-1.0f, 0.0f))) drop_via(); item_tooltip("The track chosen is forgotten, the line runs along the straight paths of the switches here again. One step of undo"); } else if (line_axis->runs_along(m_pick->track, m_pick->path)) ImGui::TextDisabled("%s", format(STR_C("Line %s runs along this track"), m_line.c_str()).c_str()); else { ImGui::TextDisabled("%s", format(STR_C("Line %s doesn't run along this track"), m_line.c_str()).c_str()); if (ImGui::Button(STR_C("Lead the km along this track"), ImVec2(-1.0f, 0.0f))) lead_along(); item_tooltip("The line runs along the track clicked, along the path of the switch clicked, and on along the straight paths of the switches " "until it meets its tracks again; where it doesn't, as at a junction, it goes on that way. One step of undo"); } if (false == m_route_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_route_status.c_str()); step_heading(3, STR_C("From the posts")); if (ImGui::Button(STR_C("Read the km from the posts"), ImVec2(-1.0f, 0.0f))) read_posts(); item_tooltip("Reads the hectometre posts standing along the line through the track clicked: a point of the km at the first and the last " "post of each run numbered by 100 m, a jump where two runs meet. The points of the line along it before give way. One step of undo"); if (false == m_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); step_heading(4, STR_C("The points of the lines")); if (lines.empty()) { ImGui::TextDisabled("%s", STR_C("None yet")); return; } for (auto const &line : lines) { auto const *axis{kilometrage::axis_of(line)}; auto const count{static_cast(std::count_if(marks.begin(), marks.end(), [&](kilometrage::mark const &Mark) { return Mark.line == line; }))}; if (false == ImGui::CollapsingHeader(format("%s %s (%zu)###kmline%s", STR_C("Line"), line.c_str(), count, line.c_str()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) continue; if (axis == nullptr) { ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", STR_C("No track by the points of the line")); continue; } ImGui::TextDisabled("%s", format(STR_C("%.1f km of the axis, %s"), axis->length() / 1000.0, axis->between ? STR_C("between the tracks") : STR_C("along the track")).c_str()); if (false == axis->sides.empty()) { ImGui::TextDisabled("%s", format(STR_P("%zu stretch with the tracks on formations of their own", "%zu stretches with the tracks on formations of their own", axis->sides.size()), axis->sides.size()).c_str()); item_tooltip("Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by " "its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)"); } auto const &vias{kilometrage::vias()}; for (std::size_t index = 0; index < vias.size(); ++index) { if (vias[index].line != line) continue; auto const blocked{std::find(axis->blocked.begin(), axis->blocked.end(), index) != axis->blocked.end()}; double km{0.0}; auto const label{kilometrage::km_at(line, vias[index].point, km, kReadoutReach) ? format(STR_C("Along the track chosen by km %s"), kilometrage::text(km).c_str()) : std::string{STR_C("Along the track chosen")}}; if (blocked) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.4f, 1.0f)); if (ImGui::Selectable((label + "##kmvia" + std::to_string(index)).c_str(), false)) show_via(index); if (blocked) ImGui::PopStyleColor(); item_tooltip(blocked ? "The line doesn't run along this track: another track chosen leads it elsewhere" : "Through the station the line runs along this track, not along the straight paths of the switches"); } auto const rows{axis->anchors.size() + axis->astray.size()}; if (false == ImGui::BeginTable(("##kmmarks" + line).c_str(), 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(260.0f, ImGui::GetTextLineHeightWithSpacing() * (rows + 1.5f))))) continue; ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn(STR_C("Km"), ImGuiTableColumnFlags_WidthStretch, 0.4f); ImGui::TableSetupColumn(STR_C("In the scenery"), ImGuiTableColumnFlags_WidthStretch, 0.22f); ImGui::TableSetupColumn(STR_C("By the km"), ImGuiTableColumnFlags_WidthStretch, 0.22f); ImGui::TableSetupColumn(STR_C("Off by"), ImGuiTableColumnFlags_WidthStretch, 0.16f); ImGui::TableHeadersRow(); auto const growth{axis->growth}; for (std::size_t i = 0; i < axis->anchors.size(); ++i) { auto const &anchor{axis->anchors[i]}; auto const &mark{marks[anchor.mark]}; ImGui::TableNextRow(); ImGui::TableNextColumn(); if (ImGui::Selectable((mark_text(mark) + "##kmmark" + std::to_string(anchor.mark)).c_str(), static_cast(anchor.mark) == m_edited, ImGuiSelectableFlags_SpanAllColumns)) show(anchor.mark); if (i == 0) continue; auto const &previous{axis->anchors[i - 1]}; auto const scenery{anchor.chainage - previous.chainage}; auto const counted{std::abs((growth > 0 ? anchor.before - previous.after : previous.before - anchor.after) * 1000.0)}; ImGui::TableNextColumn(); ImGui::Text("%.0f m", scenery); ImGui::TableNextColumn(); ImGui::Text("%.0f m", counted); ImGui::TableNextColumn(); auto off{scenery > 1.0 ? (scenery - counted) / scenery * 100.0 : 0.0}; if (std::abs(off) < 0.05) off = 0.0; if (std::abs(off) > 2.0) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%+.1f%%", off); else ImGui::Text("%+.1f%%", off); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("How much longer the stretch is in the scenery than by the km: the km between the points are stretched to it evenly")); } for (auto const index : axis->astray) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.4f, 1.0f)); if (ImGui::Selectable((mark_text(marks[index]) + "##kmmark" + std::to_string(index)).c_str(), static_cast(index) == m_edited, ImGuiSelectableFlags_SpanAllColumns)) show(index); ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The point is away from the axis of the line the other points make: the main paths of the switches don't lead to it")); } ImGui::EndTable(); } } void km_tool::draw_overlay() const { if (false == m_open) return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; glm::dvec3 const camera{Global.pCamera.Pos}; auto const near_enough = [&](glm::dvec3 const &Point, double const Range) { return Global.EditorOrtho || glm::distance(Point, camera) < Range; }; glm::dvec3 const lift{0.0, kRailHead, 0.0}; for (auto const &entry : m_drawn) { // the axis, faint where the km run on past the last point for (std::size_t i = 1; i < entry.points.size(); i += 5) { auto const next{std::min(i + 4, entry.points.size() - 1)}; if (near_enough(entry.points[next], kOverlayRange)) projection.line(drawlist, entry.points[i - 1] + lift, entry.points[next] + lift, entry.guessed[next] != 0 ? kGuessed : kAxis, Global.EditorOrtho ? 4.0f : 2.5f); } for (auto const &tick : entry.ticks) { if (false == near_enough(tick.at, kTickRange)) continue; auto const centre{tick.at + lift}; auto const whole{tick.hectometres % 10 == 0}; auto const reach{tick.jump ? 3.5 : whole ? 2.6 : 1.4}; projection.line(drawlist, centre - tick.across * reach, centre + tick.across * reach, tick.jump ? kJump : whole ? kKm : kTick, tick.jump || whole ? 3.5f : 2.0f); if (tick.jump) continue; ImVec2 screen; if (whole && near_enough(tick.at, kLabelRange) && projection.project(centre + tick.across * reach + glm::dvec3{0.0, 0.6, 0.0}, screen)) draw_label(drawlist, screen, IM_COL32(0, 0, 0, 170), kKm, format("km %d", tick.hectometres / 10)); else if (false == whole && near_enough(tick.at, kDigitRange) && projection.project(centre + tick.across * reach + glm::dvec3{0.0, 0.4, 0.0}, screen)) draw_label(drawlist, screen, IM_COL32(0, 0, 0, 140), kTick, std::to_string(std::abs(tick.hectometres) % 10)); } } // the points of the km: a pole with the km over it, the arrow the way the km rise auto const &marks{kilometrage::marks()}; for (std::size_t index = 0; index < marks.size(); ++index) { auto const &mark{marks[index]}; if (false == near_enough(mark.point, kLabelRange * 2.0)) continue; auto astray{true}; for (auto const &entry : m_drawn) if (entry.line == mark.line) if (auto const *axis{kilometrage::axis_of(mark.line)}; axis != nullptr) astray = std::find(axis->astray.begin(), axis->astray.end(), index) != axis->astray.end(); auto const colour{astray ? kAstray : mark.beyond.has_value() ? kJump : kMark}; auto const foot{mark.point + lift}; auto const top{foot + glm::dvec3{0.0, 3.0, 0.0}}; projection.line(drawlist, foot, top, colour, static_cast(index) == m_edited ? 4.0f : 2.5f); draw_arrow(projection, drawlist, foot, mark.way, 6.0, colour); ImVec2 screen; if (projection.project(top, screen)) draw_label(drawlist, screen, colour, IM_COL32(16, 18, 22, 255), mark.line + ": " + mark_text(mark)); } // the tracks chosen for the way through a station: a rhombus on the track auto const &vias{kilometrage::vias()}; for (std::size_t index = 0; index < vias.size(); ++index) { auto const &via{vias[index]}; ImVec2 screen; if (false == near_enough(via.point, kLabelRange * 2.0) || false == projection.project(via.point + lift, screen)) continue; auto blocked{false}; if (auto const *axis{kilometrage::axis_of(via.line)}; axis != nullptr) blocked = std::find(axis->blocked.begin(), axis->blocked.end(), index) != axis->blocked.end(); auto const colour{blocked ? kAstray : kAxis}; auto const r{8.0f}; drawlist->AddQuadFilled(ImVec2(screen.x, screen.y - r), ImVec2(screen.x + r, screen.y), ImVec2(screen.x, screen.y + r), ImVec2(screen.x - r, screen.y), colour); drawlist->AddQuad(ImVec2(screen.x, screen.y - r), ImVec2(screen.x + r, screen.y), ImVec2(screen.x, screen.y + r), ImVec2(screen.x - r, screen.y), IM_COL32(16, 18, 22, 255), 1.5f); draw_label(drawlist, ImVec2(screen.x, screen.y - r - 2.0f), colour, IM_COL32(16, 18, 22, 255), format(STR_C("%s: this way"), via.line.c_str())); } if (m_pick.has_value() && m_edited < 0) { auto const foot{m_pick->point + lift}; ImVec2 screen; if (projection.project(foot, screen)) drawlist->AddCircle(screen, 9.0f, kKm, 20, 2.5f); draw_arrow(projection, drawlist, foot, rising(), 8.0, kKm); } } editor_selftest_result km_tool::selftest(std::string const &Command, std::istringstream &Words) { if (Command == "kmpick") { // kmpick track= [at=<0..1>]: a click on the track auto const values{selftest_values(Words)}; auto const found{values.find("track")}; auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr}; if (track == nullptr) { WriteLog("SELFTEST kmpick: no such track"); return editor_selftest_result::handled; } auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5}; auto const path{values.count("path") ? std::atoi(values.at("path").c_str()) : 0}; pick(*track, path > 0 && path < static_cast(track->m_paths.size()) ? geometry::bezier{track->m_paths[path]}.point(at) : editor_track::point_at(*track, at)); auto const *axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f path %d along %d via %d", track->name().c_str(), m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_against ? 1 : 0, m_edited, m_pick->point.x, m_pick->point.z, m_pick->path, axis != nullptr && axis->runs_along(m_pick->track, m_pick->path) ? 1 : 0, via_here())); return editor_selftest_result::handled; } if (Command == "kmset") { // kmset line= km= [beyond=] [against=1]: the fields set and the point put auto const values{selftest_values(Words)}; if (auto const line{values.find("line")}; line != values.end()) m_line = line->second; if (auto const km{values.find("km")}; km != values.end()) km_of(km->second.c_str(), m_km); m_jump = values.count("beyond") > 0; if (m_jump) km_of(values.at("beyond").c_str(), m_beyond); if (auto const against{values.find("against")}; against != values.end()) m_against = against->second == "1"; put(); WriteLog("SELFTEST kmset: " + m_status); return editor_selftest_result::handled; } if (Command == "kmvia") { // kmvia [line=] [remove=1]: the line led along the track picked, or back onto the straight paths there auto const values{selftest_values(Words)}; if (auto const line{values.find("line")}; line != values.end()) m_line = line->second; if (values.count("remove") > 0) drop_via(); else lead_along(); auto text{m_route_status}; std::replace(text.begin(), text.end(), '\n', ';'); WriteLog("SELFTEST kmvia: " + text); return editor_selftest_result::handled; } if (Command == "kmremove") { remove(); WriteLog("SELFTEST kmremove: " + m_status); return editor_selftest_result::handled; } if (Command == "kmposts") { // kmposts line=: the km read from the posts along the line through the track picked auto const values{selftest_values(Words)}; if (auto const line{values.find("line")}; line != values.end()) m_line = line->second; read_posts(); WriteLog("SELFTEST kmposts: " + m_status); return editor_selftest_result::handled; } if (Command == "kmstate") { WriteLog(format("SELFTEST kmstate: picked %d line %s km %s jump %d edited %d", m_pick.has_value() ? 1 : 0, m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_edited)); return editor_selftest_result::handled; } if (Command == "kmshow") { // kmshow mark=: the point of the km picked, the camera over it auto const values{selftest_values(Words)}; auto const index{values.count("mark") ? std::atoi(values.at("mark").c_str()) : 0}; show(static_cast(std::max(index, 0))); WriteLog(format("SELFTEST kmshow %d: line %s km %s edited %d", index, m_line.c_str(), kilometrage::text(m_km).c_str(), m_edited)); return editor_selftest_result::handled; } if (Command == "kmlist") { refresh(); for (auto const &entry : m_drawn) { std::string list; for (std::size_t i = 0; i < entry.ticks.size() && entry.points.size() < 2000; ++i) list += format(" %d@%.0f,%.0f", entry.ticks[i].hectometres, entry.ticks[i].at.x, entry.ticks[i].at.z); WriteLog(format("SELFTEST kmlist ticks %s: %zu points, %zu ticks", entry.line.c_str(), entry.points.size(), entry.ticks.size()) + list); } auto const &marks{kilometrage::marks()}; WriteLog(format("SELFTEST kmlist: %zu points", marks.size())); for (auto const &line : kilometrage::lines()) { auto const *axis{kilometrage::axis_of(line)}; if (axis == nullptr) { WriteLog("SELFTEST kmlist line " + line + ": no axis"); continue; } std::string list; for (auto const &anchor : axis->anchors) list += format(" %s@%.0f/%.1f", mark_text(marks[anchor.mark]).c_str(), anchor.chainage, anchor.gap); for (auto const &side : axis->sides) WriteLog(format("SELFTEST kmlist apart: %.1f m of track by %.1f m of axis (%.1f..%.1f), km %s..%s", side.length, side.to - side.from, side.from, side.to, kilometrage::text(kilometrage::km_on(*axis, side, 0.0)).c_str(), kilometrage::text(kilometrage::km_on(*axis, side, side.length)).c_str())); WriteLog(format("SELFTEST kmlist line %s: axis %.0f m %s growth %d, %zu astray, %zu blocked, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.size(), axis->blocked.size(), kilometrage::text(kilometrage::km_on(*axis, 0.0)).c_str(), kilometrage::text(kilometrage::km_on(*axis, axis->length())).c_str()) + list); } return editor_selftest_result::handled; } if (Command == "kmat") { // kmat track= [at=<0..1>]: the km of the lines there auto const values{selftest_values(Words)}; auto const found{values.find("track")}; auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr}; if (track == nullptr) { WriteLog("SELFTEST kmat: no such track"); return editor_selftest_result::handled; } auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5}; auto text{readout(editor_track::point_at(*track, at))}; std::replace(text.begin(), text.end(), '\n', ';'); WriteLog(format("SELFTEST kmat %s %.2f: ", track->name().c_str(), at) + (text.empty() ? std::string{"-"} : text)); return editor_selftest_result::handled; } return editor_selftest_result::unhandled; }