mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-31 15:49:06 +01:00
Merge the track layout editor into cpp20-modules, ported to modules
The editor branch was written against headers and the PCH, both of which this
branch has done away with, so its own files were ported rather than merged: the
five headers it added are now interface units and their .cpp files module
implementation units.
- editor/{puwg1992,polandmap,turnouts,orthophoto} become eu07.editor.* modules,
application/planpanel becomes eu07.application.planpanel. The plan tool keeps
to its own module rather than joining the eu07.simcore partitions: it stands on
the editor's libraries and eu07.application.uilayer, not on the simulation core,
so :editoruilayer can export import it without a cycle.
- glm arrives by import eu07.glm, never by include, as the rest of the tree does.
maj0sted is header-only and glm-free, so its headers stay includes in the global
module fragment - every unit that names those types includes them itself, since
a global fragment is not visible across units.
- the one textual conflict was :editoruilayer's include block against this
branch's imports; resolved this branch's way plus the planpanel import.
Builds with clang 22 (-DWITH_BETTER_RENDERER=OFF) and starts into the editor on
empty ground with -editor alone.
This commit is contained in:
596
editor/orthophoto.cpp
Normal file
596
editor/orthophoto.cpp
Normal file
@@ -0,0 +1,596 @@
|
||||
module;
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include "maj0sted/editor/tile_cache.hpp"
|
||||
#include <glad/glad.h>
|
||||
#include "stb/stb_image.h"
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
#pragma comment(lib, "winhttp.lib")
|
||||
#else
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
|
||||
module eu07.editor.orthophoto;
|
||||
import eu07.utilities.logs;
|
||||
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
namespace editor
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int const kWorkers = 4;
|
||||
// the imagery service sits behind a filter that turns down browser user agents and, even for a
|
||||
// plain one, fails a request now and then; a handful of immediate retries gets past both
|
||||
int const kAttempts = 4;
|
||||
char const *kUserAgent = "curl/8.5.0";
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
// plain HTTPS GET. deliberately small: one request, one answer, no keep-alive - tiles are large
|
||||
// enough that the connection setup does not dominate, and this way a stuck transfer cannot poison
|
||||
// anything but itself
|
||||
bool http_get(std::string const &Url, std::vector<std::uint8_t> &Bytes, std::string &Contenttype)
|
||||
{
|
||||
std::wstring const url(Url.begin(), Url.end());
|
||||
|
||||
URL_COMPONENTS parts{};
|
||||
parts.dwStructSize = sizeof(parts);
|
||||
parts.dwHostNameLength = 1;
|
||||
parts.dwUrlPathLength = 1;
|
||||
parts.dwExtraInfoLength = 1;
|
||||
if (false == WinHttpCrackUrl(url.c_str(), static_cast<DWORD>(url.size()), 0, &parts))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring const host(parts.lpszHostName, parts.dwHostNameLength);
|
||||
std::wstring const resource = std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) + std::wstring(parts.lpszExtraInfo, parts.dwExtraInfoLength);
|
||||
|
||||
auto const wideagent = std::wstring(kUserAgent, kUserAgent + std::strlen(kUserAgent));
|
||||
HINTERNET session = WinHttpOpen(wideagent.c_str(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
if (session == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const cleanup = [](HINTERNET handle) {
|
||||
if (handle != nullptr)
|
||||
{
|
||||
WinHttpCloseHandle(handle);
|
||||
}
|
||||
};
|
||||
|
||||
HINTERNET connection = WinHttpConnect(session, host.c_str(), parts.nPort, 0);
|
||||
if (connection == nullptr)
|
||||
{
|
||||
cleanup(session);
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD const flags = (parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0);
|
||||
HINTERNET request = WinHttpOpenRequest(connection, L"GET", resource.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
|
||||
if (request == nullptr)
|
||||
{
|
||||
cleanup(connection);
|
||||
cleanup(session);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ok = false;
|
||||
if (WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) && WinHttpReceiveResponse(request, nullptr))
|
||||
{
|
||||
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);
|
||||
|
||||
if (status == 200)
|
||||
{
|
||||
wchar_t typebuffer[128]{};
|
||||
DWORD typesize = sizeof(typebuffer);
|
||||
if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX, typebuffer, &typesize, WINHTTP_NO_HEADER_INDEX))
|
||||
{
|
||||
std::wstring const wide(typebuffer);
|
||||
Contenttype.assign(wide.begin(), wide.end());
|
||||
}
|
||||
|
||||
Bytes.clear();
|
||||
DWORD available = 0;
|
||||
while (WinHttpQueryDataAvailable(request, &available) && available > 0)
|
||||
{
|
||||
auto const offset = Bytes.size();
|
||||
Bytes.resize(offset + available);
|
||||
DWORD read = 0;
|
||||
if (false == WinHttpReadData(request, Bytes.data() + offset, available, &read))
|
||||
{
|
||||
Bytes.clear();
|
||||
break;
|
||||
}
|
||||
Bytes.resize(offset + read);
|
||||
}
|
||||
ok = (false == Bytes.empty());
|
||||
}
|
||||
}
|
||||
|
||||
cleanup(request);
|
||||
cleanup(connection);
|
||||
cleanup(session);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// libcurl's write callback: appends one chunk of the body to the caller's buffer
|
||||
std::size_t append_body(char const *Data, std::size_t const Size, std::size_t const Count, void *Userdata)
|
||||
{
|
||||
auto &bytes = *static_cast<std::vector<std::uint8_t> *>(Userdata);
|
||||
auto const length = Size * Count;
|
||||
bytes.insert(bytes.end(), Data, Data + length);
|
||||
return length;
|
||||
}
|
||||
|
||||
// plain HTTPS GET, the same shape as the WinHttp one above: one request, one answer, nothing kept
|
||||
// between calls. libcurl wants its global init done once before any handle exists, and this is the
|
||||
// only place in the program that uses it, so a function-local static does the job thread-safely
|
||||
bool http_get(std::string const &Url, std::vector<std::uint8_t> &Bytes, std::string &Contenttype)
|
||||
{
|
||||
static bool const initialised = (curl_global_init(CURL_GLOBAL_DEFAULT) == CURLE_OK);
|
||||
if (false == initialised)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CURL *handle = curl_easy_init();
|
||||
if (handle == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Bytes.clear();
|
||||
curl_easy_setopt(handle, CURLOPT_URL, Url.c_str());
|
||||
curl_easy_setopt(handle, CURLOPT_USERAGENT, kUserAgent);
|
||||
curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, append_body);
|
||||
curl_easy_setopt(handle, CURLOPT_WRITEDATA, &Bytes);
|
||||
// a stuck transfer must not hold a worker forever; the caller retries
|
||||
curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||
curl_easy_setopt(handle, CURLOPT_TIMEOUT, 30L);
|
||||
// workers run on their own threads: keep libcurl off signals, which are process-wide
|
||||
curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
|
||||
|
||||
bool ok = (curl_easy_perform(handle) == CURLE_OK);
|
||||
if (true == ok)
|
||||
{
|
||||
long status = 0;
|
||||
curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &status);
|
||||
char const *type = nullptr;
|
||||
if ((curl_easy_getinfo(handle, CURLINFO_CONTENT_TYPE, &type) == CURLE_OK) && (type != nullptr))
|
||||
{
|
||||
Contenttype.assign(type);
|
||||
}
|
||||
ok = (status == 200) && (false == Bytes.empty());
|
||||
}
|
||||
if (false == ok)
|
||||
{
|
||||
Bytes.clear();
|
||||
}
|
||||
|
||||
curl_easy_cleanup(handle);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
orthophoto_source::orthophoto_source(maj0sted::editor::WmsConfig Config, int const Gridzoom, std::string Cachesubdir)
|
||||
: m_service([&] {
|
||||
if (Gridzoom >= maj0sted::editor::TileGrid::kCellZoomMin)
|
||||
{
|
||||
Config.tile_pixels = 512; // coarse cells do not need orto's 1024 px
|
||||
}
|
||||
return std::move(Config);
|
||||
}()),
|
||||
m_gridzoom(Gridzoom), m_cachesubdir(std::move(Cachesubdir))
|
||||
{
|
||||
// the library asks for a URL and expects the answer whenever it arrives; the transfer is queued
|
||||
// here and handed back on the thread that drains it, so the service stays single threaded
|
||||
m_service.set_fetcher([this](std::string const &Url, maj0sted::editor::TileService::DoneFn Done) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_queued.push_back({Url, std::move(Done), false, {}, {}});
|
||||
}
|
||||
++m_inflight;
|
||||
m_wakeup.notify_one();
|
||||
});
|
||||
|
||||
m_workers.reserve(kWorkers);
|
||||
for (auto i = 0; i < kWorkers; ++i)
|
||||
{
|
||||
m_workers.emplace_back([this]() { worker(); });
|
||||
}
|
||||
}
|
||||
|
||||
orthophoto_source::~orthophoto_source()
|
||||
{
|
||||
m_quitting = true;
|
||||
m_wakeup.notify_all();
|
||||
for (auto &worker : m_workers)
|
||||
{
|
||||
if (worker.joinable())
|
||||
{
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const &texture : m_textures)
|
||||
{
|
||||
if (texture.second != 0)
|
||||
{
|
||||
auto const id = texture.second;
|
||||
glDeleteTextures(1, &id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void orthophoto_source::worker()
|
||||
{
|
||||
while (false == m_quitting)
|
||||
{
|
||||
transfer job;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
m_wakeup.wait(lock, [this]() { return m_quitting || (false == m_queued.empty()); });
|
||||
if (m_quitting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
job = std::move(m_queued.front());
|
||||
m_queued.pop_front();
|
||||
}
|
||||
|
||||
for (auto attempt = 0; attempt < kAttempts && false == m_quitting; ++attempt)
|
||||
{
|
||||
if (http_get(job.url, job.bytes, job.content_type))
|
||||
{
|
||||
job.ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_finished.push_back(std::move(job));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<orthophoto_source::ready_tile> orthophoto_source::collect(maj0sted::editor::TileBBox const &View, int const Maxtiles)
|
||||
{
|
||||
// hand finished transfers to the library here, on the caller's thread
|
||||
std::deque<transfer> arrived;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
arrived.swap(m_finished);
|
||||
}
|
||||
for (auto &job : arrived)
|
||||
{
|
||||
if (job.done)
|
||||
{
|
||||
job.done(job.ok, std::move(job.bytes), std::move(job.content_type));
|
||||
}
|
||||
--m_inflight;
|
||||
}
|
||||
|
||||
// fetch only what fits the budget for the current view. the window is recentred when the
|
||||
// view is larger than the cap, and zoom-to-cursor moves the camera, so the set of keys asked
|
||||
// for changes from frame to frame - that must not decide what is drawn
|
||||
for (auto const &key : maj0sted::editor::TileGrid::tiles_for_view(View, m_gridzoom, Maxtiles))
|
||||
{
|
||||
// a tile kept from an earlier session seeds the cache, so no request goes out for it at all
|
||||
if (false == m_service.has(key))
|
||||
{
|
||||
load_from_disk(key);
|
||||
}
|
||||
|
||||
auto const status = m_service.request(key);
|
||||
if (status == maj0sted::editor::TileStatus::Failed)
|
||||
{
|
||||
// counted once per tile, not once per frame it stays unavailable
|
||||
m_failedkeys.insert(key);
|
||||
continue;
|
||||
}
|
||||
if (status != maj0sted::editor::TileStatus::Ready)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_textures.find(key) == m_textures.end())
|
||||
{
|
||||
// the first time a tile is ready is also the moment its bytes are known to be good
|
||||
save_to_disk(key);
|
||||
m_textures.emplace(key, upload(key));
|
||||
}
|
||||
}
|
||||
|
||||
// draw every texture we already hold that still covers the view. returning only the fetch
|
||||
// window made tiles blink out during zoom as soon as they fell outside the cap, then blink
|
||||
// back in once the window moved over them again
|
||||
auto const overlaps = [](maj0sted::editor::TileBBox const &Left, maj0sted::editor::TileBBox const &Right) {
|
||||
return Left.min_x < Right.max_x && Left.max_x > Right.min_x && Left.min_y < Right.max_y && Left.max_y > Right.min_y;
|
||||
};
|
||||
|
||||
std::vector<ready_tile> tiles;
|
||||
tiles.reserve(m_textures.size());
|
||||
for (auto const &[key, texture] : m_textures)
|
||||
{
|
||||
if (texture == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto const box = maj0sted::editor::TileGrid::bbox(key);
|
||||
if (overlaps(box, View))
|
||||
{
|
||||
tiles.push_back({box, texture});
|
||||
}
|
||||
}
|
||||
|
||||
return tiles;
|
||||
}
|
||||
|
||||
unsigned int orthophoto_source::upload(maj0sted::editor::TileKey const &Key)
|
||||
{
|
||||
auto const *image = m_service.peek(Key);
|
||||
if (image == nullptr || image->bytes.empty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int channels = 0;
|
||||
auto *pixels = stbi_load_from_memory(image->bytes.data(), static_cast<int>(image->bytes.size()), &width, &height, &channels, 3);
|
||||
if (pixels == nullptr)
|
||||
{
|
||||
ErrorLog("Orthophoto: could not decode tile imagery");
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels);
|
||||
|
||||
stbi_image_free(pixels);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::string orthophoto_source::tile_path(maj0sted::editor::TileKey const &Key) const
|
||||
{
|
||||
return "editor/cache/" + m_cachesubdir + "/" + std::to_string(Key.zoom) + "_" + std::to_string(Key.x) + "_" + std::to_string(Key.y) + ".img";
|
||||
}
|
||||
|
||||
bool orthophoto_source::load_from_disk(maj0sted::editor::TileKey const &Key)
|
||||
{
|
||||
std::ifstream file(tile_path(Key), std::ios::binary);
|
||||
if (false == file.is_open())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytes{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||
if (bytes.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_service.store(Key, std::move(bytes));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void orthophoto_source::save_to_disk(maj0sted::editor::TileKey const &Key)
|
||||
{
|
||||
auto const *image = m_service.peek(Key);
|
||||
if (image == nullptr || image->bytes.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto const path = tile_path(Key);
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), error);
|
||||
if (std::filesystem::exists(path, error))
|
||||
{
|
||||
return; // came from the disk in the first place
|
||||
}
|
||||
|
||||
std::ofstream file(path, std::ios::binary);
|
||||
if (file.is_open())
|
||||
{
|
||||
file.write(reinterpret_cast<char const *>(image->bytes.data()), static_cast<std::streamsize>(image->bytes.size()));
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t orthophoto_source::pending() const
|
||||
{
|
||||
return m_inflight.load();
|
||||
}
|
||||
|
||||
std::size_t orthophoto_source::failed() const
|
||||
{
|
||||
return m_failedkeys.size();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// decodes encoded image bytes straight onto a fresh texture; 0 when they are not an image
|
||||
unsigned int upload_image(std::vector<std::uint8_t> const &Bytes)
|
||||
{
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int channels = 0;
|
||||
auto *pixels = stbi_load_from_memory(Bytes.data(), static_cast<int>(Bytes.size()), &width, &height, &channels, 3);
|
||||
if (pixels == nullptr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels);
|
||||
|
||||
stbi_image_free(pixels);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
bool same_box(maj0sted::editor::TileBBox const &Left, maj0sted::editor::TileBBox const &Right)
|
||||
{
|
||||
auto const close = [](double const A, double const B) { return std::abs(A - B) < 1.0; };
|
||||
return close(Left.min_x, Right.min_x) && close(Left.min_y, Right.min_y) && close(Left.max_x, Right.max_x) && close(Left.max_y, Right.max_y);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
wms_image::wms_image(maj0sted::editor::WmsConfig Config) : m_config(std::move(Config))
|
||||
{
|
||||
m_worker = std::thread([this]() { worker(); });
|
||||
}
|
||||
|
||||
wms_image::~wms_image()
|
||||
{
|
||||
m_quitting = true;
|
||||
m_wakeup.notify_all();
|
||||
if (m_worker.joinable())
|
||||
{
|
||||
m_worker.join();
|
||||
}
|
||||
if (m_texture != 0)
|
||||
{
|
||||
auto const id = m_texture;
|
||||
glDeleteTextures(1, &id);
|
||||
}
|
||||
}
|
||||
|
||||
void wms_image::worker()
|
||||
{
|
||||
while (false == m_quitting)
|
||||
{
|
||||
std::string url;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
m_wakeup.wait(lock, [this]() { return m_quitting || m_haswork; });
|
||||
if (m_quitting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
url = m_url;
|
||||
m_haswork = false;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytes;
|
||||
std::string contenttype;
|
||||
for (auto attempt = 0; attempt < kAttempts && false == m_quitting; ++attempt)
|
||||
{
|
||||
if (http_get(url, bytes, contenttype))
|
||||
{
|
||||
break;
|
||||
}
|
||||
bytes.clear();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_bytes = std::move(bytes);
|
||||
m_hasresult = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int wms_image::texture_for(maj0sted::editor::TileBBox const &Box, int const Pixels)
|
||||
{
|
||||
// a finished download becomes the texture on this thread, where the graphics API is safe to touch
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_hasresult)
|
||||
{
|
||||
m_hasresult = false;
|
||||
m_loading = false;
|
||||
if (false == m_bytes.empty())
|
||||
{
|
||||
auto const uploaded = upload_image(m_bytes);
|
||||
if (uploaded != 0)
|
||||
{
|
||||
if (m_texture != 0)
|
||||
{
|
||||
auto const old = m_texture;
|
||||
glDeleteTextures(1, &old);
|
||||
}
|
||||
m_texture = uploaded;
|
||||
m_covered = m_inflightbox;
|
||||
}
|
||||
}
|
||||
m_bytes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// only chase a genuinely different view, and only once the previous one has landed
|
||||
if (false == same_box(Box, m_requested) && false == m_loading.load())
|
||||
{
|
||||
m_requested = Box;
|
||||
m_inflightbox = Box;
|
||||
auto config = m_config;
|
||||
config.tile_pixels = Pixels;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_url = maj0sted::editor::wms_getmap_url(config, Box);
|
||||
m_haswork = true;
|
||||
}
|
||||
m_loading = true;
|
||||
m_wakeup.notify_one();
|
||||
}
|
||||
|
||||
return m_texture;
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
139
editor/orthophoto.cppm
Normal file
139
editor/orthophoto.cppm
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
module;
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include "maj0sted/editor/tile_cache.hpp"
|
||||
|
||||
export module eu07.editor.orthophoto;
|
||||
|
||||
export {
|
||||
|
||||
namespace editor
|
||||
{
|
||||
|
||||
// orthophoto backdrop for the plan view. the tiling scheme, the cache and the retry policy come
|
||||
// from the track layout library, which already speaks EPSG:2180; this adds what a native build
|
||||
// needs and the web one got from the browser - the download itself, the decode and the texture.
|
||||
//
|
||||
// downloads run on worker threads, but nothing else does: finished transfers are handed back on
|
||||
// the thread that calls collect(), so both the library's cache and the graphics API are only ever
|
||||
// touched from there.
|
||||
class orthophoto_source
|
||||
{
|
||||
|
||||
public:
|
||||
struct ready_tile
|
||||
{
|
||||
maj0sted::editor::TileBBox box;
|
||||
unsigned int texture{0};
|
||||
};
|
||||
|
||||
// default: Geoportal ortophoto on the fine 100 m grid. pass topo config and a coarse
|
||||
// grid zoom (e.g. 1000) for the zoomed-out topographic underlay
|
||||
explicit orthophoto_source(maj0sted::editor::WmsConfig Config = maj0sted::editor::WmsConfig::geoportal_ortho(), int const Gridzoom = maj0sted::editor::TileGrid::kLevel,
|
||||
std::string Cachesubdir = "orto");
|
||||
~orthophoto_source();
|
||||
orthophoto_source(orthophoto_source const &) = delete;
|
||||
orthophoto_source &operator=(orthophoto_source const &) = delete;
|
||||
|
||||
// kicks off fetches for the view (capped) and returns every already-uploaded tile that still
|
||||
// intersects it - including ones outside the fetch window, so a zoom cannot blink them away.
|
||||
// tiles still on their way simply aren't in the answer yet; call again next frame
|
||||
std::vector<ready_tile> collect(maj0sted::editor::TileBBox const &View, int const Maxtiles = 400);
|
||||
// how many tiles are still being fetched, for the panel to report
|
||||
std::size_t pending() const;
|
||||
// how many tiles gave up after exhausting their retries
|
||||
std::size_t failed() const;
|
||||
|
||||
private:
|
||||
// types
|
||||
struct transfer
|
||||
{
|
||||
std::string url;
|
||||
maj0sted::editor::TileService::DoneFn done;
|
||||
bool ok{false};
|
||||
std::vector<std::uint8_t> bytes;
|
||||
std::string content_type;
|
||||
};
|
||||
// methods
|
||||
void worker();
|
||||
// decodes the cached bytes of a tile and uploads them; returns 0 when the image is unusable
|
||||
unsigned int upload(maj0sted::editor::TileKey const &Key);
|
||||
// imagery of a given piece of ground never changes, so a fetched tile is kept on disk and the
|
||||
// next session starts with it instead of going back to a service that is slow and flaky
|
||||
std::string tile_path(maj0sted::editor::TileKey const &Key) const;
|
||||
bool load_from_disk(maj0sted::editor::TileKey const &Key);
|
||||
void save_to_disk(maj0sted::editor::TileKey const &Key);
|
||||
// members
|
||||
maj0sted::editor::TileService m_service;
|
||||
int m_gridzoom{maj0sted::editor::TileGrid::kLevel};
|
||||
std::string m_cachesubdir{"orto"};
|
||||
std::unordered_map<maj0sted::editor::TileKey, unsigned int, maj0sted::editor::TileKeyHash> m_textures;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::condition_variable m_wakeup;
|
||||
std::deque<transfer> m_queued;
|
||||
std::deque<transfer> m_finished;
|
||||
std::vector<std::thread> m_workers;
|
||||
std::atomic<bool> m_quitting{false};
|
||||
std::atomic<std::size_t> m_inflight{0};
|
||||
std::unordered_set<maj0sted::editor::TileKey, maj0sted::editor::TileKeyHash> m_failedkeys;
|
||||
};
|
||||
|
||||
// a single WMS image covering an arbitrary box, for views too coarse for the tile grid - the whole
|
||||
// country will not fit into 100 m cells. one request is in flight at a time: asking for a different
|
||||
// box supersedes whatever was being fetched
|
||||
class wms_image
|
||||
{
|
||||
|
||||
public:
|
||||
explicit wms_image(maj0sted::editor::WmsConfig Config);
|
||||
~wms_image();
|
||||
wms_image(wms_image const &) = delete;
|
||||
wms_image &operator=(wms_image const &) = delete;
|
||||
|
||||
// asks for the given box at the given size and returns the texture currently available, which
|
||||
// may still be the previous box's until the new one arrives; 0 when there is nothing to show yet
|
||||
unsigned int texture_for(maj0sted::editor::TileBBox const &Box, int const Pixels);
|
||||
// the box the returned texture actually covers
|
||||
maj0sted::editor::TileBBox const &covered() const { return m_covered; }
|
||||
bool loading() const { return m_loading.load(); }
|
||||
|
||||
private:
|
||||
void worker();
|
||||
|
||||
maj0sted::editor::WmsConfig m_config;
|
||||
maj0sted::editor::TileBBox m_requested{};
|
||||
maj0sted::editor::TileBBox m_inflightbox{};
|
||||
maj0sted::editor::TileBBox m_covered{};
|
||||
unsigned int m_texture{0};
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::condition_variable m_wakeup;
|
||||
std::string m_url;
|
||||
bool m_haswork{false};
|
||||
std::vector<std::uint8_t> m_bytes;
|
||||
bool m_hasresult{false};
|
||||
std::thread m_worker;
|
||||
std::atomic<bool> m_quitting{false};
|
||||
std::atomic<bool> m_loading{false};
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
|
||||
} // export
|
||||
45
editor/polandmap.cpp
Normal file
45
editor/polandmap.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
module;
|
||||
#include <algorithm>
|
||||
|
||||
module eu07.editor.polandmap;
|
||||
import eu07.glm;
|
||||
import eu07.editor.puwg1992;
|
||||
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
namespace editor
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// the country's corners in degrees, wide enough to hold all of it: the Odra in the west, the Bug in
|
||||
// the east, the Tatras in the south and the coast in the north
|
||||
double const kSouthLatitude = 49.00;
|
||||
double const kNorthLatitude = 54.90;
|
||||
double const kWestLongitude = 14.10;
|
||||
double const kEastLongitude = 24.20;
|
||||
|
||||
} // namespace
|
||||
|
||||
void poland_extent(glm::dvec2 &Min, glm::dvec2 &Max)
|
||||
{
|
||||
// the projection is not axis-aligned with the graticule, so the extremes come from projecting
|
||||
// all four corners and taking the outermost values
|
||||
auto const sw = puwg1992(kSouthLatitude, kWestLongitude);
|
||||
auto const se = puwg1992(kSouthLatitude, kEastLongitude);
|
||||
auto const nw = puwg1992(kNorthLatitude, kWestLongitude);
|
||||
auto const ne = puwg1992(kNorthLatitude, kEastLongitude);
|
||||
|
||||
Min = {std::min({sw.x, se.x, nw.x, ne.x}), std::min({sw.y, se.y, nw.y, ne.y})};
|
||||
Max = {std::max({sw.x, se.x, nw.x, ne.x}), std::max({sw.y, se.y, nw.y, ne.y})};
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
24
editor/polandmap.cppm
Normal file
24
editor/polandmap.cppm
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
export module eu07.editor.polandmap;
|
||||
import eu07.glm;
|
||||
|
||||
export {
|
||||
|
||||
namespace editor
|
||||
{
|
||||
|
||||
// the whole country's extent in EPSG:2180 metres, as the opening view of a location picker. from
|
||||
// there the base map itself carries the detail, so nothing else has to be drawn on top of it
|
||||
void poland_extent(glm::dvec2 &Min, glm::dvec2 &Max);
|
||||
|
||||
} // namespace editor
|
||||
|
||||
} // export
|
||||
76
editor/puwg1992.cpp
Normal file
76
editor/puwg1992.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
module;
|
||||
#include <cmath>
|
||||
|
||||
module eu07.editor.puwg1992;
|
||||
import eu07.glm;
|
||||
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
namespace editor
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// PUWG 1992: a single transverse Mercator zone over the whole country, on the GRS80 ellipsoid
|
||||
double const kSemiMajor = 6378137.0;
|
||||
double const kFlattening = 1.0 / 298.257222101;
|
||||
double const kCentralMeridian = 19.0; // degrees east
|
||||
double const kScaleFactor = 0.9993;
|
||||
double const kFalseEasting = 500000.0;
|
||||
double const kFalseNorthing = -5300000.0;
|
||||
|
||||
} // namespace
|
||||
|
||||
glm::dvec2 puwg1992(double const Latitude, double const Longitude)
|
||||
{
|
||||
auto const e2 = 2.0 * kFlattening - kFlattening * kFlattening;
|
||||
auto const ep2 = e2 / (1.0 - e2);
|
||||
|
||||
auto const lat = glm::radians(Latitude);
|
||||
auto const dlon = glm::radians(Longitude - kCentralMeridian);
|
||||
|
||||
auto const sinlat = std::sin(lat);
|
||||
auto const coslat = std::cos(lat);
|
||||
auto const tanlat = std::tan(lat);
|
||||
|
||||
// radius of curvature in the prime vertical, and the series terms of the standard
|
||||
// transverse Mercator expansion (Snyder); good to millimetres over a zone this size
|
||||
auto const n = kSemiMajor / std::sqrt(1.0 - e2 * sinlat * sinlat);
|
||||
auto const t = tanlat * tanlat;
|
||||
auto const c = ep2 * coslat * coslat;
|
||||
auto const a = dlon * coslat;
|
||||
|
||||
// meridional arc from the equator
|
||||
auto const m =
|
||||
kSemiMajor * ((1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0) * lat -
|
||||
(3.0 * e2 / 8.0 + 3.0 * e2 * e2 / 32.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(2.0 * lat) +
|
||||
(15.0 * e2 * e2 / 256.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(4.0 * lat) -
|
||||
(35.0 * e2 * e2 * e2 / 3072.0) * std::sin(6.0 * lat));
|
||||
|
||||
auto const a2 = a * a;
|
||||
auto const a3 = a2 * a;
|
||||
auto const a4 = a3 * a;
|
||||
auto const a5 = a4 * a;
|
||||
auto const a6 = a5 * a;
|
||||
|
||||
auto const easting = kFalseEasting + kScaleFactor * n *
|
||||
(a + (1.0 - t + c) * a3 / 6.0 +
|
||||
(5.0 - 18.0 * t + t * t + 72.0 * c - 58.0 * ep2) * a5 / 120.0);
|
||||
|
||||
auto const northing = kFalseNorthing + kScaleFactor *
|
||||
(m + n * tanlat *
|
||||
(a2 / 2.0 + (5.0 - t + 9.0 * c + 4.0 * c * c) * a4 / 24.0 +
|
||||
(61.0 - 58.0 * t + t * t + 600.0 * c - 330.0 * ep2) * a6 / 720.0));
|
||||
|
||||
return {easting, northing};
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
25
editor/puwg1992.cppm
Normal file
25
editor/puwg1992.cppm
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
export module eu07.editor.puwg1992;
|
||||
import eu07.glm;
|
||||
|
||||
export {
|
||||
|
||||
namespace editor
|
||||
{
|
||||
|
||||
// projects geographic coordinates onto PUWG 1992 (EPSG:2180) - the frame the track layout library
|
||||
// works in, and the one Geoportal serves its imagery in. latitude and longitude in degrees, result
|
||||
// in metres as (easting, northing).
|
||||
glm::dvec2 puwg1992(double const Latitude, double const Longitude);
|
||||
|
||||
} // namespace editor
|
||||
|
||||
} // export
|
||||
85
editor/turnouts.cpp
Normal file
85
editor/turnouts.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
module;
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "maj0sted/domain/geometry/turnout.hpp"
|
||||
|
||||
module eu07.editor.turnouts;
|
||||
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
namespace editor
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// the blade ends where the gap to the stock rail takes the przekladka in its heel.
|
||||
// this is what fixes the length of a curved blade, so it is said once here.
|
||||
// TODO: dlugosci iglic z tablic Id-1, gdy beda pod reka
|
||||
double const heel_offset{0.136}; // metres
|
||||
// 49E1 railhead. the blade is planed from its tip until it is this wide
|
||||
double const railtop_width{0.070};
|
||||
|
||||
// a turnout with an iglica lukowa styczna do opornicy o scietym ostrzu (Koc, rys. 5.9).
|
||||
// the curve is theoretically tangent to the opornica at PR, but the blade cannot begin
|
||||
// there - it begins where there is metal enough, at the ostrze, and its planed nose (d)
|
||||
// runs on to the point where the rail is u thick. what lies between PR and the ostrze is
|
||||
// the odcinek przediglicowy: the curve is already there, the vehicle is still on the
|
||||
// through track. Koc's tablica 5.1 gives a, d and u per type, and they obey
|
||||
// a + d = the run to a gap of u, taken on the blade's curve R + s/2
|
||||
// so a is derived from u and d here rather than copied. The drawing dimensions it on the
|
||||
// rail and a piece length is on the axis, so what goes in is the run scaled by R/(R+s/2);
|
||||
// against the table itself that is a tenth of a millimetre at 1200-1:18,5, six at 190-1:9.
|
||||
//
|
||||
// b is the rear tangent from the drawing: a rozjazd podstawowy (krzyzownica lukowa) has
|
||||
// b = R*tg(alfa/2) and no straight at all, while a straight crossing carries the arc out
|
||||
// on a prosta krzyzownicowa of b - R*tg(alfa/2). Passing 0 asks for the podstawowy.
|
||||
turnout_preset curved_tangent(std::string name, double crossing_n, double radius, double tip_thickness, double nose, double back_tangent = 0.0)
|
||||
{
|
||||
auto const alfa{std::atan(1.0 / crossing_n)};
|
||||
auto const tangent{radius * std::tan(0.5 * alfa)};
|
||||
auto const b{back_tangent > 0.0 ? back_tangent : tangent};
|
||||
auto const lead{maj0sted::domain::blade_run_to_offset(radius, 0.0, tip_thickness) - nose};
|
||||
auto const blade{maj0sted::domain::blade_run_to_offset(radius, 0.0, heel_offset) - lead};
|
||||
|
||||
turnout_preset preset{std::move(name), crossing_n, tangent + b, tip_thickness, nose, railtop_width, {}};
|
||||
preset.pieces.push_back({0, lead, radius, radius, 0.0});
|
||||
preset.pieces.push_back({1, blade, radius, radius, 0.0});
|
||||
preset.pieces.push_back({2, radius * alfa - lead - blade, radius, radius, 0.0});
|
||||
if (b - tangent > 1e-6)
|
||||
{
|
||||
preset.pieces.push_back({3, b - tangent, 0.0, 0.0, 0.0});
|
||||
}
|
||||
return preset;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<turnout_preset> const &turnout_presets()
|
||||
{
|
||||
// the standard PKP rozjazdy zwyczajne. u and d come from Koc, tablica 5.1 (iglice
|
||||
// styczne do opornicy o scietym ostrzu); the rear tangents from the catalogue drawings:
|
||||
// rys. 5.13 gives Rz 49E1-190-1:9 as 27138 = 10523 + 16615 (krzyzownica prosta) and
|
||||
// rys. 5.15 gives Rz 49E1-300-1:9 as 33230 = 16615 + 16615 (lukowa, rozjazd podstawowy).
|
||||
// krzyzownice proste sa w 49E1-190-1:9 i 49E1-500-1:14, lukowe w 300-1:9, 500-1:12 i
|
||||
// 1200-1:18,5. TODO: 1:7,5 R190 i 1:14 R500 czekaja na wlasne u/d i styczne z Id-1
|
||||
static std::vector<turnout_preset> const presets{
|
||||
curved_tangent("Rz 1:7,5 R190", 7.5, 190.0, 0.0049, 0.1250),
|
||||
curved_tangent("Rz 1:9 R190", 9.0, 190.0, 0.0049, 0.1250, 16.615),
|
||||
curved_tangent("Rz 1:9 R300", 9.0, 300.0, 0.0044, 0.1250),
|
||||
curved_tangent("Rz 1:12 R500", 12.0, 500.0, 0.0053, 0.1250),
|
||||
curved_tangent("Rz 1:14 R500", 14.0, 500.0, 0.0053, 0.1250, 24.544),
|
||||
curved_tangent("Rz 1:14 R760", 14.0, 760.0, 0.0050, 0.1250),
|
||||
curved_tangent("Rz 1:18,5 R1200", 18.5, 1200.0, 0.0050, 0.1304)};
|
||||
return presets;
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
52
editor/turnouts.cppm
Normal file
52
editor/turnouts.cppm
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
module;
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
export module eu07.editor.turnouts;
|
||||
|
||||
export {
|
||||
|
||||
namespace editor
|
||||
{
|
||||
|
||||
// one piece of a preset's diverging path, in catalogue order from PR. mirrors
|
||||
// maj0sted::editor::TurnoutPieceSpec so this header stays free of the library
|
||||
struct turnout_preset_piece
|
||||
{
|
||||
int part; // 0 przediglicowy, 1 iglica, 2 luk, 3 krzyzownicowa
|
||||
double length; // metres, always explicit - nothing here is fitted
|
||||
double radius_start; // metres; 0 = straight
|
||||
double radius_end; // metres; different from radius_start = clothoid
|
||||
double turn_in; // kat nagiecia iglicy (beta), radians; 0 = tangent to the stock rail
|
||||
};
|
||||
|
||||
// a standard turnout template: its designation, the catalogue numbers the laid path is
|
||||
// checked against (skos 1:n and the length PR->KR along the through track), the pieces it
|
||||
// is laid from, and the two blade dimensions that place its real, cut tip
|
||||
struct turnout_preset
|
||||
{
|
||||
std::string name;
|
||||
double crossing_n; // skos 1:n
|
||||
double length; // catalogue length: a + b, the two tangent legs of the drawing, metres
|
||||
double tip_thickness; // grubosc iglicy na koncu zestruganego dzioba (u), metres
|
||||
double nose; // dziob iglicy: zestrugana dlugosc od ostrza (d), metres
|
||||
double railtop_width; // szerokosc glowki szyny (w), metres
|
||||
std::vector<turnout_preset_piece> pieces;
|
||||
};
|
||||
|
||||
// typical Polish turnouts (rozjazdy zwyczajne). the (skos, R) pairs are the recognised
|
||||
// standard; confirm the piece lengths and the blade against Id-1
|
||||
std::vector<turnout_preset> const &turnout_presets();
|
||||
|
||||
} // namespace editor
|
||||
|
||||
} // export
|
||||
Reference in New Issue
Block a user