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

feat(editor): heightmap terrain from the terrain model of GUGiK, fitted to the tracks of the scenery

- 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
This commit is contained in:
maj00r
2026-10-18 12:56:25 +02:00
parent 16348c655a
commit 5af699fba4
12 changed files with 1245 additions and 234 deletions

View File

@@ -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"

View File

@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
#include <fstream>
#include <regex>
#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 <typename Value_> void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
{
std::deque<std::size_t> 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

View File

@@ -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<nmt_place> 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<heightmap::chunk_key> 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_place> nmt_track_places(double const Step) const;
void nmt_locate();
void nmt_fit();
void nmt_finish_fit();
std::vector<heightmap::chunk_key> nmt_keys_around() const;
std::vector<heightmap::chunk_key> nmt_keys_along_tracks() const;
void nmt_make(std::vector<heightmap::chunk_key> 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
{

View File

@@ -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 <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};
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"));
}

View File

@@ -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;

View File

@@ -9,6 +9,9 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include <deque>
#include <vector>
#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 <typename Value_> inline void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
{
std::deque<std::size_t> 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);
}
}

224
editor/editorHttp.cpp Normal file
View File

@@ -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 <winhttp.h>
#elif defined(EU07_WITH_CURL)
#include <curl/curl.h>
#endif
#include <mutex>
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<platform>())
{
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<std::uint8_t> &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<DWORD>(-1);
parts.dwHostNameLength = static_cast<DWORD>(-1);
parts.dwUrlPathLength = static_cast<DWORD>(-1);
parts.dwExtraInfoLength = static_cast<DWORD>(-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<platform>())
{
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<std::uint8_t> &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<std::vector<std::uint8_t> *>(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<stop_check const *>(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_check *>(&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<std::uint8_t> &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

44
editor/editorHttp.hpp Normal file
View File

@@ -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 <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
// 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<bool()>;
// 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<std::uint8_t> &Out, std::string &Error, stop_check const &Stop);
private:
struct platform;
std::unique_ptr<platform> m_platform;
};
// whether this build can fetch anything
bool available();
} // namespace editor_http

373
editor/editorNmt.cpp Normal file
View File

@@ -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 <algorithm>
#include <cctype>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
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<std::string> 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<char>(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 <typename Square>
bool read_geotiff(std::vector<std::uint8_t> 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<std::uint32_t>(Data[At + 3]) << 24) :
(static_cast<std::uint32_t>(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<std::uint64_t>(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<std::uint32_t, entry> entries;
auto const ifd{static_cast<std::size_t>(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<int>(number(256, 0))};
auto const height{static_cast<int>(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<std::size_t>(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<std::size_t>(number(273, strip))};
auto const first{strip * rows};
for (std::uint32_t row = first; row < std::min<std::uint32_t>(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<std::size_t>(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<std::mutex> 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<std::mutex> lock{m_mutex};
m_north = North;
m_east = East;
}
bool editor_nmt::placed() const
{
std::lock_guard<std::mutex> 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<int>(std::floor(East / square_size)), static_cast<int>(std::floor(North / square_size))};
}
bool editor_nmt::height(double const X, double const Z, double &Height) const
{
std::shared_ptr<square const> found;
double east, north;
{
std::lock_guard<std::mutex> 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<int>(std::floor(column)), 0, grid.width - 2)};
auto const r0{std::clamp(static_cast<int>(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<std::size_t>(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<key> wanted;
bool all{true};
{
std::lock_guard<std::mutex> 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<std::mutex> lock{m_mutex};
return m_queued.size();
}
std::string editor_nmt::error() const
{
std::lock_guard<std::mutex> lock{m_mutex};
return m_error;
}
void editor_nmt::clear()
{
std::lock_guard<std::mutex> lock{m_mutex};
m_queue.clear();
m_queued.clear();
m_squares.clear();
}
void editor_nmt::work()
{
while (true)
{
key square;
{
std::unique_lock<std::mutex> 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<std::mutex> 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::square const> editor_nmt::fetch(key const &Key, std::string &Error)
{
auto result{std::make_shared<square>()};
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<std::uint8_t> data;
if (std::ifstream cached{file, std::ios::binary}; cached.is_open())
{
data.assign(std::istreambuf_iterator<char>(cached), std::istreambuf_iterator<char>());
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<std::mutex> lock{m_mutex};
return m_stop;
};
std::string coverage;
{
std::lock_guard<std::mutex> lock{m_mutex};
coverage = m_coverage;
}
if (coverage.empty())
{
std::vector<std::uint8_t> 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<std::mutex> lock{m_mutex};
m_coverage = coverage;
}
// the square with a pixel of each neighbour around it
auto const pixels{static_cast<int>(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<char const *>(data.data()), static_cast<std::streamsize>(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;
}

87
editor/editorNmt.hpp Normal file
View File

@@ -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 <condition_variable>
#include <cstdint>
#include <deque>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <glm/glm.hpp>
// 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<int, int>; // 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<float> 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<square const> fetch(key const &Key, std::string &Error);
mutable std::mutex m_mutex;
std::condition_variable m_wake;
std::vector<std::thread> m_workers;
std::deque<key> m_queue;
std::set<key> m_queued;
std::map<key, std::shared_ptr<square const>> 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};
};

View File

@@ -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 <winhttp.h>
#elif defined(EU07_WITH_CURL)
#include <curl/curl.h>
#endif
#include <algorithm>
#include <chrono>
@@ -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<bool()>;
using stop_check = editor_http::stop_check;
using http_client = editor_http::client;
double steady_seconds()
{
return std::chrono::duration<double>(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<std::uint8_t> &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<std::uint8_t> &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<DWORD>(-1);
parts.dwHostNameLength = static_cast<DWORD>(-1);
parts.dwUrlPathLength = static_cast<DWORD>(-1);
parts.dwExtraInfoLength = static_cast<DWORD>(-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<std::uint8_t> &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<std::vector<std::uint8_t> *>(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<stop_check const *>(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_check *>(&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<std::uint8_t> &Out, std::string &Error, stop_check const &)
{
Out.clear();
Error = "no http client in this build";
return false;
}
#endif
} // namespace

View File

@@ -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"