mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-27 09:27:29 +01:00
The track mode Kilometrage (X, under More) puts a point of the km where a track is clicked: the line, the km and, at a jump, the km on from it. The axis of a line follows the main paths of the switches through the tracks of its points; on a double-track line it runs between the tracks, and where they part on formations of their own and come together again it runs along the longer one, the shorter one going by its own length with its last hectometre before they meet taking up the difference (PKP GK-1 6.1.12). The km between the points are stretched to the length of the scenery evenly; the panel lists the points with the stretch, the 3D view shows the axis, a tick at each hectometre, the points with their km and the km of the lines by the mouse. Read the km from the posts turns the hectometre posts along the line into the points of its km, a jump where two runs of numbers meet, and warns of a jump on a curve or at a station (Id-1 § 19). Each change is a step of undo.
905 lines
36 KiB
C++
905 lines
36 KiB
C++
/*
|
|
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 <algorithm>
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <limits>
|
|
|
|
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<std::string> 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<int>(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<editor_track::route_sample> const &Samples, glm::dvec3 const &Point, double &Distance)
|
|
{
|
|
Distance = std::numeric_limits<double>::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<editor_track::route_sample> 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<std::size_t>(static_cast<std::size_t>(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<unsigned char>(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);
|
|
std::vector<double> jumps;
|
|
for (auto const &anchor : axis->anchors)
|
|
{
|
|
if (std::abs(anchor.after - anchor.before) < 1e-6)
|
|
continue;
|
|
jumps.push_back(anchor.chainage);
|
|
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});
|
|
}
|
|
// a tick at each hectometre the km pass along the axis, none at a jump
|
|
auto const hectometres_along = [&](std::vector<glm::dvec3> const &Points, std::vector<double> const &Km, std::vector<double> const &Chainage, std::vector<tick> &Ticks) {
|
|
for (std::size_t i = 1; i < Points.size(); ++i)
|
|
{
|
|
auto const from{std::min(Chainage[i - 1], Chainage[i])};
|
|
auto const to{std::max(Chainage[i - 1], Chainage[i])};
|
|
if (std::any_of(jumps.begin(), jumps.end(), [&](double const Jump) { return Jump >= from - 1e-6 && Jump <= to + 1e-6; }))
|
|
continue;
|
|
auto const k0{Km[i - 1]};
|
|
auto const k1{Km[i]};
|
|
if (std::abs(k1 - k0) < 1e-9)
|
|
continue;
|
|
auto way{plan_of(Points[i] - Points[i - 1])};
|
|
way = glm::length(way) > 1e-9 ? glm::normalize(way) : glm::dvec2{0.0, 1.0};
|
|
auto const first{static_cast<int>(std::floor(std::min(k0, k1) * 10.0 + 1e-9)) + 1};
|
|
auto const last{static_cast<int>(std::floor(std::max(k0, k1) * 10.0 + 1e-9))};
|
|
for (auto hectometres = first; hectometres <= last && hectometres < first + 10; ++hectometres)
|
|
Ticks.push_back({glm::mix(Points[i - 1], Points[i], (hectometres / 10.0 - k0) / (k1 - k0)), {-way.y, 0.0, way.x}, hectometres, false});
|
|
}
|
|
};
|
|
std::vector<double> km;
|
|
for (auto const chainage : axis->chainage)
|
|
km.push_back(kilometrage::km_on(*axis, chainage));
|
|
hectometres_along(axis->points, km, axis->chainage, entry.ticks);
|
|
m_drawn.push_back(std::move(entry));
|
|
// the shorter track where the tracks run apart, with its irregular hectometre
|
|
for (auto const &side : axis->sides)
|
|
{
|
|
drawn apart;
|
|
apart.line = line;
|
|
apart.points = side.points;
|
|
apart.guessed.assign(side.points.size(), 0);
|
|
km.clear();
|
|
for (auto const along : side.along)
|
|
km.push_back(kilometrage::km_on(*axis, side, along));
|
|
hectometres_along(side.points, km, side.chainage, apart.ticks);
|
|
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<int>(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};
|
|
m_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<int>(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
|
|
auto best{kReadoutReach};
|
|
for (auto const &line : kilometrage::lines())
|
|
if (auto const *axis{kilometrage::axis_of(line)}; axis != nullptr)
|
|
{
|
|
double gap{0.0};
|
|
kilometrage::chainage_at(*axis, entry.point, gap);
|
|
if (gap < best)
|
|
{
|
|
best = gap;
|
|
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<double>(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<std::size_t>(m_edited), entry);
|
|
else
|
|
{
|
|
kilometrage::put(entry);
|
|
m_edited = static_cast<int>(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<std::size_t>(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::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<post> 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<post> const &Run) { return Run.size() < 2 ? 0 : Run.back().hectometres > Run.front().hectometres ? 1 : -1; };
|
|
std::vector<std::vector<post>> 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<std::vector<post>> 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<post> 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<double> 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<double>(growth);
|
|
entry.km = Km;
|
|
entry.beyond = Beyond;
|
|
return entry;
|
|
};
|
|
std::vector<kilometrage::mark> 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<int>(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<std::size_t>(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<char, 32> 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<char, 32> 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("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(3, 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::size_t>(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 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<int>(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<int>(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<int>(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));
|
|
}
|
|
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=<name> [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};
|
|
pick(*track, editor_track::point_at(*track, at));
|
|
WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f", 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));
|
|
return editor_selftest_result::handled;
|
|
}
|
|
if (Command == "kmset")
|
|
{
|
|
// kmset line=<number> km=<km> [beyond=<km>] [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 == "kmremove")
|
|
{
|
|
remove();
|
|
WriteLog("SELFTEST kmremove: " + m_status);
|
|
return editor_selftest_result::handled;
|
|
}
|
|
if (Command == "kmposts")
|
|
{
|
|
// kmposts line=<number>: 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=<index>: 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::size_t>(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, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.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=<name> [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;
|
|
}
|