mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-19 04:59:20 +02:00
Put Geoportal imagery behind the plan and clear the scenery on a new map
The location picker now shows the real topographic map instead of a hand-drawn outline, fetched as one WMS image for whatever box is on screen; the schematic border is gone and only the city list remains, to jump the view somewhere recognisable. Under the plan itself a georeferenced map gets the orthophoto, tiled through the layout library's own EPSG:2180 grid and cache. What a native build was missing is here: the download (WinHTTP, on worker threads), the decode and the texture. Finished transfers are handed back on the thread that drains them, so both the library's cache and the graphics API stay on one thread. The imagery service turns down browser user agents and fails the odd request even for a plain one, so requests go out as curl and are retried a few times. Starting a new map now drops the loaded scenery and comes back on empty ground, instead of leaving the previous scenery's contents under a plan that has nothing to do with them. The reload is deferred to the top of the frame: taking the editor off the mode stack while it is drawing its own panels would pull the ground out from under them.
This commit is contained in:
@@ -169,6 +169,7 @@ set(SOURCES
|
||||
"editor/editorSettings.cpp"
|
||||
"editor/puwg1992.cpp"
|
||||
"editor/polandmap.cpp"
|
||||
"editor/orthophoto.cpp"
|
||||
"editor/editorTerrain.cpp"
|
||||
"editor/editorTerrainStreamer.cpp"
|
||||
"application/editormode.cpp"
|
||||
|
||||
@@ -609,6 +609,19 @@ void editor_mode::redo_last()
|
||||
|
||||
bool editor_mode::update()
|
||||
{
|
||||
if (Global.editor_reset_scenery)
|
||||
{
|
||||
// starting a new map: drop whatever scenery is loaded and come back on empty ground. done
|
||||
// here rather than where it was asked for, because taking the editor off the mode stack in
|
||||
// the middle of drawing its own panels would pull the ground out from under them
|
||||
Global.editor_reset_scenery = false;
|
||||
Global.editor_startup = true; // the editor is the base mode from here on, whatever started it
|
||||
Global.SceneryFile = "pusta.scn";
|
||||
Application.pop_mode();
|
||||
Application.push_mode(eu07_application::mode::scenarioloader);
|
||||
return true;
|
||||
}
|
||||
|
||||
Timer::UpdateTimers(true);
|
||||
|
||||
simulation::State.update_clocks();
|
||||
|
||||
@@ -151,31 +151,24 @@ void plan_panel::render_location_dialog()
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 const mapsize{560.0f, 460.0f};
|
||||
// the request is one square image, so the view is square too and the pixels map one to one
|
||||
int const mappixels = 512;
|
||||
ImVec2 const mapsize{static_cast<float>(mappixels), static_cast<float>(mappixels)};
|
||||
auto const origin{ImGui::GetCursorScreenPos()};
|
||||
ImGui::InvisibleButton("poland_map", mapsize);
|
||||
auto const hovered{ImGui::IsItemHovered()};
|
||||
|
||||
auto const &outline{editor::poland_outline()};
|
||||
auto const &places{editor::poland_places()};
|
||||
|
||||
// fit the whole country the first time round; afterwards the wheel decides
|
||||
if (m_mapscale <= 0.0)
|
||||
{
|
||||
auto minx{std::numeric_limits<double>::max()};
|
||||
auto miny{std::numeric_limits<double>::max()};
|
||||
auto maxx{std::numeric_limits<double>::lowest()};
|
||||
auto maxy{std::numeric_limits<double>::lowest()};
|
||||
for (auto const &point : outline)
|
||||
{
|
||||
minx = std::min(minx, point.x);
|
||||
maxx = std::max(maxx, point.x);
|
||||
miny = std::min(miny, point.y);
|
||||
maxy = std::max(maxy, point.y);
|
||||
}
|
||||
m_mapviewx = (minx + maxx) * 0.5;
|
||||
m_mapviewy = (miny + maxy) * 0.5;
|
||||
m_mapscale = std::min(mapsize.x / std::max(1.0, maxx - minx), mapsize.y / std::max(1.0, maxy - miny)) * 0.92;
|
||||
glm::dvec2 min;
|
||||
glm::dvec2 max;
|
||||
editor::poland_extent(min, max);
|
||||
m_mapviewx = (min.x + max.x) * 0.5;
|
||||
m_mapviewy = (min.y + max.y) * 0.5;
|
||||
m_mapscale = mapsize.x / std::max({max.x - min.x, max.y - min.y, 1.0});
|
||||
}
|
||||
|
||||
ImVec2 const centre{origin.x + mapsize.x * 0.5f, origin.y + mapsize.y * 0.5f};
|
||||
@@ -203,16 +196,25 @@ void plan_panel::render_location_dialog()
|
||||
}
|
||||
|
||||
auto *drawlist{ImGui::GetWindowDrawList()};
|
||||
drawlist->PushClipRect(origin, ImVec2(origin.x + mapsize.x, origin.y + mapsize.y), true);
|
||||
drawlist->AddRectFilled(origin, ImVec2(origin.x + mapsize.x, origin.y + mapsize.y), IM_COL32(22, 30, 38, 255));
|
||||
auto const mapend{ImVec2(origin.x + mapsize.x, origin.y + mapsize.y)};
|
||||
drawlist->PushClipRect(origin, mapend, true);
|
||||
drawlist->AddRectFilled(origin, mapend, IM_COL32(22, 30, 38, 255));
|
||||
|
||||
std::vector<ImVec2> border;
|
||||
border.reserve(outline.size());
|
||||
for (auto const &point : outline)
|
||||
// the topographic base map behind everything, asked for at exactly the box on screen
|
||||
double viewminx{0.0};
|
||||
double viewminy{0.0};
|
||||
double viewmaxx{0.0};
|
||||
double viewmaxy{0.0};
|
||||
to_map(origin, viewminx, viewmaxy);
|
||||
to_map(mapend, viewmaxx, viewminy);
|
||||
auto const backdrop{m_topomap.texture_for({viewminx, viewminy, viewmaxx, viewmaxy}, mappixels)};
|
||||
if (backdrop != 0)
|
||||
{
|
||||
border.push_back(to_screen(point.x, point.y));
|
||||
// the picture on screen is the one that was fetched, which may lag the current view by a
|
||||
// request; drawing it against its own box keeps it registered while the new one arrives
|
||||
auto const &covered{m_topomap.covered()};
|
||||
drawlist->AddImage(reinterpret_cast<ImTextureID>(static_cast<intptr_t>(backdrop)), to_screen(covered.min_x, covered.max_y), to_screen(covered.max_x, covered.min_y));
|
||||
}
|
||||
drawlist->AddPolyline(border.data(), static_cast<int>(border.size()), IM_COL32(120, 190, 140, 255), true, 1.5f);
|
||||
|
||||
for (auto const &place : places)
|
||||
{
|
||||
@@ -228,7 +230,7 @@ void plan_panel::render_location_dialog()
|
||||
|
||||
drawlist->PopClipRect();
|
||||
|
||||
ImGui::TextDisabled("schematic outline - click to place the scenery's zero, wheel zooms");
|
||||
ImGui::TextDisabled(m_topomap.loading() ? "fetching the topographic map..." : "click to place the scenery's zero, wheel zooms");
|
||||
|
||||
ImGui::SetNextItemWidth(180.0f);
|
||||
ImGui::InputDouble("easting", &m_pickx, 100.0, 1000.0, "%.0f");
|
||||
@@ -284,6 +286,9 @@ void plan_panel::start_map(bool const Georeferenced, double const Originx, doubl
|
||||
camera.Pos.x = 0.0;
|
||||
camera.Pos.z = 0.0;
|
||||
|
||||
// a new map starts on empty ground - whatever scenery was loaded has nothing to do with it
|
||||
Global.editor_reset_scenery = true;
|
||||
|
||||
m_status = Georeferenced ? "map pinned at " + to_string(Originx, 0) + ", " + to_string(Originy, 0) + " (EPSG:2180)" : "fictional map";
|
||||
}
|
||||
|
||||
@@ -347,6 +352,32 @@ void plan_panel::draw_on_scene()
|
||||
// stay readable on top of the drawing
|
||||
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
||||
|
||||
// orthophoto under everything else. only a georeferenced map knows where on the ground it sits,
|
||||
// so a fictional one gets no imagery
|
||||
if (m_showortho && Global.scenery_georeferenced)
|
||||
{
|
||||
auto const centre{Global.pCamera.Pos};
|
||||
double planx{0.0};
|
||||
double plany{0.0};
|
||||
world_to_plan(centre, planx, plany);
|
||||
auto const reach{static_cast<double>(Global.editor_ortho_extent) * 1.4};
|
||||
|
||||
for (auto const &tile : m_ortho.collect({planx - reach, plany - reach, planx + reach, plany + reach}))
|
||||
{
|
||||
ImVec2 corners[4];
|
||||
// the tile is a ground rectangle; on screen it is whatever the camera makes of it
|
||||
auto const ok = world_to_screen(plan_to_world(tile.box.min_x, tile.box.max_y), corners[0]) &&
|
||||
world_to_screen(plan_to_world(tile.box.max_x, tile.box.max_y), corners[1]) &&
|
||||
world_to_screen(plan_to_world(tile.box.max_x, tile.box.min_y), corners[2]) &&
|
||||
world_to_screen(plan_to_world(tile.box.min_x, tile.box.min_y), corners[3]);
|
||||
if (false == ok)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
drawlist->AddImageQuad(reinterpret_cast<ImTextureID>(static_cast<intptr_t>(tile.texture)), corners[0], corners[1], corners[2], corners[3]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ImVec2> points;
|
||||
for (std::size_t n = 0; n < m_solved.size(); ++n)
|
||||
{
|
||||
@@ -458,6 +489,16 @@ void plan_panel::render_toolbar()
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Draw", &m_drawing);
|
||||
if (Global.scenery_georeferenced)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Orthophoto", &m_showortho);
|
||||
if (m_showortho && m_ortho.pending() > 0)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(%d tiles on the way)", static_cast<int>(m_ortho.pending()));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_document.niwelety.size() > 1)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#include "application/uilayer.h"
|
||||
|
||||
#include "editor/orthophoto.h"
|
||||
#include "maj0sted/app/editor_document.hpp"
|
||||
#include "maj0sted/web/editor.hpp"
|
||||
|
||||
@@ -76,4 +77,8 @@ class plan_panel : public ui_panel
|
||||
double m_mapviewx{0.0};
|
||||
double m_mapviewy{0.0};
|
||||
double m_mapscale{0.0};
|
||||
// topographic base map behind the picker, and the orthophoto under the plan itself
|
||||
editor::wms_image m_topomap{maj0sted::web::WmsConfig::geoportal_topo()};
|
||||
editor::orthophoto_source m_ortho;
|
||||
bool m_showortho{true};
|
||||
};
|
||||
|
||||
440
editor/orthophoto.cpp
Normal file
440
editor/orthophoto.cpp
Normal file
@@ -0,0 +1,440 @@
|
||||
/*
|
||||
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/orthophoto.h"
|
||||
|
||||
#include "stb/stb_image.h"
|
||||
#include "utilities/Logs.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
#pragma comment(lib, "winhttp.lib")
|
||||
#endif
|
||||
|
||||
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
|
||||
|
||||
bool http_get(std::string const &, std::vector<std::uint8_t> &, std::string &)
|
||||
{
|
||||
return false; // no native transport on this platform yet
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
orthophoto_source::orthophoto_source()
|
||||
{
|
||||
// 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::web::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::web::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;
|
||||
}
|
||||
|
||||
std::vector<ready_tile> tiles;
|
||||
for (auto const &key : maj0sted::web::TileGrid::tiles_for_view(View, maj0sted::web::TileGrid::kLevel, Maxtiles))
|
||||
{
|
||||
auto const status = m_service.request(key);
|
||||
if (status == maj0sted::web::TileStatus::Failed)
|
||||
{
|
||||
++m_failed;
|
||||
continue;
|
||||
}
|
||||
if (status != maj0sted::web::TileStatus::Ready)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto found = m_textures.find(key);
|
||||
if (found == m_textures.end())
|
||||
{
|
||||
found = m_textures.emplace(key, upload(key)).first;
|
||||
}
|
||||
if (found->second != 0)
|
||||
{
|
||||
tiles.push_back({maj0sted::web::TileGrid::bbox(key), found->second});
|
||||
}
|
||||
}
|
||||
|
||||
return tiles;
|
||||
}
|
||||
|
||||
unsigned int orthophoto_source::upload(maj0sted::web::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::size_t orthophoto_source::pending() const
|
||||
{
|
||||
return m_inflight.load();
|
||||
}
|
||||
|
||||
std::size_t orthophoto_source::failed() const
|
||||
{
|
||||
return m_failed;
|
||||
}
|
||||
|
||||
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::web::TileBBox const &Left, maj0sted::web::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::web::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::web::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::web::wms_getmap_url(config, Box);
|
||||
m_haswork = true;
|
||||
}
|
||||
m_loading = true;
|
||||
m_wakeup.notify_one();
|
||||
}
|
||||
|
||||
return m_texture;
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
124
editor/orthophoto.h
Normal file
124
editor/orthophoto.h
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
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 <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "maj0sted/web/tile_cache.hpp"
|
||||
|
||||
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::web::TileBBox box;
|
||||
unsigned int texture{0};
|
||||
};
|
||||
|
||||
orthophoto_source();
|
||||
~orthophoto_source();
|
||||
orthophoto_source(orthophoto_source const &) = delete;
|
||||
orthophoto_source &operator=(orthophoto_source const &) = delete;
|
||||
|
||||
// asks for every tile covering the view and returns those that are already drawable. tiles still
|
||||
// on their way simply aren't in the answer yet; call again next frame
|
||||
std::vector<ready_tile> collect(maj0sted::web::TileBBox const &View, int const Maxtiles = 48);
|
||||
// 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::web::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::web::TileKey const &Key);
|
||||
// members
|
||||
maj0sted::web::TileService m_service;
|
||||
std::unordered_map<maj0sted::web::TileKey, unsigned int, maj0sted::web::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::size_t m_failed{0};
|
||||
};
|
||||
|
||||
// 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::web::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::web::TileBBox const &Box, int const Pixels);
|
||||
// the box the returned texture actually covers
|
||||
maj0sted::web::TileBBox const &covered() const { return m_covered; }
|
||||
bool loading() const { return m_loading.load(); }
|
||||
|
||||
private:
|
||||
void worker();
|
||||
|
||||
maj0sted::web::WmsConfig m_config;
|
||||
maj0sted::web::TileBBox m_requested{};
|
||||
maj0sted::web::TileBBox m_inflightbox{};
|
||||
maj0sted::web::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
|
||||
@@ -26,21 +26,10 @@ struct geo_point
|
||||
double longitude;
|
||||
};
|
||||
|
||||
// the border walked clockwise from the north-west corner: Baltic coast, the Kaliningrad and
|
||||
// Lithuanian borders, the Bug, the Carpathians and Sudetes, then the Nysa and Odra back north.
|
||||
// tens of kilometres of error in places - this is a locator sketch, nothing more
|
||||
constexpr geo_point kOutline[]{
|
||||
{53.93, 14.27}, {54.05, 14.60}, {54.18, 15.58}, {54.42, 16.41}, {54.58, 16.86}, {54.76, 17.55},
|
||||
{54.79, 18.40}, {54.60, 18.65}, {54.52, 18.55}, {54.35, 18.95}, {54.37, 19.35}, {54.45, 19.64},
|
||||
{54.37, 20.50}, {54.35, 21.50}, {54.36, 22.00}, {54.36, 22.79}, {54.25, 23.20}, {54.15, 23.48},
|
||||
{53.95, 23.90}, {53.20, 23.93}, {52.70, 23.85}, {52.30, 23.60}, {52.07, 23.62}, {51.55, 23.55},
|
||||
{50.87, 24.15}, {50.72, 24.15}, {50.40, 23.70}, {50.28, 23.50}, {49.80, 22.92}, {49.20, 22.68},
|
||||
{49.09, 22.55}, {49.25, 22.00}, {49.42, 21.75}, {49.35, 21.00}, {49.40, 20.50}, {49.20, 20.10},
|
||||
{49.40, 19.80}, {49.40, 19.50}, {49.40, 19.20}, {49.51, 18.85}, {49.75, 18.63}, {49.95, 18.05},
|
||||
{50.10, 17.70}, {50.31, 17.38}, {50.45, 16.90}, {50.20, 16.60}, {50.40, 16.35}, {50.44, 16.25},
|
||||
{50.75, 15.80}, {50.75, 15.55}, {50.85, 15.00}, {50.87, 14.82}, {51.30, 14.95}, {51.60, 14.75},
|
||||
{52.00, 14.70}, {52.35, 14.55}, {52.59, 14.65}, {52.85, 14.40}, {53.10, 14.35}, {53.55, 14.30},
|
||||
{53.75, 14.28}};
|
||||
// the country's corners, 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
|
||||
constexpr geo_point kSouthWest{49.00, 14.10};
|
||||
constexpr geo_point kNorthEast{54.90, 24.20};
|
||||
|
||||
struct named_geo_point
|
||||
{
|
||||
@@ -59,19 +48,17 @@ constexpr named_geo_point kPlaces[]{
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<glm::dvec2> const &poland_outline()
|
||||
void poland_extent(glm::dvec2 &Min, glm::dvec2 &Max)
|
||||
{
|
||||
static std::vector<glm::dvec2> const outline = []() {
|
||||
std::vector<glm::dvec2> points;
|
||||
points.reserve(std::size(kOutline));
|
||||
for (auto const &point : kOutline)
|
||||
{
|
||||
points.push_back(puwg1992(point.latitude, point.longitude));
|
||||
}
|
||||
return points;
|
||||
}();
|
||||
// the projection is not axis-aligned with the graticule, so the extreme corners come from
|
||||
// projecting all four of them and taking the outermost values
|
||||
auto const sw = puwg1992(kSouthWest.latitude, kSouthWest.longitude);
|
||||
auto const se = puwg1992(kSouthWest.latitude, kNorthEast.longitude);
|
||||
auto const nw = puwg1992(kNorthEast.latitude, kSouthWest.longitude);
|
||||
auto const ne = puwg1992(kNorthEast.latitude, kNorthEast.longitude);
|
||||
|
||||
return outline;
|
||||
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})};
|
||||
}
|
||||
|
||||
std::vector<map_place> const &poland_places()
|
||||
|
||||
@@ -24,11 +24,10 @@ struct map_place
|
||||
glm::dvec2 position;
|
||||
};
|
||||
|
||||
// schematic outline of the country's border, in EPSG:2180 metres. coarse on purpose: it is here so
|
||||
// a region can be pointed at, not so anything can be measured against it
|
||||
std::vector<glm::dvec2> const &poland_outline();
|
||||
|
||||
// larger cities, to put the view somewhere recognisable before picking the exact spot
|
||||
std::vector<map_place> const &poland_places();
|
||||
|
||||
// the whole country's extent in EPSG:2180 metres, as the starting view of a location picker
|
||||
void poland_extent(glm::dvec2 &Min, glm::dvec2 &Max);
|
||||
|
||||
} // namespace editor
|
||||
|
||||
@@ -86,6 +86,9 @@ struct global_settings {
|
||||
// nie znaczą nic poza nią samą
|
||||
bool scenery_georeferenced{ false };
|
||||
glm::dvec2 scenery_origin{ 0.0, 0.0 };
|
||||
// prośba o zrzucenie wczytanej scenerii i zaczęcie na pustym terenie; obsługiwana na początku
|
||||
// klatki, bo zmiana trybu w środku rysowania panelu wyrwałaby grunt spod niego samego
|
||||
bool editor_reset_scenery{ false };
|
||||
std::string local_start_vehicle{ "EU07-424" };
|
||||
int iConvertModels{ 0 }; // tworzenie plików binarnych
|
||||
int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan
|
||||
|
||||
Reference in New Issue
Block a user