/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "editor/editorSwitchStore.hpp" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorAlignmentStore.hpp" #include "world/Track.h" #include "simulation/simulation.h" #include "utilities/Logs.h" #include #include #include #include namespace switch_store { namespace { using geometry::plan_of; std::string const kMark{"//$r"}; double const kTolerance{0.02}; std::vector g_records; std::vector g_base; // as they were before the changes not yet in the undo history bool g_changed{false}; bool g_loaded{false}; // format of the lines, coordinates in the space of the file: // // parts ... shape
start dir // km number source std::string word(std::string const &Text) { if (Text.empty()) return "-"; auto result{Text}; std::replace(result.begin(), result.end(), ' ', '_'); return result; } glm::dvec2 direction_of(TTrack const &Track) { geometry::bezier const curve{Track.m_paths.front()}; auto const direction{plan_of(curve.first(0.0))}; return glm::length(direction) > 1e-12 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; } TTrack *usable(std::string const &Name) { if (Name.empty() || Name == "-") return nullptr; auto *track{simulation::Paths.find(Name)}; return track != nullptr && false == track->m_editorremoved && false == track->m_paths.empty() ? track : nullptr; } bool in_place(record const &Record, TTrack const &Track) { return geometry::plan_distance(Track.m_paths.front().points[segment_data::point::start], Record.start) <= kTolerance && glm::dot(direction_of(Track), Record.direction) > 0.9999; } glm::dvec4 measure(TTrack const &Track) { auto const &main{Track.m_paths.front()}; auto const start{plan_of(main.points[segment_data::point::start])}; auto const along{direction_of(Track)}; glm::dvec2 const across{-along.y, along.x}; auto const local = [&](glm::dvec3 const &Point) { auto const offset{plan_of(Point) - start}; return glm::dvec2{glm::dot(offset, along), glm::dot(offset, across)}; }; auto const mainend{local(main.points[segment_data::point::end])}; auto const branchend{Track.m_paths.size() > 1 ? local(Track.m_paths[1].points[segment_data::point::end]) : glm::dvec2{0.0}}; return {mainend.x, mainend.y, branchend.x, branchend.y}; } bool in_shape(record const &Record, TTrack const &Track) { if (false == Record.shaped) return in_place(Record, Track); auto const now{measure(Track)}; for (int i = 0; i < 4; ++i) if (std::abs(now[i] - Record.shape[i]) > kTolerance) return false; return true; } void mark(scene::layer_handle const Layer) { if (false == scene::Layers.valid(Layer)) return; auto const &context{scene::Layers.layer(Layer).context_insert()}; std::vector lines; for (auto const &entry : g_records) { if (entry.layer != Layer) continue; if (lines.empty()) lines.emplace_back("# switches placed in the track editor, the simulation skips these lines"); auto const start{context.to_local(entry.start)}; auto const ahead{context.to_local(entry.start + glm::dvec3{entry.direction.x, 0.0, entry.direction.y})}; auto direction{plan_of(ahead - start)}; direction = glm::length(direction) > 1e-12 ? glm::normalize(direction) : entry.direction; auto text{format("%s %s %s %s %.3f %.3f %c %.3f %.3f %.3f parts %zu", entry.kind.c_str(), word(entry.name).c_str(), word(entry.type).c_str(), word(entry.rail).c_str(), entry.radius, entry.ratio, entry.hand, entry.a, entry.b, entry.main_radius, entry.parts.size())}; for (auto const &part : entry.parts) text += ' ' + word(part); if (entry.shaped) text += format(" shape %.4f %.4f %.4f %.4f", entry.shape.x, entry.shape.y, entry.shape.z, entry.shape.w); text += format(" start %.4f %.4f %.4f dir %.9f %.9f km ", start.x, start.y, start.z, direction.x, direction.y); text += entry.km.has_value() ? format("%.3f", *entry.km / 1000.0) : std::string{"-"}; text += " number " + word(entry.number) + " source " + word(entry.source); lines.push_back(text); } scene::Layers.mark(Layer, kMark, std::move(lines)); } } // namespace void reset() { g_records.clear(); g_base.clear(); g_changed = false; g_loaded = false; } void load() { if (g_loaded || scene::Layers.empty()) return; g_loaded = true; g_records.clear(); for (auto const layer : scene::Layers.marked_layers(kMark)) { auto const &context{scene::Layers.layer(layer).context_insert()}; for (auto const &text : scene::Layers.marked(layer, kMark)) { std::istringstream line{text}; record entry; entry.layer = layer; std::string keyword, hand, km; std::size_t count{0}; line >> entry.kind; if (entry.kind != "switch" && entry.kind != "slip" && entry.kind != "crossing") continue; line >> entry.name >> entry.type >> entry.rail >> entry.radius >> entry.ratio >> hand >> entry.a >> entry.b >> entry.main_radius >> keyword >> count; if (line.fail() || keyword != "parts" || count > 64) continue; entry.hand = hand.empty() ? '-' : hand.front(); entry.parts.resize(count); for (auto &part : entry.parts) line >> part; glm::dvec3 start{0.0}, ahead{0.0}; glm::dvec2 direction{0.0, 1.0}; line >> keyword; if (keyword == "shape") { line >> entry.shape.x >> entry.shape.y >> entry.shape.z >> entry.shape.w >> keyword; entry.shaped = true; } line >> start.x >> start.y >> start.z >> keyword >> direction.x >> direction.y >> keyword >> km >> keyword >> entry.number >> keyword >> entry.source; if (line.fail()) continue; entry.start = context.to_world(start); ahead = context.to_world(start + glm::dvec3{direction.x, 0.0, direction.y}); entry.direction = plan_of(ahead - entry.start); entry.direction = glm::length(entry.direction) > 1e-12 ? glm::normalize(entry.direction) : glm::dvec2{0.0, 1.0}; if (km != "-") entry.km = std::stod(km) * 1000.0; g_records.push_back(std::move(entry)); } } g_base = g_records; g_changed = false; if (false == g_records.empty()) WriteLog("Editor: " + std::to_string(g_records.size()) + " switch record(s) read from the scenery", logtype::generic); } std::vector const &records() { load(); return g_records; } std::optional named(TTrack const &Track) { load(); auto const &name{Track.name()}; if (name.empty()) return std::nullopt; for (auto const &entry : g_records) if (std::find(entry.parts.begin(), entry.parts.end(), name) != entry.parts.end()) return entry; return std::nullopt; } std::optional of(TTrack const &Track) { auto const found{named(Track)}; if (false == found.has_value()) return std::nullopt; auto const *first{usable(found->parts.front())}; if (first == nullptr || false == in_shape(*found, *first)) return std::nullopt; return found; } std::vector follow() { load(); std::vector moved; std::set layers; for (auto &entry : g_records) { auto const *first{usable(entry.parts.front())}; if (first == nullptr || false == entry.shaped || false == in_shape(entry, *first) || in_place(entry, *first)) continue; entry.start = first->m_paths.front().points[segment_data::point::start]; entry.direction = direction_of(*first); entry.km = alignment_store::chainage_at(entry.start); moved.push_back(entry.name); layers.insert(entry.layer); } for (auto const layer : layers) mark(layer); g_changed = g_changed || false == moved.empty(); return moved; } double branch_radius(record const &Record) { if (Record.radius <= 0.0) return 0.0; auto const side{Record.hand == 'L' ? -1.0 : 1.0}; auto const curvature{side / Record.radius + (Record.main_radius != 0.0 ? 1.0 / Record.main_radius : 0.0)}; return std::abs(curvature) > 1e-9 ? 1.0 / std::abs(curvature) : 0.0; } void put(record Entry, scene::layer_handle const Layer) { load(); if (auto const *first{usable(Entry.parts.empty() ? std::string{} : Entry.parts.front())}) { Entry.shape = measure(*first); Entry.shaped = true; } auto const existing{std::find_if(g_records.begin(), g_records.end(), [&](record const &Other) { return Other.name == Entry.name && Other.kind == Entry.kind; })}; if (existing != g_records.end()) { Entry.layer = existing->layer; if (Entry.number == "-") Entry.number = existing->number; *existing = std::move(Entry); g_changed = true; mark(existing->layer); return; } Entry.layer = Layer; g_records.push_back(std::move(Entry)); g_changed = true; mark(g_records.back().layer); } void forget(std::string const &Name) { load(); std::set layers; for (auto const &entry : g_records) if (entry.name == Name) layers.insert(entry.layer); auto const count{g_records.size()}; g_records.erase(std::remove_if(g_records.begin(), g_records.end(), [&](record const &Entry) { return Entry.name == Name; }), g_records.end()); g_changed = g_changed || g_records.size() != count; for (auto const layer : layers) mark(layer); } void tidy() { load(); follow(); std::set layers; for (auto &entry : g_records) { if (entry.kind != "switch" || usable(entry.parts.front()) != nullptr) continue; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.empty() || track->name().empty() || false == in_place(entry, *track)) continue; entry.name = track->name(); entry.parts = {entry.name}; layers.insert(entry.layer); break; } } auto const gone = [](record const &Entry) { return std::any_of(Entry.parts.begin(), Entry.parts.end(), [](std::string const &Part) { return usable(Part) == nullptr; }); }; for (auto const &entry : g_records) if (gone(entry)) layers.insert(entry.layer); g_records.erase(std::remove_if(g_records.begin(), g_records.end(), gone), g_records.end()); for (auto const layer : layers) mark(layer); g_changed = g_changed || false == layers.empty(); } std::string label(record const &Record) { auto text{Record.type}; if (Record.rail != "-") text += ' ' + Record.rail + '-'; else text += ' '; if (Record.radius > 0.0) text += format("%.0f-", Record.radius); if (Record.ratio > 0.0) text += format(std::abs(Record.ratio - std::round(Record.ratio)) < 1e-3 ? "1:%.0f" : "1:%.1f", Record.ratio); if (Record.hand == 'L' || Record.hand == 'R') text += ' ' + std::string(1, Record.hand == 'L' ? 'L' : 'P'); if (Record.main_radius != 0.0) text += format(", Rg %.0f", std::abs(Record.main_radius)); return text; } std::optional> take_changes() { if (false == g_changed) return std::nullopt; auto before{std::move(g_base)}; g_base = g_records; g_changed = false; return before; } void settle() { g_base = g_records; g_changed = false; } void restore(std::vector Records) { std::set layers; for (auto const *list : {&g_records, &Records}) for (auto const &entry : *list) layers.insert(entry.layer); g_records = std::move(Records); for (auto const layer : layers) mark(layer); settle(); } void place(record &Entry, TTrack const &First) { auto const &main{First.m_paths.front()}; geometry::bezier const curve{main}; Entry.start = main.points[segment_data::point::start]; auto const direction{geometry::plan_of(curve.first(0.0))}; Entry.direction = glm::length(direction) > 1e-12 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; Entry.km = alignment_store::chainage_at(Entry.start); } void classify(record &Entry, TTrack const &Switch) { geometry::bezier const main{Switch.m_paths[0]}; auto const &diverging{Switch.m_paths[1]}; Entry.hand = geometry::cross(Entry.direction, geometry::plan_of(diverging.points[segment_data::point::end] - Entry.start)) > 0.0 ? 'R' : 'L'; auto const first{geometry::plan_of(main.first(0.0))}; auto const last{geometry::plan_of(main.first(1.0))}; auto const turn{glm::length(first) > 1e-12 && glm::length(last) > 1e-12 ? geometry::signed_angle(glm::normalize(first), glm::normalize(last)) : 0.0}; Entry.main_radius = std::abs(turn) > 1e-6 ? main.plan_length() / turn : 0.0; if (std::abs(Entry.main_radius) > 20000.0) Entry.main_radius = 0.0; if (Entry.type == "Rz" || Entry.type == "Rlj" || Entry.type == "Rld") Entry.type = Entry.main_radius == 0.0 ? "Rz" : (Entry.main_radius > 0.0) == (Entry.hand == 'R') ? "Rlj" : "Rld"; } } // namespace switch_store