17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-03 20:10:23 +01:00

feat(editor): the work of the editor sessions not committed yet, as it stands: a curve traced on from the end of the diverging track of a switch goes on at its radius with no transition in, or from a straight right at the switch (alignment start_held, fit_going_on), the plan taking an end adjoining R 10 km or more for a straight; the switches and slips laid into curves, slid along them and laid alone on one straight (frame_path, switch slide); the route plan reading curves going on from the tracks adjoining, the kept designs taken or forgotten; the orthophoto and terrain model settings, downloads and layers, the tracks onto the terrain; the curvature of a path in the plan (bezier::plan_curvature); the Polish texts of all of it. Not built as committed

This commit is contained in:
maj00r
2026-10-29 22:19:06 +01:00
parent 2eda26b94c
commit 80379b991c
49 changed files with 4718 additions and 1114 deletions

View File

@@ -720,6 +720,10 @@ result fit_between(design const &Design)
fit.compound = true;
fit.curved = true;
fit.deflection = deflection;
// going on from the diverging track of a switch: at its radius, no transition between them
auto const held{from ? Design.start_held : Design.end_held};
if (held)
(from ? fit.transition_in : fit.transition_out) = 0.0;
fit.arcs = {{fit.radius, 0.0, 1.0, -1.0}};
if (vertex.compound && false == vertex.arcs.empty())
{
@@ -727,6 +731,8 @@ result fit_between(design const &Design)
for (auto const &arc : vertex.arcs)
fit.arcs.push_back({std::max(1.0, arc.radius), std::max(0.0, arc.transition), arc.share, arc.cant});
}
if (held)
(from ? fit.arcs.front() : fit.arcs.back()).radius = from ? Design.start_radius : Design.end_radius;
if (from)
{
fit.ends.from = 1.0 / Design.start_radius;
@@ -743,11 +749,24 @@ result fit_between(design const &Design)
fit.arcs.back().cant = fit.ends.to_cant;
}
auto const normalin{perpendicular(directionin) * static_cast<double>(side)};
auto const tangents_at = [&](double const Scale, double &Tangentin, double &Tangentout) {
// the size of the curve which begins it right at the start, or ends it at the end: the scale of its radii, the arc held at the
// radius of a switch kept; of a single arc held so the length of the transition on its other side
auto const lengthened{held && fit.arcs.size() == 1};
auto &beyond{from ? fit.transition_out : fit.transition_in};
auto const sized = [&](double const Value, std::vector<compound_arc> &Arcs) {
for (std::size_t i = 0; i < Arcs.size(); ++i)
if (false == held || i != (from ? 0 : Arcs.size() - 1))
Arcs[i].radius *= Value;
};
auto const tangents_at = [&](double const Value, double &Tangentin, double &Tangentout) {
auto arcs{fit.arcs};
for (auto &arc : arcs)
arc.radius *= Scale;
auto const shape{build_compound(deflection, arcs, fit.transition_in, fit.transition_out, {}, fit.ends)};
auto in{fit.transition_in};
auto out{fit.transition_out};
if (lengthened)
(from ? out : in) = Value;
else
sized(Value, arcs);
auto const shape{build_compound(deflection, arcs, in, out, {}, fit.ends)};
auto const end{directionin * shape.end.x + normalin * shape.end.y};
Tangentin = cross(directionout, end) / cross(directionout, directionin);
Tangentout = glm::dot(end - directionin * Tangentin, directionout);
@@ -761,14 +780,23 @@ result fit_between(design const &Design)
return (from ? in : out) - wanted;
};
// the radii grow with the tangent: from those given the way the miss says, doubled or halved until it changes its sign, then
// the root between, by the regula falsi of Illinois
auto low{1.0};
// the root between, by the regula falsi of Illinois. The transition out of an arc held moves it in, the tangent growing with it:
// from none, doubled up to one turning by nearly all the deflection
auto low{lengthened ? 0.0 : 1.0};
auto lowmiss{miss(low)};
auto high{low};
auto highmiss{lowmiss};
auto const smallest{5.0 / fit.radius};
auto const largest{1e6 / fit.radius};
while ((lowmiss < 0.0) == (highmiss < 0.0) && high < largest && low > smallest)
auto const longest{1.96 * fit.arcs.front().radius * deflection};
while (lengthened && (lowmiss < 0.0) == (highmiss < 0.0) && lowmiss < -1e-5 && high < longest)
{
low = high;
lowmiss = highmiss;
high = std::min(longest, std::max(1.0, 2.0 * high));
highmiss = miss(high);
}
while (false == lengthened && (lowmiss < 0.0) == (highmiss < 0.0) && high < largest && low > smallest)
{
if (lowmiss < 0.0)
{
@@ -785,14 +813,16 @@ result fit_between(design const &Design)
lowmiss = miss(low);
}
}
if ((lowmiss < 0.0) == (highmiss < 0.0))
// an arc held which begins right there with no transition out of it
auto const met{lengthened && std::abs(lowmiss) <= 1e-5};
if (false == met && (lowmiss < 0.0) == (highmiss < 0.0))
{
r.errors.emplace_back(from ? format(STR_C("Vertex %zu: no curve goes on from the curve of the adjoining track (R %.0f m) to the straight after it"), k + 1, Design.start_radius) :
format(STR_C("Vertex %zu: no curve goes on from the straight before it into the curve of the adjoining track (R %.0f m)"), k + 1, Design.end_radius));
return r;
}
auto scale{low};
for (int i = 0, kept = 0; i < 60; ++i)
for (int i = 0, kept = 0; i < 60 && false == met; ++i)
{
scale = (low * highmiss - high * lowmiss) / (highmiss - lowmiss);
auto const value{miss(scale)};
@@ -815,10 +845,12 @@ result fit_between(design const &Design)
kept = 1;
}
}
for (auto &arc : fit.arcs)
arc.radius *= scale;
if (lengthened)
beyond = scale;
else
sized(scale, fit.arcs);
fit.radius = fit.arcs.front().radius;
tangents_at(1.0, fit.tangent_in, fit.tangent_out);
tangents_at(lengthened ? beyond : 1.0, fit.tangent_in, fit.tangent_out);
continue;
}
if (vertex.compound && false == vertex.arcs.empty())
@@ -1294,15 +1326,8 @@ result fit_between(design const &Design)
auto const &e{r.elements[End ? r.elements.size() - 1 - n : n]};
if (e.length < 0.01)
continue;
auto const arc{e.radius > 0.0 ? 1.0 / e.radius : 0.0};
switch (e.kind)
{
case element_kind::arc: return arc * e.turn;
case element_kind::transition_in: return End ? arc * e.turn : 0.0;
case element_kind::transition_out: return End ? 0.0 : arc * e.turn;
case element_kind::spiral: return (End ? e.curvature_end : e.curvature_start) * e.turn;
default: return 0.0;
}
auto const [start, end]{end_curvatures(e)};
return (End ? end : start) * e.turn;
}
return 0.0;
};
@@ -1333,6 +1358,19 @@ glm::dvec3 along_end(glm::dvec3 const &Point, glm::dvec2 const &Direction, doubl
} // namespace
std::pair<double, double> end_curvatures(element const &Element)
{
auto const own{Element.radius > 0.0 ? 1.0 / Element.radius : 0.0};
switch (Element.kind)
{
case element_kind::transition_in: return {0.0, own};
case element_kind::arc: return {own, own};
case element_kind::transition_out: return {own, 0.0};
case element_kind::spiral: return {Element.curvature_start, Element.curvature_end};
default: return {0.0, 0.0};
}
}
result compute(design const &Design)
{
auto first{fit_between(Design)};
@@ -1372,7 +1410,7 @@ double speed_limit(double const Radius, limits const &Limits)
return std::sqrt(Radius * (Limits.cant_max + allowance) / factor);
}
result compute_recommended(design &Design, bool const Transitions)
result compute_recommended(design &Design)
{
for (auto &vertex : Design.vertices)
{
@@ -1381,7 +1419,7 @@ result compute_recommended(design &Design, bool const Transitions)
vertex.transition_out = 0.0;
}
auto r{compute(Design)};
if (false == Transitions || false == r.valid)
if (false == r.valid)
return r;
auto const speed{Design.speed};
for (int i = 0; i < 3; ++i)
@@ -1540,7 +1578,7 @@ design join_design(join_frame const &Frame, join_kind const Kind)
std::optional<join_result> fitted_join(join_frame const &Frame, join_kind const Kind)
{
auto shape{join_design(Frame, Kind)};
auto const r{compute_recommended(shape, Frame.request.transitions)};
auto const r{compute_recommended(shape)};
auto const sharp{std::any_of(r.curves.begin(), r.curves.end(), [](curve_report const &Report) { return Report.radius < 10.0; })};
if (false == r.valid || sharp || turning(r) > Frame.heading + kPi + 1e-6 || r.length > 3.0 * Frame.distance + 1.0)
return std::nullopt;
@@ -2433,6 +2471,8 @@ constexpr double kTraceRobust{0.25};
constexpr double kPointsWithin{0.3};
// m, how far they may be from it, clicked by hand far out: a row of bends making a curve no closer is cut in parts
constexpr double kPointsLoose{1.0};
// m, the shortest arc of a compound curve fitted through points: a shorter one is a kink the points made, no arc
constexpr double kShortestArc{10.0};
// the parameters: the logarithms of the radii, then the weights of the shares of all but the first radius, as logarithms too.
// Between: the transitions between the arcs those of the norms, else the ones of the base
@@ -2567,8 +2607,14 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector<
bestcost = cost;
bestlog = low + spacing * i;
}
if (bestcost == std::numeric_limits<double>::max())
// two points for each radius; a curve going on from the one of the adjoining track has its start, its curvature there given too, and
// where one radius doesn't reach the end from there two are tried: a sharper one after the one it goes on from
auto const going{Design.start_curved || Design.end_curved};
auto const alone{bestcost < std::numeric_limits<double>::max()};
auto const most{std::clamp(Radii > 0 ? Radii : 3, 1, std::max(going && false == alone ? 2 : 1, static_cast<int>(Points.size()) / 2))};
if (false == alone && most < 2)
return best;
if (alone)
{
constexpr double kGolden{0.6180339887498949};
auto a{bestlog - spacing};
@@ -2607,7 +2653,6 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector<
};
std::array<candidate, 4> fits;
fits[1] = {{bestlog}, cost_of(one, {bestlog})};
auto const most{std::clamp(Radii > 0 ? Radii : 3, 1, std::max(1, static_cast<int>(Points.size()) / 2))};
auto const shift{std::log(1.3)};
auto const share = [](double const Second, double const First) { return std::log(Second / First); };
auto const rms = [&](int const Count) { return std::sqrt(fits[Count].cost / static_cast<double>(Points.size())); };
@@ -2624,13 +2669,18 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector<
if (wanted(2))
{
auto const two{residuals(2)};
for (auto const fraction : {0.3, 0.5, 0.7})
for (auto const sign : {-1.0, 1.0})
{
std::vector<double> parameters{bestlog + sign * shift, bestlog - sign * shift, share(1.0 - fraction, fraction)};
if (auto const cost{least_squares(parameters, two)}; cost < fits[2].cost)
fits[2] = {parameters, cost};
}
// radii far apart too where the curve goes on from another one
std::vector<double> shifts{shift};
if (going)
shifts.insert(shifts.end(), {std::log(3.0), std::log(8.0)});
for (auto const apart : shifts)
for (auto const fraction : {0.3, 0.5, 0.7})
for (auto const sign : {-1.0, 1.0})
{
std::vector<double> parameters{bestlog + sign * apart, bestlog - sign * apart, share(1.0 - fraction, fraction)};
if (auto const cost{least_squares(parameters, two)}; cost < fits[2].cost)
fits[2] = {parameters, cost};
}
take(2);
}
if (wanted(3))
@@ -3361,6 +3411,33 @@ double shortest_between(design const &Design)
} // namespace
void settle_laid(std::vector<vertex> &Vertices, result const &Laid)
{
for (auto const &curve : Laid.curves)
{
// a curve going on from an adjoining one is begun by that one's radius, not made smaller
if (curve.vertex < 0 || curve.vertex >= static_cast<int>(Vertices.size()) || curve.curvature_in > 0.0 || curve.curvature_out > 0.0)
continue;
auto &vertex{Vertices[curve.vertex]};
// two transitions meeting have their lengths from the radius they meet at
auto const transitions{vertex.spirals || (curve.transition_in >= vertex.transition_in - 1e-6 && curve.transition_out >= vertex.transition_out - 1e-6)};
if (curve.radius >= vertex.radius * (1.0 - 1e-6) && transitions)
continue;
auto const scale{curve.radius / std::max(1.0, vertex.radius)};
vertex.radius = curve.radius;
for (auto &arc : vertex.arcs)
{
arc.radius *= scale;
arc.transition *= scale;
}
if (false == vertex.spirals)
{
vertex.transition_in = curve.transition_in;
vertex.transition_out = curve.transition_out;
}
}
}
result ease_corners(design &Design, std::vector<char> &Corners, corner_radii const &Radii, stretch_fit const &Fit)
{
// a point following a slight bend clicked closer than the curves leave a straight of the shortest length was clicked on a curve
@@ -3483,6 +3560,8 @@ result ease_corners(design &Design, std::vector<char> &Corners, corner_radii con
break;
line = ease_bends(Design, Corners, Radii);
}
if (line.valid)
settle_laid(Design.vertices, line);
return line;
}
@@ -3513,6 +3592,7 @@ std::optional<trace_stretch> stretch_of(design const &Design, std::vector<glm::d
for (auto &arc : vertex.arcs)
arc.radius *= scale;
}
settle_laid(stretch.vertices, laid);
double sum{0.0};
for (auto const &projection : project(laid, Points))
{
@@ -3724,6 +3804,26 @@ void fit_curved(design &Design, std::size_t const Vertex, std::vector<glm::dvec2
best = parameters;
}
}
// a curve of another radius than the adjoining one, from too few points to show the transition between them: one as long as the
// norms ask for at the speed the sharper of them allows, rather than the curvature jumping at the joint
if (best.size() == 2 && Points.size() <= 3)
for (int pass = 0; pass < 2; ++pass)
{
design trial;
shaped(best, trial);
trial.norms.checked = false;
laid_with(compute(trial));
auto const allowed{std::max(1.0, std::min(speed, speed_limit(std::min(radius, adjoining), Design.norms)))};
auto const needed{std::min(between_curves(allowed, 1.0 / adjoining, adjoiningcant, 1.0 / radius, 0.0, Design.norms), 0.9 * longest)};
if (length_of(best[0]) >= needed - 0.5)
break;
auto const between{parameter_of(needed)};
residuals_of const rest{[&](std::vector<double> const &Parameters, std::vector<double> &Residuals) { return residuals({between, Parameters[0]}, Residuals); }};
std::vector<double> out{best[1]};
if (least_squares(out, rest) == std::numeric_limits<double>::max())
break;
best = {between, out[0]};
}
if (best.empty())
return;
design fitted;
@@ -3747,38 +3847,70 @@ void fit_curve(design &Design, std::size_t const Vertex, std::vector<glm::dvec2>
auto const speed{std::max(1.0, Design.speed)};
// the transitions together turn no more than the curve: longer ones would only be cut to it
auto const deflection{vertex_deflection(Design, Vertex)};
// the transition into the curve and the one out of it each as long as the points show it, where there are points enough for both: a
// curve begun short after a free end and running out long into the straight after it is one curve, not a sharp arc with a flat
// one after it
auto const apart{Points.size() >= 4};
auto const shaped = [&](std::vector<double> const &Parameters, design &Trial) {
Trial = Design;
auto const radius{std::exp(std::clamp(Parameters[0], std::log(kTraceRadiusMin), std::log(kTraceRadiusMax)))};
plain_curve(Trial.vertices[Vertex], radius, std::min({Parameters[1] * Parameters[1], radius * deflection, longest_transition(speed, radius, Trial.norms)}), speed, Trial.norms);
auto const longest{longest_transition(speed, radius, Trial.norms)};
auto in{std::min(Parameters[1] * Parameters[1], longest)};
auto out{std::min(Parameters.back() * Parameters.back(), longest)};
if (auto const room{2.0 * radius * deflection}; in + out > room)
{
auto const scale{room / (in + out)};
in *= scale;
out *= scale;
}
auto &curve{Trial.vertices[Vertex]};
plain_curve(curve, radius, std::min(in, out), speed, Trial.norms);
curve.transition_in = in;
curve.transition_out = out;
};
residuals_of const residuals{[&](std::vector<double> const &Parameters, std::vector<double> &Residuals) {
design trial;
shaped(Parameters, trial);
return stretch_residuals(trial, Points, Residuals);
}};
// from the transitions it has and from those of the norms; the square root of the length, as a length of nought would hold the
// step at nought
// from the transitions it has and from those of the norms; the square root of the length, as a length of nought would hold the step
// at nought
auto const radius{std::max(kTraceRadiusMin, Design.vertices[Vertex].radius)};
auto const allowed{std::max(1.0, std::min(speed, speed_limit(radius, Design.norms)))};
// started below the longest transition: above it the length is held, the fit no longer moving it
auto const started = [&](double const Length) { return std::sqrt(std::max(std::min(Length, 0.9 * std::min(radius * deflection, longest_transition(speed, radius, Design.norms))), 1.0)); };
auto const norm{std::max(recommend(allowed, radius, Design.norms).transition, 10.0)};
auto const fitted_from = [&](std::vector<std::vector<double>> const &Starts, std::vector<double> &Best) {
auto lowest{std::numeric_limits<double>::max()};
for (auto parameters : Starts)
if (auto const cost{least_squares(parameters, residuals)}; cost < lowest)
{
lowest = cost;
Best = parameters;
}
return lowest;
};
std::vector<double> best;
auto lowest{std::numeric_limits<double>::max()};
for (auto const length : {std::max(Design.vertices[Vertex].transition_in, 10.0), std::max(recommend(allowed, radius, Design.norms).transition, 10.0)})
auto const alike{fitted_from({{std::log(radius), started(std::max(Design.vertices[Vertex].transition_in, 10.0))}, {std::log(radius), started(norm)}}, best)};
// the two apart, from the ones alike and from one short and the other long: taken where they keep to the points clearly closer, not
// only to points clicked a little off
constexpr double kApart{0.8};
if (apart && false == best.empty())
{
// started below the longest transition: above it the length is held, the fit no longer moving it
std::vector<double> parameters{std::log(radius), std::sqrt(std::min(length, 0.9 * std::min(radius * deflection, longest_transition(speed, radius, Design.norms))))};
if (auto const cost{least_squares(parameters, residuals)}; cost < lowest)
{
lowest = cost;
best = parameters;
}
std::vector<double> two;
if (fitted_from({{best[0], best[1], best[1]}, {std::log(radius), started(10.0), started(norm)}, {std::log(radius), started(norm), started(10.0)}}, two) < kApart * alike)
best = two;
}
if (best.empty())
return;
design fitted;
shaped(best, fitted);
auto &curve{fitted.vertices[Vertex]};
plain_curve(curve, std::max(kTraceRadiusMin, std::round(curve.radius)), std::round(curve.transition_in), speed, fitted.norms);
auto const in{std::round(curve.transition_in)};
auto const out{std::round(curve.transition_out)};
plain_curve(curve, std::max(kTraceRadiusMin, std::round(curve.radius)), std::min(in, out), speed, fitted.norms);
curve.transition_in = in;
curve.transition_out = out;
Design = fitted;
}
@@ -3931,7 +4063,7 @@ std::optional<trace_stretch> fit_arcs(design const &Single, std::vector<glm::dve
fit_compound(Shaped, 0, Points);
auto const laid{compute(Shaped)};
if (std::any_of(laid.elements.begin(), laid.elements.end(), [](element const &Element) {
return Element.vertex == 0 && Element.curvature_start == Element.curvature_end && Element.curvature_start > 0.0 && Element.length < 10.0;
return Element.vertex == 0 && Element.curvature_start == Element.curvature_end && Element.curvature_start > 0.0 && Element.length < kShortestArc;
}))
return std::nullopt;
auto const stretch{stretch_of(Shaped, Points)};
@@ -4108,30 +4240,22 @@ std::optional<trace_stretch> fit_chain(design const &Base, std::vector<glm::dvec
origins.push_back(start + across * Parameters[2 * j + 1]);
}
// where two lines cross: along the first one, along the second
auto const crossing = [](glm::dvec2 const &A, glm::dvec2 const &Along, glm::dvec2 const &B, glm::dvec2 const &Other, double &First, double &Second) {
auto const determinant{cross(Along, Other)};
if (std::abs(determinant) < 1e-9)
return false;
First = cross(B - A, Other) / determinant;
Second = cross(B - A, Along) / determinant;
return true;
};
std::vector<glm::dvec2> vertices;
double first;
double along;
if (false == crossing(start, startdirection, origins.front(), directions.front(), first, along) || first <= 0.0)
if (false == geometry::lines_cross(start, startdirection, origins.front(), directions.front(), first, along) || first <= 0.0)
return false;
vertices.push_back(start + startdirection * first);
for (std::size_t j = 0; j + 1 < lines; ++j)
{
double a;
double b;
if (false == crossing(origins[j], directions[j], origins[j + 1], directions[j + 1], a, b))
if (false == geometry::lines_cross(origins[j], directions[j], origins[j + 1], directions[j + 1], a, b))
return false;
vertices.push_back(origins[j] + directions[j] * a);
}
double last;
if (false == crossing(end, -enddirection, origins.back(), directions.back(), last, along) || last <= 0.0)
if (false == geometry::lines_cross(end, -enddirection, origins.back(), directions.back(), last, along) || last <= 0.0)
return false;
vertices.push_back(end - enddirection * last);
// each vertex on from the one before along the line between them
@@ -4210,6 +4334,364 @@ std::optional<trace_stretch> fit_chain(design const &Base, std::vector<glm::dvec
} // namespace
trace_stretch fit_going_on(design const &Base, std::vector<glm::dvec2> const &Points)
{
trace_stretch fit;
if (Points.empty() || false == Base.start_curved || Base.start_radius <= 0.0 || glm::length(Base.start_direction) < 1e-9)
return fit;
auto const speed{std::max(1.0, Base.speed)};
auto const start{plan_of(Base.start)};
auto const heading{glm::normalize(Base.start_direction)};
auto const side{Base.start_turn > 0 ? 1.0 : -1.0};
auto const from{1.0 / Base.start_radius};
auto const fromcant{std::max(0.0, Base.start_cant)};
// going on from the diverging track of a switch: its first arc of the radius of the switch, with no transition before it
auto const held{Base.start_held};
auto const target{Points.back()};
// none where the last point lies on the other side of the heading
auto const offset{target - start};
auto const lateral{glm::dot(offset, perpendicular(heading) * side)};
auto const chord{glm::length(offset)};
if (lateral < 1e-3 || chord < 1.0)
return fit;
// the shape: the transition from the curvature of the adjoining track to the first arc, the arcs with the transitions between them,
// the transition from the last one to the straight, which goes on to the last point where the points clicked after the curve show it
struct shape
{
double in{0.0};
std::vector<double> curvatures;
std::vector<double> arcs;
std::vector<double> between;
double out{0.0};
double straight{0.0};
};
auto const curvature_of = [](double const Logarithm) { return 1.0 / std::exp(std::clamp(Logarithm, std::log(kTraceRadiusMin), std::log(kTraceRadiusMax))); };
auto const segments_of = [&](shape const &Shape) {
std::vector<std::array<double, 3>> segments{{from, Shape.curvatures.front(), Shape.in}};
for (std::size_t i = 0; i < Shape.curvatures.size(); ++i)
{
if (i > 0)
segments.push_back({Shape.curvatures[i - 1], Shape.curvatures[i], Shape.between[i - 1]});
segments.push_back({Shape.curvatures[i], Shape.curvatures[i], Shape.arcs[i]});
}
segments.push_back({Shape.curvatures.back(), 0.0, Shape.out});
segments.push_back({0.0, 0.0, Shape.straight});
return segments;
};
auto const turn_of = [&](shape const &Shape) {
double turn{0.0};
for (auto const &[first, last, length] : segments_of(Shape))
turn += 0.5 * (first + last) * length;
return turn;
};
// where the shape ends, heading which way
auto const end_of = [&](shape const &Shape, glm::dvec2 &End, glm::dvec2 &Leaving) {
glm::dvec2 origin{start};
glm::dvec2 direction{heading};
for (auto const &[first, last, length] : segments_of(Shape))
{
if (length < 1e-9)
continue;
auto const local{spiral_point(first, last, length, length)};
origin += direction * local.x + perpendicular(direction) * side * local.y;
direction = turned(direction, side * local.angle);
}
End = origin;
Leaving = direction;
};
// the line of the shape in the plan, a point every half a metre about
auto const line_of = [&](shape const &Shape) {
plan_line line;
glm::dvec2 origin{start};
glm::dvec2 direction{heading};
double chainage{0.0};
line.points.push_back(origin);
line.chainages.push_back(0.0);
for (auto const &[first, last, length] : segments_of(Shape))
{
if (length < 1e-6)
continue;
glm::dvec2 const across{perpendicular(direction) * side};
int const count{std::clamp(static_cast<int>(std::ceil(length / 0.5)), 1, 400)};
for (int i = 1; i <= count; ++i)
{
auto const local{spiral_point(first, last, length, length * i / count)};
line.points.push_back(origin + direction * local.x + across * local.y);
line.chainages.push_back(chainage + length * i / count);
}
auto const local{spiral_point(first, last, length, length)};
origin += direction * local.x + across * local.y;
direction = turned(direction, side * local.angle);
chainage += length;
}
return line;
};
// the shape of the parameters: the square roots of the lengths of the transitions and of the straight, then for each arc but the
// last the logarithm of its radius, the square root of its length and of the transition after it; the last arc, its radius and
// length, Last. Held, the transition in is none and the first arc of the radius of the switch; a single arc so is given the square
// root of the straight after it in Last in place of its radius
auto const single_held = [&](std::vector<double> const &Parameters) { return held && Parameters.size() == 3; };
auto const shaped = [&](std::vector<double> const &Parameters, std::array<double, 2> const &Last) {
shape result;
result.in = held ? 0.0 : Parameters[0] * Parameters[0];
result.out = Parameters[1] * Parameters[1];
result.straight = Parameters[2] * Parameters[2];
for (std::size_t i = 3; i + 2 < Parameters.size(); i += 3)
{
result.curvatures.push_back(held && i == 3 ? from : curvature_of(Parameters[i]));
result.arcs.push_back(Parameters[i + 1] * Parameters[i + 1]);
result.between.push_back(Parameters[i + 2] * Parameters[i + 2]);
}
if (single_held(Parameters))
{
result.curvatures.push_back(from);
result.straight = Last[0] * Last[0];
}
else
result.curvatures.push_back(curvature_of(Last[0]));
result.arcs.push_back(Last[1] * Last[1]);
return result;
};
// the radius and the length of the last arc bringing the end to the last point, the rest given: by Newton from the ones found last
auto const reach = [&](std::vector<double> const &Parameters, std::array<double, 2> &Solution) {
auto const miss = [&](std::array<double, 2> const &At, glm::dvec2 &Miss) {
auto const trial{shaped(Parameters, At)};
if (turn_of(trial) > kPi - 1e-2)
return false;
glm::dvec2 end;
glm::dvec2 leaving;
end_of(trial, end, leaving);
Miss = end - target;
return true;
};
constexpr double kStep{1e-6};
auto at{Solution};
for (int i = 0; i < 40; ++i)
{
glm::dvec2 here;
if (false == miss(at, here))
return false;
if (glm::length(here) < 1e-5)
{
Solution = at;
return true;
}
glm::dvec2 radial;
glm::dvec2 along;
auto byradius{at};
byradius[0] += kStep;
auto byarc{at};
byarc[1] += kStep;
if (false == miss(byradius, radial) || false == miss(byarc, along))
return false;
radial = (radial - here) / kStep;
along = (along - here) / kStep;
auto const determinant{radial.x * along.y - radial.y * along.x};
if (std::abs(determinant) < 1e-12)
return false;
auto const stepradius{(along.x * here.y - here.x * along.y) / determinant};
auto const steparc{(here.x * radial.y - radial.x * here.y) / determinant};
// half a step of the logarithm of the radius at most, the curve not swinging round; of the root of the straight after an arc
// held two
auto const damped{std::min(1.0, (single_held(Parameters) ? 2.0 : 0.5) / std::max(std::abs(stepradius), 1e-12))};
at[0] += stepradius * damped;
if (single_held(Parameters))
at[0] = std::abs(at[0]);
at[1] = std::abs(at[1] + steparc * damped);
}
return false;
};
// the transitions as long as the norms ask for where the points don't show them shorter: a metre short of it weighed as a millimetre
// off a point, which only chooses among the shapes too few points leave open
constexpr double kShort{0.001};
std::array<double, 2> solution{};
residuals_of const residuals{[&](std::vector<double> const &Parameters, std::vector<double> &Residuals) {
auto at{solution};
if (false == reach(Parameters, at))
return false;
solution = at;
auto const trial{shaped(Parameters, at)};
auto const line{line_of(trial)};
Residuals.clear();
for (std::size_t i = 0; i + 1 < Points.size(); ++i)
Residuals.push_back(project_on(line, Points[i]).offset);
auto const need_between = [&](double const First, double const Second) {
auto const sharper{1.0 / std::max({First, Second, 1e-9})};
auto const allowed{std::max(1.0, std::min(speed, speed_limit(sharper, Base.norms)))};
return std::min(between_curves(allowed, First, 0.0, Second, 0.0, Base.norms), 0.9 * longest_transition(speed, sharper, Base.norms));
};
auto const last{1.0 / trial.curvatures.back()};
auto const allowed{std::max(1.0, std::min(speed, speed_limit(last, Base.norms)))};
Residuals.push_back(kShort * std::max(0.0, need_between(from, trial.curvatures.front()) - trial.in));
for (std::size_t i = 1; i < trial.curvatures.size(); ++i)
Residuals.push_back(kShort * std::max(0.0, need_between(trial.curvatures[i - 1], trial.curvatures[i]) - trial.between[i - 1]));
Residuals.push_back(kShort * std::max(0.0, std::min(transition_length(allowed, last, 0.0, Base.norms), 0.9 * longest_transition(speed, last, Base.norms)) - trial.out));
// the curve ending at the last point where the points don't show a straight after it
Residuals.push_back(kShort * trial.straight);
return true;
}};
std::vector<double> best;
std::array<double, 2> bestsolution{};
auto lowest{std::numeric_limits<double>::max()};
// the square root of the straight to start from: off nought, where the fit couldn't move it
constexpr double kSeed{0.5};
// an arc of a compound curve shorter than the shortest one, a sharp one squeezed between two long transitions to bring the curve to
// the points (R 21 m for a millimetre after R 229 m): no curve to lay, as the fit of the arcs through the points has it. The first arc
// going on from a switch at its radius may be as short as it is
auto const kinked = [&](shape const &Shape) {
if (Shape.curvatures.size() < 2)
return false;
for (std::size_t i = held ? 1 : 0; i < Shape.arcs.size(); ++i)
if (Shape.arcs[i] < kShortestArc)
return true;
return false;
};
// the parameters fitted: all but those of a curve going on from a switch which the shape doesn't take, the transition in and the
// radius of the first arc, or the straight after a single arc, which the reach finds then
auto const attempt = [&](std::vector<double> Parameters, std::array<double, 2> const &Start) {
solution = Start;
std::vector<std::size_t> loose;
for (std::size_t i = 0; i < Parameters.size(); ++i)
if (false == held || (i != 0 && i != (single_held(Parameters) ? 2u : 3u)))
loose.push_back(i);
auto const whole = [&](std::vector<double> const &Loose) {
auto result{Parameters};
for (std::size_t i = 0; i < loose.size(); ++i)
result[loose[i]] = Loose[i];
return result;
};
std::vector<double> fitted;
for (auto const i : loose)
fitted.push_back(Parameters[i]);
residuals_of const some{[&](std::vector<double> const &Loose, std::vector<double> &Residuals) { return residuals(whole(Loose), Residuals); }};
if (auto const cost{least_squares(fitted, some)}; cost < lowest && false == kinked(shaped(whole(fitted), solution)))
{
lowest = cost;
best = whole(fitted);
bestsolution = solution;
}
};
// one arc, from two guesses of how far it turns: as the points head at the end, Base ending that way, and as the circle going on
// from the start through the last point does; held at the radius of a switch, as far round as that, the straight after it the rest
// of the way
std::vector<double> turns;
if (glm::length(Base.end_direction) > 1e-9)
{
auto const ending{glm::normalize(Base.end_direction)};
auto turn{std::atan2(side * cross(heading, ending), glm::dot(heading, ending))};
if (turn < 0.0)
turn += 2.0 * kPi;
if (turn > 1e-3 && turn < kPi - 2e-2)
turns.push_back(turn);
}
turns.push_back(2.0 * std::asin(std::clamp(lateral / chord, 0.0, 1.0)));
for (auto const turn : turns)
for (auto const length : {10.0, 25.0})
{
if (held)
{
auto const arc{std::max(1.0, Base.start_radius * turn - 0.5 * length)};
attempt({0.0, std::sqrt(length), 0.0}, {std::sqrt(std::max(1.0, chord - arc)), std::sqrt(arc)});
continue;
}
auto const radius{std::clamp(chord / std::max(1e-3, 2.0 * std::sin(0.5 * turn)), kTraceRadiusMin, kTraceRadiusMax)};
attempt({std::sqrt(length), std::sqrt(length), kSeed}, {std::log(radius), std::sqrt(std::max(1.0, radius * turn - length))});
}
// Abreast: the shape ends abreast of the last point, not on it
auto const settle = [&](shape const &Found, bool const Abreast) -> std::optional<trace_stretch> {
glm::dvec2 end;
glm::dvec2 leaving;
end_of(Found, end, leaving);
if (false == Abreast && glm::distance(end, target) > 0.01)
return std::nullopt;
// laid as the design lays a curve going on from an adjoining one: its vertex where the straights cross, its radii scaled to make
// the tangent before it reach the start, which they do
design laid{Base};
laid.end = {end.x, Base.end.y, end.y};
laid.end_direction = leaving;
laid.end_curved = false;
laid.end_reserve = 0.0;
laid.vertices.assign(1, vertex{});
glm::dvec2 meeting;
if (false == tangent_intersection(laid, meeting))
return std::nullopt;
auto &curve{laid.vertices.front()};
curve.position = meeting;
curve.radius = 1.0 / Found.curvatures.front();
curve.transition_in = Found.in;
curve.transition_out = Found.out;
if (Found.curvatures.size() > 1)
{
// the shares of the arcs by how far they turn
curve.compound = true;
curve.share = std::max(1e-6, Found.arcs.front() * Found.curvatures.front());
for (std::size_t i = 1; i < Found.curvatures.size(); ++i)
{
compound_arc arc;
arc.radius = 1.0 / Found.curvatures[i];
arc.transition = Found.between[i - 1];
arc.share = std::max(1e-6, Found.arcs[i] * Found.curvatures[i]);
curve.arcs.push_back(arc);
}
}
curve.cant = Found.curvatures.size() > 1 ? 0.0 : curved_cant(curve.radius, Found.in, Found.out, fromcant, speed, Base.norms);
auto stretch{stretch_of(laid, Points)};
if (stretch.has_value())
{
stretch->leaving = leaving;
stretch->ending = end;
}
return stretch;
};
std::optional<trace_stretch> one;
if (false == best.empty())
one = settle(shaped(best, bestsolution), false);
// held, the arc of the switch alone, as far round as abreast of the last point seen from its centre and ending there: as the points
// clicked along it show it, no transition out of it bringing it to a point on it
if (held)
{
auto const centre{start + perpendicular(heading) * side * Base.start_radius};
auto const outward{target - centre};
auto sweep{std::fmod(side * (std::atan2(outward.y, outward.x) - std::atan2(start.y - centre.y, start.x - centre.x)), 2.0 * kPi)};
if (sweep < 0.0)
sweep += 2.0 * kPi;
if (glm::length(outward) > 0.5 && sweep > 1e-3 && sweep < kPi - 2e-2)
{
shape arc;
arc.curvatures = {from};
arc.arcs = {Base.start_radius * sweep};
if (auto const alone{settle(arc, true)}; alone.has_value() && (false == one.has_value() || alone->rms < one->rms))
one = alone;
}
}
// two arcs where one doesn't keep to the points and there are enough of them to tell: a flatter one going on from the end, even a
// straight about, then a sharper one, as where the line turns hard at the last points. Going on from a switch, the first one of its
// radius, then a flatter or a sharper one
if (Points.size() >= 3 && (false == one.has_value() || one->worst > kPointsWithin))
{
auto const single{best.empty() || held ? std::array<double, 2>{std::log(std::clamp(chord, kTraceRadiusMin, kTraceRadiusMax)), std::sqrt(chord)} : bestsolution};
auto const radius{std::exp(single[0])};
lowest = std::numeric_limits<double>::max();
best.clear();
std::vector<double> const firsts{held ? std::vector<double>{Base.start_radius} : std::vector<double>{Base.start_radius, 2.0 * Base.start_radius, 3000.0}};
std::vector<double> const seconds{held ? std::vector<double>{0.5 * Base.start_radius, 2.0 * Base.start_radius} : std::vector<double>{0.5 * radius}};
auto const shares{held ? std::array<double, 2>{0.15, 0.5} : std::array<double, 2>{0.4, 0.7}};
for (auto const first : firsts)
for (auto const second : seconds)
for (auto const share : shares)
{
auto const along{std::max(1.0, share * chord)};
attempt({std::sqrt(10.0), std::sqrt(10.0), kSeed, std::log(first), std::sqrt(along), std::sqrt(10.0)}, {std::log(std::max(kTraceRadiusMin, second)), std::sqrt(std::max(1.0, (1.0 - share) * chord))});
}
if (false == best.empty())
if (auto const two{settle(shaped(best, bestsolution), false)}; two.has_value() && (false == one.has_value() || two->rms < 0.6 * one->rms))
one = two;
}
if (one.has_value())
fit = *one;
return fit;
}
trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> const &Points, int const Radii, bool const Straights)
{
trace_stretch chosen;
@@ -4258,19 +4740,38 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> cons
int const side{cross(startdirection, enddirection) > 0.0 ? 1 : -1};
auto const fromstart{Base.start_curved && Base.start_radius > 0.0 && Base.start_turn == side};
auto const curved{fromstart || (Base.end_curved && Base.end_radius > 0.0 && Base.end_turn == side)};
if (curved)
// going on from the diverging track of a switch: at its radius, the transition on the other side of the arc making it begin there
auto const held{fromstart ? Base.start_held : Base.end_held};
if (curved && held)
{
curve.radius = fromstart ? Base.start_radius : Base.end_radius;
one = stretch_of(single, Points);
}
else if (curved)
{
curve.radius = fromstart ? Base.start_radius : Base.end_radius;
fit_curved(single, 0, Points, fromstart);
one = stretch_of(single, Points);
// a compound curve where one radius doesn't keep to points enough for two; where it doesn't reach the end at all, as where a
// curve sharper than the one it goes on from ends the stretch, of two radii from fewer points too. Its transition from the curve
// of the adjoining track as long as the norms ask for at least, the curvature not jumping at the joint
auto const loose{one.has_value() && (one->rms > kStretchClose || one->worst > kPointsWithin)};
if (one.has_value() && Radii != 1 && (Radii > 1 || loose))
auto const missing{false == one.has_value()};
if (Radii != 1 && (missing || ((Radii > 1 || loose) && Points.size() >= 4)))
if (auto const fit{fit_trace(single, 0, Points, Radii > 1 ? Radii : 2)}; fit.valid && fit.fitted.compound)
{
auto several{single};
several.vertices.front() = fit.fitted;
fit_compound(several, 0, Points);
if (auto const compound{stretch_of(several, Points)}; compound.has_value() && (Radii > 1 || compound->rms < 0.75 * one->rms))
auto &shape{several.vertices.front()};
auto const joining{fromstart ? shape.radius : shape.arcs.empty() ? shape.radius : shape.arcs.back().radius};
auto const adjoining{fromstart ? Base.start_radius : Base.end_radius};
auto const allowed{std::max(1.0, std::min(speed, speed_limit(std::min(joining, adjoining), Base.norms)))};
auto const needed{std::min(between_curves(allowed, 1.0 / adjoining, std::max(0.0, fromstart ? Base.start_cant : Base.end_cant), 1.0 / std::max(1.0, joining), 0.0, Base.norms),
0.9 * longest_transition(speed, adjoining, Base.norms))};
auto &between{fromstart ? shape.transition_in : shape.transition_out};
between = std::max(between, std::round(needed));
if (auto const compound{stretch_of(several, Points)}; compound.has_value() && (Radii > 1 || false == one.has_value() || compound->rms < 0.75 * one->rms))
one = compound;
}
}
@@ -4310,9 +4811,12 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> cons
fit_compound(several, 0, Points);
compound = stretch_of(several, Points);
}
if (auto const more{fit_arcs(single, Points, Radii > 1 ? Radii : 8)}; more.has_value() && (false == compound.has_value() || more->worst < compound->worst))
// no more radii than half the points: a few points clicked a little off don't show a curve of many radii
if (auto const more{fit_arcs(single, Points, Radii > 1 ? Radii : std::clamp(static_cast<int>(Points.size()) / 2, 2, 8))}; more.has_value() && (false == compound.has_value() || more->worst < compound->worst))
compound = more;
if (compound.has_value() && (Radii > 1 || compound->rms < 0.75 * one->rms || (one->worst > kPointsWithin && compound->worst < one->worst)))
// one with an arc as flat as a straight between its curves only where it keeps to all the points much closer: not to come
// nearer to a point clicked a little off, sharp curves either side of a straight
if (compound.has_value() && (Radii > 1 || compound->rms < 0.75 * one->rms || (one->worst > kPointsWithin && compound->worst < one->worst && (compound->vertices.empty() || false == flattened(compound->vertices.front())))))
one = compound;
}
}
@@ -4490,7 +4994,7 @@ void stretch_fitter::work()
m_asked.pop_back();
auto const asked{*m_fitting};
lock.unlock();
auto fit{fit_trace_stretch(asked.base, asked.points, asked.radii, asked.straights)};
auto fit{asked.going ? fit_going_on(asked.base, asked.points) : fit_trace_stretch(asked.base, asked.points, asked.radii, asked.straights)};
lock.lock();
constexpr std::size_t kKept{16};
m_results.insert(m_results.begin(), {asked.key, std::move(fit)});

View File

@@ -105,6 +105,11 @@ struct design
// of the only vertex goes on from the start alone
bool start_curved{false};
bool end_curved{false};
// the curve going on from the adjoining track at the start, or into the one at the end, goes on from the diverging track of a switch:
// at its radius, with no transition between them. The arcs after the first one, or the transition out of a single one, are what
// makes it begin right there
bool start_held{false};
bool end_held{false};
// mm, the cant of the adjoining track at the start and at the end, leaning into its turn
double start_cant{0.0};
double end_cant{0.0};
@@ -165,6 +170,9 @@ struct element
glm::dvec2 normal{0.0};
};
// the curvature of the element at its start and at its end, not signed by the side it turns to: an arc of a compound curve its own
std::pair<double, double> end_curvatures(element const &Element);
struct curve_report
{
int vertex{-1};
@@ -236,7 +244,7 @@ struct result
result compute(design const &Design);
double speed_limit(double const Radius, limits const &Limits);
result compute_recommended(design &Design, bool const Transitions);
result compute_recommended(design &Design);
bool tangent_intersection(design const &Design, glm::dvec2 &Point);
bool collinear(design const &Design);
bool spiral_tangents(transition_shape const Shape, double const TurnIn, double const TurnOut, double &TangentIn, double &TangentOut);
@@ -312,7 +320,6 @@ struct join_request
join_end end;
double radius{0.0};
int variant{0};
bool transitions{false};
double speed{100.0};
double kink{0.1};
transition_shape shape{transition_shape::cubic_parabola};
@@ -405,8 +412,14 @@ struct trace_stretch
std::vector<double> offsets; // of the points from the line laid, positive to the left
double rms{0.0};
double worst{0.0};
glm::dvec2 leaving{0.0}; // the heading at the end where the fit chose it, fit_going_on
glm::dvec2 ending{0.0}; // where it ends there: the last point, or abreast of it on the arc of a switch going on
};
trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> const &Points, int const Radii, bool const Straights);
// a curve going on from the curve of the free end Base starts at (start_curved), along the points to the last of them, where it ends on a
// straight heading as it comes out: from the curvature of that end through a transition to an arc and through another to the straight.
// The transitions as long as the norms ask for where the points don't show them shorter. Not valid where no such curve follows them
trace_stretch fit_going_on(design const &Base, std::vector<glm::dvec2> const &Points);
// the fit of a stretch traced, with up to three radii and no straight in it; none while it's being fitted elsewhere
using stretch_fit = std::function<std::optional<trace_stretch>(design const &, std::vector<glm::dvec2> const &)>;
@@ -417,6 +430,9 @@ using stretch_fit = std::function<std::optional<trace_stretch>(design const &, s
// each part of the row where the whole doesn't keep to them; such a vertex is marked 0 then. Fit fits it, by fit_trace_stretch
// where none is given
result ease_corners(design &Design, std::vector<char> &Corners, corner_radii const &Radii, stretch_fit const &Fit = {});
// the curves laid smaller than their vertices ask for, to fit between their straights or into their deflection, given the radii and the
// transitions they were laid with: the design tells what's laid, and the plan lays the same curve again
void settle_laid(std::vector<vertex> &Vertices, result const &Laid);
// the stretches traced fitted on a thread of their own while the cursor moves: fitting one takes tens of milliseconds. The one asked
// for last is fitted first, the oldest of too many waiting left out, the fits of the last ones kept
@@ -430,6 +446,7 @@ class stretch_fitter
std::vector<glm::dvec2> points;
int radii{0};
bool straights{true};
bool going{false}; // fit_going_on
};
~stretch_fitter();
void ask(request Request);

View File

@@ -709,6 +709,14 @@ std::optional<designed_vertex> vertex_of(std::vector<std::pair<TTrack *, bool>>
auto const end{project(r, entering(Run.back(), true), tolerance)};
if (false == start.has_value() || false == end.has_value())
continue;
// the tracks of a part of the curve only, as on either side of a switch laid into it, aren't laid by its design: it's theirs only
// where they reach over the whole curve, else they're recognized as they lie
auto const low{std::min(start->chainage, end->chainage) - 0.5};
auto const high{std::max(start->chainage, end->chainage) + 0.5};
if (std::any_of(entry.result.elements.begin(), entry.result.elements.end(), [&](alignment::element const &E) {
return E.vertex == *vertices.begin() && E.kind != alignment::element_kind::straight && (E.chainage < low || E.chainage + E.length > high);
}))
continue;
designed_vertex result;
result.record = static_cast<int>(r);
result.vertex = *vertices.begin();

View File

@@ -50,6 +50,18 @@ inline double cross(glm::dvec2 const &Direction, glm::dvec2 const &Point)
return Direction.x * Point.y - Direction.y * Point.x;
}
// where the line from A along AlongA crosses the one from B along AlongB, as the distances along each in the lengths of their
// directions; none where they run alike
inline bool lines_cross(glm::dvec2 const &A, glm::dvec2 const &AlongA, glm::dvec2 const &B, glm::dvec2 const &AlongB, double &First, double &Second)
{
auto const determinant{cross(AlongA, AlongB)};
if (std::abs(determinant) < 1e-9)
return false;
First = cross(B - A, AlongB) / determinant;
Second = cross(B - A, AlongA) / determinant;
return true;
}
inline glm::dvec2 turned(glm::dvec2 const &Vector, double const Angle)
{
auto const c{std::cos(Angle)};
@@ -137,6 +149,14 @@ struct bezier
{
return 6.0 * (1.0 - T) * (p2 - 2.0 * p1 + p0) + 6.0 * T * (p3 - 2.0 * p2 + p1);
}
// the curvature in the plan at T, 1/m, positive to the left as the curve runs; none where it barely moves
double plan_curvature(double const T) const
{
auto const along{first(T)};
auto const turning{second(T)};
auto const speed{std::hypot(along.x, along.z)};
return speed < 1e-9 ? 0.0 : (along.x * turning.z - along.z * turning.x) / (speed * speed * speed);
}
double plan_length(int const Steps = 32) const
{
double length{0.0};

View File

@@ -23,6 +23,7 @@ http://mozilla.org/MPL/2.0/.
#include <algorithm>
#include <cctype>
#include <chrono>
#include <climits>
#include <cmath>
@@ -31,6 +32,7 @@ http://mozilla.org/MPL/2.0/.
#include <fstream>
#include <functional>
#include <limits>
#include <mutex>
#include <optional>
#include <sstream>
#include <glad/glad.h>
@@ -42,12 +44,9 @@ namespace fs = std::filesystem;
constexpr int standard_pixels{1024}; // ~25 cm/px for a 256 m tile, matches the standard orthophoto
constexpr int hires_pixels{4096}; // ~6 cm/px, the WMS MaxWidth/MaxHeight of the geoportal services
constexpr int probe_pixels{64}; // coverage test of the high-resolution product
constexpr int map_pixels{1024}; // a side of a tile of the map, whatever its size in metres
constexpr int map_stamp{32}; // rows under a tile of the map the service writes its credit into, cut off
constexpr char const *map_service{"https://mapy.geoportal.gov.pl/wss/ext/OSM/BaseMap/service"};
constexpr int worker_count{8}; // concurrent requests; the service is slow but shouldn't be hammered
// of the workers at most that many on the tiles around the camera, each holding an image decoded (a 4K one is 64 MB); the rest only
// download the tiles of the area to the disk cache
// of the workers at most that many on the 4K tiles around the camera, each holding an image of 64 MB decoded; the standard tiles
// are taken by all of them, ahead of the download of the area to the disk cache
constexpr std::size_t camera_workers{3};
constexpr std::size_t upload_budget{16u << 20}; // bytes of texture data uploaded per frame (always at least one tile)
constexpr std::size_t vertex_budget{60000}; // ImGui uses 16-bit indices and not every backend supports vertex offsets
@@ -130,12 +129,42 @@ std::string wms_url(tile_request const &Request, bool Hires, int Pixels, bool Pn
return url;
}
// among the files kept to be fetched again faster, which don't roam with the profile. The orthophoto an older editor kept with the
// settings, the standard and the 4K imagery of each year, is moved there once, as the first tile is asked for; whatever else it
// kept there, the base maps of old among it, is deleted, and so is the orthophoto where it can't be moved or the new place has its own
fs::path cache_root()
{
fs::path root = user_config_path(".cache");
fs::path root = user_cache_path("geoportal");
if (root.empty())
root = ".cache";
return root / "geoportal";
root = fs::path{".cache"} / "geoportal";
static std::once_flag moved;
std::call_once(moved, [&]() {
auto const old{user_config_path(".cache")};
std::error_code ec;
if (old.empty() || false == fs::exists(old, ec))
return;
std::vector<fs::path> products;
for (auto const &entry : fs::directory_iterator(old / "geoportal", ec))
{
auto const name{entry.path().filename().string()};
if (entry.is_directory(ec) && (name.rfind("sr_", 0) == 0 || name.rfind("hr_", 0) == 0))
products.push_back(entry.path());
}
if (false == products.empty() && false == fs::exists(root, ec))
{
fs::create_directories(root, ec);
std::size_t count{0};
for (auto const &product : products)
{
fs::rename(product, root / product.filename(), ec);
count += ec ? 0 : 1;
}
auto const path{root.u8string()};
WriteLog("Editor orthophoto: " + std::to_string(count) + " products of the orthophoto moved to " + std::string(path.begin(), path.end()));
}
fs::remove_all(old, ec);
});
return root;
}
fs::path cache_file(tile_request const &Request, bool Hires, std::string const &Suffix)
@@ -154,11 +183,17 @@ bool is_image(std::vector<std::uint8_t> const &Data)
bool read_file(fs::path const &Path, std::vector<std::uint8_t> &Out)
{
std::ifstream file(Path, std::ios::binary);
// read whole, not a character at a time: a tile of the cache is megabytes
std::ifstream file(Path, std::ios::binary | std::ios::ate);
if (!file.is_open())
return false;
Out.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
return !Out.empty();
auto const size{static_cast<std::streamoff>(file.tellg())};
if (size <= 0)
return false;
Out.resize(static_cast<std::size_t>(size));
file.seekg(0);
file.read(reinterpret_cast<char *>(Out.data()), size);
return file.gcount() == size;
}
void write_file(fs::path const &Path, std::vector<std::uint8_t> const &Data)
@@ -190,10 +225,11 @@ void write_file(fs::path const &Path, std::vector<std::uint8_t> const &Data)
// the requests made of the services, the disk cache left out
std::atomic<int> requests_made{0};
// cached file if present, otherwise downloads (with a few retries, the service drops connections and answers 404 now and then)
// cached file if present, otherwise downloads (with a few retries, the service drops connections and answers 404 now and then);
// with no path nothing is read from the disk nor written to it
bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &Stop)
{
if (read_file(Path, Out) && is_image(Out))
if (false == Path.empty() && read_file(Path, Out) && is_image(Out))
return true;
for (int attempt = 0; attempt < 4; ++attempt)
@@ -218,7 +254,8 @@ bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, st
Error = "unexpected response: " + std::string(Out.begin(), Out.begin() + std::min<std::size_t>(Out.size(), 200));
continue;
}
write_file(Path, Out);
if (false == Path.empty())
write_file(Path, Out);
return true;
}
return false;
@@ -394,28 +431,49 @@ bool load_tile(http_client &Client, tile_request const &Request, image &Out, boo
return true;
}
// the base map of OpenStreetMap from geoportal.gov.pl, in the metres of PUWG 1992 as the imagery. the service writes its credit into
// the corner of every image: the image reaches a little further south and the rows with it are cut off
bool load_map(http_client &Client, int const Level, int const Ix, int const Iy, image &Out, std::string &Error, stop_check const &Stop)
// a parameter of a request as an address carries it: the names of the layers keep their letters, digits, separators and colons
std::string url_encoded(std::string const &Text)
{
auto const size{static_cast<long long>(editor_orthophoto::tile_size) << Level};
auto const below{size * map_stamp / map_pixels};
auto const url{std::string{map_service} + "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=osm&STYLES=&SRS=EPSG:2180&FORMAT=image/png&WIDTH=" + std::to_string(map_pixels) +
"&HEIGHT=" + std::to_string(map_pixels + map_stamp) + "&BBOX=" + std::to_string(Ix * size) + "," + std::to_string(Iy * size - below) + "," + std::to_string((Ix + 1) * size) + "," +
std::to_string((Iy + 1) * size)};
std::vector<std::uint8_t> data;
auto const path{cache_root() / "osm" / (std::to_string(size) + "_" + std::to_string(Iy) + "_" + std::to_string(Ix) + ".png")};
if (!fetch(Client, url, path, data, Error, Stop) || !decode(data, Out, Error))
return false;
if (Out.width != map_pixels || Out.height != map_pixels + map_stamp)
std::string out;
for (unsigned char const c : Text)
{
Error = "unexpected size of the map " + std::to_string(Out.width) + "x" + std::to_string(Out.height);
std::error_code ec;
fs::remove(path, ec);
return false;
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == ':' || c == ',')
out += static_cast<char>(c);
else
{
char hex[4];
std::snprintf(hex, sizeof(hex), "%%%02X", c);
out += hex;
}
}
Out.height = map_pixels;
Out.rgba.resize(static_cast<std::size_t>(map_pixels) * map_pixels * 4);
return out;
}
// a tile of a WMS of the user's, in EPSG:2180 as the imagery: kept in memory only, never on the disk. Empty where it draws nothing
bool load_service(http_client &Client, editor_orthophoto::config const &Source, tile_request const &Request, image &Out, bool &Empty, std::string &Error, stop_check const &Stop)
{
Empty = false;
auto const size{static_cast<long long>(editor_orthophoto::tile_size)};
auto const east{Request.ix * size};
auto const north{Request.iy * size};
auto url{Source.service};
if (url.find('?') == std::string::npos)
url += '?';
else if (url.back() != '?' && url.back() != '&')
url += '&';
// a style for each layer, the default one
auto const count{static_cast<std::size_t>(std::count(Source.layers.begin(), Source.layers.end(), ',')) + 1};
url += "SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:2180&FORMAT=image/png&TRANSPARENT=TRUE&LAYERS=" + url_encoded(Source.layers) + "&STYLES=" + std::string(count - 1, ',') +
"&WIDTH=" + std::to_string(Request.level) + "&HEIGHT=" + std::to_string(Request.level) + "&BBOX=" + std::to_string(east) + "," + std::to_string(north) + "," + std::to_string(east + size) + "," +
std::to_string(north + size);
std::vector<std::uint8_t> data;
if (!fetch(Client, url, {}, data, Error, Stop) || !decode(data, Out, Error))
return false;
for (std::size_t i = 3; i < Out.rgba.size(); i += 4)
if (Out.rgba[i] != 0)
return true;
Empty = true;
Out = image{};
return true;
}
@@ -465,6 +523,8 @@ constexpr float no_height{-std::numeric_limits<float>::max()};
// a bank of its own for every geometry chunk: adding a chunk to a bank makes the renderer upload the whole bank again,
// and the tiles fitted onto a heightmap terrain carry its full grid (hundreds of thousands of vertices each)
std::vector<std::uint32_t> scene_banks;
// the textures made by all the layers, named apart: main thread only
int texture_slots{0};
std::string slot_name(int Index)
{
@@ -548,6 +608,136 @@ editor_orthophoto::~editor_orthophoto()
worker.join();
}
namespace
{
// the text of an element of the capabilities, its entities read
std::string xml_text(std::string const &Text)
{
std::string out;
for (std::size_t i = 0; i < Text.size(); ++i)
{
if (Text[i] != '&')
{
out += Text[i];
continue;
}
auto const end{Text.find(';', i)};
if (end == std::string::npos)
{
out += Text[i];
continue;
}
auto const entity{Text.substr(i + 1, end - i - 1)};
if (entity == "amp")
out += '&';
else if (entity == "lt")
out += '<';
else if (entity == "gt")
out += '>';
else if (entity == "quot")
out += '"';
else if (entity == "apos")
out += '\'';
else if (false == entity.empty() && entity[0] == '#')
{
auto const code{entity.size() > 1 && (entity[1] == 'x' || entity[1] == 'X') ? std::strtoul(entity.c_str() + 2, nullptr, 16) : std::strtoul(entity.c_str() + 1, nullptr, 10)};
// carriage returns left out, the rest as UTF-8
if (code == 13)
;
else if (code < 0x80)
out += static_cast<char>(code);
else if (code < 0x800)
{
out += static_cast<char>(0xC0 | (code >> 6));
out += static_cast<char>(0x80 | (code & 0x3F));
}
else
{
out += static_cast<char>(0xE0 | (code >> 12));
out += static_cast<char>(0x80 | ((code >> 6) & 0x3F));
out += static_cast<char>(0x80 | (code & 0x3F));
}
}
else
out += Text.substr(i, end - i + 1);
i = end;
}
// the blanks at the ends of the text dropped
auto const first{out.find_first_not_of(" \t\r\n")};
if (first == std::string::npos)
return {};
return out.substr(first, out.find_last_not_of(" \t\r\n") - first + 1);
}
// the text of the first element of the tag between From and To, empty where there's none
std::string xml_element(std::string const &Xml, std::string const &Tag, std::size_t const From, std::size_t const To)
{
auto const open{"<" + Tag + ">"};
auto const start{Xml.find(open, From)};
if (start == std::string::npos || start >= To)
return {};
auto const end{Xml.find("</" + Tag + ">", start)};
if (end == std::string::npos || end > To)
return {};
return xml_text(Xml.substr(start + open.size(), end - start - open.size()));
}
} // namespace
editor_orthophoto::service_info editor_orthophoto::describe(std::string const &Service)
{
service_info info;
auto url{Service};
if (url.find('?') == std::string::npos)
url += '?';
else if (url.back() != '?' && url.back() != '&')
url += '&';
url += "SERVICE=WMS&REQUEST=GetCapabilities";
// a few tries, the services drop connections and time out now and then
std::vector<std::uint8_t> data;
auto read{false};
for (int attempt = 0; attempt < 3 && false == read; ++attempt)
read = editor_http_get(url, data, info.error);
if (false == read)
return info;
info.error.clear();
std::string const xml(data.begin(), data.end());
auto const service{xml.find("<Service>")};
auto const capability{xml.find("<Capability>")};
if (service == std::string::npos || capability == std::string::npos)
{
info.error = "no WMS capabilities: " + xml.substr(0, std::min<std::size_t>(xml.size(), 200));
return info;
}
info.title = xml_element(xml, "Title", service, capability);
auto const fees{xml_element(xml, "Fees", service, capability)};
auto const constraints{xml_element(xml, "AccessConstraints", service, capability)};
for (auto const &part : {fees, constraints})
if (false == part.empty() && part != "none" && part != "None" && part != "NONE")
info.terms += (info.terms.empty() ? "" : "\n") + part;
info.puwg = xml.find("EPSG:2180", capability) != std::string::npos;
// each layer up to the next one or to its end: a name of its own makes it one to ask for
auto const layer_at = [&](std::size_t From) {
for (auto at{xml.find("<Layer", From)}; at != std::string::npos; at = xml.find("<Layer", at + 6))
if (at + 6 < xml.size() && (xml[at + 6] == '>' || std::isspace(static_cast<unsigned char>(xml[at + 6]))))
return at;
return std::string::npos;
};
for (auto at{layer_at(capability)}; at != std::string::npos; at = layer_at(at + 6))
{
auto const next{layer_at(at + 6)};
auto const close{xml.find("</Layer>", at)};
auto const end{std::min(next, close)};
auto const name{xml_element(xml, "Name", at, end)};
if (false == name.empty())
info.layers.emplace_back(name, xml_element(xml, "Title", at, end));
}
if (info.layers.empty())
info.error = "the service has no layer to ask for";
return info;
}
bool editor_orthophoto::can_download()
{
#if defined(_WIN32) || defined(EU07_WITH_CURL)
@@ -570,8 +760,10 @@ bool editor_orthophoto::owns(gfx::geometry_handle const &Geometry)
std::string editor_orthophoto::mark(config const &Config, bool const Enabled, bool const Origin)
{
// how the imagery is shown is the user's, kept with the editor (EditorSettings); the scenery keeps whether it's on and where the
// scenery lies
char text[256];
std::snprintf(text, sizeof(text), "on %d year %d opacity %.2f radius %d height %.2f drape %d lift %.2f hires %d scene %d", Enabled ? 1 : 0, Config.year, Config.opacity, Config.radius, Config.height, Config.drape ? 1 : 0, Config.lift, Config.hires ? 1 : 0, Config.in_scene ? 1 : 0);
std::snprintf(text, sizeof(text), "on %d", Enabled ? 1 : 0);
std::string result{text};
if (Origin)
{
@@ -586,26 +778,11 @@ bool editor_orthophoto::read_mark(std::string const &Text, config &Config, bool
std::istringstream words{Text};
std::string key;
auto found{false};
// the year, opacity, radius and the rest the lines of the older sceneries carry are passed over: the user's settings stand
while (words >> key)
{
if (key == "on")
words >> Enabled;
else if (key == "year")
words >> Config.year;
else if (key == "opacity")
words >> Config.opacity;
else if (key == "radius")
words >> Config.radius;
else if (key == "height")
words >> Config.height;
else if (key == "drape")
words >> Config.drape;
else if (key == "lift")
words >> Config.lift;
else if (key == "hires")
words >> Config.hires;
else if (key == "scene")
words >> Config.in_scene;
else if (key == "origin")
words >> Config.north >> Config.east;
else
@@ -614,8 +791,6 @@ bool editor_orthophoto::read_mark(std::string const &Text, config &Config, bool
return found;
found = true;
}
Config.radius = std::clamp(Config.radius, 0, max_radius);
Config.opacity = std::clamp(Config.opacity, 0.0f, 1.0f);
return found;
}
@@ -634,9 +809,6 @@ void editor_orthophoto::enabled(bool State)
for (auto &entry : m_tiles)
release_tile(entry.second);
m_tiles.clear();
for (auto &entry : m_maps)
release_tile(entry.second);
m_maps.clear();
++m_generation; // results still in flight are now stale
std::lock_guard<std::mutex> lock(m_mutex);
m_done.clear();
@@ -681,16 +853,11 @@ void editor_orthophoto::area(std::vector<glm::dvec2> Polygon)
{
// the jobs for the tiles outside it forgotten, the tiles shown let go by the next update
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == 0 && 0 == m_area_tiles.count(Job.key); }), m_queue.end());
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return 0 == m_area_tiles.count(Job.key); }), m_queue.end());
}
restart_prefetch();
}
bool editor_orthophoto::in_area(double const East, double const North) const
{
return m_area.empty() || geometry::square_touches(m_area, East, North, 0.0);
}
void editor_orthophoto::area_paused(bool const Paused)
{
{
@@ -703,7 +870,8 @@ void editor_orthophoto::area_paused(bool const Paused)
void editor_orthophoto::restart_prefetch()
{
std::vector<tile_key> keys;
if (m_enabled && can_download())
// a service of the user's is asked for no tile ahead
if (m_enabled && can_download() && m_config.service.empty())
keys.assign(m_area_tiles.begin(), m_area_tiles.end());
auto const centre{m_camera_tile};
auto const distance = [&](tile_key const &Key) {
@@ -718,7 +886,7 @@ void editor_orthophoto::restart_prefetch()
m_prefetch_next = 0;
m_prefetch_failed.clear();
m_prefetch_cached.clear();
m_prefetch_source = source{m_config.year, m_config.hires};
m_prefetch_source = source{m_config.year, m_config.hires, m_config.service, m_config.layers};
++m_prefetch_generation;
}
m_wakeup.notify_all();
@@ -801,8 +969,6 @@ void editor_orthophoto::cancel_pending()
// jobs already picked up by a worker still deliver their result, which is accepted if the tile is still wanted
for (auto &entry : m_tiles)
entry.second.requested = 0;
for (auto &entry : m_maps)
entry.second.requested = 0;
}
void editor_orthophoto::clear_cache()
@@ -811,9 +977,6 @@ void editor_orthophoto::clear_cache()
for (auto &entry : m_tiles)
release_tile(entry.second);
m_tiles.clear();
for (auto &entry : m_maps)
release_tile(entry.second);
m_maps.clear();
++m_generation;
{
std::lock_guard<std::mutex> lock(m_mutex);
@@ -830,11 +993,6 @@ void editor_orthophoto::retry_failed()
entry.second.failures = 0;
entry.second.retry_at = 0.0;
}
for (auto &entry : m_maps)
{
entry.second.failures = 0;
entry.second.retry_at = 0.0;
}
// the tiles of the area which failed taken again
{
std::lock_guard<std::mutex> lock(m_mutex);
@@ -868,8 +1026,6 @@ editor_orthophoto::statistics editor_orthophoto::stats() const
};
for (auto const &entry : m_tiles)
count(entry.second);
for (auto const &entry : m_maps)
count(entry.second);
return out;
}
@@ -902,7 +1058,8 @@ void editor_orthophoto::start_workers()
{
if (!m_workers.empty())
return;
for (int i = 0; i < worker_count; ++i)
// a service of the user's is asked fewer things at once than the orthophoto, which has the download of the area to do as well
for (int i = 0; i < (m_config.service.empty() ? worker_count : 2); ++i)
m_workers.emplace_back(&editor_orthophoto::worker, this);
}
@@ -915,7 +1072,7 @@ editor_orthophoto::texture_slot editor_orthophoto::acquire_slot()
return slot;
}
texture_slot slot;
slot.index = m_texture_count++;
slot.index = texture_slots++;
slot.texture = GfxRenderer->Fetch_Texture(slot_name(slot.index), true);
return slot;
}
@@ -933,12 +1090,12 @@ void editor_orthophoto::release_tile(tile &Tile)
Tile.slot = texture_slot{};
}
void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha, int const Map)
void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha)
{
{
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == Key && Job.map == Map; }), m_queue.end());
m_queue.push_back(job{Key, Map, Level, Alpha, source{m_config.year, m_config.hires}, m_generation.load()});
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == Key; }), m_queue.end());
m_queue.push_back(job{Key, Level, Alpha, source{m_config.year, m_config.hires, m_config.service, m_config.layers}, m_generation.load()});
}
Tile.requested = Level;
Tile.requested_alpha = Alpha;
@@ -955,17 +1112,10 @@ std::size_t editor_orthophoto::apply(result &Result, double Now)
{
if (Result.generation != m_generation.load())
return 0;
tile *found{nullptr};
if (Result.map > 0)
{
if (auto const lookup = m_maps.find({Result.map, Result.key}); lookup != m_maps.end())
found = &lookup->second;
}
else if (auto const lookup = m_tiles.find(Result.key); lookup != m_tiles.end())
found = &lookup->second;
if (found == nullptr)
auto const found{m_tiles.find(Result.key)};
if (found == m_tiles.end())
return 0; // left the area in the meantime; the imagery stays in the disk cache
tile &t = *found;
tile &t = found->second;
bool const current = (t.requested == Result.level && t.requested_alpha == Result.alpha);
if (current)
t.requested = 0;
@@ -981,7 +1131,7 @@ std::size_t editor_orthophoto::apply(result &Result, double Now)
}
// an image the tile no longer wants, as a 4K one it asked for before the camera moved on, isn't sent to the graphics card: the
// one wanted now is on its way. A tile showing nothing yet takes it meanwhile
if (false == current && Result.map == 0 && t.level > 0)
if (false == current && t.level > 0)
{
++m_stale;
return 0;
@@ -1027,7 +1177,7 @@ std::pair<std::size_t, std::size_t> editor_orthophoto::grounded() const
std::vector<std::pair<glm::dvec2, glm::dvec2>> editor_orthophoto::waiting() const
{
std::vector<std::pair<glm::dvec2, glm::dvec2>> result;
if (false == m_enabled || m_map_level > 0)
if (false == m_enabled)
return result;
for (auto const &entry : m_tiles)
if (entry.second.level == 0 && entry.second.requested > 0 && false == entry.second.empty)
@@ -1437,19 +1587,6 @@ void editor_orthophoto::update(glm::dvec3 const &Camera)
std::lock_guard<std::mutex> lock(m_mutex);
m_focus = centre;
}
if (m_map_level > 0)
{
update_map(Camera, now);
return;
}
if (false == m_maps.empty())
{
for (auto &entry : m_maps)
release_tile(entry.second);
m_maps.clear();
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [](job const &Job) { return Job.map > 0; }), m_queue.end());
}
int const radius = m_config.radius;
auto const distance = [&](tile_key const &Key) { return std::max(std::abs(Key.first - centre.first), std::abs(Key.second - centre.second)); };
@@ -1479,7 +1616,7 @@ void editor_orthophoto::update(glm::dvec3 const &Camera)
if (evicted)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == 0 && distance(Job.key) > radius; }), m_queue.end());
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return distance(Job.key) > radius; }), m_queue.end());
}
// tiles which need (different) imagery
@@ -1500,90 +1637,49 @@ void editor_orthophoto::update(glm::dvec3 const &Camera)
tile &t = m_tiles[key];
if (!in_service_area(key.first, key.second))
t.empty = true;
int const level = level_for(std::max(std::abs(dx), std::abs(dy)), m_config.hires);
int level = level_for(std::max(std::abs(dx), std::abs(dy)), m_config.hires);
// a tile showing nothing yet takes the standard image first, from the disk cache at once mostly: the 4K one comes from a slow
// service a few at a time, the tiles around the camera stayed blank meanwhile though their standard images lay on the disk
if (level > standard_pixels && t.level < standard_pixels)
level = standard_pixels;
if (t.empty || (t.level == level && t.alpha == m_alpha) || (t.requested == level && t.requested_alpha == m_alpha) || now < t.retry_at)
continue;
schedule(key, t, level, m_alpha);
}
// ground fit, one tile per frame (it walks the scene geometry), nearest first
// ground fit, nearest first, as many tiles a frame as 4 ms let walk the scene geometry, one at least: a tile not fitted yet lies
// at the height of the layer, under the terrain of a scenery high above it, as if it hadn't come
if (m_config.drape && m_ground && simulation::Region != nullptr)
{
tile *nearest{nullptr};
tile_key nearestkey{0, 0};
int nearestdistance{INT_MAX};
for (auto &entry : m_tiles)
auto const until{now + 0.004};
for (;;)
{
if (entry.second.grounded || entry.second.empty)
continue;
int const d = distance(entry.first);
if (d < nearestdistance)
tile *nearest{nullptr};
tile_key nearestkey{0, 0};
int nearestdistance{INT_MAX};
for (auto &entry : m_tiles)
{
nearestdistance = d;
nearest = &entry.second;
nearestkey = entry.first;
if (entry.second.grounded || entry.second.empty)
continue;
int const d = distance(entry.first);
if (d < nearestdistance)
{
nearestdistance = d;
nearest = &entry.second;
nearestkey = entry.first;
}
}
}
if (nearest != nullptr)
if (nearest == nullptr)
break;
sample_ground(nearestkey, *nearest);
if (steady_seconds() >= until)
break;
}
}
update_scene();
}
void editor_orthophoto::overview(double const Scale, double const Reach)
{
// a pixel of a tile of the map is as large as one of the screen, or up to twice as small
m_map_level = Scale > 0.0 ? std::clamp(static_cast<int>(std::floor(std::log2(Scale * map_pixels / tile_size))), 2, 9) : 0;
m_map_reach = Reach;
}
void editor_orthophoto::update_map(glm::dvec3 const &Camera, double const Now)
{
// the imagery gives way to the map, its tiles come back from the disk cache once the view is near again
if (false == m_tiles.empty())
{
for (auto &entry : m_tiles)
release_tile(entry.second);
m_tiles.clear();
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [](job const &Job) { return Job.map == 0; }), m_queue.end());
}
double const east = m_config.east - Camera.x;
double const north = m_config.north + Camera.z;
auto const in_view = [&](map_key const &Key) {
auto const size{tile_size * static_cast<double>(1 << Key.first)};
return (Key.second.first + 1) * size > east - m_map_reach && Key.second.first * size < east + m_map_reach && (Key.second.second + 1) * size > north - m_map_reach &&
Key.second.second * size < north + m_map_reach;
};
// the tiles of the other levels stay under the ones of the level shown until these come
auto const size{tile_size * static_cast<double>(1 << m_map_level)};
auto complete{true};
for (int iy = static_cast<int>(std::floor((north - m_map_reach) / size)); iy <= static_cast<int>(std::floor((north + m_map_reach) / size)); ++iy)
for (int ix = static_cast<int>(std::floor((east - m_map_reach) / size)); ix <= static_cast<int>(std::floor((east + m_map_reach) / size)); ++ix)
{
map_key const key{m_map_level, {ix, iy}};
tile &t = m_maps[key];
if (t.level == 0 && false == t.empty)
complete = false;
if (t.empty || t.level == map_pixels || t.requested == map_pixels || Now < t.retry_at)
continue;
schedule(key.second, t, map_pixels, 255, m_map_level);
}
for (auto it = m_maps.begin(); it != m_maps.end();)
{
if (false == in_view(it->first) || std::abs(it->first.first - m_map_level) > 1 || (complete && it->first.first != m_map_level))
{
release_tile(it->second);
it = m_maps.erase(it);
}
else
++it;
}
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map > 0 && m_maps.count({Job.map, Job.key}) == 0; }), m_queue.end());
}
void editor_orthophoto::worker()
{
stbi_set_flip_vertically_on_load_thread(0); // the texture loader flips globally; tiles keep the north edge in the first row
@@ -1602,17 +1698,16 @@ void editor_orthophoto::worker()
// the nearest tile first, of the ones no other worker is on; none while enough workers are on tiles around the camera
auto const nearest = [&]() {
auto best = m_queue.end();
if (m_running.size() >= camera_workers)
if (m_running.size() >= static_cast<std::size_t>(worker_count))
return best;
long long bestdistance = LLONG_MAX;
for (auto it = m_queue.begin(); it != m_queue.end(); ++it)
{
if (m_running.count({it->map, it->key}) > 0)
// a 4K image is 64 MB decoded: that many at most at once, the standard tiles by all the workers
if (m_running.count(it->key) > 0 || (it->level >= hires_pixels && m_running_hires >= camera_workers))
continue;
// a tile of the map is that many tiles of the imagery a side
long long const scale = 1ll << it->map;
long long const dx = it->key.first * scale + scale / 2 - m_focus.first;
long long const dy = it->key.second * scale + scale / 2 - m_focus.second;
long long const dx = it->key.first - m_focus.first;
long long const dy = it->key.second - m_focus.second;
long long const d = dx * dx + dy * dy;
if (d < bestdistance)
{
@@ -1630,7 +1725,9 @@ void editor_orthophoto::worker()
{
current = *best;
m_queue.erase(best);
m_running.insert({current.map, current.key});
m_running.insert(current.key);
if (current.level >= hires_pixels)
++m_running_hires;
}
else
{
@@ -1662,7 +1759,6 @@ void editor_orthophoto::worker()
result out;
out.key = current.key;
out.map = current.map;
out.level = current.level;
out.generation = current.generation;
out.alpha = current.alpha;
@@ -1673,13 +1769,21 @@ void editor_orthophoto::worker()
// a job of the same tile queued since: the tile wants another image now
stop_check const superseded = [&]() {
std::lock_guard<std::mutex> lock(m_mutex);
return std::any_of(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == current.map && Job.key == current.key; });
return std::any_of(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == current.key; });
};
out.ok = current.map > 0 ? load_map(client, current.map, current.key.first, current.key.second, picture, error, stopping) : load_tile(client, request, picture, out.empty, error, stopping, superseded);
if (current.src.service.empty())
out.ok = load_tile(client, request, picture, out.empty, error, stopping, superseded);
else
{
config service;
service.service = current.src.service;
service.layers = current.src.layers;
out.ok = load_service(client, service, request, picture, out.empty, error, stopping);
}
if (m_stop)
return;
if (!out.ok && error != "superseded")
ErrorLog(std::string("Editor orthophoto: ") + (current.map > 0 ? "map " + std::to_string(current.map) + " " : std::string{}) + "tile " + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error);
ErrorLog(std::string("Editor orthophoto: tile ") + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error);
if (out.ok && current.alpha < 255)
for (std::size_t i = 3; i < picture.rgba.size(); i += 4)
picture.rgba[i] = static_cast<std::uint8_t>((picture.rgba[i] * current.alpha + 127u) / 255u);
@@ -1690,7 +1794,9 @@ void editor_orthophoto::worker()
{
std::lock_guard<std::mutex> lock(m_mutex);
m_done.push_back(std::move(out));
m_running.erase({current.map, current.key});
m_running.erase(current.key);
if (current.level >= hires_pixels)
--m_running_hires;
}
// a job of the same tile may have waited for this one
m_wakeup.notify_all();
@@ -1701,11 +1807,6 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const &
{
if (!m_enabled || ScreenWidth <= 0.0f || ScreenHeight <= 0.0f)
return;
if (m_map_level > 0)
{
draw_map(ViewProjection, CameraPos, ScreenWidth, ScreenHeight);
return;
}
if (m_config.in_scene || m_config.opacity <= 0.0f)
return;
@@ -1851,46 +1952,3 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const &
drawlist->PopTextureID();
}
}
void editor_orthophoto::draw_map(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const
{
// the map takes the place of the ground, opaque; the top view sees it straight down, a tile is one quad
ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport());
auto const toscreen = [&](glm::vec4 const &Clip) { return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * ScreenWidth, (0.5f - Clip.y / Clip.w * 0.5f) * ScreenHeight); };
float constexpr guard{1.02f};
for (auto const shown : {false, true})
for (auto const &entry : m_maps)
{
if ((entry.first.first == m_map_level) != shown)
continue;
tile const &t = entry.second;
if (t.level == 0 || t.empty || t.slot.texture == null_handle)
continue;
std::size_t const textureid = GfxRenderer->Texture(t.slot.texture).get_id();
if (textureid == 0 || textureid == static_cast<std::size_t>(static_cast<std::uint32_t>(-1)))
continue;
auto const size{tile_size * static_cast<double>(1 << entry.first.first)};
glm::dvec2 const corner{m_config.east - entry.first.second.first * size, (entry.first.second.second + 1) * size - m_config.north};
auto const at = [&](double U, double V) { return ViewProjection * glm::vec4(glm::vec3(glm::dvec3(corner.x - U * size, CameraPos.y - 1.0, corner.y - V * size) - CameraPos), 1.0f); };
clip_vertex buffer0[12] = {{at(0.0, 0.0), {0.0f, 0.0f}}, {at(1.0, 0.0), {1.0f, 0.0f}}, {at(1.0, 1.0), {1.0f, 1.0f}}, {at(0.0, 1.0), {0.0f, 1.0f}}};
clip_vertex buffer1[12];
int count = clip_polygon(buffer0, 4, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.x; });
count = clip_polygon(buffer1, count, buffer0, [&](glm::vec4 const &v) { return v.w * guard + v.x; });
count = clip_polygon(buffer0, count, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.y; });
count = clip_polygon(buffer1, count, buffer0, [&](glm::vec4 const &v) { return v.w * guard + v.y; });
if (count < 3)
continue;
drawlist->PushTextureID((ImTextureID)(intptr_t)(textureid));
drawlist->PrimReserve((count - 2) * 3, count);
auto const base = static_cast<ImDrawIdx>(drawlist->_VtxCurrentIdx);
for (int k = 1; k + 1 < count; ++k)
{
drawlist->PrimWriteIdx(base);
drawlist->PrimWriteIdx(static_cast<ImDrawIdx>(base + k));
drawlist->PrimWriteIdx(static_cast<ImDrawIdx>(base + k + 1));
}
for (int k = 0; k < count; ++k)
drawlist->PrimWriteVtx(toscreen(buffer0[k].position), ImVec2(buffer0[k].uv.x, buffer0[k].uv.y), IM_COL32_WHITE);
drawlist->PopTextureID();
}
}

View File

@@ -65,6 +65,10 @@ class editor_orthophoto
bool drape{true}; // fit the imagery onto the ground geometry instead of a flat plane
float lift{0.05f}; // with drape: distance above the ground (in the scene it has to clear the ground to be seen)
bool in_scene{false}; // render as opaque scene geometry, covered by whatever is in front of it
// a WMS of the user's in place of the orthophoto of geoportal.gov.pl: its address and the layers asked of it in EPSG:2180, PNG
// clear where it draws nothing. Its tiles are kept in memory only and asked for only around the camera, none ahead
std::string service;
std::string layers;
};
using world_triangle = std::array<glm::dvec3, 3>;
@@ -102,6 +106,8 @@ class editor_orthophoto
// disabling drops the queue and returns the gpu memory of the resident tiles
void enabled(bool State);
config const &settings() const { return m_config; }
// the line of the scenery (//$o): whether the imagery is on and, with Origin, where the scenery lies; the rest of the settings
// are the user's
static std::string mark(config const &Config, bool Enabled, bool Origin);
static bool read_mark(std::string const &Text, config &Config, bool &Enabled);
// changes of year / resolution invalidate resident tiles, the rest applies immediately
@@ -116,7 +122,6 @@ class editor_orthophoto
// tiles around the camera as before
void area(std::vector<glm::dvec2> Polygon);
std::vector<glm::dvec2> const &area() const { return m_area; }
bool in_area(double const East, double const North) const;
// the download in the background held, or going on
void area_paused(bool const Paused);
// the squares of the tiles of the area in the world (x, z): 0 waiting, 1 in the disk cache, 2 failed
@@ -146,10 +151,6 @@ class editor_orthophoto
// true for geometry created by the layer, so ground queries can skip it
static bool owns(gfx::geometry_handle const &Geometry);
// a top view of Scale metres a pixel shows the base map of OpenStreetMap in place of the imagery, as far as Reach from the
// camera; 0: the imagery
void overview(double const Scale, double const Reach);
bool overview() const { return m_map_level > 0; }
// main thread, once per frame: schedules tiles around the camera and uploads finished ones
void update(glm::dvec3 const &Camera);
// main thread, inside an ImGui frame: emits the overlay into the background draw list.
@@ -165,6 +166,17 @@ class editor_orthophoto
statistics stats() const;
// false when the build has no http client; cached tiles are still shown
static bool can_download();
// what a WMS tells of itself in its capabilities: its title, its fees and access constraints, its layers with a name and whether it
// draws in EPSG:2180, the one the tiles are laid out in. Blocking, for a thread of its own
struct service_info
{
std::string title;
std::string terms;
std::vector<std::pair<std::string, std::string>> layers; // name, title
bool puwg{false};
std::string error;
};
static service_info describe(std::string const &Service);
static std::string cache_directory();
private:
@@ -174,12 +186,13 @@ class editor_orthophoto
{
int year{0};
bool hires{false};
std::string service;
std::string layers;
};
struct job
{
tile_key key;
int map{0}; // level of a tile of the map, 256 m << map a side; 0: of the imagery
int level{0}; // requested texture size in pixels
std::uint8_t alpha{255}; // baked into the texture, for translucency in the scene
source src;
@@ -189,7 +202,6 @@ class editor_orthophoto
struct result
{
tile_key key;
int map{0};
int level{0};
std::uint8_t alpha{255};
std::uint32_t generation{0};
@@ -235,7 +247,7 @@ class editor_orthophoto
static int level_for(int Distance, bool Hires);
tile_key camera_tile(glm::dvec3 const &Camera) const;
glm::dvec2 tile_corner(tile_key const &Key) const; // world (x,z) of the north-west corner
void schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha, int const Map = 0);
void schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha);
std::uint8_t texture_alpha() const; // alpha the textures should carry with the current settings
std::size_t apply(result &Result, double Now); // returns bytes uploaded
void release_tile(tile &Tile);
@@ -247,8 +259,6 @@ class editor_orthophoto
void remove_shape(tile &Tile);
void release_geometry(tile &Tile);
void update_scene();
void update_map(glm::dvec3 const &Camera, double const Now);
void draw_map(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const;
void start_workers();
void worker();
// the tiles of the area queued for the download in the background again, nearest to the camera first
@@ -258,12 +268,7 @@ class editor_orthophoto
bool m_enabled{false};
std::map<tile_key, tile> m_tiles; // main thread only
tile_key m_camera_tile{0, 0};
using map_key = std::pair<int, tile_key>; // level, then the place on the grid of the tiles of that level
std::map<map_key, tile> m_maps; // main thread only
int m_map_level{0}; // of the map shown, 0: the imagery
double m_map_reach{0.0}; // m from the camera the view of the map reaches
std::vector<texture_slot> m_free_slots; // pool of textures, reused between tiles
int m_texture_count{0};
ground_query m_ground;
height_query m_height;
height_query m_guess;
@@ -285,7 +290,8 @@ class editor_orthophoto
std::deque<job> m_queue;
std::deque<result> m_done;
tile_key m_focus{0, 0}; // workers pick the queued tile closest to it
std::set<std::pair<int, tile_key>> m_running; // (map, tile) a worker is on: another job of it waits till that one is done
std::set<tile_key> m_running; // tiles a worker is on: another job of one waits till that one is done
std::size_t m_running_hires{0}; // of them, the ones of a 4K image
// the download of the area in the background: the tiles nearest to the camera first, taken when no tile around the camera waits
std::vector<tile_key> m_prefetch;
std::size_t m_prefetch_next{0};

View File

@@ -27,6 +27,8 @@ namespace
// a switch, and the timbers it shares, may lie on a vertical curve: a concave one, or a convex one no sharper than this
double constexpr switch_crest{2000.0}; // m
// per mille: the change of the grade on a switch taken for a break of it, 3 mm over 30 m of it; below it the switch lies in one plane
double constexpr switch_break{0.1};
double polyline(line const &Line, double const Chainage)
{
@@ -246,7 +248,9 @@ std::vector<issue> check(line const &Line, context const &Context)
add(false, static_cast<int>(k), points[k].chainage, format(STR_C("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(STR_C("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)
// a change as small as the grade lines laid one after another meet with, a few millimetres over the length of a switch, is no
// break of it: the tracks of a slip met so lie in one plane all the same (pila.scn: 0.05 per mille inside shared timbers)
if (radius <= 0.0 && change > switch_break)
for (auto const &zone : Context.switches)
// a break on the bound of a switch is let be, within what the chainages of routes laid over each other differ by
if (points[k].chainage > zone.from + 0.05 && points[k].chainage < zone.to - 0.05)

View File

@@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/utilities.h"
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
@@ -48,16 +49,52 @@ template <typename Type_> void parse(std::string const &Text, Type_ &Out)
}
const std::string origin_prefix{"ortho_origin@"};
const std::string offset_prefix{"nmt_offset@"};
// settings are whitespace separated key/value pairs, so the scenery name used in the key can't contain spaces
std::string origin_key(std::string const &Scenery)
std::string scenery_key(std::string const &Prefix, std::string const &Scenery)
{
std::string key = origin_prefix + Scenery;
std::string key = Prefix + Scenery;
for (char &c : key)
if (std::isspace(static_cast<unsigned char>(c)))
c = '_';
return key;
}
// a field of a WMS layer in the settings, which keep values with no blank in them: the blanks, the separator of the fields and the
// percent sign go as %XX
std::string encoded(std::string const &Text)
{
std::string out;
for (unsigned char const c : Text)
{
if (std::isspace(c) || c == ';' || c == '%')
{
char hex[4];
std::snprintf(hex, sizeof(hex), "%%%02X", c);
out += hex;
}
else
out += static_cast<char>(c);
}
return out;
}
std::string decoded(std::string const &Text)
{
std::string out;
for (std::size_t i = 0; i < Text.size(); ++i)
{
if (Text[i] == '%' && i + 2 < Text.size() && std::isxdigit(static_cast<unsigned char>(Text[i + 1])) && std::isxdigit(static_cast<unsigned char>(Text[i + 2])))
{
out += static_cast<char>(std::strtoul(Text.substr(i + 1, 2).c_str(), nullptr, 16));
i += 2;
}
else
out += Text[i];
}
return out;
}
}
bool editorSettings::load()
@@ -92,6 +129,8 @@ bool editorSettings::load()
parse(value, m_orthophoto.lift);
else if (key == "ortho_in_scene")
m_orthophoto.in_scene = (value == "1");
else if (key == "ortho_on")
m_orthophoto.on = (value == "1");
else if (key == "platform_edge")
parse(value, m_platform_edge);
else if (key == "signal_offset")
@@ -108,6 +147,16 @@ bool editorSettings::load()
m_terrain_offer = (value == "1");
else if (key == "track_tools_all")
m_track_tools_more = (value == "1");
else if (key == "terrain_while_laying")
m_terrain_while_laying = (value == "1");
else if (key == "nmt_band")
parse(value, m_terrain_model.band);
else if (key == "nmt_around")
parse(value, m_terrain_model.around);
else if (key == "nmt_shape")
m_terrain_model.shape = (value == "1");
else if (key == "nmt_follow")
m_terrain_model.follow = (value == "1");
else if (key == "top_ballast")
m_top_tracks = value == "1" ? 0 : 1;
else if (key == "top_tracks")
@@ -130,6 +179,31 @@ bool editorSettings::load()
// before, when they were the default, are left behind
else if (key == "laying_heights")
m_heights = value == "points" ? lay_heights::points : value == "level" ? lay_heights::level : lay_heights::fitted;
// visible;opacity;address;layers;title;terms
else if (key == "wms_layer")
{
std::vector<std::string> fields;
for (std::size_t from{0};;)
{
auto const separator{value.find(';', from)};
fields.push_back(decoded(value.substr(from, separator == std::string::npos ? std::string::npos : separator - from)));
if (separator == std::string::npos)
break;
from = separator + 1;
}
if (fields.size() < 4 || fields[2].empty() || fields[3].empty())
continue;
wms_layer layer;
layer.visible = fields[0] == "1";
parse(fields[1], layer.opacity);
layer.url = fields[2];
layer.layers = fields[3];
if (fields.size() > 4)
layer.title = fields[4];
if (fields.size() > 5)
layer.terms = fields[5];
m_wms_layers.push_back(layer);
}
else if (key.rfind(origin_prefix, 0) == 0)
{
// northing;easting
@@ -141,6 +215,16 @@ bool editorSettings::load()
parse(value.substr(separator + 1), east);
m_orthophoto_origins[key] = {north, east};
}
else if (key.rfind(offset_prefix, 0) == 0)
{
// offset;fitted
auto const separator = value.find(';');
if (separator == std::string::npos)
continue;
double offset{0.0};
parse(value.substr(0, separator), offset);
m_terrain_model_offsets[key] = {offset, value.substr(separator + 1) == "1"};
}
}
return true;
}
@@ -168,6 +252,8 @@ bool editorSettings::save()
stream << "ortho_drape " << (m_orthophoto.drape ? 1 : 0) << "\n";
stream << "ortho_lift " << m_orthophoto.lift << "\n";
stream << "ortho_in_scene " << (m_orthophoto.in_scene ? 1 : 0) << "\n";
if (m_orthophoto.on.has_value())
stream << "ortho_on " << (*m_orthophoto.on ? 1 : 0) << "\n";
stream << "platform_edge " << m_platform_edge << "\n";
stream << "signal_offset " << m_signal_offset << "\n";
stream << "nodebank_previews " << (m_nodebank_previews ? 1 : 0) << "\n";
@@ -176,19 +262,28 @@ bool editorSettings::save()
stream << "first_steps " << (m_first_steps ? 1 : 0) << "\n";
stream << "terrain_offer " << (m_terrain_offer ? 1 : 0) << "\n";
stream << "track_tools_all " << (m_track_tools_more ? 1 : 0) << "\n";
stream << "terrain_while_laying " << (m_terrain_while_laying ? 1 : 0) << "\n";
stream << "nmt_band " << m_terrain_model.band << "\n";
stream << "nmt_around " << m_terrain_model.around << "\n";
stream << "nmt_shape " << (m_terrain_model.shape ? 1 : 0) << "\n";
stream << "nmt_follow " << (m_terrain_model.follow ? 1 : 0) << "\n";
stream << "top_tracks " << m_top_tracks << "\n";
stream << "top_rails " << std::hex << (m_top_rails & 0xffffffu) << std::dec << "\n";
stream << "corner_radii " << m_corner_low << ";" << m_corner_high << "\n";
stream << "laying_heights " << (m_heights == lay_heights::points ? "points" : m_heights == lay_heights::level ? "level" : "fitted") << "\n";
for (auto const &layer : m_wms_layers)
stream << "wms_layer " << (layer.visible ? 1 : 0) << ";" << layer.opacity << ";" << encoded(layer.url) << ";" << encoded(layer.layers) << ";" << encoded(layer.title) << ";" << encoded(layer.terms) << "\n";
stream << std::fixed << std::setprecision(3);
for (auto const &origin : m_orthophoto_origins)
stream << origin.first << " " << origin.second.first << ";" << origin.second.second << "\n";
for (auto const &offset : m_terrain_model_offsets)
stream << offset.first << " " << offset.second.first << ";" << (offset.second.second ? 1 : 0) << "\n";
return true;
}
bool editorSettings::orthophoto_origin(std::string const &Scenery, double &North, double &East) const
{
auto const lookup = m_orthophoto_origins.find(origin_key(Scenery));
auto const lookup = m_orthophoto_origins.find(scenery_key(origin_prefix, Scenery));
if (lookup == m_orthophoto_origins.end())
return false;
North = lookup->second.first;
@@ -198,5 +293,24 @@ bool editorSettings::orthophoto_origin(std::string const &Scenery, double &North
void editorSettings::orthophoto_origin(std::string const &Scenery, double North, double East)
{
m_orthophoto_origins[origin_key(Scenery)] = {North, East};
m_orthophoto_origins[scenery_key(origin_prefix, Scenery)] = {North, East};
}
bool editorSettings::terrain_model_offset(std::string const &Scenery, double &Offset, bool &Fitted) const
{
auto const lookup = m_terrain_model_offsets.find(scenery_key(offset_prefix, Scenery));
if (lookup == m_terrain_model_offsets.end())
return false;
Offset = lookup->second.first;
Fitted = lookup->second.second;
return true;
}
void editorSettings::terrain_model_offset(std::string const &Scenery, double Offset, bool Fitted)
{
// the model as it is, never fitted, is what a scenery without the line has
if (Offset == 0.0 && false == Fitted)
m_terrain_model_offsets.erase(scenery_key(offset_prefix, Scenery));
else
m_terrain_model_offsets[scenery_key(offset_prefix, Scenery)] = {Offset, Fitted};
}

View File

@@ -2,8 +2,10 @@
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
class editorSettings
{
@@ -23,7 +25,8 @@ public:
movement_scheme movement() const { return m_movement; }
void movement(movement_scheme scheme) { m_movement = scheme; }
// geoportal orthophoto layer (editor_orthophoto); the origin is kept per scenery
// geoportal orthophoto layer (editor_orthophoto), how it's shown and whether it's on; the origin is kept per scenery, and the
// scenery keeps it too
struct orthophoto_settings
{
int radius{2};
@@ -34,11 +37,25 @@ public:
bool drape{true};
float lift{0.05f};
bool in_scene{false};
// turned on or off by the user, for every scenery; none chosen yet: as the scenery keeps it, or the terrain model turns it on
std::optional<bool> on;
};
orthophoto_settings &orthophoto() { return m_orthophoto; }
// PUWG 1992 northing/easting of the scenery's (0,0,0) point; false when none was stored
bool orthophoto_origin(std::string const &Scenery, double &North, double &East) const;
void orthophoto_origin(std::string const &Scenery, double North, double East);
// the WMS layers the user added under the scenery, over the orthophoto, the top one first: the address of the service, the
// layers asked of it, its title and terms as its capabilities gave them. Kept with the editor, not with the scenery
struct wms_layer
{
std::string url;
std::string layers;
std::string title;
std::string terms;
float opacity{0.8f};
bool visible{true};
};
std::vector<wms_layer> &wms_layers() { return m_wms_layers; }
double platform_edge() const { return m_platform_edge; }
void platform_edge(double Distance) { m_platform_edge = Distance; }
@@ -60,6 +77,22 @@ public:
// a scenery begun with no terrain is offered the terrain model as the first track is laid
bool terrain_offer() const { return m_terrain_offer; }
void terrain_offer(bool Offer) { m_terrain_offer = Offer; }
// the terrain of the terrain model made under the tracks as they're laid, around the cursor from the first point placed
bool terrain_while_laying() const { return m_terrain_while_laying; }
void terrain_while_laying(bool const Make) { m_terrain_while_laying = Make; }
// the terrain model in the terrain panel: the band along the tracks it's loaded in, m; the chunks along a side of the square
// made around the camera; the formation and the slopes made along the tracks; the terrain following the tracks edited
struct terrain_model_settings
{
double band{300.0};
int around{4};
bool shape{true};
bool follow{true};
};
terrain_model_settings &terrain_model() { return m_terrain_model; }
// the height offset of the terrain model to the tracks of the scenery, and whether it was fitted to them; false when none was stored
bool terrain_model_offset(std::string const &Scenery, double &Offset, bool &Fitted) const;
void terrain_model_offset(std::string const &Scenery, double Offset, bool Fitted);
bool track_tools_more() const { return m_track_tools_more; }
void track_tools_more(bool Show) { m_track_tools_more = Show; }
// the ballast of the railway drawn in the top view
@@ -90,6 +123,7 @@ private:
movement_scheme m_movement{movement_scheme::wsad};
orthophoto_settings m_orthophoto;
std::map<std::string, std::pair<double, double>> m_orthophoto_origins;
std::vector<wms_layer> m_wms_layers;
double m_platform_edge{1.725};
double m_signal_offset{2.25};
bool m_nodebank_previews{true};
@@ -98,6 +132,9 @@ private:
bool m_first_steps{true};
bool m_terrain_offer{true};
bool m_track_tools_more{false};
bool m_terrain_while_laying{true};
terrain_model_settings m_terrain_model;
std::map<std::string, std::pair<double, bool>> m_terrain_model_offsets;
int m_top_tracks{0}; // the railway in the top view: whole, without the ballast, the rails alone
std::uint32_t m_top_rails{0xffff6e28}; // colour of the rails alone, as ImGui keeps it: ABGR
double m_corner_low{3000.0};

View File

@@ -133,7 +133,7 @@ std::vector<double> ramp_ends(std::vector<track_point> const &Track)
}
switch_report check(std::vector<track_point> const &Main, std::vector<track_point> const &Branch, std::vector<track_point> const &Line, double const From, double const To, double const Radius,
double const Ratio)
double const Ratio, double const Straight)
{
switch_report report;
if (Main.size() < 2 || Branch.size() < 2 || Line.size() < 2)
@@ -149,13 +149,15 @@ switch_report check(std::vector<track_point> const &Main, std::vector<track_poin
double high{std::numeric_limits<double>::lowest()};
double sharpest{0.0};
};
// of the part bent: up to the frog of a switch bent up to it
auto const along = [&](std::vector<track_point> const &Track) {
span result;
auto const length{Track.back().station - Track.front().station};
auto const finish{Track.back().station - (Straight > 0.5 ? Straight : 0.0)};
auto const length{finish - Track.front().station};
auto const margin{std::min(1.0, length * 0.25)};
result.first = sign * curvature_at(Track, Track.front().station + margin);
result.last = sign * curvature_at(Track, Track.back().station - margin);
for (auto s{Track.front().station + margin}; s <= Track.back().station - margin + 1e-9; s += 0.5)
result.last = sign * curvature_at(Track, finish - margin);
for (auto s{Track.front().station + margin}; s <= finish - margin + 1e-9; s += 0.5)
{
auto const curvature{sign * curvature_at(Track, s)};
result.low = std::min(result.low, curvature);
@@ -190,8 +192,14 @@ switch_report check(std::vector<track_point> const &Main, std::vector<track_poin
if (main.sharpest > kStraight && 1.0 / main.sharpest < limit->main - 1e-6)
report.warnings.emplace_back(format(STR_C("The main track bent to R %.0f m, below the least %.0f m for the switch of R %.0f m (table 11.9)"), 1.0 / main.sharpest, limit->main, Radius));
}
if (Ratio > 0.0 && Ratio < 9.41)
// 1:9 itself is bent, as the Rz 300-1:9 of table 11.9; the ratio may come from the angle of the tracks, a bit off the catalogue,
// 1:9.403 told from 1:9.51 by half of what keeps them apart
if (Ratio > 0.0 && (Ratio < 8.95 || std::abs(Ratio - 9.403) < 0.05))
report.warnings.emplace_back(format(STR_C("A switch of 1:%.3f isn't bent in the running and the main tracks: none of 1:9.403 nor steeper than 1:9 (11.4 pkt 4)"), Ratio));
// the basic switches of table 11.9 have the curve of the diverging track running through the frog; one with a straight frog is
// bent up to the frog, the frog and both tracks over it straight, the curvature jumping there
if (Straight > 0.5)
report.warnings.emplace_back(format(STR_C("Bent up to the frog, as the DB bends the switches with a straight frog: both tracks keep straight over its last %.1f m, the curvature jumps there; table 11.9 lists no such bent switch"), Straight));
}
check_cant(Line, From, To, report.curved, report.cant_start, report.cant_end, report.notes, report.warnings);

View File

@@ -51,9 +51,10 @@ struct switch_report
// Main, Branch: the tracks of the switch from its start, the stations from 0 there; Line: the track it's laid into, From and To the
// stations of the start and the end of the switch along it, From after To where it goes back along the line. Radius and Ratio:
// of the diverging track of the basic switch it's bent from
// of the diverging track of the basic switch it's bent from; Straight: how far both tracks keep straight at the end of the switch, over
// a straight frog, the switch bent up to it as the DB bends such switches
switch_report check(std::vector<track_point> const &Main, std::vector<track_point> const &Branch, std::vector<track_point> const &Line, double const From, double const To, double const Radius,
double const Ratio);
double const Ratio, double const Straight = 0.0);
// a slip switch bent into a curve, as ST-T1-A6 sees it: in a side track only (11.4 pkt 4b), the curvature of each track through it of
// one sign all along it (9.7 pkt 12), its slip curves no sharper than a side track may be (table 9.2), the cant as over a switch bent,

View File

@@ -224,6 +224,12 @@ std::vector<record> const &records()
return g_records;
}
bool has(std::string const &Name)
{
auto const &all{records()};
return std::any_of(all.begin(), all.end(), [&](record const &Record) { return Record.name == Name; });
}
std::optional<record> named(TTrack const &Track)
{
load();

View File

@@ -58,6 +58,8 @@ struct record
void reset();
void load();
std::vector<record> const &records();
// a record of the name is kept
bool has(std::string const &Name);
// the record of the switch, slip or crossing the track is a part of, while its first part keeps the shape the record says
std::optional<record> of(TTrack const &Track);
// the record of the track by its name alone, also when the switch lost its shape

View File

@@ -598,15 +598,17 @@ void terrain_streamer::update(glm::dvec3 const &Camera)
m_settled = false;
}
// the top view of the tracks wants the terrain as far as the corners of the screen, as fine as its pixels need
auto const top{Global.EditorOrtho && Global.editor_tracks && false == Global.EditorOrthoMap};
// the top view of the tracks wants the terrain as far as the corners of the screen, as fine as its pixels need, though no
// farther than the scenery reaches in the game: zoomed out, the tracks alone are drawn farther
auto const top{Global.EditorOrtho && Global.editor_tracks};
auto const aspect{Global.window_size.x / std::max(1.0, static_cast<double>(Global.window_size.y))};
auto const reach{top ? Global.EditorOrthoExtent * std::sqrt(1.0 + aspect * aspect) : 0.0};
auto const scenery{static_cast<double>(Global.BaseDrawRange) * std::max(1.0, static_cast<double>(Global.fDistanceFactor))};
auto const reach{top ? std::min(Global.EditorOrthoExtent * std::sqrt(1.0 + aspect * aspect), scenery) : 0.0};
m_pixel = top ? 2.0 * Global.EditorOrthoExtent / std::max(1, Global.window_size.y) : 0.0;
// chunks wanted around the camera and around where it's heading; the top view only as far as the screen reaches, not the
// range of the 3D view: zoomed in it would keep making anew the meshes of chunks far off the screen at each step of the zoom
auto const range{(top ? std::max(m_radius * heightmap::chunk_size, reach) : std::max({m_radius * heightmap::chunk_size, static_cast<double>(Global.BaseDrawRange) * std::max(1.0, static_cast<double>(Global.fDistanceFactor)), reach})) + heightmap::chunk_size * 0.5};
auto const range{(top ? std::max(m_radius * heightmap::chunk_size, reach) : std::max({m_radius * heightmap::chunk_size, scenery, reach})) + heightmap::chunk_size * 0.5};
auto const radius{std::min(top ? top_radius : 64, static_cast<int>(std::ceil(range / heightmap::chunk_size)))};
if (m_ringradius < radius)
{

View File

@@ -89,11 +89,7 @@ double path_radius(segment_data const &Path, double const T)
{
if (Path.points[segment_data::point::control1] == glm::dvec3{} && Path.points[segment_data::point::control2] == glm::dvec3{})
return 0.0;
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)};
auto const curvature{std::abs(bezier{Path}.plan_curvature(T))};
return curvature > 1e-7 ? 1.0 / curvature : 0.0;
}
@@ -108,25 +104,118 @@ bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool
auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]};
if (glm::distance(point, Joint) > kSamePoint)
continue;
auto const tangent{glm::normalize(path_tangent(path, atend))};
// measured on the piece at the end where the path has them: the path only approximates them, its curvature off at its ends,
// R 300.9 at the end of the diverging track of a switch of R 300
auto const &end{path.pieces.empty() ? path : atend ? path.pieces.back() : path.pieces.front()};
auto const tangent{glm::normalize(path_tangent(end, atend))};
auto const along{Leaving != atend};
Direction = along ? tangent : -tangent;
Radius = path_radius(path, atend ? 1.0 : 0.0);
Radius = path_radius(end, atend ? 1.0 : 0.0);
// the side it turns to as the chain runs, the path gone against its points turning the other way
Turn = 0;
if (Radius > 0.0)
{
bezier const curve{path};
auto const first{curve.first(atend ? 1.0 : 0.0)};
auto const second{curve.second(atend ? 1.0 : 0.0)};
Turn = (first.x * second.z - first.z * second.x > 0.0) == along ? 1 : -1;
}
Turn = (bezier{end}.plan_curvature(atend ? 1.0 : 0.0) > 0.0) == along ? 1 : -1;
return true;
}
}
return false;
}
// the element of a stored design a regular track lies along from end to end: its curvature as designed rather than as the path
// approximates it, an arc of its radius however short it is, or a transition. None where the track lies along no design, or across
// the joint of two of its elements
std::optional<alignment::element> designed_element(TTrack const &Track)
{
if (Track.eType != tt_Normal || Track.m_paths.empty())
return std::nullopt;
auto const found{alignment_store::on_track(Track)};
if (false == found.has_value())
return std::nullopt;
bezier const curve{Track.m_paths.front()};
for (auto const t : {0.1, 0.9})
if (auto const at{alignment_store::on_record(found->record, curve.point(t), 0.05)}; false == at.has_value() || at->element != found->element)
return std::nullopt;
auto const &elements{alignment_store::records()[static_cast<std::size_t>(found->record)].result.elements};
if (found->element < 0 || found->element >= static_cast<int>(elements.size()))
return std::nullopt;
return elements[static_cast<std::size_t>(found->element)];
}
// a stretch of a curve along the run of its paths: its length, its curvature at its start and at its end, unsigned
struct curve_piece
{
double length{0.0};
double from{0.0};
double to{0.0};
};
// the curvature of an element of a design at its start and at its end, unsigned
std::pair<double, double> element_curvatures(alignment::element const &Element)
{
auto const [start, end]{alignment::end_curvatures(Element)};
return {std::abs(start), std::abs(end)};
}
// the pieces of a curve along the run of its paths: of the elements of the stored design a path lies along, of the path itself where
// it lies along none, its curvature as at its middle where it barely changes along it. False where no path lies along a design
bool designed_pieces(std::vector<std::pair<TTrack *, bool>> const &Run, std::vector<curve_piece> &Pieces)
{
Pieces.clear();
auto designed{false};
for (auto const &[track, forward] : Run)
{
auto const &path{track->m_paths.front()};
auto const length{track->eType == tt_Switch ? bezier{path}.plan_length() : track->Length()};
auto const &begin{path.points[forward ? segment_data::point::start : segment_data::point::end]};
auto const &finish{path.points[forward ? segment_data::point::end : segment_data::point::start]};
std::optional<alignment_store::hit> first;
std::optional<alignment_store::hit> last;
if (auto const found{track->eType == tt_Normal ? alignment_store::on_track(*track) : std::nullopt})
{
first = alignment_store::on_record(found->record, begin, 0.05);
last = alignment_store::on_record(found->record, finish, 0.05);
}
if (false == first.has_value() || false == last.has_value() || first->record != last->record || std::abs(last->chainage - first->chainage) < 1e-3)
{
auto const curvature_at = [&](double const T) {
auto const radius{path_radius(path, forward ? T : 1.0 - T)};
return radius > 0.0 ? 1.0 / radius : 0.0;
};
auto const start{curvature_at(0.0)};
auto const end{curvature_at(1.0)};
if (std::abs(start - end) <= 0.1 * std::max(start, end))
Pieces.push_back({length, curvature_at(0.5), curvature_at(0.5)});
else
Pieces.push_back({length, start, end});
continue;
}
designed = true;
auto const &elements{alignment_store::records()[static_cast<std::size_t>(first->record)].result.elements};
auto const low{std::min(first->chainage, last->chainage)};
auto const high{std::max(first->chainage, last->chainage)};
std::vector<curve_piece> own;
for (auto const &element : elements)
{
auto const from{std::max(low, element.chainage)};
auto const to{std::min(high, element.chainage + element.length)};
if (to - from < 1e-6)
continue;
auto const [start, end]{element_curvatures(element)};
auto const at = [&](double const Chainage) { return element.length > 1e-9 ? start + (end - start) * (Chainage - element.chainage) / element.length : start; };
own.push_back({to - from, at(from), at(to)});
}
// the design running against the run
if (last->chainage < first->chainage)
{
std::reverse(own.begin(), own.end());
for (auto &piece : own)
std::swap(piece.from, piece.to);
}
Pieces.insert(Pieces.end(), own.begin(), own.end());
}
return designed;
}
}
editor_track::observer *editor_track::s_observer{nullptr};
@@ -628,21 +717,15 @@ std::vector<std::pair<TTrack *, editor_track::point_ref>> editor_track::connecte
return result;
}
namespace
glm::dvec2 editor_track::end_direction(segment_data const &Path, point_kind const Kind)
{
// direction of the path at the end, into the path
glm::dvec2 end_direction(segment_data const &Path, editor_track::point_kind const Kind)
{
bool const start{Kind == editor_track::point_kind::start};
bool const start{Kind == point_kind::start};
auto const &point{Path.points[start ? segment_data::point::start : segment_data::point::end]};
auto const &control{Path.points[start ? segment_data::point::control1 : segment_data::point::control2]};
auto const direction{plan_of(control != glm::dvec3{} ? control : Path.points[start ? segment_data::point::end : segment_data::point::start] - point)};
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0};
}
} // namespace
std::vector<editor_track::joint> editor_track::joints(TTrack const &Track)
{
std::vector<joint> result;
@@ -1625,7 +1708,14 @@ std::vector<editor_track::straight> editor_track::find_straights(double const Mi
bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve)
{
Curve = {};
auto const curved = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && false == is_straight(*Other, Tolerance); };
// a track too short to tell its bend from a straight is a part of the curve where it lies along an arc or a transition of a design
auto const designed_bend = [](TTrack const &Other) {
auto const element{designed_element(Other)};
return element.has_value() && element->kind != alignment::element_kind::straight;
};
auto const curved = [&](TTrack const *Other) {
return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && (false == is_straight(*Other, Tolerance) || designed_bend(*Other));
};
if (false == curved(&Track))
return false;
@@ -1814,6 +1904,48 @@ bool editor_track::analyse_curve(std::vector<std::pair<TTrack *, bool>> const &R
Curve.arcs = arcs;
}
}
// along the stored designs the curve is read from their elements, as it was laid, the paths along none by their own curvature: the
// curve of two lines laid one after another made of the arcs and the transitions of both, however short they are
if (std::vector<curve_piece> pieces; Curve.reversals == 0 && designed_pieces(run, pieces))
{
std::vector<curve::arc_part> designedarcs;
double in{0.0};
double between{0.0};
auto inside{false}; // a straight between two arcs: no curve of one vertex
auto straight{false};
for (auto const &piece : pieces)
{
auto const even{std::abs(piece.from - piece.to) <= 0.02 * std::max(piece.from, piece.to)};
if (even && piece.from < 1e-6)
{
straight = false == designedarcs.empty();
continue;
}
if (false == even)
{
(designedarcs.empty() ? in : between) += piece.length;
continue;
}
inside = inside || straight;
auto const radius{2.0 / (piece.from + piece.to)};
if (false == designedarcs.empty() && between < 1e-6 && std::abs(designedarcs.back().radius - radius) <= 0.02 * radius)
designedarcs.back().turn += piece.length / radius;
else
designedarcs.push_back({radius, piece.length / radius, designedarcs.empty() ? 0.0 : between});
between = 0.0;
}
if (false == designedarcs.empty() && false == inside)
{
Curve.laid = true;
Curve.transition_in = in;
Curve.transition_out = between;
Curve.radius = designedarcs.front().radius;
Curve.compound = designedarcs.size() > 1;
Curve.arcs.clear();
if (Curve.compound)
Curve.arcs = designedarcs;
}
}
if (Curve.reversals == 0)
{
if (auto const designed{alignment_store::vertex_of(Run)})
@@ -2853,6 +2985,8 @@ std::optional<editor_track::switch_template> editor_track::template_of(switch_de
entry.movable = Design.movable;
entry.heel = Design.heel;
entry.bladethrow = Design.bladethrow;
if (false == Design.parts.empty() && Design.parts.back().radius1 <= 0.0 && Design.parts.back().radius2 <= 0.0)
entry.frog_straight = Design.parts.back().length;
entry.local = {shape.paths[0], shape.paths[1]};
entry.local[1].radius = static_cast<float>(shape.radius);
if (shape.paths.size() >= 5)
@@ -3022,7 +3156,7 @@ std::vector<editor_track::switch_template> editor_track::standard_switch_templat
slip.rail = std::string{entry.name}.substr(0, std::string{entry.name}.find('-'));
slip.label = std::string{entry.type} + ' ' + entry.name + format(" a %.3f", entry.half) + (slip.type == "Rkp" ? " single slip" : " double slip") +
(entry.middle < 0.0 ? ", the blades outside of the diamond" : "") +
(slip.type == "Rkp" ? " (click at a crossing of two straights, on the side of the joined tracks)" : " (click at a crossing of two straights)");
(slip.type == "Rkp" ? " (click at a crossing of two straights, on the side of the joined tracks, or drag it out on a straight)" : " (click at a crossing of two straights, or drag it out on a straight)");
result.push_back(slip);
}
struct crossing
@@ -3060,7 +3194,7 @@ std::vector<editor_track::switch_template> editor_track::standard_switch_templat
plain.a = entry.half;
plain.ratio = ratio_of(entry.name);
plain.rail = std::string{entry.name}.substr(0, std::string{entry.name}.find('-'));
plain.label = std::string{"Skrz "} + entry.name + format(" a %.3f", entry.half) + " crossing (click at a crossing of two straights)";
plain.label = std::string{"Skrz "} + entry.name + format(" a %.3f", entry.half) + " crossing (click at a crossing of two straights, or drag it out on a straight)";
result.push_back(plain);
}
return result;

View File

@@ -106,6 +106,8 @@ class editor_track
static void commit_parameters(TTrack &Track);
static glm::dvec3 point_position(TTrack const &Track, point_ref const &Point);
// the direction of the path in the plan at its end, into the path
static glm::dvec2 end_direction(segment_data const &Path, point_kind const Kind);
static void move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position);
static void translate(TTrack &Track, glm::dvec3 const &Offset);
static void rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle);
@@ -342,6 +344,8 @@ class editor_track
double transition{0.0};
};
std::vector<arc_part> arcs;
// the arcs read from the elements of the stored designs the paths lie along, as they were laid, not approximated by the paths
bool laid{false};
// the parameters of the vertex in the alignment kept in the scenery, in the direction of the run, when the paths follow one
std::optional<alignment::vertex> designed;
};
@@ -451,6 +455,7 @@ class editor_track
bool own{false};
bool movable{false}; // the frog with a movable nose
double heel{0.0}; // where the moving blades end, 0: as long as the flangeway behind them needs
double frog_straight{0.0}; // the straight the diverging track ends with, through the frog; 0: its curve runs through the frog
double bladethrow{0.0}; // how far the tips of the blades move over, 0: the usual throw
int design{-1}; // in the list of the designs the template was built from
double middle{0.0}; // a double slip with the blades outside of the diamond: the radius in the middle of its slip curves, below 0 a straight there

View File

@@ -680,7 +680,9 @@ std::optional<editor_track::bend> editor_track::bend_at(TTrack const &Neighbour,
result.direction = glm::normalize(chord) * sign;
return result;
}
bezier const curve{path};
// on the piece at the end where the path has them: the path only approximates them, its curvature off at its ends, R 300.9 at
// the end of the diverging track of a switch of R 300
bezier const curve{path.pieces.empty() ? path : atend ? path.pieces.back() : path.pieces.front()};
auto const first{curve.first(atend ? 1.0 : 0.0)};
auto const second{curve.second(atend ? 1.0 : 0.0)};
auto const speed{std::hypot(first.x, first.z)};