From 5af699fba424e3e33e7785139db2b0c751655ad2 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 18 Oct 2026 12:56:25 +0200 Subject: [PATCH] feat(editor): heightmap terrain from the terrain model of GUGiK, fitted to the tracks of the scenery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - editorNmt: squares of 500 m of the NMT at 1 m from the WCS service of geoportal.gov.pl, as GeoTIFF, fetched in the background, kept in editor/cache/nmt and sampled in the coordinates of the scenery, placed as its orthophoto is - the terrain panel's Terrain model section: Fit to the tracks moves the model up or down by the median of the differences to the formation of the tracks (Biała Podlaska, built at 0: -145.73 m, 0.28 m around it); Along the tracks and Around the camera make the chunks as the model comes in; each path gets its formation and slopes 1:1.5 as a modifier of the terrain, laid over each other into one corridor, bridges and tunnels keep the model; the orthophoto is turned on and laid onto the new ground - the http client of the orthophoto in editorHttp, shared; fill_from_nearest in editormodeshared.h - after the generator of Hirek (MaszynaTerrainGenerator): the WCS requests, the cache of squares, the corridor by envelopes - self-tests nmtfit, nmtmake, nmtwait --- CMakeLists.txt | 3 + application/editormode.cpp | 34 +- application/editormode.h | 43 +++ application/editormodenmt.cpp | 337 +++++++++++++++++++ application/editormodeselftestterrain.cpp | 30 ++ application/editormodeshared.h | 33 ++ editor/editorHttp.cpp | 224 +++++++++++++ editor/editorHttp.hpp | 44 +++ editor/editorNmt.cpp | 373 ++++++++++++++++++++++ editor/editorNmt.hpp | 87 +++++ editor/editorOrthophoto.cpp | 208 +----------- lang/editor_pl.po | 63 ++++ 12 files changed, 1245 insertions(+), 234 deletions(-) create mode 100644 application/editormodenmt.cpp create mode 100644 editor/editorHttp.cpp create mode 100644 editor/editorHttp.hpp create mode 100644 editor/editorNmt.cpp create mode 100644 editor/editorNmt.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a382990..83bdf621 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,6 +200,8 @@ set(SOURCES "editor/editorInfra.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" +"editor/editorHttp.cpp" +"editor/editorNmt.cpp" "editor/editorPreviews.cpp" "editor/editorIcons.cpp" "editor/editorClips.cpp" @@ -213,6 +215,7 @@ set(SOURCES "application/editormoderecords.cpp" "application/editormodeswitchdrive.cpp" "application/editormodeswitchtool.cpp" +"application/editormodenmt.cpp" "application/editormodeswitchslide.cpp" "application/editormodeswitchdesign.cpp" "application/editormodetrackbed.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 3eb0a454..16467e6f 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include "application/editormode.h" +#include "application/editormodeshared.h" #include "scene/scenenodegroups.h" #include "application/editoruilayer.h" #ifndef _WIN32 @@ -1396,6 +1397,7 @@ bool editor_mode::update() // --- geoportal orthophoto: streamed around the camera, drawn beneath the other overlays --- m_orthophoto.update(Camera.Pos); + nmt_update(); draw_orthophoto(); // --- ImGuizmo: in-viewport transform gizmo for the selected node --- @@ -1561,35 +1563,6 @@ namespace return changed; } - // grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid) - template void fill_from_nearest(std::vector &Heights, std::vector &Found, std::size_t const Side) - { - std::deque queue; - for (std::size_t index = 0; index < Found.size(); ++index) - if (Found[index] != 0) - queue.push_back(index); - while (false == queue.empty()) - { - auto const index = queue.front(); - queue.pop_front(); - auto const ix = index % Side, iz = index / Side; - auto const visit = [&](std::size_t const Next) { - if (Found[Next] != 0) - return; - Found[Next] = 1; - Heights[Next] = Heights[index]; - queue.push_back(Next); - }; - if (ix > 0) - visit(index - 1); - if (ix + 1 < Side) - visit(index + 1); - if (iz > 0) - visit(index - Side); - if (iz + 1 < Side) - visit(index + Side); - } - } } // namespace void editor_mode::render_terrain_ui() @@ -1665,6 +1638,9 @@ void editor_mode::render_terrain_ui() } } + if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"), ImGuiTreeNodeFlags_DefaultOpen)) + render_nmt_ui(); + if (ImGui::CollapsingHeader(STR_C("Chunk resolution"), m_terrain_tool == terrain_tool::spacing ? ImGuiTreeNodeFlags_DefaultOpen : 0)) { // chunks removed since are no longer selected diff --git a/application/editormode.h b/application/editormode.h index 475a5756..57b9db65 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTerrain.hpp" #include "editor/editorTerrainStreamer.hpp" #include "editor/editorOrthophoto.hpp" +#include "editor/editorNmt.hpp" #include "editor/editorTrack.hpp" #include "editor/editorConnections.hpp" #include "editor/editorAlignment.hpp" @@ -319,6 +320,48 @@ class editor_mode : public application_mode, private editor_track::observer // adds a chunk of the terrain shaped as the ground under it; where there's no ground, carried on level with the nearest point // of it, or at the base height. returns: false if there's a chunk already bool add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer = 0); + + // the terrain from the digital terrain model of GUGiK (editormodenmt.cpp) + struct nmt_place + { + glm::dvec3 position{0.0}; // on the axis of the path + double formation{0.0}; // the height of the formation under it + double half_width{3.0}; // from the axis to the foot of the ballast + TTrack *track{nullptr}; + int path{0}; + bool shapes{true}; // not a bridge nor a tunnel + }; + struct nmt_state + { + double offset{0.0}; // added to the heights of the model, so they meet the tracks of the scenery + bool fitted{false}; + bool fitting{false}; // waiting for the model under the places of the tracks the offset is worked out from + std::vector fit; + double spread{0.0}; // how much the tracks differ from the model about the offset, m + std::size_t samples{0}; + double band{300.0}; // m from the tracks the chunks made along them reach + bool shape{true}; // the formation and the slopes of the tracks as modifiers of the terrain + double slope{1.5}; + double reach{60.0}; + std::vector pending; // chunks to make once their model is in + std::size_t made{0}; + std::size_t shaped{0}; + std::string status; + std::string error; + }; + editor_nmt m_nmt; + nmt_state m_nmt_state; + std::vector nmt_track_places(double const Step) const; + void nmt_locate(); + void nmt_fit(); + void nmt_finish_fit(); + std::vector nmt_keys_around() const; + std::vector nmt_keys_along_tracks() const; + void nmt_make(std::vector const &Keys); + bool add_chunk_from_nmt(heightmap::chunk_key const &Key); + void nmt_shape_tracks(); + void nmt_update(); + void render_nmt_ui(); // the tool of the terrain the left mouse button works enum class terrain_tool { diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp new file mode 100644 index 00000000..b5ef539f --- /dev/null +++ b/application/editormodenmt.cpp @@ -0,0 +1,337 @@ +/* +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 "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include +#include +#include + +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_track_places(double const Step) const +{ + std::vector 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(std::ceil(chord * 1.1 / Step)))}; + for (int i = 0; i <= count; ++i) + { + auto const at{curve.point(static_cast(i) / count)}; + result.push_back({at, at.y - ballast.first, ballast.second, track, static_cast(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 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(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 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 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 editor_mode::nmt_keys_around() const +{ + std::vector 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 editor_mode::nmt_keys_along_tracks() const +{ + std::set keys; + auto const reach{static_cast(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 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(cells) + 1}; + auto const step{heightmap::chunk_size / cells}; + std::vector heights(side * side, m_terrain_baseheight); + std::vector 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::clamp(std::lround((X - low.x) / step), 0, cells))}; + auto const iz{static_cast(std::clamp(std::lround((Z - low.y) / step), 0, cells))}; + return static_cast(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, 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}; + 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")); +} diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp index 9dd22cf2..c95bfb18 100644 --- a/application/editormodeselftestterrain.cpp +++ b/application/editormodeselftestterrain.cpp @@ -63,6 +63,36 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : "")); return selftest_result::handled; } + if (command == "nmtfit") + { + // the heights of the terrain model fitted to the tracks, as the button of the terrain panel does + nmt_fit(); + WriteLog("SELFTEST nmtfit: " + m_nmt_state.status); + return selftest_result::handled; + } + if (command == "nmtmake") + { + // chunks from the terrain model along the tracks (with the band in metres) or around the camera + std::string where; + double band{0.0}; + words >> where >> band; + if (band > 0.0) + m_nmt_state.band = band; + nmt_make(where == "around" ? nmt_keys_around() : nmt_keys_along_tracks()); + WriteLog("SELFTEST nmtmake " + where + ": " + m_nmt_state.status); + return selftest_result::handled; + } + if (command == "nmtwait") + { + // till the model is in and the chunks waiting for it are made + if (m_nmt_state.fitting || false == m_nmt_state.pending.empty()) + { + --m_selftest.next; + return selftest_result::pause; + } + WriteLog("SELFTEST nmtwait: " + m_nmt_state.status + (m_nmt_state.error.empty() ? std::string{} : " (" + m_nmt_state.error + ")")); + return selftest_result::handled; + } if (command == "terrainmode") { m_terrain_open = true; diff --git a/application/editormodeshared.h b/application/editormodeshared.h index 8d482e74..494a7f39 100644 --- a/application/editormodeshared.h +++ b/application/editormodeshared.h @@ -9,6 +9,9 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include + #include "imgui/imgui.h" #include "utilities/translation.h" @@ -19,3 +22,33 @@ inline void item_tooltip(char const *Text) if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C(Text)); } + +// grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid) +template inline void fill_from_nearest(std::vector &Heights, std::vector &Found, std::size_t const Side) +{ + std::deque queue; + for (std::size_t index = 0; index < Found.size(); ++index) + if (Found[index] != 0) + queue.push_back(index); + while (false == queue.empty()) + { + auto const index = queue.front(); + queue.pop_front(); + auto const ix = index % Side, iz = index / Side; + auto const visit = [&](std::size_t const Next) { + if (Found[Next] != 0) + return; + Found[Next] = 1; + Heights[Next] = Heights[index]; + queue.push_back(Next); + }; + if (ix > 0) + visit(index - 1); + if (ix + 1 < Side) + visit(index + 1); + if (iz > 0) + visit(index - Side); + if (iz + 1 < Side) + visit(index + Side); + } +} diff --git a/editor/editorHttp.cpp b/editor/editorHttp.cpp new file mode 100644 index 00000000..3b612675 --- /dev/null +++ b/editor/editorHttp.cpp @@ -0,0 +1,224 @@ +/* +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/editorHttp.hpp" + +#if defined(_WIN32) +#include +#elif defined(EU07_WITH_CURL) +#include +#endif + +#include + +namespace editor_http +{ + +struct client::platform +{ +#if defined(_WIN32) + HINTERNET session{nullptr}; +#elif defined(EU07_WITH_CURL) + CURL *curl{nullptr}; +#endif +}; + + +#if defined(_WIN32) + +client::client() : + m_platform(std::make_unique()) +{ + wchar_t const *agent = L"MaSzyna-editor"; +#ifdef WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY + m_platform->session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); +#endif + if (m_platform->session == nullptr) // automatic proxy needs windows 8.1+ + m_platform->session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (m_platform->session != nullptr) + ::WinHttpSetTimeouts(m_platform->session, 10000, 10000, 15000, 20000); +} + +client::~client() +{ + if (m_platform->session != nullptr) + ::WinHttpCloseHandle(m_platform->session); +} + +bool client::get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop) +{ + Out.clear(); + if (m_platform->session == nullptr) + { + Error = "WinHttpOpen failed (" + std::to_string(::GetLastError()) + ")"; + return false; + } + + std::wstring const url(Url.begin(), Url.end()); // the requests are plain ascii + URL_COMPONENTS parts{}; + parts.dwStructSize = sizeof(parts); + parts.dwSchemeLength = static_cast(-1); + parts.dwHostNameLength = static_cast(-1); + parts.dwUrlPathLength = static_cast(-1); + parts.dwExtraInfoLength = static_cast(-1); + if (!::WinHttpCrackUrl(url.c_str(), 0, 0, &parts)) + { + Error = "invalid url"; + return false; + } + std::wstring const host(parts.lpszHostName, parts.dwHostNameLength); + std::wstring const path = std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) + std::wstring(parts.lpszExtraInfo, parts.dwExtraInfoLength); + + HINTERNET connection = ::WinHttpConnect(m_platform->session, host.c_str(), parts.nPort, 0); + HINTERNET request = connection ? ::WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, + parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0) + : nullptr; + auto const close = [&]() { + if (request != nullptr) + ::WinHttpCloseHandle(request); + if (connection != nullptr) + ::WinHttpCloseHandle(connection); + }; + + if (request == nullptr || !::WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || !::WinHttpReceiveResponse(request, nullptr)) + { + Error = "WinHTTP error " + std::to_string(::GetLastError()); + close(); + return false; + } + + DWORD status{0}; + DWORD statussize{sizeof(status)}; + ::WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &statussize, WINHTTP_NO_HEADER_INDEX); + + for (;;) + { + if (Stop()) + { + Error = "cancelled"; + close(); + return false; + } + DWORD available{0}; + if (!::WinHttpQueryDataAvailable(request, &available)) + { + Error = "WinHTTP error " + std::to_string(::GetLastError()); + close(); + return false; + } + if (available == 0) + break; + std::size_t const offset = Out.size(); + Out.resize(offset + available); + DWORD received{0}; + if (!::WinHttpReadData(request, Out.data() + offset, available, &received)) + { + Error = "WinHTTP error " + std::to_string(::GetLastError()); + close(); + return false; + } + Out.resize(offset + received); + } + close(); + + if (status != 200) + { + Error = "HTTP " + std::to_string(status); + return false; + } + return true; +} + +#elif defined(EU07_WITH_CURL) + +client::client() : + m_platform(std::make_unique()) +{ + static std::once_flag initialized; + std::call_once(initialized, []() { ::curl_global_init(CURL_GLOBAL_DEFAULT); }); + m_platform->curl = ::curl_easy_init(); +} + +client::~client() +{ + if (m_platform->curl != nullptr) + ::curl_easy_cleanup(m_platform->curl); +} + +bool client::get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop) +{ + Out.clear(); + if (m_platform->curl == nullptr) + { + Error = "curl_easy_init failed"; + return false; + } + + auto const write = +[](char *Data, std::size_t Size, std::size_t Count, void *User) -> std::size_t { + auto *out = static_cast *>(User); + out->insert(out->end(), Data, Data + Size * Count); + return Size * Count; + }; + auto const progress = +[](void *User, curl_off_t, curl_off_t, curl_off_t, curl_off_t) -> int { return (*static_cast(User))() ? 1 : 0; }; + + ::curl_easy_reset(m_platform->curl); + ::curl_easy_setopt(m_platform->curl, CURLOPT_URL, Url.c_str()); + ::curl_easy_setopt(m_platform->curl, CURLOPT_USERAGENT, "MaSzyna-editor"); + ::curl_easy_setopt(m_platform->curl, CURLOPT_FOLLOWLOCATION, 1L); + ::curl_easy_setopt(m_platform->curl, CURLOPT_NOSIGNAL, 1L); + ::curl_easy_setopt(m_platform->curl, CURLOPT_CONNECTTIMEOUT, 10L); + ::curl_easy_setopt(m_platform->curl, CURLOPT_LOW_SPEED_LIMIT, 1L); + ::curl_easy_setopt(m_platform->curl, CURLOPT_LOW_SPEED_TIME, 20L); + ::curl_easy_setopt(m_platform->curl, CURLOPT_WRITEFUNCTION, write); + ::curl_easy_setopt(m_platform->curl, CURLOPT_WRITEDATA, &Out); + ::curl_easy_setopt(m_platform->curl, CURLOPT_NOPROGRESS, 0L); + ::curl_easy_setopt(m_platform->curl, CURLOPT_XFERINFOFUNCTION, progress); + ::curl_easy_setopt(m_platform->curl, CURLOPT_XFERINFODATA, const_cast(&Stop)); + + CURLcode const code = ::curl_easy_perform(m_platform->curl); + if (code != CURLE_OK) + { + Error = ::curl_easy_strerror(code); + return false; + } + long status{0}; + ::curl_easy_getinfo(m_platform->curl, CURLINFO_RESPONSE_CODE, &status); + if (status != 200) + { + Error = "HTTP " + std::to_string(status); + return false; + } + return true; +} + +#else + +client::client() = default; +client::~client() = default; + +bool client::get(std::string const &, std::vector &Out, std::string &Error, stop_check const &) +{ + Out.clear(); + Error = "no http client in this build"; + return false; +} + +#endif + +bool available() +{ +#if defined(_WIN32) || defined(EU07_WITH_CURL) + return true; +#else + return false; +#endif +} + +} // namespace editor_http diff --git a/editor/editorHttp.hpp b/editor/editorHttp.hpp new file mode 100644 index 00000000..0758cd53 --- /dev/null +++ b/editor/editorHttp.hpp @@ -0,0 +1,44 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +// a minimal blocking https GET for the services the editor fetches its aids from (orthophoto, terrain model) +namespace editor_http +{ + +// true to give the transfer up +using stop_check = std::function; + +// one instance per worker thread +class client +{ + public: + client(); + ~client(); + client(client const &) = delete; + client &operator=(client const &) = delete; + + bool get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop); + + private: + struct platform; + std::unique_ptr m_platform; +}; + +// whether this build can fetch anything +bool available(); + +} // namespace editor_http diff --git a/editor/editorNmt.cpp b/editor/editorNmt.cpp new file mode 100644 index 00000000..484b34b9 --- /dev/null +++ b/editor/editorNmt.cpp @@ -0,0 +1,373 @@ +/* +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/editorNmt.hpp" +#include "editor/editorHttp.hpp" +#include "utilities/Logs.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +namespace fs = std::filesystem; + +// the model at 1 m of the WCS service of geoportal.gov.pl; the name of the model is read from its capabilities +char const *const service{"https://mapy.geoportal.gov.pl/wss/service/PZGIK/NMT/GRID1/WCS/DigitalTerrainModelFormatTIFF"}; +char const *const fallback_coverage{"DTM_PL-KRON86-NH_TIFF"}; +char const *const cache_folder{"editor/cache/nmt/grid1/"}; +int constexpr worker_count{2}; // the service is quick, but isn't to be hammered +int constexpr border{1}; // pixels of the neighbouring squares fetched around a square, so the heights interpolate across its edges +float constexpr no_data{-1000.f}; // the model gives far lower values outside of the country + +// the name of the model among the names the capabilities of the service list: the first which says it's a terrain model +std::string coverage_of(std::string const &Capabilities) +{ + std::vector names; + for (std::size_t at{0}; (at = Capabilities.find("name>", at)) != std::string::npos;) + { + at += 5; + auto const end{Capabilities.find('<', at)}; + if (end == std::string::npos) + break; + names.emplace_back(Capabilities.substr(at, end - at)); + } + for (auto const &name : names) + { + std::string lower{name}; + std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char const Letter) { return static_cast(std::tolower(Letter)); }); + if (lower.find("dtm") != std::string::npos || lower.find("nmt") != std::string::npos || lower.find("terrain") != std::string::npos) + return name; + } + return fallback_coverage; +} + +// a GeoTIFF as the service gives it: one band of 32-bit floats in strips, not compressed, placed on the grid by its tie point +// and pixel scale. false for anything else +template +bool read_geotiff(std::vector const &Data, Square &Out) +{ + if (Data.size() < 8) + return false; + bool const little{Data[0] == 'I' && Data[1] == 'I'}; + if (false == little && false == (Data[0] == 'M' && Data[1] == 'M')) + return false; + auto const u16 = [&](std::size_t const At) -> std::uint32_t { + if (At + 2 > Data.size()) + return 0; + return little ? Data[At] | (Data[At + 1] << 8) : (Data[At] << 8) | Data[At + 1]; + }; + auto const u32 = [&](std::size_t const At) -> std::uint32_t { + if (At + 4 > Data.size()) + return 0; + return little ? Data[At] | (Data[At + 1] << 8) | (Data[At + 2] << 16) | (static_cast(Data[At + 3]) << 24) : + (static_cast(Data[At]) << 24) | (Data[At + 1] << 16) | (Data[At + 2] << 8) | Data[At + 3]; + }; + auto const f64 = [&](std::size_t const At) { + std::uint64_t bits{0}; + for (int i = 0; i < 8; ++i) + bits |= static_cast(At + i < Data.size() ? Data[At + (little ? i : 7 - i)] : 0) << (8 * i); + double value; + std::memcpy(&value, &bits, sizeof(value)); + return value; + }; + if (u16(2) != 42) + return false; + struct entry + { + std::uint32_t type{0}; + std::uint32_t count{0}; + std::size_t at{0}; // of the values: in the entry when they fit in four bytes + }; + std::map entries; + auto const ifd{static_cast(u32(4))}; + auto const count{u16(ifd)}; + for (std::uint32_t i = 0; i < count; ++i) + { + auto const at{ifd + 2 + i * 12}; + entry value{u16(at + 2), u32(at + 4), at + 8}; + auto const size{(value.type == 3 ? 2u : value.type == 12 ? 8u : 4u) * value.count}; + if (size > 4) + value.at = u32(at + 8); + entries[u16(at)] = value; + } + auto const number = [&](std::uint32_t const Tag, std::uint32_t const Index) -> std::uint32_t { + auto const found{entries.find(Tag)}; + if (found == entries.end() || Index >= found->second.count) + return 0; + return found->second.type == 3 ? u16(found->second.at + Index * 2) : u32(found->second.at + Index * 4); + }; + auto const real = [&](std::uint32_t const Tag, std::uint32_t const Index) { + auto const found{entries.find(Tag)}; + return found == entries.end() || Index >= found->second.count ? 0.0 : f64(found->second.at + Index * 8); + }; + auto const width{static_cast(number(256, 0))}; + auto const height{static_cast(number(257, 0))}; + if (width < 2 || height < 2 || number(258, 0) != 32 || number(339, 0) != 3 || std::max(1u, number(259, 0)) != 1 || false == entries.contains(273) || false == entries.contains(33550) || + false == entries.contains(33922)) + return false; + auto const rows{std::max(1u, number(278, 0))}; + Out.width = width; + Out.height = height; + Out.heights.assign(static_cast(width) * height, no_data - 1.f); + auto const strips{entries[273].count}; + for (std::uint32_t strip = 0; strip < strips; ++strip) + { + auto const offset{static_cast(number(273, strip))}; + auto const first{strip * rows}; + for (std::uint32_t row = first; row < std::min(first + rows, height); ++row) + for (int column = 0; column < width; ++column) + { + auto const bits{u32(offset + ((row - first) * width + column) * 4)}; + float value; + std::memcpy(&value, &bits, sizeof(value)); + Out.heights[static_cast(row) * width + column] = value; + } + } + Out.pixel = real(33550, 0); + Out.east = real(33922, 3) - real(33922, 0) * Out.pixel; + Out.north = real(33922, 4) + real(33922, 1) * Out.pixel; + return Out.pixel > 0.0; +} + +} // namespace + +editor_nmt::editor_nmt() = default; + +editor_nmt::~editor_nmt() +{ + { + std::lock_guard lock{m_mutex}; + m_stop = true; + m_queue.clear(); + } + m_wake.notify_all(); + for (auto &worker : m_workers) + if (worker.joinable()) + worker.join(); +} + +bool editor_nmt::can_download() +{ + return editor_http::available(); +} + +void editor_nmt::place(double const North, double const East) +{ + std::lock_guard lock{m_mutex}; + m_north = North; + m_east = East; +} + +bool editor_nmt::placed() const +{ + std::lock_guard lock{m_mutex}; + return m_north != 0.0 || m_east != 0.0; +} + +editor_nmt::key editor_nmt::key_of(double const East, double const North) const +{ + return {static_cast(std::floor(East / square_size)), static_cast(std::floor(North / square_size))}; +} + +bool editor_nmt::height(double const X, double const Z, double &Height) const +{ + std::shared_ptr found; + double east, north; + { + std::lock_guard lock{m_mutex}; + east = m_east - X; + north = m_north + Z; + auto const it{m_squares.find(key_of(east, north))}; + if (it == m_squares.end()) + return false; + found = it->second; + } + auto const &grid{*found}; + if (grid.failed) + return false; + // the values lie in the middles of the pixels + auto const column{(east - grid.east) / grid.pixel - 0.5}; + auto const row{(grid.north - north) / grid.pixel - 0.5}; + auto const c0{std::clamp(static_cast(std::floor(column)), 0, grid.width - 2)}; + auto const r0{std::clamp(static_cast(std::floor(row)), 0, grid.height - 2)}; + auto const fc{std::clamp(column - c0, 0.0, 1.0)}; + auto const fr{std::clamp(row - r0, 0.0, 1.0)}; + auto const at = [&](int const C, int const R) { return grid.heights[static_cast(R) * grid.width + C]; }; + float const corners[4]{at(c0, r0), at(c0 + 1, r0), at(c0, r0 + 1), at(c0 + 1, r0 + 1)}; + if (std::all_of(std::begin(corners), std::end(corners), [](float const Value) { return std::isfinite(Value) && Value > no_data; })) + { + Height = (corners[0] * (1.0 - fc) + corners[1] * fc) * (1.0 - fr) + (corners[2] * (1.0 - fc) + corners[3] * fc) * fr; + return true; + } + // at the edge of the data: the nearest value there is + auto const nearest{corners[(fr > 0.5 ? 2 : 0) + (fc > 0.5 ? 1 : 0)]}; + if (false == std::isfinite(nearest) || nearest <= no_data) + return false; + Height = nearest; + return true; +} + +bool editor_nmt::request(glm::dvec2 const &Min, glm::dvec2 const &Max) +{ + std::vector wanted; + bool all{true}; + { + std::lock_guard lock{m_mutex}; + auto const low{key_of(m_east - Max.x, m_north + Min.y)}; + auto const high{key_of(m_east - Min.x, m_north + Max.y)}; + auto const middle{key_of(m_east - 0.5 * (Min.x + Max.x), m_north + 0.5 * (Min.y + Max.y))}; + for (auto e = low.first; e <= high.first; ++e) + for (auto n = low.second; n <= high.second; ++n) + { + key const square{e, n}; + if (m_squares.contains(square)) + continue; + all = false; + if (false == m_queued.contains(square)) + wanted.push_back(square); + } + std::sort(wanted.begin(), wanted.end(), [&](key const &Left, key const &Right) { + auto const distance = [&](key const &Square) { return std::abs(Square.first - middle.first) + std::abs(Square.second - middle.second); }; + return distance(Left) < distance(Right); + }); + for (auto const &square : wanted) + { + m_queued.insert(square); + m_queue.push_back(square); + } + if (false == wanted.empty() && m_workers.empty()) + for (int i = 0; i < worker_count; ++i) + m_workers.emplace_back([this]() { work(); }); + } + if (false == wanted.empty()) + m_wake.notify_all(); + return all; +} + +std::size_t editor_nmt::waiting() const +{ + std::lock_guard lock{m_mutex}; + return m_queued.size(); +} + +std::string editor_nmt::error() const +{ + std::lock_guard lock{m_mutex}; + return m_error; +} + +void editor_nmt::clear() +{ + std::lock_guard lock{m_mutex}; + m_queue.clear(); + m_queued.clear(); + m_squares.clear(); +} + +void editor_nmt::work() +{ + while (true) + { + key square; + { + std::unique_lock lock{m_mutex}; + m_wake.wait(lock, [this]() { return m_stop || false == m_queue.empty(); }); + if (m_stop) + return; + square = m_queue.front(); + m_queue.pop_front(); + } + std::string error; + auto fetched{fetch(square, error)}; + { + std::lock_guard lock{m_mutex}; + if (false == m_queued.contains(square)) + continue; // cleared meanwhile + m_queued.erase(square); + m_squares[square] = std::move(fetched); + m_error = error; + } + } +} + +std::shared_ptr editor_nmt::fetch(key const &Key, std::string &Error) +{ + auto result{std::make_shared()}; + std::error_code fault; + fs::create_directories(cache_folder, fault); + auto const file{std::string{cache_folder} + std::to_string(Key.first) + "_" + std::to_string(Key.second) + ".tif"}; + std::vector data; + if (std::ifstream cached{file, std::ios::binary}; cached.is_open()) + { + data.assign(std::istreambuf_iterator(cached), std::istreambuf_iterator()); + if (read_geotiff(data, *result)) + return result; + // damaged by a fetch broken off: fetched again + fs::remove(file, fault); + } + if (false == can_download()) + { + Error = "this build can't fetch from the internet"; + result->failed = true; + return result; + } + editor_http::client client; + auto const stop = [this]() { + std::lock_guard lock{m_mutex}; + return m_stop; + }; + std::string coverage; + { + std::lock_guard lock{m_mutex}; + coverage = m_coverage; + } + if (coverage.empty()) + { + std::vector capabilities; + std::string unused; + coverage = client.get(std::string{service} + "?SERVICE=WCS&VERSION=1.0.0&REQUEST=GetCapabilities", capabilities, unused, stop) ? + coverage_of(std::string(capabilities.begin(), capabilities.end())) : + fallback_coverage; + std::lock_guard lock{m_mutex}; + m_coverage = coverage; + } + // the square with a pixel of each neighbour around it + auto const pixels{static_cast(square_size) + 2 * border}; + auto const east{Key.first * square_size - border}; + auto const north{Key.second * square_size - border}; + char bbox[128]; + std::snprintf(bbox, sizeof(bbox), "%.0f,%.0f,%.0f,%.0f", east, north, east + pixels, north + pixels); + auto const url{std::string{service} + "?SERVICE=WCS&VERSION=1.0.0&REQUEST=GetCoverage&COVERAGE=" + coverage + "&CRS=EPSG:2180&RESPONSE_CRS=EPSG:2180&BBOX=" + bbox + + "&WIDTH=" + std::to_string(pixels) + "&HEIGHT=" + std::to_string(pixels) + "&FORMAT=GEOTIFF"}; + for (int attempt = 0; attempt < 3; ++attempt) + { + if (client.get(url, data, Error, stop) && read_geotiff(data, *result)) + { + if (std::ofstream out{file, std::ios::binary}; out.is_open()) + out.write(reinterpret_cast(data.data()), static_cast(data.size())); + Error.clear(); + return result; + } + if (stop()) + break; + if (Error.empty()) + Error = "the service gave no terrain model"; + } + WriteLog("Editor: NMT square " + std::to_string(Key.first) + "," + std::to_string(Key.second) + " not fetched: " + Error, logtype::generic); + result->failed = true; + return result; +} diff --git a/editor/editorNmt.hpp b/editor/editorNmt.hpp new file mode 100644 index 00000000..fdddac30 --- /dev/null +++ b/editor/editorNmt.hpp @@ -0,0 +1,87 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// heights of the ground from the digital terrain model of GUGiK (NMT, the WCS service of geoportal.gov.pl): squares of the +// EPSG:2180 grid fetched in the background at 1 m, kept on the disk and sampled in the coordinates of the scenery. the +// scenery is placed on the grid by the PUWG 1992 coordinates of its point (0,0,0), as the orthophoto is: easting = east - x, +// northing = north + z. the heights are those of the model, metres over the sea +class editor_nmt +{ + public: + editor_nmt(); + ~editor_nmt(); + editor_nmt(editor_nmt const &) = delete; + editor_nmt &operator=(editor_nmt const &) = delete; + + // where the point (0,0,0) of the scenery lies in PUWG 1992; the squares fetched stay, only the mapping changes + void place(double const North, double const East); + // a scenery at (0,0) isn't placed anywhere + bool placed() const; + // the height of the model at the point of the scenery; false where its square isn't in yet or the model has no data + bool height(double const X, double const Z, double &Height) const; + // the squares under the rectangle of the scenery (x, z) asked for in the background, the ones nearest its middle first; + // true if all of them are in already, those the service couldn't give included + bool request(glm::dvec2 const &Min, glm::dvec2 const &Max); + // squares asked for and not in yet + std::size_t waiting() const; + // what the last fetch said went wrong, empty when it went right + std::string error() const; + // the squares dropped from the memory, and those waiting; the files on the disk stay + void clear(); + // whether this build can fetch from the service at all + static bool can_download(); + + // side of a square of the grid, m + static double constexpr square_size{500.0}; + + private: + using key = std::pair; // easting and northing index of the square on the grid + struct square + { + double east{0.0}; // of the outer edge of the first column + double north{0.0}; // of the outer edge of the first row, the northernmost + double pixel{1.0}; + int width{0}; + int height{0}; + std::vector heights; // rows from the north, columns from the west + bool failed{false}; // the service gave nothing usable: the square has no data + }; + + key key_of(double const East, double const North) const; + void work(); + std::shared_ptr fetch(key const &Key, std::string &Error); + + mutable std::mutex m_mutex; + std::condition_variable m_wake; + std::vector m_workers; + std::deque m_queue; + std::set m_queued; + std::map> m_squares; + std::string m_coverage; // the name of the model in the service, read from its capabilities once + std::string m_error; + double m_north{0.0}; + double m_east{0.0}; + bool m_stop{false}; +}; diff --git a/editor/editorOrthophoto.cpp b/editor/editorOrthophoto.cpp index 1d692570..f9349777 100644 --- a/editor/editorOrthophoto.cpp +++ b/editor/editorOrthophoto.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorOrthophoto.hpp" +#include "editor/editorHttp.hpp" #include "rendering/renderer.h" #include "scene/scene.h" @@ -19,11 +20,6 @@ http://mozilla.org/MPL/2.0/. #include "imgui/imgui.h" #include "stb/stb_image.h" -#if defined(_WIN32) -#include -#elif defined(EU07_WITH_CURL) -#include -#endif #include #include @@ -49,212 +45,14 @@ constexpr std::size_t upload_budget{16u << 20}; // bytes of texture data uploade constexpr std::size_t vertex_budget{60000}; // ImGui uses 16-bit indices and not every backend supports vertex offsets constexpr float near_w{0.5f}; -using stop_check = std::function; +using stop_check = editor_http::stop_check; +using http_client = editor_http::client; double steady_seconds() { return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); } -// --------------------------------------------------------------------------------------------- -// minimal blocking https GET; one instance per worker thread - -class http_client -{ - public: - http_client(); - ~http_client(); - http_client(http_client const &) = delete; - http_client &operator=(http_client const &) = delete; - - bool get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop); - - private: -#if defined(_WIN32) - HINTERNET m_session{nullptr}; -#elif defined(EU07_WITH_CURL) - CURL *m_curl{nullptr}; -#endif -}; - -#if defined(_WIN32) - -http_client::http_client() -{ - wchar_t const *agent = L"MaSzyna-editor"; -#ifdef WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY - m_session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); -#endif - if (m_session == nullptr) // automatic proxy needs windows 8.1+ - m_session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); - if (m_session != nullptr) - ::WinHttpSetTimeouts(m_session, 10000, 10000, 15000, 20000); -} - -http_client::~http_client() -{ - if (m_session != nullptr) - ::WinHttpCloseHandle(m_session); -} - -bool http_client::get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop) -{ - Out.clear(); - if (m_session == nullptr) - { - Error = "WinHttpOpen failed (" + std::to_string(::GetLastError()) + ")"; - return false; - } - - std::wstring const url(Url.begin(), Url.end()); // the requests are plain ascii - URL_COMPONENTS parts{}; - parts.dwStructSize = sizeof(parts); - parts.dwSchemeLength = static_cast(-1); - parts.dwHostNameLength = static_cast(-1); - parts.dwUrlPathLength = static_cast(-1); - parts.dwExtraInfoLength = static_cast(-1); - if (!::WinHttpCrackUrl(url.c_str(), 0, 0, &parts)) - { - Error = "invalid url"; - return false; - } - std::wstring const host(parts.lpszHostName, parts.dwHostNameLength); - std::wstring const path = std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) + std::wstring(parts.lpszExtraInfo, parts.dwExtraInfoLength); - - HINTERNET connection = ::WinHttpConnect(m_session, host.c_str(), parts.nPort, 0); - HINTERNET request = connection ? ::WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, - parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0) - : nullptr; - auto const close = [&]() { - if (request != nullptr) - ::WinHttpCloseHandle(request); - if (connection != nullptr) - ::WinHttpCloseHandle(connection); - }; - - if (request == nullptr || !::WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || !::WinHttpReceiveResponse(request, nullptr)) - { - Error = "WinHTTP error " + std::to_string(::GetLastError()); - close(); - return false; - } - - DWORD status{0}; - DWORD statussize{sizeof(status)}; - ::WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &statussize, WINHTTP_NO_HEADER_INDEX); - - for (;;) - { - if (Stop()) - { - Error = "cancelled"; - close(); - return false; - } - DWORD available{0}; - if (!::WinHttpQueryDataAvailable(request, &available)) - { - Error = "WinHTTP error " + std::to_string(::GetLastError()); - close(); - return false; - } - if (available == 0) - break; - std::size_t const offset = Out.size(); - Out.resize(offset + available); - DWORD received{0}; - if (!::WinHttpReadData(request, Out.data() + offset, available, &received)) - { - Error = "WinHTTP error " + std::to_string(::GetLastError()); - close(); - return false; - } - Out.resize(offset + received); - } - close(); - - if (status != 200) - { - Error = "HTTP " + std::to_string(status); - return false; - } - return true; -} - -#elif defined(EU07_WITH_CURL) - -http_client::http_client() -{ - static std::once_flag initialized; - std::call_once(initialized, []() { ::curl_global_init(CURL_GLOBAL_DEFAULT); }); - m_curl = ::curl_easy_init(); -} - -http_client::~http_client() -{ - if (m_curl != nullptr) - ::curl_easy_cleanup(m_curl); -} - -bool http_client::get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop) -{ - Out.clear(); - if (m_curl == nullptr) - { - Error = "curl_easy_init failed"; - return false; - } - - auto const write = +[](char *Data, std::size_t Size, std::size_t Count, void *User) -> std::size_t { - auto *out = static_cast *>(User); - out->insert(out->end(), Data, Data + Size * Count); - return Size * Count; - }; - auto const progress = +[](void *User, curl_off_t, curl_off_t, curl_off_t, curl_off_t) -> int { return (*static_cast(User))() ? 1 : 0; }; - - ::curl_easy_reset(m_curl); - ::curl_easy_setopt(m_curl, CURLOPT_URL, Url.c_str()); - ::curl_easy_setopt(m_curl, CURLOPT_USERAGENT, "MaSzyna-editor"); - ::curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1L); - ::curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1L); - ::curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT, 10L); - ::curl_easy_setopt(m_curl, CURLOPT_LOW_SPEED_LIMIT, 1L); - ::curl_easy_setopt(m_curl, CURLOPT_LOW_SPEED_TIME, 20L); - ::curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, write); - ::curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &Out); - ::curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, 0L); - ::curl_easy_setopt(m_curl, CURLOPT_XFERINFOFUNCTION, progress); - ::curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, const_cast(&Stop)); - - CURLcode const code = ::curl_easy_perform(m_curl); - if (code != CURLE_OK) - { - Error = ::curl_easy_strerror(code); - return false; - } - long status{0}; - ::curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &status); - if (status != 200) - { - Error = "HTTP " + std::to_string(status); - return false; - } - return true; -} - -#else - -http_client::http_client() = default; -http_client::~http_client() = default; - -bool http_client::get(std::string const &, std::vector &Out, std::string &Error, stop_check const &) -{ - Out.clear(); - Error = "no http client in this build"; - return false; -} - -#endif } // namespace diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 7dca57ac..5c05ea7b 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -7058,3 +7058,66 @@ msgstr "Upuść sygnalizator przy torze" msgid "This signal is in a file the editor doesn't change, it can't be moved" msgstr "Ten sygnalizator jest w pliku, którego edytor nie zmienia, nie da się go przesunąć" + +msgid "%zu chunks to make from the terrain model" +msgstr "%zu kafli do zrobienia z NMT" + +msgid "; the formation of %zu paths shaped" +msgstr "; torowisko %zu torów ukształtowane" + +msgid "Along the tracks" +msgstr "Wzdłuż torów" + +msgid "Band along the tracks (m)" +msgstr "Pas wzdłuż torów (m)" + +msgid "Chunks made from the terrain model: %zu, waiting for it: %zu" +msgstr "Kafle z NMT: zrobione %zu, czeka %zu" + +msgid "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" +msgstr "Porównuje torowisko z NMT i przesuwa NMT w pionie, żeby się spotkały, np. dla scenerii zbudowanej na wysokości 0" + +msgid "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" +msgstr "Każdy tor dostaje torowisko ze skarpami 1:1,5 w dół albo w górę do terenu, jako modyfikator terenu; pod mostami i w tunelach zostaje NMT" + +msgid "Fit to the tracks" +msgstr "Dopasuj do torów" + +msgid "Formation and slopes along the tracks" +msgstr "Torowisko i skarpy wzdłuż torów" + +msgid "Height offset (m)" +msgstr "Przesunięcie wysokości (m)" + +msgid "Place: N %.0f, E %.0f (PUWG 1992), as the orthophoto" +msgstr "Położenie: N %.0f, E %.0f (PUWG 1992), jak ortofotomapa" + +msgid "Squares of the model being fetched: %zu" +msgstr "Pobierane kafle NMT: %zu" + +msgid "Terrain model: GUGiK, geoportal.gov.pl" +msgstr "NMT: GUGiK, geoportal.gov.pl" + +msgid "The scenery isn't placed anywhere: give its place in the settings of the orthophoto first" +msgstr "Sceneria nie ma położenia: najpierw podaj je w ustawieniach ortofotomapy" + +msgid "The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)" +msgstr "Tory leżą %+.2f m względem NMT (%zu miejsc, rozrzut %.2f m)" + +msgid "Terrain model (NMT)" +msgstr "Teren z NMT" + +msgid "No tracks to fit the terrain model to: its heights are taken as they are" +msgstr "Brak torów do dopasowania: wysokości NMT bez przesunięcia" + +msgid "Fetching the terrain model under the tracks..." +msgstr "Pobieranie NMT pod torami..." + +msgid "The terrain model has no data under the tracks: is the scenery placed where it is?" +msgstr "NMT nie ma danych pod torami: czy położenie scenerii jest dobre?" + +msgid ". That's far too much: the place of the scenery is likely wrong, check the orthophoto" +msgstr ". To za dużo: położenie scenerii jest pewnie złe, sprawdź ortofotomapę" + +msgid "There's terrain there already" +msgstr "Tam już jest teren"