17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 02:49:19 +02:00

Merge branch 'feat/modern-editor' of https://github.com/MaSzyna-EU07/maszyna into feat/modern-editor

This commit is contained in:
2026-10-04 21:58:19 +02:00
32 changed files with 7287 additions and 739 deletions

View File

@@ -9,11 +9,12 @@ http:
#include "stdafx.h"
#include "editor/editorAlignment.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <functional>
@@ -26,43 +27,20 @@ namespace
double const kGravity{9.81};
double const kPi{3.14159265358979323846};
double cross(glm::dvec2 const &A, glm::dvec2 const &B)
{
return A.x * B.y - A.y * B.x;
}
using geometry::cross;
using geometry::plan_of;
using geometry::turned;
glm::dvec2 perpendicular(glm::dvec2 const &Vector)
{
return {-Vector.y, Vector.x};
}
glm::dvec2 rotate(glm::dvec2 const &Vector, double const Angle)
{
auto const c{std::cos(Angle)};
auto const s{std::sin(Angle)};
return {Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c};
}
glm::dvec2 plan(glm::dvec3 const &Point)
{
return {Point.x, Point.z};
}
double ceil_to(double const Value, double const Step)
{
return std::ceil(Value / Step - 1e-9) * Step;
}
std::string format(char const *Format, ...)
{
char buffer[512];
va_list arguments;
va_start(arguments, Format);
std::vsnprintf(buffer, sizeof(buffer), Format, arguments);
va_end(arguments);
return buffer;
}
struct local_point
{
double x{0.0};
@@ -157,8 +135,8 @@ void element_point(element const &Element, transition_shape const Shape, double
case element_kind::arc:
{
auto const angle{Element.turn * Distance / Element.radius};
Position = Element.origin + rotate(Element.normal, angle);
Direction = rotate(Element.direction, angle);
Position = Element.origin + turned(Element.normal, angle);
Direction = turned(Element.direction, angle);
break;
}
case element_kind::transition_in:
@@ -267,7 +245,6 @@ recommendation recommend(double const Speed, double const Radius, limits const &
auto const factor{Limits.gauge / (kGravity * 12.96)};
auto const allowance{Limits.gauge * Limits.unbalanced / kGravity};
result.radius_min = factor * Speed * Speed / (Limits.cant_max + allowance);
result.vertical_radius = std::max(Limits.vertical_min, Limits.vertical_factor * Speed * Speed);
if (Radius <= 0.0)
return result;
result.cant_equilibrium = factor * Speed * Speed / Radius;
@@ -277,14 +254,21 @@ recommendation recommend(double const Speed, double const Radius, limits const &
return result;
}
result compute(design const &Design)
namespace
{
result fit_between(design const &Design)
{
result r;
r.shape = Design.shape;
r.gauge = Design.norms.gauge;
r.start = Design.start;
r.end = Design.end;
auto const startreserve{std::max(0.0, Design.start_reserve)};
auto const endreserve{std::max(0.0, Design.end_reserve)};
auto const start{plan(Design.start)};
auto const end{plan(Design.end)};
auto const start{plan_of(Design.start)};
auto const end{plan_of(Design.end)};
auto const startdirection{glm::normalize(Design.start_direction)};
auto const enddirection{glm::normalize(Design.end_direction)};
auto const count{Design.vertices.size()};
@@ -351,6 +335,16 @@ result compute(design const &Design)
straight.length = Length;
push(straight);
};
auto const starts_early = [&](double const Straight) {
if (Straight < 0.0)
r.start_extension = -Straight;
if (Straight >= -1e-4 || -Straight <= startreserve + 1e-4)
return;
if (startreserve > 0.0)
r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the adjoining straight", -Straight - startreserve));
else
r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -Straight));
};
struct fitted
{
@@ -441,9 +435,9 @@ result compute(design const &Design)
std::vector<double> scales(count, 1.0);
for (std::size_t leg = 0; leg + 1 < polygon.size(); ++leg)
{
auto const available{glm::distance(polygon[leg], polygon[leg + 1])};
auto const available{glm::distance(polygon[leg], polygon[leg + 1]) + (leg == 0 ? startreserve : 0.0) + (leg == count ? endreserve : 0.0)};
auto const required{(leg > 0 ? fits[leg - 1].tangent_out : 0.0) + (leg < count ? fits[leg].tangent_in : 0.0)};
if (required <= available)
if (required <= available + 1e-6)
continue;
auto const scale{available / required * (1.0 - 1e-9)};
if (leg > 0)
@@ -534,13 +528,10 @@ result compute(design const &Design)
auto const along{cross(directionout, endoffset) / cross(directionout, directionin)};
auto const curvestart{position - directionin * along};
auto const straight{glm::dot(curvestart - cursor, directionin)};
if (straight < -1e-4)
{
if (k == 0)
r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight));
else
r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight));
}
if (k == 0)
starts_early(straight);
else if (straight < -1e-4)
r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight));
push_straight(cursor, directionin, straight);
auto const startchainage{chainage};
glm::dvec2 origin{curvestart};
@@ -564,7 +555,7 @@ result compute(design const &Design)
push(piece);
auto const local{spiral_point(segment[0], segment[1], segment[2], segment[2])};
origin += heading * local.x + piece.normal * local.y;
heading = rotate(heading, side * local.angle);
heading = turned(heading, side * local.angle);
}
vertexchainage[k] = (startchainage + chainage) * 0.5;
cursor = origin;
@@ -615,13 +606,10 @@ result compute(design const &Design)
}
auto const straight{glm::dot(curvestart - cursor, directionin)};
if (straight < -1e-4)
{
if (k == 0)
r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight));
else
r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight));
}
if (k == 0)
starts_early(straight);
else if (straight < -1e-4)
r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight));
push_straight(cursor, directionin, straight);
element curve;
@@ -644,7 +632,7 @@ result compute(design const &Design)
curve.length = radius * std::max(0.0, arcangle);
curve.origin = centre;
curve.normal = arcstart - centre;
curve.direction = rotate(directionin, side * endin.angle);
curve.direction = turned(directionin, side * endin.angle);
vertexchainage[k] = chainage + curve.length * 0.5;
if (curve.length > 1e-6)
push(curve);
@@ -698,8 +686,15 @@ result compute(design const &Design)
}
auto const lastdirection{glm::normalize(polygon.back() - polygon[polygon.size() - 2])};
auto const tail{glm::dot(end - cursor, lastdirection)};
if (tail < -1e-4)
r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail));
if (tail < 0.0)
r.end_extension = -tail;
if (tail < -1e-4 && -tail > endreserve + 1e-4)
{
if (endreserve > 0.0)
r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the adjoining straight", count, -tail - endreserve));
else
r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail));
}
push_straight(cursor, lastdirection, tail);
r.length = chainage;
@@ -716,12 +711,11 @@ result compute(design const &Design)
r.vertex_chainages = vertexchainage;
r.profile.push_back({0.0, Design.start.y, 0.0, 0.0});
auto const radius{std::max(Design.norms.vertical_min, Design.norms.vertical_factor * Design.speed * Design.speed)};
for (std::size_t k = 0; k < count; ++k)
{
auto const &vertex{Design.vertices[k]};
auto const elevation{vertex.auto_elevation ? Design.start.y + (Design.end.y - Design.start.y) * vertexchainage[k] / std::max(r.length, 1e-6) : vertex.elevation};
auto const elevation{Design.start.y + (Design.end.y - Design.start.y) * vertexchainage[k] / std::max(r.length, 1e-6)};
r.vertex_elevations.push_back(elevation);
auto const radius{vertex.vertical_radius > 0.0 ? vertex.vertical_radius : std::max(Design.norms.vertical_min, Design.norms.vertical_factor * Design.speed * Design.speed)};
if (vertexchainage[k] <= r.profile.back().chainage + 1e-3)
continue;
r.profile.push_back({vertexchainage[k], elevation, radius, 0.0});
@@ -734,20 +728,6 @@ result compute(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)
@@ -757,10 +737,43 @@ result compute(design const &Design)
return r;
}
glm::dvec3 along_end(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Distance)
{
auto const direction{glm::normalize(Direction)};
return {Point.x + direction.x * Distance, Point.y + Grade * Distance, Point.z + direction.y * Distance};
}
} // namespace
result compute(design const &Design)
{
auto first{fit_between(Design)};
auto const startextension{first.start_extension > 1e-4 ? first.start_extension : 0.0};
auto const endextension{first.end_extension > 1e-4 ? first.end_extension : 0.0};
if (false == first.errors.empty() || (startextension == 0.0 && endextension == 0.0))
{
first.start_extension = first.end_extension = 0.0;
return first;
}
auto extended{Design};
extended.start = along_end(Design.start, Design.start_direction, Design.start_grade, -startextension);
extended.end = along_end(Design.end, Design.end_direction, Design.end_grade, endextension);
extended.start_reserve = extended.end_reserve = 0.0;
if (extended.vertices.size() >= 2)
{
extended.vertices.front().offset += startextension;
extended.vertices.back().offset += endextension;
}
auto r{fit_between(extended)};
r.start_extension = startextension;
r.end_extension = endextension;
return r;
}
bool tangent_intersection(design const &Design, glm::dvec2 &Point)
{
auto const start{plan(Design.start)};
auto const offset{plan(Design.end) - start};
auto const start{plan_of(Design.start)};
auto const offset{plan_of(Design.end) - start};
auto const startdirection{glm::normalize(Design.start_direction)};
auto const enddirection{glm::normalize(Design.end_direction)};
auto const determinant{cross(startdirection, enddirection)};
@@ -776,7 +789,7 @@ bool tangent_intersection(design const &Design, glm::dvec2 &Point)
bool collinear(design const &Design)
{
auto const offset{plan(Design.end) - plan(Design.start)};
auto const offset{plan_of(Design.end) - plan_of(Design.start)};
auto const startdirection{glm::normalize(Design.start_direction)};
auto const enddirection{glm::normalize(Design.end_direction)};
return std::abs(cross(startdirection, offset)) <= 0.01 && glm::dot(startdirection, enddirection) >= std::cos(1e-4) && glm::dot(startdirection, offset) > 0.0;

View File

@@ -45,9 +45,6 @@ struct vertex
double transition_in{0.0};
double transition_out{0.0};
double cant{0.0};
bool auto_elevation{true};
double elevation{0.0};
double vertical_radius{0.0};
bool reverse_turn{false};
bool compound{false};
double radius2{600.0};
@@ -65,6 +62,9 @@ struct design
double end_grade{0.0};
double start_radius{0.0};
double end_radius{0.0};
// length of the straight beyond the start and the end, which the curves can take over
double start_reserve{0.0};
double end_reserve{0.0};
std::vector<vertex> vertices;
transition_shape shape{transition_shape::cubic_parabola};
double speed{100.0};
@@ -80,7 +80,6 @@ struct recommendation
double cant{0.0};
double radius_min{0.0};
double transition{0.0};
double vertical_radius{0.0};
};
recommendation recommend(double const Speed, double const Radius, limits const &Limits);
double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits);
@@ -152,6 +151,10 @@ struct result
std::vector<curve_report> curves;
std::vector<profile_point> profile;
double length{0.0};
glm::dvec3 start{0.0};
glm::dvec3 end{0.0};
double start_extension{0.0};
double end_extension{0.0};
};
result compute(design const &Design);

26
editor/editorFormat.hpp Normal file
View File

@@ -0,0 +1,26 @@
/*
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 <cstdio>
#include <string>
// printf-style formatting into a string, for the messages and the data lines of the editor
template <typename... Args>
std::string format(char const *Format, Args... Arguments)
{
auto const length{std::snprintf(nullptr, 0, Format, Arguments...)};
if (length <= 0)
return {};
std::string text(static_cast<std::size_t>(length) + 1, '\0');
std::snprintf(text.data(), text.size(), Format, Arguments...);
text.resize(static_cast<std::size_t>(length));
return text;
}

320
editor/editorGauge.cpp Normal file
View File

@@ -0,0 +1,320 @@
/*
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/editorGauge.hpp"
#include <cmath>
#include <fstream>
#include <limits>
#include <sstream>
namespace gauge
{
namespace
{
// the widening runs up ahead of the change: on the inner side from 20 m before it, on the outer side from 26 m
// to 6 m before it (figures 3 to 5 of the standard)
double constexpr inner_lead{20.0};
double constexpr outer_lead{26.0};
double constexpr outer_full{6.0};
struct side_widening
{
double inner{0.0};
double outer{0.0};
};
// above the lower part the gauge includes the curves of R >= 250 m already
side_widening upper_widening(double const Radius)
{
if (Radius >= 250.0)
return {};
return {std::max(0.0, 50.0 / Radius - 0.185), std::max(0.0, 60.0 / Radius - 0.225)};
}
side_widening lower_widening(double const Radius)
{
if (Radius >= 250.0)
return {3.75 / Radius, 3.75 / Radius};
return upper_widening(Radius);
}
// values at the points of a route, read as linear between them and continued past the ends
class field
{
public:
field(std::vector<double> const &Chainage, std::vector<double> Values) : m_chainage{Chainage}, m_values{std::move(Values)}, m_integral(m_values.size(), 0.0)
{
for (std::size_t i = 1; i < m_values.size(); ++i)
m_integral[i] = m_integral[i - 1] + 0.5 * (m_values[i - 1] + m_values[i]) * (m_chainage[i] - m_chainage[i - 1]);
}
double mean(double const From, double const To) const
{
return (integral(To) - integral(From)) / (To - From);
}
// larger of the means over the window placed ahead of the point in both directions
double ahead(double const Chainage, double const Near, double const Far) const
{
return std::max(mean(Chainage + Near, Chainage + Far), mean(Chainage - Far, Chainage - Near));
}
private:
double integral(double const Chainage) const
{
if (Chainage <= m_chainage.front())
return m_values.front() * (Chainage - m_chainage.front());
if (Chainage >= m_chainage.back())
return m_integral.back() + m_values.back() * (Chainage - m_chainage.back());
auto const next{static_cast<std::size_t>(std::upper_bound(m_chainage.begin(), m_chainage.end(), Chainage) - m_chainage.begin())};
auto const i{next - 1};
auto const span{m_chainage[next] - m_chainage[i]};
auto const offset{Chainage - m_chainage[i]};
auto const value{span > 0.0 ? m_values[i] + (m_values[next] - m_values[i]) * offset / span : m_values[i]};
return m_integral[i] + 0.5 * (m_values[i] + value) * offset;
}
std::vector<double> const &m_chainage;
std::vector<double> m_values;
std::vector<double> m_integral;
};
// half of the distance between the heads of the rails
double constexpr rail_spread{0.75};
// the point turned by the angle of the sine about the head of the lower rail
place turn(section const &Section, place const &Point, double const Sine)
{
auto const pivot{Section.tilt > 0.0 ? -rail_spread : rail_spread};
auto const cosine{std::sqrt(std::max(0.0, 1.0 - Sine * Sine))};
auto const lateral{Point.lateral - pivot};
return {pivot + lateral * cosine - Point.height * Sine, lateral * Sine + Point.height * cosine};
}
// lower part common to all gauges, from the axis between the rails: the wheel zone at the running edge (717.5 mm
// from the axis) with the flangeway of the dimension "b" 58 mm wide and 38 mm deep, the dimension "a" of 150 mm
// at the rail top outside of it, then the limit installation gauge up to 380 mm
std::vector<profile::point> const common_lower{{0.6595, 0.055}, {0.6595, -0.038}, {0.7175, -0.038}, {0.7175, 0.0}, {0.8675, 0.0},
{0.8675, 0.055}, {1.211, 0.055}, {1.585, 0.380}, {1.675, 0.380}};
// the wheel zone follows the rails, without the widening in the curves
bool wheel_zone(profile::point const &Point)
{
return Point.height <= 0.055 + 1e-9 && Point.half_width < 1.0;
}
// names of the kinds in the file
char const *const kind_names[]{"unified", "installation", "road"};
// outline from the points listed top down, in millimetres, as in the tables of the standard, over the lower part
profile installation(std::string const &Name, std::vector<std::pair<int, int>> const &Points)
{
profile result{Name, kind::installation, common_lower};
for (auto point = Points.rbegin(); point != Points.rend(); ++point)
result.outline.push_back({point->second * 0.001, point->first * 0.001});
return result;
}
// point of the outline on the side (+1 left, -1 right) at the section
place outline_point(profile const &Profile, section const &Section, std::size_t const Index, int const Side)
{
auto const &point{Profile.outline[Index]};
// the step at the top of the lower part belongs to it from below
bool const lower{point.height < lower_part - 1e-9 || (point.height < lower_part + 1e-9 && Index > 0 && Profile.outline[Index - 1].height < lower_part - 1e-9)};
auto const i{Side > 0 ? 1 : 0};
auto const widening{wheel_zone(point) ? 0.0 : lower ? Section.lower[i] : Section.upper[i] + Section.cant[i] * point.height / 1.5};
auto const width{Section.base + point.half_width + widening};
place const result{Side * width, point.height};
return lower && Section.tilt != 0.0 ? turn(Section, result, Section.tilt) : result;
}
// sign of the curvature at the points, from the nearest curve along the line where it's straight
std::vector<double> curve_sides(std::vector<double> const &Curvature)
{
auto const side{[](double const Curvature) { return std::abs(Curvature) > 1e-6 ? (Curvature > 0.0 ? 1.0 : -1.0) : 0.0; }};
std::vector<double> sides(Curvature.size(), 0.0);
double last{0.0};
for (std::size_t i = 0; i < Curvature.size(); ++i)
sides[i] = last = side(Curvature[i]) != 0.0 ? side(Curvature[i]) : last;
last = 0.0;
for (std::size_t i = Curvature.size(); i-- > 0;)
{
if (side(Curvature[i]) != 0.0)
last = side(Curvature[i]);
else if (sides[i] == 0.0)
sides[i] = last;
}
return sides;
}
} // namespace
profile const *choice::of(int const Category) const
{
if ((Category & 1) != 0)
return railway;
if ((Category & 2) != 0)
return road;
return nullptr;
}
std::vector<profile> default_profiles()
{
// limit installation gauge up to 1170 mm, then the outline of the unified gauge
auto lower{common_lower};
lower.insert(lower.end(), {{1.675, 1.170}, {2.000, 1.170}});
std::vector<profile::point> const gpl1{{2.000, 3.050}, {1.900, 3.850}, {1.800, 4.250}, {1.600, 4.500}, {1.450, 4.632}};
std::vector<profile::point> const gpl2{{2.000, 3.625}, {1.920, 4.900}};
std::vector<profile::point> const pantograph{{1.450, 6.600}, {1.150, 6.900}, {0.0, 6.900}};
auto const make{[&](std::string const &Name, std::vector<std::vector<profile::point>> const &Parts) {
profile result{Name, kind::unified, lower};
for (auto const &part : Parts)
result.outline.insert(result.outline.end(), part.begin(), part.end());
return result;
}};
// tables 3, 5, 7, 9 of the type cards and the drawing of GC, above 380 mm
return {make("GPL-1", {gpl1, {{1.260, 4.800}, {1.180, 4.850}, {0.0, 4.850}}}),
make("GPL-1 + pantograf", {gpl1, pantograph}),
make("GPL-2", {gpl2, {{0.0, 4.900}}}),
make("GPL-2 + pantograf", {gpl2, {{1.450, 4.900}}, pantograph}),
installation("G1 graniczna (GSZ)", {{4375, 0}, {4375, 666}, {4010, 1255}, {3700, 1550}, {3250, 1755}, {1170, 1695}, {1170, 1675}}),
installation("G1 nominalna (NSZ)", {{4385, 0}, {4385, 785}, {4010, 1365}, {3700, 1655}, {3250, 1860}, {1170, 1860}, {1170, 1675}}),
installation("G2 graniczna (GSZ)", {{4765, 0}, {4765, 940}, {3835, 1600}, {3530, 1765}, {1170, 1695}, {1170, 1675}}),
installation("G2 nominalna (NSZ)", {{4770, 0}, {4770, 1060}, {3835, 1710}, {3530, 1870}, {1170, 1870}, {1170, 1675}}),
installation("GA graniczna (GSZ)", {{4415, 0}, {4415, 687}, {4080, 1225}, {3808, 1490}, {3250, 1755}, {1170, 1700}, {1170, 1675}}),
installation("GA nominalna (NSZ)", {{4425, 0}, {4425, 805}, {4080, 1340}, {3880, 1600}, {3250, 1860}, {1170, 1860}, {1170, 1675}}),
installation("GB graniczna (GSZ)", {{4415, 0}, {4415, 687}, {4080, 1495}, {3250, 1755}, {1170, 1700}, {1170, 1675}}),
installation("GB nominalna (NSZ)", {{4425, 0}, {4425, 805}, {4110, 1610}, {3250, 1860}, {1170, 1860}, {1170, 1675}}),
installation("GC graniczna (GSZ)", {{4800, 0}, {4800, 1693}, {3550, 1765}, {1170, 1765}, {1170, 1675}}),
installation("GC nominalna (NSZ)", {{4820, 0}, {4820, 1815}, {3550, 1870}, {1170, 1870}, {1170, 1675}}),
{"Droga 4,70 m", kind::road, {{0.5, 0.0}, {0.5, 4.70}}},
{"Droga 4,50 m", kind::road, {{0.5, 0.0}, {0.5, 4.50}}}};
}
// one outline per line: the name in quotes, the kind, then pairs of half-width and height
std::vector<profile> load_profiles(std::string const &File)
{
std::ifstream input{File};
std::vector<profile> profiles;
std::string line;
while (std::getline(input, line))
{
std::istringstream stream{line};
profile profile;
std::string kind;
if (false == static_cast<bool>(stream >> std::quoted(profile.name) >> kind))
continue;
auto const known{std::find(std::begin(kind_names), std::end(kind_names), kind)};
if (known == std::end(kind_names))
continue;
profile.kind = static_cast<gauge::kind>(known - std::begin(kind_names));
profile::point point;
while (stream >> point.half_width >> point.height)
profile.outline.push_back(point);
if (profile.outline.size() >= 2)
profiles.push_back(std::move(profile));
}
return profiles.empty() ? default_profiles() : profiles;
}
void save_profiles(std::string const &File, std::vector<profile> const &Profiles)
{
std::ofstream output{File};
for (auto const &profile : Profiles)
{
output << std::quoted(profile.name) << ' ' << kind_names[static_cast<int>(profile.kind)];
for (auto const &point : profile.outline)
output << ' ' << point.half_width << ' ' << point.height;
output << '\n';
}
}
std::vector<section> sections(kind const Kind, std::vector<double> const &Chainage, std::vector<double> const &Curvature, std::vector<double> const &Cant)
{
auto const count{Chainage.size()};
std::vector<section> result(count);
if (count == 0 || Kind == kind::road)
return result;
// per side, the parts when it's the inner side of the curve and when it's the outer one
for (int side = 0; side < 2; ++side)
{
std::vector<double> upperinner(count), upperouter(count), lowerinner(count), lowerouter(count), cantinner(count), cantouter(count);
for (std::size_t i = 0; i < count; ++i)
{
if (std::abs(Curvature[i]) < 1e-6)
continue;
auto const radius{1.0 / std::abs(Curvature[i])};
bool const inner{(Curvature[i] > 0.0) == (side == 1)};
auto const upper{Kind == kind::unified ? upper_widening(radius) : lower_widening(radius)};
auto const lower{lower_widening(radius)};
(inner ? upperinner : upperouter)[i] = inner ? upper.inner : upper.outer;
(inner ? lowerinner : lowerouter)[i] = inner ? lower.inner : lower.outer;
(inner ? cantinner : cantouter)[i] = Cant[i];
}
field const fields[]{{Chainage, std::move(upperinner)}, {Chainage, std::move(upperouter)}, {Chainage, std::move(lowerinner)},
{Chainage, std::move(lowerouter)}, {Chainage, std::move(cantinner)}, {Chainage, std::move(cantouter)}};
for (std::size_t i = 0; i < count; ++i)
{
auto const inner{[&](field const &Field) { return Field.ahead(Chainage[i], 0.0, inner_lead); }};
auto const outer{[&](field const &Field) { return Field.ahead(Chainage[i], outer_full, outer_lead); }};
result[i].upper[side] = inner(fields[0]) + outer(fields[1]);
result[i].lower[side] = inner(fields[2]) + outer(fields[3]);
result[i].cant[side] = inner(fields[4]) - outer(fields[5]);
}
}
auto const sides{curve_sides(Curvature)};
for (std::size_t i = 0; i < count; ++i)
result[i].tilt = -sides[i] * std::min(1.0, Cant[i] / 1.5);
return result;
}
void ring(profile const &Profile, section const &Section, std::vector<place> &Points)
{
auto const count{Profile.outline.size()};
Points.clear();
for (std::size_t j = 0; j < count; ++j)
Points.push_back(outline_point(Profile, Section, j, -1));
for (std::size_t j = count; j-- > 0;)
Points.push_back(outline_point(Profile, Section, j, 1));
}
double intrusion(std::vector<place> const &Ring, place const &Point)
{
if (Ring.size() < 3)
return 0.0;
bool inside{false};
auto nearest{std::numeric_limits<double>::max()};
auto const *previous{&Ring.back()};
for (auto const &current : Ring)
{
auto const lateral{current.lateral - previous->lateral};
auto const height{current.height - previous->height};
if ((current.height > Point.height) != (previous->height > Point.height) && Point.lateral < previous->lateral + lateral * (Point.height - previous->height) / height)
inside = false == inside;
auto const length{lateral * lateral + height * height};
auto const t{length > 0.0 ? std::clamp(((Point.lateral - previous->lateral) * lateral + (Point.height - previous->height) * height) / length, 0.0, 1.0) : 0.0};
nearest = std::min(nearest, std::hypot(Point.lateral - previous->lateral - lateral * t, Point.height - previous->height - height * t));
previous = &current;
}
return inside ? nearest : -nearest;
}
double widest(profile const &Profile)
{
double result{0.0};
for (auto const &point : Profile.outline)
result = std::max(result, point.half_width);
return result;
}
} // namespace gauge

95
editor/editorGauge.hpp Normal file
View File

@@ -0,0 +1,95 @@
/*
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 <array>
#include <string>
#include <vector>
// structure gauge (skrajnia budowli) of the PKP PLK technical standards, volume II: the space along the track
// which has to stay clear of the objects
namespace gauge
{
// how the outline widens along the track
enum class kind
{
unified, // skrajnia budowli ujednolicona GPL: the curves of R >= 250 m are included above the lower part
installation, // graniczna or nominalna skrajnia zabudowy: widened by 3750/R in all curves
road // skrajnia drogi: measured from the edge of the carriageway, no widening
};
// right half of the outline on a straight track, in metres: half-width from the axis (for the roads from the edge
// of the carriageway) and height above the rail top or the road surface, listed from the bottom up; the left half
// is the mirror
struct profile
{
std::string name;
gauge::kind kind{kind::unified};
struct point
{
double half_width{0.0};
double height{0.0};
};
std::vector<point> outline;
};
// the outlines checked along the paths: the railway one on the tracks, the road one on the roads
struct choice
{
profile const *railway{nullptr};
profile const *road{nullptr};
// by the category flags of the path, 1 a track and 2 a road; null when there's no outline for it
profile const *of(int Category) const;
};
// GPL-1 and GPL-2 over the limit installation gauge below 1170 mm, with and without the pantograph gauge, the limit
// and nominal installation gauges G1, G2, GA, GB, GC of the PKP PLK technical standards, and the road gauges
std::vector<profile> default_profiles();
// outlines kept in the file, the defaults when there's none
std::vector<profile> load_profiles(std::string const &File);
void save_profiles(std::string const &File, std::vector<profile> const &Profiles);
// the points of the paths in the scenery lie at the rail foot, the gauge is measured from the rail head
double constexpr rail_height{0.18};
// below this height the outline is the limit installation gauge, which leaves the room for the platforms
double constexpr lower_part{1.17};
// widening of the sides of the gauge at a place of the track, metres; index 0 is the right side, 1 the left one
struct section
{
double base{0.0}; // added to the half-width of the outline: half of the carriageway of a road
std::array<double, 2> upper{}; // from the radius, above the lower part
std::array<double, 2> lower{}; // from the radius, in the lower part
std::array<double, 2> cant{}; // cant in metres, positive on the inner side of the curve and negative on the outer one
double tilt{0.0}; // sine of the slope of the plane of the rail heads, positive with the left rail raised
};
// point of the cross-section, metres: lateral positive to the left, height above the rail top
struct place
{
double lateral{0.0};
double height{0.0};
};
// sections at the points of a route, from their chainage, curvature (1/m, positive turning left) and cant (m).
// The widening begins and ends ahead of the changes of the curvature and cant, as the vehicles enter a curve;
// the plane of the rail heads tilts with the cant, the outer rail raised
std::vector<section> sections(kind Kind, std::vector<double> const &Chainage, std::vector<double> const &Curvature, std::vector<double> const &Cant);
// the outline across the track at the section, from the bottom right up and down to the bottom left, closed across
// the bottom. With the widening: the cant shifts the part above the lower one, the lower part turns with the plane
// of the rail heads instead
void ring(profile const &Profile, section const &Section, std::vector<place> &Points);
// distance of the point from the ring in any direction, metres, positive inside and negative outside
double intrusion(std::vector<place> const &Ring, place const &Point);
// largest half-width of the outline, metres
double widest(profile const &Profile);
} // namespace gauge

131
editor/editorGeometry.hpp Normal file
View File

@@ -0,0 +1,131 @@
/*
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 <algorithm>
#include <cmath>
#include <glm/glm.hpp>
#include "world/Segment.h"
// plan (x, z) geometry and cubic Bezier curves of the paths, shared by the track tools
namespace geometry
{
// points of the paths closer than this are taken for the same point
double constexpr same_point{0.05};
inline glm::dvec2 plan_of(glm::dvec3 const &Point)
{
return {Point.x, Point.z};
}
inline double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B)
{
return std::hypot(A.x - B.x, A.z - B.z);
}
// z component of the 3d cross product; for a unit Direction it's the offset of the Point to its left
inline double cross(glm::dvec2 const &Direction, glm::dvec2 const &Point)
{
return Direction.x * Point.y - Direction.y * Point.x;
}
inline glm::dvec2 turned(glm::dvec2 const &Vector, double const Angle)
{
auto const c{std::cos(Angle)};
auto const s{std::sin(Angle)};
return {Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c};
}
inline double signed_angle(glm::dvec2 const &From, glm::dvec2 const &To)
{
return std::atan2(cross(From, To), glm::dot(From, To));
}
// slope of the direction, rise over the run in the plan
inline double grade_of(glm::dvec3 const &Direction)
{
return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z));
}
// path as a cubic Bezier curve; a path without control points is a straight with evenly spread parameter
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(p0)};
for (int i = 1; i <= Steps; ++i)
{
auto const next{plan_of(point(static_cast<double>(i) / Steps))};
length += glm::distance(previous, next);
previous = next;
}
return length;
}
// parameter at specified plan distance from the start
double parameter(double const Distance, int const Steps = 256) const
{
double length{0.0};
auto previous{plan_of(p0)};
for (int i = 1; i <= Steps; ++i)
{
auto const next{plan_of(point(static_cast<double>(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;
}
};
// distance between the ends of the path, in the plan
inline double chord_length(segment_data const &Path)
{
return glm::length(plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start]));
}
} // namespace geometry

284
editor/editorInfra.cpp Normal file
View File

@@ -0,0 +1,284 @@
/*
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/editorInfra.hpp"
#include "editor/editorGeometry.hpp"
#include "world/Track.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <sstream>
namespace infra
{
namespace
{
using geometry::bezier;
using geometry::plan_distance;
using geometry::plan_of;
int const kSteps{128};
// arc length in the plan at the evenly spread parameters
std::array<double, kSteps + 1> lengths(bezier const &Curve)
{
std::array<double, kSteps + 1> result{};
auto previous{Curve.p0};
for (int i = 1; i <= kSteps; ++i)
{
auto const next{Curve.point(static_cast<double>(i) / kSteps)};
result[i] = result[i - 1] + plan_distance(previous, next);
previous = next;
}
return result;
}
double parameter(std::array<double, kSteps + 1> const &Lengths, double const S)
{
if (S <= 0.0)
return 0.0;
if (S >= Lengths.back())
return 1.0;
auto const next{std::upper_bound(Lengths.begin(), Lengths.end(), S)};
auto const index{static_cast<int>(std::distance(Lengths.begin(), next)) - 1};
auto const run{Lengths[index + 1] - Lengths[index]};
auto const fraction{run > 1e-12 ? (S - Lengths[index]) / run : 0.0};
return (index + fraction) / kSteps;
}
double length_at(std::array<double, kSteps + 1> const &Lengths, double const T)
{
auto const position{std::clamp(T, 0.0, 1.0) * kSteps};
auto const index{std::min(static_cast<int>(position), kSteps - 1)};
return Lengths[index] + (Lengths[index + 1] - Lengths[index]) * (position - index);
}
glm::dvec2 plan_direction(bezier const &Curve, double const T)
{
auto tangent{Curve.first(T)};
glm::dvec2 result{tangent.x, tangent.z};
if (glm::length(result) < 1e-9)
result = {Curve.p3.x - Curve.p0.x, Curve.p3.z - Curve.p0.z};
return glm::length(result) > 1e-9 ? glm::normalize(result) : glm::dvec2{0.0, 1.0};
}
std::string lower(std::string Text)
{
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return Text;
}
} // namespace
char const *name(kind const Kind)
{
switch (Kind)
{
case kind::model: return "model";
case kind::include: return "include";
case kind::traction: return "traction";
case kind::memcell: return "memcell";
case kind::launcher: return "eventlauncher";
}
return "";
}
char const *name(category const Category)
{
switch (Category)
{
case category::signal: return "Signals";
case category::sign: return "Signs";
case category::pole: return "Poles and gantries";
case category::catenary: return "Catenary";
case category::logic: return "Logic";
case category::other: return "Other";
}
return "";
}
std::vector<category> const &categories()
{
static std::vector<category> const result{category::signal, category::sign, category::pole, category::catenary, category::logic, category::other};
return result;
}
double plan_length(segment_data const &Path)
{
return lengths(bezier{Path}).back();
}
std::vector<glm::dvec3> outline(segment_data const &Path, double const Step)
{
bezier const shape{Path};
// the speed along the curve doesn't exceed three times the longest leg of the control polygon
auto const leg{std::max({plan_distance(shape.p0, shape.p1), plan_distance(shape.p1, shape.p2), plan_distance(shape.p2, shape.p3)})};
auto const count{std::clamp(static_cast<int>(std::ceil(3.0 * leg / std::max(Step, 0.1))), 1, 4096)};
std::vector<glm::dvec3> result;
result.reserve(count + 1);
for (int i = 0; i <= count; ++i)
result.push_back(shape.point(static_cast<double>(i) / count));
return result;
}
frame frame_at(segment_data const &Path, double const S)
{
bezier const shape{Path};
auto const t{parameter(lengths(shape), S)};
return {shape.point(t), plan_direction(shape, t)};
}
frame frame_at(station const &Station)
{
if (Station.track == nullptr || Station.path < 0 || Station.path >= static_cast<int>(Station.track->m_paths.size()))
return {};
return frame_at(Station.track->m_paths[Station.path], Station.s);
}
double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior)
{
bezier const shape{Path};
auto const table{lengths(shape)};
auto const distance = [&](double const T) { return plan_distance(shape.point(T), Point); };
auto best{0};
auto bestdistance{distance(0.0)};
for (int i = 1; i <= kSteps; ++i)
{
auto const d{distance(static_cast<double>(i) / kSteps)};
if (d < bestdistance)
{
bestdistance = d;
best = i;
}
}
// golden section search around the nearest sample
auto low{std::max(0, best - 1) / static_cast<double>(kSteps)};
auto high{std::min(kSteps, best + 1) / static_cast<double>(kSteps)};
double const ratio{0.6180339887498949};
auto a{high - ratio * (high - low)};
auto b{low + ratio * (high - low)};
auto da{distance(a)}, db{distance(b)};
for (int i = 0; i < 40; ++i)
{
if (da < db)
{
high = b;
b = a;
db = da;
a = high - ratio * (high - low);
da = distance(a);
}
else
{
low = a;
a = b;
da = db;
b = low + ratio * (high - low);
db = distance(b);
}
}
auto t{(low + high) * 0.5};
if (distance(0.0) <= distance(t))
t = 0.0;
if (distance(1.0) <= distance(t))
t = 1.0;
S = length_at(table, t);
Interior = (t > 1e-6 && t < 1.0 - 1e-6);
return distance(t);
}
double project(TTrack &Track, glm::dvec3 const &Point, station &Station, bool &Interior)
{
auto best{-1.0};
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
{
double s;
bool interior;
auto const d{project(Track.m_paths[i], Point, s, interior)};
// an interior point takes precedence over an end shared with the other path
if (best < 0.0 || d < best - 1e-6 || (std::abs(d - best) <= 1e-6 && interior && false == Interior))
{
best = d;
Station = {&Track, i, s};
Interior = interior;
}
}
return best;
}
double heading(glm::dvec2 const &Forward)
{
return glm::degrees(std::atan2(Forward.x, Forward.y));
}
anchor make_anchor(TTrack *Track, int const Path, glm::dvec3 const &Point)
{
double s;
bool interior;
project(Track->m_paths[Path], Point, s, interior);
return make_anchor({Track, Path, s}, Point);
}
anchor make_anchor(station const &At, glm::dvec3 const &Point)
{
anchor result;
result.at = At;
auto const axis{frame_at(At)};
glm::dvec2 const side{axis.forward.y, -axis.forward.x};
result.offset = glm::dot(plan_of(Point - axis.point), side);
result.height = Point.y - axis.point.y;
result.foot = axis.point;
if (At.track != nullptr && At.path >= 0 && At.path < static_cast<int>(At.track->m_paths.size()))
{
auto const &path{At.track->m_paths[At.path]};
result.start = path.points[segment_data::point::start];
result.end = path.points[segment_data::point::end];
result.length = plan_length(path);
}
return result;
}
glm::dvec3 place(anchor &Anchor)
{
auto const axis{frame_at(Anchor.at)};
Anchor.foot = axis.point;
glm::dvec2 const side{axis.forward.y, -axis.forward.x};
return {axis.point.x + side.x * Anchor.offset, axis.point.y + Anchor.height, axis.point.z + side.y * Anchor.offset};
}
std::vector<rule> default_rules()
{
return {
{category::signal, "semafor sem_ sem- tarcza tarcz_ tz_ tm_ tzw powtarz sygnaliz sygnal signal"},
{category::sign, "wskaz wsk_ wsk- w4 w8 w11 w24 w27 kilometr hektometr slupek_km znak tablic sign"},
{category::catenary, "trakcj siec_ siec- catenary przewod lina_nosna"},
{category::pole, "slup maszt wysieg bramk konstrukcj kotw odciag pole gantry"},
};
}
category classify(std::string const &Name, std::vector<rule> const &Rules)
{
auto const text{lower(Name)};
for (auto const &rule : Rules)
{
std::istringstream words{lower(rule.keywords)};
std::string word;
while (words >> word)
if (text.find(word) != std::string::npos)
return rule.group;
}
return category::other;
}
} // namespace infra

130
editor/editorInfra.hpp Normal file
View File

@@ -0,0 +1,130 @@
/*
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 <array>
#include <string>
#include <vector>
#include <glm/glm.hpp>
#include "world/Segment.h"
#include "scene/scenenode.h"
class TTrack;
// objects which belong to the railway infrastructure (signals, signs, catenary, poles, the logic placed along the
// line) bound to the paths they stand by, so they follow the changes of the track geometry
namespace infra
{
enum class kind
{
model,
include,
traction,
memcell,
launcher
};
enum class category
{
signal,
sign,
pole,
catenary,
logic,
other
};
char const *name(kind Kind);
char const *name(category Category);
std::vector<category> const &categories();
// point of a path, measured in the plan from the start of the path
struct station
{
TTrack *track{nullptr};
int path{0};
double s{0.0};
};
struct frame
{
glm::dvec3 point{0.0};
glm::dvec2 forward{0.0, 1.0}; // in the plan, x and z
};
double plan_length(segment_data const &Path);
// points of the path, no farther apart in the plan than about specified step; the ends included
std::vector<glm::dvec3> outline(segment_data const &Path, double Step);
frame frame_at(segment_data const &Path, double S);
frame frame_at(station const &Station);
// nearest point of specified path in the plan. returns: distance in the plan; Interior tells the nearest point
// isn't one of the ends of the path
double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior);
// nearest point among the paths of the track
double project(TTrack &Track, glm::dvec3 const &Point, station &Station, bool &Interior);
// angle around the vertical axis which turns a model to face specified direction, in degrees
double heading(glm::dvec2 const &Forward);
// location of a point relative to the rail axis: the station, a sideways offset and a height above the rail top
struct anchor
{
station at;
double offset{0.0}; // positive to the right of the direction of the path
double height{0.0};
glm::dvec3 foot{0.0}; // point of the axis the location was measured from
// ends and length of the path when the station was measured, to tell whether the path changed since
glm::dvec3 start{0.0}, end{0.0};
double length{0.0};
};
anchor make_anchor(station const &At, glm::dvec3 const &Point);
// anchor at the place of the path nearest to the point
anchor make_anchor(TTrack *Track, int const Path, glm::dvec3 const &Point);
// location for the current geometry of the path
glm::dvec3 place(anchor &Anchor);
// rules telling infrastructure apart by the names of the files and nodes
struct rule
{
category group{category::other};
std::string keywords; // separated by spaces, matched as parts of the lower case name
};
std::vector<rule> default_rules();
// first rule in the list which matches. returns: category::other if none does
category classify(std::string const &Name, std::vector<rule> const &Rules);
struct binding
{
kind type{kind::model};
category group{category::other};
scene::basic_node *node{nullptr}; // the object, unless it's an include
scene::instance_handle include{0};
std::vector<anchor> anchors; // one per point which defines the object: four for a traction piece
double yaw{0.0}; // rotation of the object relative to the direction of the path
bool turns{false}; // the object has a rotation which follows the path
std::string label;
bool lost{false}; // none of the paths around could take the object over
bool same(binding const &Other) const
{
return type == Other.type && node == Other.node && include == Other.include;
}
};
// placement of a bound object, to take a change back
struct object_state
{
binding saved;
glm::dvec3 location{0.0};
glm::vec3 angles{0.f};
std::array<glm::dvec3, 4> points{};
std::string directive;
bool moved{false}; // the object followed a change since the state was taken
};
} // namespace infra

415
editor/editorProfile.cpp Normal file
View File

@@ -0,0 +1,415 @@
/*
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 "editor/editorFormat.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <optional>
namespace profile
{
namespace
{
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::size_t>(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<pvi> {
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<curve> curves(line const &Line)
{
std::vector<curve> 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<issue> check(line const &Line, context const &Context)
{
std::vector<issue> 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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<double> const &Chainages, std::vector<double> const &Elevations, std::vector<double> 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<double>(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<part> 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::size_t>(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<double> breaks(line const &Line)
{
std::vector<double> 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<long>(value / 1000.0)};
return format("%s%ld+%06.2f", Chainage < 0.0 ? "-" : "", kilometres, value - kilometres * 1000.0);
}
}

119
editor/editorProfile.hpp Normal file
View File

@@ -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 <string>
#include <utility>
#include <vector>
// 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<pvi> 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<curve> 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<zone> switches;
// elevations which the grade line has to keep (chainage, elevation)
std::vector<std::pair<double, double>> 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<issue> 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<double> const &Chainages, std::vector<double> const &Elevations, std::vector<double> 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<double> breaks(line const &Line);
std::string format_chainage(double const Chainage);
}

View File

@@ -9,6 +9,8 @@ http:
#include "stdafx.h"
#include "editor/editorTrack.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "world/Track.h"
#include "vehicle/DynObj.h"
@@ -27,12 +29,20 @@ http:
#include <numeric>
#include <functional>
#include <sstream>
#include <tuple>
#include <unordered_set>
namespace
{
double const kSamePoint{0.05};
using geometry::bezier;
using geometry::chord_length;
using geometry::cross;
using geometry::plan_of;
using geometry::signed_angle;
using geometry::turned;
double const kSamePoint{geometry::same_point};
segment_data::point point_index(editor_track::point_kind const Kind)
{
@@ -45,11 +55,6 @@ segment_data::point point_index(editor_track::point_kind const Kind)
}
}
bool is_end(editor_track::point_kind const Kind)
{
return Kind == editor_track::point_kind::start || Kind == editor_track::point_kind::end;
}
bool has_switch_paths(TTrack const &Track)
{
return Track.SwitchExtension != nullptr && (Track.eType == tt_Switch || Track.eType == tt_Cross);
@@ -74,17 +79,11 @@ glm::dvec3 path_tangent(segment_data const &Path, bool const Atend)
double path_radius(segment_data const &Path, double const T)
{
auto const &p0{Path.points[segment_data::point::start]};
auto const &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{})
if (Path.points[segment_data::point::control1] == glm::dvec3{} && Path.points[segment_data::point::control2] == glm::dvec3{})
return 0.0;
auto const p1{p0 + control1};
auto const p2{p3 + control2};
auto const u{1.0 - T};
auto const first{3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2)};
auto const second{6.0 * u * (p2 - 2.0 * p1 + p0) + 6.0 * T * (p3 - 2.0 * p2 + p1)};
bezier const curve{Path};
auto const first{curve.first(T)};
auto const second{curve.second(T)};
auto const speed{std::hypot(first.x, first.z)};
auto const curvature{std::abs(first.x * second.z - first.z * second.x) / (speed * speed * speed)};
return curvature > 1e-7 ? 1.0 / curvature : 0.0;
@@ -123,8 +122,12 @@ bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool
}
editor_track::observer *editor_track::s_observer{nullptr};
editor_track::state editor_track::capture(TTrack const &Track)
{
if (s_observer != nullptr)
s_observer->track_captured(Track);
state result;
result.paths = Track.m_paths;
result.velocity = Track.fVelocity;
@@ -237,6 +240,8 @@ void editor_track::commit(std::vector<TTrack *> const &Tracks)
for (auto *track : affected)
for (auto *vehicle : track->Dynamics)
vehicle->Move(0.000001);
if (s_observer != nullptr)
s_observer->tracks_committed(Tracks);
}
void editor_track::commit_parameters(TTrack &Track)
@@ -384,7 +389,7 @@ std::vector<std::pair<TTrack *, editor_track::point_ref>> editor_track::connecte
bool editor_track::is_connected(TTrack const &Track, point_ref const &Point)
{
if (false == is_end(Point.kind))
if (false == editor_track::is_end(Point.kind))
return false;
return false == connected_points(Track, point_position(Track, Point)).empty();
}
@@ -447,7 +452,7 @@ editor_track::snap_target editor_track::find_snap_target(TTrack const &Track, gl
void editor_track::snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent)
{
if (Target.track == nullptr || false == is_end(Point.kind))
if (Target.track == nullptr || false == editor_track::is_end(Point.kind))
return;
move_point(Track, Point, Target.position);
@@ -777,23 +782,6 @@ bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::strin
namespace
{
glm::dvec2 plan_of(glm::dvec3 const &Point)
{
return {Point.x, Point.z};
}
glm::dvec2 turned(glm::dvec2 const &Vector, double const Angle)
{
auto const c{std::cos(Angle)};
auto const s{std::sin(Angle)};
return {Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c};
}
double lateral_of(glm::dvec2 const &Direction, glm::dvec2 const &Offset)
{
return Direction.x * Offset.y - Direction.y * Offset.x;
}
bool plan_straight(segment_data const &Path, editor_track::straight_tolerance const &Tolerance)
{
auto const chord{plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])};
@@ -808,7 +796,7 @@ bool plan_straight(segment_data const &Path, editor_track::straight_tolerance co
return true;
auto const direction{chord / length};
for (auto const index : {segment_data::point::control1, segment_data::point::control2})
if (std::abs(lateral_of(direction, plan_of(Path.points[index]))) > Tolerance.offset)
if (std::abs(cross(direction, plan_of(Path.points[index]))) > Tolerance.offset)
return false;
auto const tangentstart{plan_of(path_tangent(Path, false))};
auto const tangentend{plan_of(path_tangent(Path, true))};
@@ -816,8 +804,8 @@ bool plan_straight(segment_data const &Path, editor_track::straight_tolerance co
return false;
auto const a{glm::normalize(tangentstart)};
auto const b{glm::normalize(tangentend)};
auto const turn{std::abs(std::atan2(lateral_of(a, b), glm::dot(a, b)))};
auto const deviation{std::max(std::abs(lateral_of(direction, a)), std::abs(lateral_of(direction, b)))};
auto const turn{std::abs(std::atan2(cross(a, b), glm::dot(a, b)))};
auto const deviation{std::max(std::abs(cross(direction, a)), std::abs(cross(direction, b)))};
return turn * Tolerance.radius <= length && deviation * Tolerance.radius <= length;
}
@@ -841,11 +829,6 @@ glm::dvec2 direction_at(TTrack const &Track, glm::dvec3 const &Joint, bool const
return {0.0, 1.0};
}
double signed_angle(glm::dvec2 const &From, glm::dvec2 const &To)
{
return std::atan2(lateral_of(From, To), glm::dot(From, To));
}
TTrack *neighbour_at(TTrack const &Track, int const Path, bool const Atend)
{
if (has_switch_paths(Track))
@@ -928,7 +911,7 @@ editor_track::straight editor_track::find_straight(TTrack &Track, straight_toler
auto const &first{Track.m_paths[firstpath]};
auto const origin{first.points[segment_data::point::start]};
auto const direction{glm::normalize(plan_of(first.points[segment_data::point::end] - origin))};
auto const lateral = [&](glm::dvec3 const &Point) { return std::abs(lateral_of(direction, plan_of(Point - origin))); };
auto const lateral = [&](glm::dvec3 const &Point) { return std::abs(cross(direction, plan_of(Point - origin))); };
auto const accepts = [&](TTrack const *Other, glm::dvec3 const &Joint) {
if (Other == nullptr || false == is_supported(*Other) || Other->m_editorremoved)
return -1;
@@ -942,7 +925,7 @@ editor_track::straight editor_track::find_straight(TTrack &Track, straight_toler
if (glm::distance(start, Joint) > kSamePoint && glm::distance(end, Joint) > kSamePoint)
continue;
auto const otherdirection{glm::normalize(plan_of(end - start))};
if (std::abs(lateral_of(direction, otherdirection)) > Tolerance.angle)
if (std::abs(cross(direction, otherdirection)) > Tolerance.angle)
continue;
if (lateral(start) > Tolerance.offset || lateral(end) > Tolerance.offset)
continue;
@@ -1182,6 +1165,538 @@ bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance
return true;
}
namespace
{
// shorter remainders of a straight path are absorbed by moving the joint instead of splitting the path
double split_margin(double const Length)
{
return std::max(0.5, 0.002 * Length);
}
} // namespace
editor_track::straight_run editor_track::straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance)
{
straight_run run;
if (Chain.tracks.empty())
return run;
auto *edge{Atend ? Chain.tracks.back() : Chain.tracks.front()};
bool const forward{Atend ? Chain.forward.back() : Chain.forward.front()};
TTrack *current{forward == Atend ? edge->trNext : edge->trPrev};
auto joint{Atend ? Chain.end : Chain.start};
auto const origin{plan_of(joint)};
auto direction{glm::normalize(plan_of(Atend ? Chain.end_direction : -Chain.start_direction))};
while (current != nullptr && run.tracks.size() < 1000)
{
std::string reason;
if (current->eType != tt_Normal || current->m_editorremoved || false == can_edit_geometry(*current, reason))
break;
if (std::find(Chain.tracks.begin(), Chain.tracks.end(), current) != Chain.tracks.end() || std::find(run.tracks.begin(), run.tracks.end(), current) != run.tracks.end())
break;
auto const &path{current->m_paths.front()};
if (false == plan_straight(path, Tolerance))
break;
bool outward;
if (glm::distance(path.points[segment_data::point::start], joint) <= kSamePoint)
outward = true;
else if (glm::distance(path.points[segment_data::point::end], joint) <= kSamePoint)
outward = false;
else
break;
auto const &opposite{path.points[outward ? segment_data::point::end : segment_data::point::start]};
if (run.tracks.empty())
{
auto const own{glm::normalize(plan_of(opposite - joint))};
if (std::abs(cross(direction, own)) > 0.01 || glm::dot(direction, own) <= 0.0)
break;
direction = own;
}
auto const offset{plan_of(opposite) - origin};
auto const along{glm::dot(offset, direction)};
if (along <= run.length + 1e-3 || std::abs(cross(direction, offset)) > Tolerance.offset)
break;
run.tracks.push_back(current);
run.outward.push_back(outward);
run.length = along;
joint = opposite;
current = outward ? current->trNext : current->trPrev;
}
return run;
}
double editor_track::run_reserve(straight_run const &Run)
{
if (Run.tracks.empty())
return 0.0;
return std::max(0.0, Run.length - split_margin(chord_length(Run.tracks.back()->m_paths.front())) - 0.01);
}
TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector<std::pair<TTrack *, state>> &States, std::vector<TTrack *> &Created)
{
auto const length{glm::length(plan_of(Cut - Joint))};
auto *edge{Edge};
double covered{0.0};
for (std::size_t i = 0; i < Run.tracks.size(); ++i)
{
auto *track{Run.tracks[i]};
auto const outward{Run.outward[i]};
auto const &path{track->m_paths.front()};
auto const pathlength{chord_length(path)};
auto const margin{split_margin(pathlength)};
auto const remaining{length - covered};
if (remaining <= margin)
{
if (std::abs(remaining) > 1e-4)
{
States.emplace_back(track, capture(*track));
move_point(*track, {0, outward ? point_kind::start : point_kind::end}, Cut);
commit({track});
}
break;
}
if (remaining < pathlength - margin)
{
auto const &nearpoint{path.points[outward ? segment_data::point::start : segment_data::point::end]};
auto const &farpoint{path.points[outward ? segment_data::point::end : segment_data::point::start]};
auto const t{nearest_parameter(*track, nearpoint + (farpoint - nearpoint) * (remaining / pathlength))};
auto const before{capture(*track)};
auto *created{split_path(*track, t)};
if (created == nullptr)
break;
States.emplace_back(track, before);
Created.push_back(created);
edge = outward ? track : created;
break;
}
edge = track;
covered += pathlength;
}
return edge;
}
namespace
{
using route_node = std::tuple<TTrack *, int, bool>;
std::vector<route_node> route_next(route_node const &Node)
{
std::vector<route_node> 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<int>(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<route_node> nodes;
std::vector<int> parents;
std::map<route_node, int> visited;
for (int i = 0; i < static_cast<int>(From->m_paths.size()); ++i)
for (auto const forward : {true, false})
{
route_node const node{From, i, forward};
visited.emplace(node, static_cast<int>(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<int>(nodes.size()));
nodes.push_back(next);
parents.push_back(static_cast<int>(index));
if (std::get<0>(next) == To)
{
goal = static_cast<int>(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<route_node> 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::route_sample> editor_track::sample_route(route &Route, double const Step)
{
std::vector<route_sample> samples;
double chainage{0.0};
for (std::size_t i = 0; i < Route.spans.size(); ++i)
{
auto &span{Route.spans[i]};
auto const &path{span.track->m_paths[span.path]};
bezier const curve{path};
auto const count{std::max(2, static_cast<int>(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<double>(j) / count : 1.0 - static_cast<double>(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;
sample.cant = 1.5 * std::sin(std::abs(glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t)));
if (planar > 1e-9)
{
sample.direction = plan_of(derivative) / 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 <typename Getter>
auto interpolate(std::vector<editor_track::route_sample> const &Samples, double const Chainage, Getter const &Get) -> decltype(Get(Samples.front()))
{
if (Samples.empty())
return {};
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(plan_of(Point - Origin), 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<route_sample> const &Samples, double const Chainage)
{
return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position.y; });
}
double editor_track::sampled_grade(std::vector<route_sample> const &Samples, double const Chainage)
{
return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.grade; });
}
glm::dvec3 editor_track::sampled_position(std::vector<route_sample> const &Samples, double const Chainage)
{
return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position; });
}
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::height_gap> editor_track::apply_profile(route &Route, std::function<double(double)> const &Elevation, std::function<double(double)> const &Grade, std::vector<double> const &Breaks, std::vector<std::pair<TTrack *, state>> &States, std::vector<TTrack *> &Created)
{
double constexpr margin{0.25};
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<route_span> spans;
for (auto const &span : Route.spans)
{
if (span.track->eType != tt_Normal)
{
spans.push_back(span);
continue;
}
std::vector<double> 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<TTrack *> 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<joint> joints;
std::vector<TTrack *> 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<int>(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<height_gap> 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<double>::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<segment_data> &Pieces)
{
auto existing{route_of(Chain)};
auto const samples{sample_route(existing, 1.0)};
if (samples.empty() || existing.length <= 1e-6)
return;
std::vector<double> 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::switch_template> editor_track::find_switch_templates()
{
std::vector<switch_template> result;
@@ -1213,7 +1728,7 @@ std::vector<editor_track::switch_template> editor_track::find_switch_templates()
local.points[segment_data::point::control1] = tolocal(path.points[segment_data::point::control1]);
local.points[segment_data::point::control2] = tolocal(path.points[segment_data::point::control2]);
}
if (glm::length(glm::dvec2{entry.local[1].points[segment_data::point::start].x, entry.local[1].points[segment_data::point::start].z}) > kSamePoint)
if (glm::length(plan_of(entry.local[1].points[segment_data::point::start])) > kSamePoint)
continue;
if (entry.local[1].points[segment_data::point::end].x < 0.0)
{
@@ -1229,8 +1744,7 @@ std::vector<editor_track::switch_template> editor_track::find_switch_templates()
auto const angle{std::atan2(std::abs(tangent.x), tangent.z)};
entry.ratio = angle > 1e-6 ? 1.0 / std::tan(angle) : 0.0;
entry.radius = path_radius(entry.local[1], 0.25);
char key[256];
std::snprintf(key, sizeof(key), "%.2f|%.2f|%.2f|%s|%s", entry.length, entry.local[1].points[segment_data::point::end].x, entry.local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str());
auto const key{format("%.2f|%.2f|%.2f|%s|%s", entry.length, entry.local[1].points[segment_data::point::end].x, entry.local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str())};
auto const found{lookup.find(key)};
if (found != lookup.end())
{
@@ -1238,9 +1752,7 @@ std::vector<editor_track::switch_template> editor_track::find_switch_templates()
continue;
}
entry.count = 1;
char label[256];
std::snprintf(label, sizeof(label), "L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(track->m_material1).c_str());
entry.label = label;
entry.label = format("L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(track->m_material1).c_str());
lookup.emplace(key, result.size());
result.push_back(entry);
}
@@ -1324,6 +1836,44 @@ std::vector<editor_track::switch_template> editor_track::standard_switch_templat
return result;
}
namespace
{
// definition of a path in the scenery text, up to its points: type, length, parameters, environment and textures
void write_header(std::ostream &Text, char const *Type, double const Length, double const Sound, TTrack const &Style, material_handle const Second)
{
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"};
Text << Type << ' ' << Length << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << Sound << ' ' << Style.iQualityFlag << ' ' << 0 << ' ' << environment << ' ';
if (Style.m_visible)
Text << "vis " << editor_track::material_name(Style.m_material1) << ' ' << Style.fTexLength << ' ' << editor_track::material_name(Second) << ' ' << editor_track::texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' ';
else
Text << "unvis ";
}
void write_points(std::ostream &Text, segment_data const &Path)
{
auto const &start{Path.points[segment_data::point::start]};
auto const &control1{Path.points[segment_data::point::control1]};
auto const &control2{Path.points[segment_data::point::control2]};
auto const &end{Path.points[segment_data::point::end]};
Text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' ';
}
void write_ending(std::ostream &Text, TTrack const &Style)
{
if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty())
Text << "railprofile " << Style.m_profile1.first << ' ';
Text << "endtrack\n";
}
material_handle ballast_of(TTrack const &Style)
{
return Style.eType == tt_Switch ? (Style.SwitchExtension ? Style.SwitchExtension->m_material3 : null_handle) : Style.m_material2;
}
} // namespace
TTrack *editor_track::create_switch(switch_template const &Template, std::vector<segment_data> const &Paths, TTrack const &Style, std::string const &Name)
{
if (Paths.size() < 2)
@@ -1335,31 +1885,14 @@ TTrack *editor_track::create_switch(switch_template const &Template, std::vector
}
else
{
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"};
std::ostringstream text;
text.precision(std::numeric_limits<double>::digits10);
text << "switch " << Template.length << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << 10.0 << ' ' << Style.iQualityFlag << ' ' << 0 << ' ' << environment << ' ';
auto const rails{Style.m_material1};
auto const trackbed{Style.eType == tt_Switch ? (Style.SwitchExtension ? Style.SwitchExtension->m_material3 : null_handle) : Style.m_material2};
if (Style.m_visible)
text << "vis " << material_name(rails) << ' ' << Style.fTexLength << ' ' << material_name(rails) << ' ' << texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' ';
else
text << "unvis ";
for (int i = 0; i < 2; ++i)
{
auto const &path{Paths[i]};
auto const &start{path.points[segment_data::point::start]};
auto const &control1{path.points[segment_data::point::control1]};
auto const &control2{path.points[segment_data::point::control2]};
auto const &end{path.points[segment_data::point::end]};
text << start.x << ' ' << start.y << ' ' << start.z << ' ' << path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << path.rolls[1] << ' ' << path.radius << ' ';
}
if (trackbed != null_handle)
write_header(text, "switch", Template.length, 10.0, Style, Style.m_material1);
write_points(text, Paths[0]);
write_points(text, Paths[1]);
if (auto const trackbed{ballast_of(Style)}; trackbed != null_handle)
text << "trackbed " << material_name(trackbed) << ' ';
if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty())
text << "railprofile " << Style.m_profile1.first << ' ';
text << "endtrack\n";
write_ending(text, Style);
track = load_path(text.str(), Style, Name);
}
track->m_paths.assign(Paths.begin(), Paths.begin() + 2);
@@ -1443,7 +1976,7 @@ void editor_track::move_straight(straight const &Line, glm::dvec3 const &Start,
if (newlength < 1e-3)
return;
auto const newdirection{newplan / newlength};
auto const angle{std::atan2(lateral_of(direction, newdirection), glm::dot(direction, newdirection))};
auto const angle{std::atan2(cross(direction, newdirection), glm::dot(direction, newdirection))};
auto const along = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - Line.start), direction); };
struct interval
@@ -1483,7 +2016,7 @@ void editor_track::move_straight(straight const &Line, glm::dvec3 const &Start,
};
auto const transform = [&](glm::dvec3 const &Point) {
auto const a{along(Point)};
auto const offset{lateral_of(direction, plan_of(Point - Line.start))};
auto const offset{cross(direction, plan_of(Point - Line.start))};
auto const mapped{remap(a)};
auto const oldheight{Line.start.y + (Line.end.y - Line.start.y) * a / Line.length};
auto const newheight{Start.y + (End.y - Start.y) * mapped / newlength};
@@ -1613,47 +2146,35 @@ TTrack *editor_track::clone(TTrack const &Template)
TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path)
{
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"};
auto const category{Style.iCategoryFlag & 15};
std::ostringstream text;
text.precision(std::numeric_limits<double>::digits10);
text << (category == 2 ? "road " : category == 4 ? "river " : "normal ") << glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end]) << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << Style.fSoundDistance << ' ' << Style.iQualityFlag << ' ' << 0 << ' ' << environment << ' ';
auto const ballast{Style.eType == tt_Switch ? (Style.SwitchExtension ? Style.SwitchExtension->m_material3 : null_handle) : Style.m_material2};
if (Style.m_visible)
text << "vis " << material_name(Style.m_material1) << ' ' << Style.fTexLength << ' ' << material_name(ballast) << ' ' << texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' ';
else
text << "unvis ";
auto const &start{Path.points[segment_data::point::start]};
auto const &control1{Path.points[segment_data::point::control1]};
auto const &control2{Path.points[segment_data::point::control2]};
auto const &end{Path.points[segment_data::point::end]};
text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' ';
auto const speed{velocity(Style)};
if (speed > 0.0)
write_header(text, category == 2 ? "road" : category == 4 ? "river" : "normal", glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end]), Style.fSoundDistance, Style, ballast_of(Style));
write_points(text, Path);
if (auto const speed{velocity(Style)}; speed > 0.0)
text << "velocity " << speed << ' ';
if (category == 1 && false == Style.m_profile1.first.empty())
text << "railprofile " << Style.m_profile1.first << ' ';
text << "endtrack\n";
write_ending(text, Style);
auto *track{load_path(text.str(), Style)};
track->m_paths = {Path};
return track;
}
glm::dvec3 editor_track::point_at(TTrack const &Track, double const T)
{
auto const &path{Track.m_paths.front()};
auto const p0{path.points[segment_data::point::start]};
auto const p3{path.points[segment_data::point::end]};
auto const p1{p0 + path.points[segment_data::point::control1]};
auto const p2{p3 + path.points[segment_data::point::control2]};
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;
}
double editor_track::nearest_parameter(TTrack const &Track, glm::dvec3 const &Point)
{
if (Track.m_paths.empty())
return 0.5;
auto const &path{Track.m_paths.front()};
auto const p0{path.points[segment_data::point::start]};
auto const p3{path.points[segment_data::point::end]};
auto const p1{p0 + path.points[segment_data::point::control1]};
auto const p2{p3 + path.points[segment_data::point::control2]};
auto const at = [&](double const T) {
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;
};
auto const distance = [&](double const T) { auto const q{at(T)}; return glm::distance(glm::dvec2{q.x, q.z}, glm::dvec2{Point.x, Point.z}); };
auto const distance = [&](double const T) { return geometry::plan_distance(point_at(Track, T), Point); };
double best{0.5};
for (int i = 0; i <= 200; ++i)
if (distance(i / 200.0) < distance(best))
@@ -1707,6 +2228,8 @@ TTrack *editor_track::split_path(TTrack &Track, double const T)
second.rolls[0] = roll;
auto *created{create_path(Track, second)};
Track.m_paths.front() = first;
if (s_observer != nullptr)
s_observer->track_split(Track, *created, first);
commit({&Track, created});
return created;
}
@@ -1743,12 +2266,8 @@ void editor_track::retire(TTrack &Track)
update_transition(*neighbour);
rebuild_geometry(*neighbour);
}
}
void editor_track::revive(TTrack &Track)
{
Track.m_editorremoved = false;
commit({&Track});
if (s_observer != nullptr)
s_observer->track_retired(Track);
}
void editor_track::rebuild_geometry(TTrack &Track)

View File

@@ -25,6 +25,7 @@ class editor_track
{
public:
enum class point_kind { start, control1, control2, end };
static bool is_end(point_kind const Kind) { return Kind == point_kind::start || Kind == point_kind::end; }
struct point_ref
{
int path{-1};
@@ -108,6 +109,8 @@ class editor_track
double length{0.0};
double grade{0.0};
double azimuth{0.0};
// distance of the point from the start, measured along the line in the plan
double along(glm::dvec3 const &Point) const { return (Point.x - start.x) * direction.x + (Point.z - start.z) * direction.y; }
};
struct straight_tolerance
{
@@ -137,6 +140,63 @@ class editor_track
double split{0.5};
};
static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve);
// regular straight paths continuing a chain end in line, which the chain can take over
struct straight_run
{
std::vector<TTrack *> tracks;
std::vector<bool> outward;
double length{0.0};
};
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<std::pair<TTrack *, state>> &States, std::vector<TTrack *> &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<route_span> 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
double cant{0.0}; // m, raise of the outer rail
std::size_t span{0};
};
static std::vector<route_sample> sample_route(route &Route, double const Step);
static double sampled_elevation(std::vector<route_sample> const &Samples, double const Chainage);
static double sampled_grade(std::vector<route_sample> const &Samples, double const Chainage);
static glm::dvec3 sampled_position(std::vector<route_sample> 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<height_gap> apply_profile(route &Route, std::function<double(double)> const &Elevation, std::function<double(double)> const &Grade, std::vector<double> const &Breaks, std::vector<std::pair<TTrack *, state>> &States, std::vector<TTrack *> &Created);
// carries elevations of the chain over to the new pieces, in proportion of the length
static void keep_heights(chain const &Chain, std::vector<segment_data> &Pieces);
struct switch_template
{
TTrack *source{nullptr};
@@ -156,12 +216,13 @@ class editor_track
static TTrack *create_path(TTrack const &Style, segment_data const &Path);
static TTrack *split_path(TTrack &Track, double const T);
static double nearest_parameter(TTrack const &Track, glm::dvec3 const &Point);
// point of the first path at the parameter of nearest_parameter()
static glm::dvec3 point_at(TTrack const &Track, double const T);
static std::vector<segment_data> place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade);
static TTrack *create_switch(switch_template const &Template, std::vector<segment_data> const &Paths, TTrack const &Style, std::string const &Name = {});
static void move_straights(std::vector<straight> const &Lines, std::vector<std::pair<glm::dvec3, glm::dvec3>> const &Ends);
static std::vector<TTrack *> relay(chain const &Chain, std::vector<segment_data> const &Pieces);
static void retire(TTrack &Track);
static void revive(TTrack &Track);
static std::string material_name(material_handle const Material);
static material_handle fetch_material(std::string const &Name);
@@ -172,7 +233,21 @@ class editor_track
static void velocity(TTrack &Track, double const Velocity);
static void damage(TTrack &Track, int const Damage);
// told about the changes of the paths, to let the objects which belong to them follow
class observer
{
public:
virtual ~observer() = default;
virtual void track_captured(TTrack const &Track) = 0;
virtual void tracks_committed(std::vector<TTrack *> const &Tracks) = 0;
virtual void track_retired(TTrack &Track) = 0;
// called before the commit; Original keeps the beginning of the path, given in First
virtual void track_split(TTrack &Original, TTrack &Created, segment_data const &First) = 0;
};
static void observe(observer *Observer) { s_observer = Observer; }
private:
static observer *s_observer;
static void disconnect(TTrack &Track);
static void join(TTrack &Track);
static void connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid);