17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-30 20:10:29 +01: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

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