17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 11:34:53 +02:00
Files
maszyna/application/editormodenmt.cpp
maj00r 5a00d88be9 feat(editor): a scenery begun from nothing is asked for its terrain as the first track is laid
With the lay tool on, a scenery with no tracks nor terrain chunks gets the offer of the terrain model and the aerial photos
around the camera; without a place yet it asks for the PUWG 1992 coordinates of this point, as geoportal.gov.pl gives them,
and keeps them as the place of its orthophoto. The offer can be turned off (editor setting terrain_offer). Self-test
nmtoffer.
2026-10-18 13:02:01 +02:00

418 lines
15 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
// the heightmap terrain made from the digital terrain model of GUGiK: the scenery placed as its orthophoto is, the heights
// of the model moved up or down to meet the tracks it has already, the chunks around the camera or along the tracks made as
// the model comes in, and the formation of each track with its slopes kept as a modifier of the terrain
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorSettings.hpp"
#include "utilities/Globals.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "utilities/translation.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <cmath>
#include <set>
namespace
{
// how many places of the tracks the offset of the model is worked out from at most, each needing its square of the model
std::size_t constexpr fit_places{48};
bool rail(TTrack const &Track)
{
return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && false == Track.m_paths.empty();
}
// a bridge or a tunnel keeps the model under it as it is
bool shapes_ground(TTrack const &Track)
{
return Track.eEnvironment != e_bridge && Track.eEnvironment != e_tunnel;
}
} // namespace
std::vector<editor_mode::nmt_place> editor_mode::nmt_track_places(double const Step) const
{
std::vector<nmt_place> result;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track))
continue;
auto const ballast{profile_ballast(*track)};
for (std::size_t path = 0; path < track->m_paths.size(); ++path)
{
geometry::bezier const curve{track->m_paths[path]};
auto const &points{track->m_paths[path].points};
auto const chord{glm::distance(points[segment_data::point::start], points[segment_data::point::end])};
auto const count{std::max(1, static_cast<int>(std::ceil(chord * 1.1 / Step)))};
for (int i = 0; i <= count; ++i)
{
auto const at{curve.point(static_cast<double>(i) / count)};
result.push_back({at, at.y - ballast.first, ballast.second, track, static_cast<int>(path), shapes_ground(*track)});
}
}
}
return result;
}
void editor_mode::nmt_locate()
{
auto const &config{m_orthophoto.settings()};
m_nmt.place(config.north, config.east);
}
void editor_mode::nmt_fit()
{
nmt_locate();
auto &state{m_nmt_state};
state.fit.clear();
auto const places{nmt_track_places(25.0)};
std::vector<nmt_place> shaped;
std::copy_if(places.begin(), places.end(), std::back_inserter(shaped), [](nmt_place const &Place) { return Place.shapes; });
if (shaped.empty())
{
state.offset = 0.0;
state.fitted = true;
state.status = STR("No tracks to fit the terrain model to: its heights are taken as they are");
return;
}
// spread over the whole scenery, each place asking for its square of the model
auto const every{std::max<std::size_t>(1, shaped.size() / fit_places)};
for (std::size_t i = 0; i < shaped.size(); i += every)
state.fit.push_back(shaped[i]);
state.fitting = true;
state.status = STR("Fetching the terrain model under the tracks...");
}
void editor_mode::nmt_finish_fit()
{
auto &state{m_nmt_state};
std::vector<double> differences;
for (auto const &place : state.fit)
if (double height; m_nmt.height(place.position.x, place.position.z, height))
differences.push_back(place.formation - height);
state.fitting = false;
if (differences.empty())
{
state.status = STR("The terrain model has no data under the tracks: is the scenery placed where it is?");
return;
}
std::sort(differences.begin(), differences.end());
state.offset = differences[differences.size() / 2];
std::vector<double> deviations;
for (auto const difference : differences)
deviations.push_back(std::abs(difference - state.offset));
std::sort(deviations.begin(), deviations.end());
state.spread = deviations[deviations.size() / 2];
state.fitted = true;
state.samples = differences.size();
state.status = format(STR_C("The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)"), state.offset, state.samples, state.spread);
if (state.spread > 10.0)
state.status += STR(". That's far too much: the place of the scenery is likely wrong, check the orthophoto");
WriteLog("Editor: NMT - " + state.status, logtype::generic);
}
std::vector<heightmap::chunk_key> editor_mode::nmt_keys_around() const
{
std::vector<heightmap::chunk_key> result;
auto const centre{heightmap::chunk_at(Camera.Pos.x, Camera.Pos.z)};
auto const first{-(m_terrain_add_count - 1) / 2};
for (int dz = 0; dz < m_terrain_add_count; ++dz)
for (int dx = 0; dx < m_terrain_add_count; ++dx)
result.push_back({centre.first + first + dx, centre.second + first + dz});
return result;
}
std::vector<heightmap::chunk_key> editor_mode::nmt_keys_along_tracks() const
{
std::set<heightmap::chunk_key> keys;
auto const reach{static_cast<int>(std::ceil(m_nmt_state.band / heightmap::chunk_size))};
for (auto const &place : nmt_track_places(heightmap::chunk_size * 0.5))
{
auto const at{heightmap::chunk_at(place.position.x, place.position.z)};
for (int dz = -reach; dz <= reach; ++dz)
for (int dx = -reach; dx <= reach; ++dx)
{
heightmap::chunk_key const key{at.first + dx, at.second + dz};
auto const low{heightmap::chunk_corner(key)};
// the chunk reaches within the band of the track
glm::dvec2 const nearest{std::clamp(place.position.x, low.x, low.x + heightmap::chunk_size), std::clamp(place.position.z, low.y, low.y + heightmap::chunk_size)};
if (glm::distance(nearest, glm::dvec2{place.position.x, place.position.z}) <= m_nmt_state.band)
keys.insert(key);
}
}
return {keys.begin(), keys.end()};
}
void editor_mode::nmt_make(std::vector<heightmap::chunk_key> const &Keys)
{
auto &state{m_nmt_state};
if (false == ensure_terrain())
{
state.status = STR("The heightmap terrain can't be opened");
return;
}
nmt_locate();
std::size_t queued{0};
for (auto const &key : Keys)
if (false == m_streamer.exists(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end())
{
state.pending.push_back(key);
++queued;
}
state.made = 0;
// the terrain comes with its photos
if (queued > 0 && false == m_orthophoto.enabled())
m_orthophoto.enabled(true);
state.status = queued > 0 ? format(STR_C("%zu chunks to make from the terrain model"), queued) : STR("There's terrain there already");
}
bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key)
{
auto const low{heightmap::chunk_corner(Key)};
auto const high{low + glm::dvec2{heightmap::chunk_size}};
if (false == m_nmt.request(low - glm::dvec2{2.0}, high + glm::dvec2{2.0}))
return false;
auto const cells{heightmap::cells_for(m_terrain_spacing)};
auto const side{static_cast<std::size_t>(cells) + 1};
auto const step{heightmap::chunk_size / cells};
std::vector<double> heights(side * side, m_terrain_baseheight);
std::vector<char> found(side * side, 0);
bool any{false};
for (std::size_t iz = 0; iz < side; ++iz)
for (std::size_t ix = 0; ix < side; ++ix)
{
auto const index{iz * side + ix};
if (double height; m_nmt.height(low.x + ix * step, low.y + iz * step, height))
{
heights[index] = height + m_nmt_state.offset;
found[index] = 1;
any = true;
}
}
// where the model has no data the chunk goes on level with its nearest point
if (any)
fill_from_nearest(heights, found, side);
m_streamer.add_chunk(Key.first, Key.second, m_terrain_spacing, [&](double const X, double const Z) {
auto const ix{static_cast<std::size_t>(std::clamp<long>(std::lround((X - low.x) / step), 0, cells))};
auto const iz{static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / step), 0, cells))};
return static_cast<float>(heights[iz * side + ix]);
});
return true;
}
void editor_mode::nmt_shape_tracks()
{
auto &state{m_nmt_state};
if (false == ensure_terrain())
return;
// one modifier per path: the formation under its axis and the slopes from its edges to the ground. laid over each other the
// modifiers keep the highest of the slopes going down and the lowest of those going up, so tracks side by side share one
// formation instead of a saw of their own ones
std::map<std::pair<TTrack *, int>, heightmap::modifier> modifiers;
double constexpr step{5.0};
for (auto const &place : nmt_track_places(step))
{
if (false == place.shapes)
continue;
auto &modifier{modifiers[{place.track, place.path}]};
if (modifier.name.empty())
{
modifier.name = "nmt " + (place.track->name().empty() ? std::string{"track"} : place.track->name()) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{});
modifier.slope = state.slope;
modifier.reach = state.reach;
}
modifier.points.push_back({{place.position.x, place.position.z}, place.formation, place.half_width});
}
for (auto &[path, modifier] : modifiers)
if (modifier.points.size() >= 2)
m_streamer.modifier(std::move(modifier));
forget_ground();
state.shaped = modifiers.size();
}
void editor_mode::nmt_update()
{
auto &state{m_nmt_state};
nmt_offer();
if (state.fitting)
{
bool all{true};
for (auto const &place : state.fit)
all = m_nmt.request(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all;
if (all)
nmt_finish_fit();
return;
}
if (state.pending.empty())
return;
// a few chunks a frame, those whose model is in
int made{0};
for (auto it{state.pending.begin()}; it != state.pending.end() && made < 4;)
{
if (m_streamer.exists(*it))
{
it = state.pending.erase(it);
continue;
}
if (add_chunk_from_nmt(*it))
{
it = state.pending.erase(it);
++made;
++state.made;
continue;
}
++it;
}
if (made > 0)
{
forget_ground();
state.status = format(STR_C("Chunks made from the terrain model: %zu, waiting for it: %zu"), state.made, state.pending.size());
if (state.pending.empty())
{
if (state.shape)
{
nmt_shape_tracks();
state.status += format(STR_C("; the formation of %zu paths shaped"), state.shaped);
}
// the photos laid onto the new ground
m_orthophoto.refit();
}
}
if (auto const error{m_nmt.error()}; false == error.empty())
state.error = error;
}
void editor_mode::render_nmt_ui()
{
auto &state{m_nmt_state};
nmt_locate();
if (false == m_nmt.placed())
{
ImGui::TextWrapped("%s", STR_C("The scenery isn't placed anywhere: give its place in the settings of the orthophoto first"));
if (ImGui::Button(STR_C("Orthophoto settings...")))
m_orthophoto_window = true;
return;
}
auto const &config{m_orthophoto.settings()};
ImGui::TextDisabled(STR_C("Place: N %.0f, E %.0f (PUWG 1992), as the orthophoto"), config.north, config.east);
if (ImGui::Button(STR_C("Fit to the tracks")))
nmt_fit();
item_tooltip("Compares the formation of the tracks with the terrain model and moves the model up or down to meet them, as a scenery built at height 0 needs");
ImGui::SameLine();
ImGui::SetNextItemWidth(110.0f);
ImGui::InputDouble(STR_C("Height offset (m)"), &state.offset, 0.0, 0.0, "%.2f");
ImGui::SetNextItemWidth(110.0f);
ImGui::InputDouble(STR_C("Band along the tracks (m)"), &state.band, 0.0, 0.0, "%.0f");
state.band = std::clamp(state.band, 50.0, 3000.0);
ImGui::Checkbox(STR_C("Formation and slopes along the tracks"), &state.shape);
item_tooltip("Each track gets its formation with slopes 1:1.5 down or up to the terrain, as a modifier of the terrain; bridges and tunnels keep the model under them");
ImGui::BeginDisabled(state.fitting);
if (ImGui::Button(STR_C("Along the tracks")))
nmt_make(nmt_keys_along_tracks());
ImGui::SameLine();
if (ImGui::Button(STR_C("Around the camera")))
nmt_make(nmt_keys_around());
ImGui::EndDisabled();
if (false == state.pending.empty() || m_nmt.waiting() > 0)
ImGui::TextDisabled(STR_C("Squares of the model being fetched: %zu"), m_nmt.waiting());
if (false == state.status.empty())
ImGui::TextWrapped("%s", state.status.c_str());
if (false == state.error.empty())
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", state.error.c_str());
ImGui::TextDisabled("%s", STR_C("Terrain model: GUGiK, geoportal.gov.pl"));
}
void editor_mode::nmt_offer()
{
auto &state{m_nmt_state};
if (state.offered || false == m_lay.active || false == EditorSettings.terrain_offer())
return;
state.offered = true;
if (m_streamer.resident() > 0 || std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); }))
return;
nmt_locate();
state.north = m_orthophoto.settings().north;
state.east = m_orthophoto.settings().east;
state.prompt = true;
}
void editor_mode::nmt_accept_offer()
{
auto &state{m_nmt_state};
if (false == m_nmt.placed())
{
auto config{m_orthophoto.settings()};
config.north = state.north;
config.east = state.east;
m_orthophoto.settings(config);
m_orthophoto_origin_edit = {config.north, config.east};
save_orthophoto_settings();
}
// no tracks to fit to: the heights of the model as they are
state.offset = 0.0;
nmt_make(nmt_keys_around());
state.prompt = false;
}
void editor_mode::render_nmt_prompt()
{
auto &state{m_nmt_state};
if (false == state.prompt)
return;
char const *const title{STR_C("Terrain for the new scenery###nmtoffer")};
if (false == ImGui::IsPopupOpen(title))
ImGui::OpenPopup(title);
ImGui::SetNextWindowSize(ImVec2(520.0f * Global.ui_scale, 0.0f));
if (false == ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoResize))
return;
ImGui::TextWrapped("%s", STR_C("The scenery has no terrain yet. Load the terrain model and the aerial photos around here? The tracks laid then go along the real ground."));
bool const placed{m_nmt.placed()};
if (false == placed)
{
ImGui::TextWrapped("%s", STR_C("Where is it? PUWG 1992 of this point, X to the north and Y to the east, as geoportal.gov.pl gives them:"));
ImGui::SetNextItemWidth(150.0f);
ImGui::InputDouble("X##nmtnorth", &state.north, 0.0, 0.0, "%.0f");
ImGui::SameLine();
ImGui::SetNextItemWidth(150.0f);
ImGui::InputDouble("Y##nmteast", &state.east, 0.0, 0.0, "%.0f");
}
auto const known{placed || (state.north > 100000.0 && state.east > 100000.0)};
ImGui::BeginDisabled(false == known);
if (ImGui::Button(STR_C("Load the terrain")))
{
nmt_accept_offer();
ImGui::CloseCurrentPopup();
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button(STR_C("Without terrain")))
{
state.prompt = false;
ImGui::CloseCurrentPopup();
}
bool ask{EditorSettings.terrain_offer()};
if (ImGui::Checkbox(STR_C("Ask with every new scenery"), &ask))
{
EditorSettings.terrain_offer(ask);
EditorSettings.save();
}
ImGui::EndPopup();
}