From 0f9b00c74a2be142399ff73ba8b8c6527cedc4f9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 13:20:25 +0200 Subject: [PATCH] feat(editor): a map of Poland to pick a place on, shared by the scenery wizard and the terrain model --- CMakeLists.txt | 1 + editor/editorGeoMap.cpp | 327 ++++++++++++++++++++++++++++++++++++ editor/editorGeoMap.hpp | 80 +++++++++ launcher/scenery_wizard.cpp | 237 ++------------------------ launcher/scenery_wizard.h | 31 +--- 5 files changed, 425 insertions(+), 251 deletions(-) create mode 100644 editor/editorGeoMap.cpp create mode 100644 editor/editorGeoMap.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 84ac684e..9d85d027 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,6 +204,7 @@ set(SOURCES "editor/editorOrthophoto.cpp" "editor/editorHttp.cpp" "editor/editorNmt.cpp" +"editor/editorGeoMap.cpp" "editor/editorPreviews.cpp" "editor/editorIcons.cpp" "editor/editorClips.cpp" diff --git a/editor/editorGeoMap.cpp b/editor/editorGeoMap.cpp new file mode 100644 index 00000000..52e837e5 --- /dev/null +++ b/editor/editorGeoMap.cpp @@ -0,0 +1,327 @@ +/* +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/editorGeoMap.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorOrthophoto.hpp" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "rendering/renderer.h" +#include "stb/stb_image.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + +double constexpr kSemiMajor{6378137.0}; +double constexpr kFlattening{1.0 / 298.257222101}; +double constexpr kScale{0.9993}; +double constexpr kFalseEasting{500000.0}; +double constexpr kFalseNorthing{-5300000.0}; +double constexpr kCentralMeridian{19.0}; +glm::dvec2 const kPolandLow{140000.0, 120000.0}; +glm::dvec2 const kPolandHigh{890000.0, 800000.0}; +std::string const kMapService{"https://mapy.geoportal.gov.pl/wss/ext/OSM/BaseMap/service"}; + +double seconds() +{ + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +} // namespace + +namespace editor_geo +{ + +glm::dvec2 to_puwg(double const Latitude, double const Longitude) +{ + auto const e2{2.0 * kFlattening - kFlattening * kFlattening}; + auto const ep2{e2 / (1.0 - e2)}; + auto const phi{glm::radians(Latitude)}; + auto const sine{std::sin(phi)}; + auto const cosine{std::cos(phi)}; + auto const n{kSemiMajor / std::sqrt(1.0 - e2 * sine * sine)}; + auto const t{std::tan(phi) * std::tan(phi)}; + auto const c{ep2 * cosine * cosine}; + auto const a{glm::radians(Longitude - kCentralMeridian) * cosine}; + auto const m{kSemiMajor * ((1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0) * phi - (3.0 * e2 / 8.0 + 3.0 * e2 * e2 / 32.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(2.0 * phi) + + (15.0 * e2 * e2 / 256.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(4.0 * phi) - (35.0 * e2 * e2 * e2 / 3072.0) * std::sin(6.0 * phi))}; + auto const x{kScale * n * (a + (1.0 - t + c) * std::pow(a, 3) / 6.0 + (5.0 - 18.0 * t + t * t + 72.0 * c - 58.0 * ep2) * std::pow(a, 5) / 120.0)}; + auto const y{kScale * (m + n * std::tan(phi) * (a * a / 2.0 + (5.0 - t + 9.0 * c + 4.0 * c * c) * std::pow(a, 4) / 24.0 + (61.0 - 58.0 * t + t * t + 600.0 * c - 330.0 * ep2) * std::pow(a, 6) / 720.0))}; + return {kFalseEasting + x, y + kFalseNorthing}; +} + +glm::dvec2 to_degrees(glm::dvec2 const &Point) +{ + auto const e2{2.0 * kFlattening - kFlattening * kFlattening}; + auto const ep2{e2 / (1.0 - e2)}; + auto const x{Point.x - kFalseEasting}; + auto const m{(Point.y - kFalseNorthing) / kScale}; + auto const mu{m / (kSemiMajor * (1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0))}; + auto const e1{(1.0 - std::sqrt(1.0 - e2)) / (1.0 + std::sqrt(1.0 - e2))}; + auto const phi1{mu + (3.0 * e1 / 2.0 - 27.0 * std::pow(e1, 3) / 32.0) * std::sin(2.0 * mu) + (21.0 * e1 * e1 / 16.0 - 55.0 * std::pow(e1, 4) / 32.0) * std::sin(4.0 * mu) + + (151.0 * std::pow(e1, 3) / 96.0) * std::sin(6.0 * mu) + (1097.0 * std::pow(e1, 4) / 512.0) * std::sin(8.0 * mu)}; + auto const sine{std::sin(phi1)}; + auto const c1{ep2 * std::cos(phi1) * std::cos(phi1)}; + auto const t1{std::tan(phi1) * std::tan(phi1)}; + auto const n1{kSemiMajor / std::sqrt(1.0 - e2 * sine * sine)}; + auto const r1{kSemiMajor * (1.0 - e2) / std::pow(1.0 - e2 * sine * sine, 1.5)}; + auto const d{x / (n1 * kScale)}; + auto const phi{phi1 - (n1 * std::tan(phi1) / r1) * (d * d / 2.0 - (5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * ep2) * std::pow(d, 4) / 24.0 + + (61.0 + 90.0 * t1 + 298.0 * c1 + 45.0 * t1 * t1 - 252.0 * ep2 - 3.0 * c1 * c1) * std::pow(d, 6) / 720.0)}; + auto const lambda{(d - (1.0 + 2.0 * t1 + c1) * std::pow(d, 3) / 6.0 + (5.0 - 2.0 * c1 + 28.0 * t1 - 3.0 * c1 * c1 + 8.0 * ep2 + 24.0 * t1 * t1) * std::pow(d, 5) / 120.0) / std::cos(phi1)}; + return {glm::degrees(phi), kCentralMeridian + glm::degrees(lambda)}; +} + +bool in_poland(glm::dvec2 const &Point) +{ + return Point.x >= kPolandLow.x && Point.x <= kPolandHigh.x && Point.y >= kPolandLow.y && Point.y <= kPolandHigh.y; +} + +bool parse_place(std::string const &Text, glm::dvec2 &Point) +{ + // the letters and the signs around the numbers out, as of "52.2297° N, 21.0122° E" or "X: 456787 Y: 579897" + std::string text; + for (unsigned char const letter : Text) + text += std::isdigit(letter) || letter == '.' || letter == ',' || letter == '-' ? static_cast(letter) : ' '; + std::vector words; + for (std::size_t at{0}; at < text.size();) + { + auto const begin{text.find_first_not_of(" ,", at)}; + if (begin == std::string::npos) + break; + auto end{text.find(' ', begin)}; + if (end == std::string::npos) + end = text.size(); + auto word{text.substr(begin, end - begin)}; + while (false == word.empty() && word.back() == ',') + word.pop_back(); + words.push_back(word); + at = end; + } + std::vector numbers; + if (words.size() == 1) + { + // one word: the comma parts the numbers, or the numbers and their decimals + std::vector parts; + for (std::size_t at{0};;) + { + auto const comma{words[0].find(',', at)}; + parts.push_back(words[0].substr(at, comma == std::string::npos ? std::string::npos : comma - at)); + if (comma == std::string::npos) + break; + at = comma + 1; + } + if (parts.size() == 2) + numbers = parts; + else if (parts.size() == 4) + numbers = {parts[0] + "." + parts[1], parts[2] + "." + parts[3]}; + } + else if (words.size() == 2) + for (auto word : words) + { + std::replace(word.begin(), word.end(), ',', '.'); + numbers.push_back(word); + } + if (numbers.size() != 2) + return false; + double values[2]; + for (int i = 0; i < 2; ++i) + { + char *end{nullptr}; + values[i] = std::strtod(numbers[i].c_str(), &end); + if (end == numbers[i].c_str() || *end != '\0') + return false; + } + auto const latitude = [](double const Value) { return Value >= 48.0 && Value <= 56.0; }; + auto const longitude = [](double const Value) { return Value >= 13.0 && Value <= 25.0; }; + if (latitude(values[0]) && longitude(values[1])) + Point = to_puwg(values[0], values[1]); + else if (longitude(values[0]) && latitude(values[1])) + Point = to_puwg(values[1], values[0]); + else if (values[0] > 1000.0 && values[1] > 1000.0) + Point = {values[1], values[0]}; + else + return false; + return in_poland(Point); +} + +} // namespace editor_geo + +editor_map::~editor_map() +{ + if (m_request.valid()) + m_request.wait(); + release(); +} + +void editor_map::release() +{ + if (m_texture != 0 && GfxRenderer && GfxRenderer->GetImguiRenderer() != nullptr) + GfxRenderer->GetImguiRenderer()->Release_Image(m_texture); + m_texture = 0; +} + +void editor_map::pick(glm::dvec2 const &Point) +{ + m_point = Point; + m_picked = true; +} + +void editor_map::show(glm::dvec2 const &Point, double const Scale) +{ + m_fitted = true; + m_centre = Point; + m_scale = std::min(m_scale, Scale); + m_dirty = true; +} + +bool editor_map::render(glm::vec2 const &Size) +{ + upload(); + auto const origin{ImGui::GetCursorScreenPos()}; + if (false == m_fitted) + { + m_fitted = true; + m_centre = (kPolandLow + kPolandHigh) * 0.5; + m_scale = std::max((kPolandHigh.x - kPolandLow.x) / Size.x, (kPolandHigh.y - kPolandLow.y) / Size.y); + } + ImGui::InvisibleButton("##map", ImVec2(Size.x, Size.y)); + auto const hovered{ImGui::IsItemHovered()}; + auto const active{ImGui::IsItemActive()}; + auto const &io{ImGui::GetIO()}; + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + glm::vec2 const middle{origin.x + Size.x * 0.5f, origin.y + Size.y * 0.5f}; + auto const to_world = [&](glm::vec2 const &Screen) { return glm::dvec2{m_centre.x + (Screen.x - middle.x) * m_scale, m_centre.y - (Screen.y - middle.y) * m_scale}; }; + auto const to_screen = [&](glm::dvec2 const &World) { return ImVec2(middle.x + static_cast((World.x - m_centre.x) / m_scale), middle.y - static_cast((World.y - m_centre.y) / m_scale)); }; + + if (hovered && io.MouseWheel != 0.f) + { + auto const under{to_world(mouse)}; + m_scale = std::clamp(m_scale * std::pow(0.8, static_cast(io.MouseWheel)), 0.25, 2000.0); + m_centre = {under.x - (mouse.x - middle.x) * m_scale, under.y + (mouse.y - middle.y) * m_scale}; + m_dirty = true; + m_changed = seconds(); + } + if (ImGui::IsItemActivated()) + { + m_pressed = mouse; + m_dragging = false; + } + if (active) + { + if (glm::length(mouse - m_pressed) > 4.f) + m_dragging = true; + if (m_dragging && (io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f)) + { + m_centre.x -= io.MouseDelta.x * m_scale; + m_centre.y += io.MouseDelta.y * m_scale; + m_dirty = true; + m_changed = seconds(); + } + } + auto const clicked{ImGui::IsItemDeactivated() && false == m_dragging}; + if (clicked) + pick(to_world(mouse)); + + if (m_dirty && seconds() - m_changed > 0.25 && false == m_request.valid()) + request(Size); + + auto *drawlist{ImGui::GetWindowDrawList()}; + drawlist->PushClipRect(origin, ImVec2(origin.x + Size.x, origin.y + Size.y), true); + drawlist->AddRectFilled(origin, ImVec2(origin.x + Size.x, origin.y + Size.y), IM_COL32(200, 214, 222, 255)); + if (m_texture != 0) + drawlist->AddImage(static_cast(m_texture), to_screen({m_image_low.x, m_image_high.y}), to_screen({m_image_high.x, m_image_low.y}), ImVec2(0, 1), ImVec2(1, 0)); + if (m_picked) + { + auto const at{to_screen(m_point)}; + auto const colour{IM_COL32(220, 30, 30, 255)}; + drawlist->AddCircle(at, 10.f, colour, 24, 2.5f); + drawlist->AddLine(ImVec2(at.x - 16.f, at.y), ImVec2(at.x + 16.f, at.y), colour, 2.f); + drawlist->AddLine(ImVec2(at.x, at.y - 16.f), ImVec2(at.x, at.y + 16.f), colour, 2.f); + } + std::string info; + if (hovered) + { + auto const world{to_world(mouse)}; + auto const degrees{editor_geo::to_degrees(world)}; + info = format(STR_C("E %.3f km, N %.3f km (%.5f N, %.5f E)"), world.x / 1000.0, world.y / 1000.0, degrees.x, degrees.y); + } + if (m_request.valid()) + info += (info.empty() ? "" : " ") + std::string{STR_C("loading the map...")}; + else if (false == m_error.empty()) + info += (info.empty() ? "" : " ") + m_error; + if (false == info.empty()) + { + auto const size{ImGui::CalcTextSize(info.c_str())}; + drawlist->AddRectFilled(ImVec2(origin.x + 4.f, origin.y + 4.f), ImVec2(origin.x + 12.f + size.x, origin.y + 8.f + size.y), IM_COL32(0, 0, 0, 170), 4.f); + drawlist->AddText(ImVec2(origin.x + 8.f, origin.y + 6.f), IM_COL32(255, 255, 255, 240), info.c_str()); + } + drawlist->PopClipRect(); + return clicked; +} + +void editor_map::request(glm::vec2 const &Size) +{ + m_dirty = false; + auto const width{std::clamp(static_cast(Size.x), 64, 2048)}; + auto const height{std::clamp(static_cast(Size.y), 64, 2048)}; + glm::dvec2 const half{width * 0.5 * m_scale, height * 0.5 * m_scale}; + auto const low{m_centre - half}; + auto const high{m_centre + half}; + char bbox[160]; + std::snprintf(bbox, sizeof(bbox), "%.1f,%.1f,%.1f,%.1f", low.x, low.y, high.x, high.y); + auto const url{kMapService + "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=osm&STYLES=&SRS=EPSG:2180&BBOX=" + bbox + "&WIDTH=" + std::to_string(width) + "&HEIGHT=" + std::to_string(height) + "&FORMAT=image/png"}; + m_request = std::async(std::launch::async, [url, low, high]() { + image result; + result.low = low; + result.high = high; + std::vector data; + if (false == editor_http_get(url, data, result.error)) + return result; + int w{0}, h{0}, components{0}; + auto *pixels{stbi_load_from_memory(data.data(), static_cast(data.size()), &w, &h, &components, 4)}; + if (pixels == nullptr) + { + result.error = STR("The map service sent no image"); + return result; + } + result.width = w; + result.height = h; + result.pixels.assign(pixels, pixels + static_cast(w) * h * 4); + stbi_image_free(pixels); + return result; + }); +} + +void editor_map::upload() +{ + if (false == m_request.valid() || m_request.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return; + auto result{m_request.get()}; + if (false == result.error.empty() || result.pixels.empty()) + { + m_error = result.error.empty() ? std::string{STR("The map service sent no image")} : STR("No map: ") + result.error; + return; + } + m_error.clear(); + release(); + auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr}; + m_texture = renderer != nullptr ? renderer->Create_Image(result.pixels.data(), result.width, result.height) : 0; + m_image_low = result.low; + m_image_high = result.high; +} diff --git a/editor/editorGeoMap.hpp b/editor/editorGeoMap.hpp new file mode 100644 index 00000000..340c7d25 --- /dev/null +++ b/editor/editorGeoMap.hpp @@ -0,0 +1,80 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +// the place of a scenery in Poland: PUWG 1992 (EPSG:2180), the metres of the Polish maps, as (east, north) +namespace editor_geo +{ + +glm::dvec2 to_puwg(double const Latitude, double const Longitude); +// latitude, longitude +glm::dvec2 to_degrees(glm::dvec2 const &Point); +// two numbers as a map gives them: degrees (latitude, longitude, as the online maps copy them) or the X and Y of PUWG 1992 +// (north, then east, as geoportal.gov.pl shows them); false when they're neither, or the place isn't in Poland +bool parse_place(std::string const &Text, glm::dvec2 &Point); +bool in_poland(glm::dvec2 const &Point); + +} // namespace editor_geo + +// the map of Poland to pick a place on: the base map of OpenStreetMap from geoportal.gov.pl, the wheel zooms it, a drag moves +// it, a click picks the place +class editor_map +{ + public: + editor_map() = default; + ~editor_map(); + editor_map(editor_map const &) = delete; + editor_map &operator=(editor_map const &) = delete; + + // at the cursor, Size big; true when a place was clicked + bool render(glm::vec2 const &Size); + void pick(glm::dvec2 const &Point); + // the map goes there, at most Scale m a pixel + void show(glm::dvec2 const &Point, double const Scale = 4.0); + bool picked() const { return m_picked; } + glm::dvec2 const &point() const { return m_point; } + + private: + struct image + { + glm::dvec2 low{0.0}; + glm::dvec2 high{0.0}; + int width{0}; + int height{0}; + std::vector pixels; + std::string error; + }; + + void request(glm::vec2 const &Size); + void upload(); + void release(); + + glm::dvec2 m_centre{515000.0, 460000.0}; + double m_scale{1000.0}; // m a pixel + bool m_fitted{false}; + double m_changed{0.0}; + bool m_dirty{true}; + glm::vec2 m_pressed{0.f}; + bool m_dragging{false}; + std::future m_request; + std::uint64_t m_texture{0}; + glm::dvec2 m_image_low{0.0}; + glm::dvec2 m_image_high{0.0}; + std::string m_error; + bool m_picked{false}; + glm::dvec2 m_point{0.0}; +}; diff --git a/launcher/scenery_wizard.cpp b/launcher/scenery_wizard.cpp index f53b88f1..092b4b91 100644 --- a/launcher/scenery_wizard.cpp +++ b/launcher/scenery_wizard.cpp @@ -11,15 +11,13 @@ http://mozilla.org/MPL/2.0/. #include "launcher/scenery_wizard.h" #include "application/application.h" #include "editor/editorFormat.hpp" +#include "editor/editorGeoMap.hpp" #include "editor/editorOrthophoto.hpp" #include "editor/editorSettings.hpp" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/translation.h" -#include "stb/stb_image.h" -#include - #include #include #include @@ -32,61 +30,6 @@ http://mozilla.org/MPL/2.0/. namespace { -double constexpr kSemiMajor{6378137.0}; -double constexpr kFlattening{1.0 / 298.257222101}; -double constexpr kScale{0.9993}; -double constexpr kFalseEasting{500000.0}; -double constexpr kFalseNorthing{-5300000.0}; -double constexpr kCentralMeridian{19.0}; -glm::dvec2 const kPolandLow{140000.0, 120000.0}; -glm::dvec2 const kPolandHigh{890000.0, 800000.0}; -std::string const kMapService{"https://mapy.geoportal.gov.pl/wss/ext/OSM/BaseMap/service"}; - -double seconds() -{ - return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); -} - -glm::dvec2 to_puwg(double const Latitude, double const Longitude) -{ - auto const e2{2.0 * kFlattening - kFlattening * kFlattening}; - auto const ep2{e2 / (1.0 - e2)}; - auto const phi{glm::radians(Latitude)}; - auto const sine{std::sin(phi)}; - auto const cosine{std::cos(phi)}; - auto const n{kSemiMajor / std::sqrt(1.0 - e2 * sine * sine)}; - auto const t{std::tan(phi) * std::tan(phi)}; - auto const c{ep2 * cosine * cosine}; - auto const a{glm::radians(Longitude - kCentralMeridian) * cosine}; - auto const m{kSemiMajor * ((1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0) * phi - (3.0 * e2 / 8.0 + 3.0 * e2 * e2 / 32.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(2.0 * phi) + - (15.0 * e2 * e2 / 256.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(4.0 * phi) - (35.0 * e2 * e2 * e2 / 3072.0) * std::sin(6.0 * phi))}; - auto const x{kScale * n * (a + (1.0 - t + c) * std::pow(a, 3) / 6.0 + (5.0 - 18.0 * t + t * t + 72.0 * c - 58.0 * ep2) * std::pow(a, 5) / 120.0)}; - auto const y{kScale * (m + n * std::tan(phi) * (a * a / 2.0 + (5.0 - t + 9.0 * c + 4.0 * c * c) * std::pow(a, 4) / 24.0 + (61.0 - 58.0 * t + t * t + 600.0 * c - 330.0 * ep2) * std::pow(a, 6) / 720.0))}; - return {kFalseEasting + x, y + kFalseNorthing}; -} - -glm::dvec2 to_degrees(glm::dvec2 const &Point) -{ - auto const e2{2.0 * kFlattening - kFlattening * kFlattening}; - auto const ep2{e2 / (1.0 - e2)}; - auto const x{Point.x - kFalseEasting}; - auto const m{(Point.y - kFalseNorthing) / kScale}; - auto const mu{m / (kSemiMajor * (1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0))}; - auto const e1{(1.0 - std::sqrt(1.0 - e2)) / (1.0 + std::sqrt(1.0 - e2))}; - auto const phi1{mu + (3.0 * e1 / 2.0 - 27.0 * std::pow(e1, 3) / 32.0) * std::sin(2.0 * mu) + (21.0 * e1 * e1 / 16.0 - 55.0 * std::pow(e1, 4) / 32.0) * std::sin(4.0 * mu) + - (151.0 * std::pow(e1, 3) / 96.0) * std::sin(6.0 * mu) + (1097.0 * std::pow(e1, 4) / 512.0) * std::sin(8.0 * mu)}; - auto const sine{std::sin(phi1)}; - auto const c1{ep2 * std::cos(phi1) * std::cos(phi1)}; - auto const t1{std::tan(phi1) * std::tan(phi1)}; - auto const n1{kSemiMajor / std::sqrt(1.0 - e2 * sine * sine)}; - auto const r1{kSemiMajor * (1.0 - e2) / std::pow(1.0 - e2 * sine * sine, 1.5)}; - auto const d{x / (n1 * kScale)}; - auto const phi{phi1 - (n1 * std::tan(phi1) / r1) * (d * d / 2.0 - (5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * ep2) * std::pow(d, 4) / 24.0 + - (61.0 + 90.0 * t1 + 298.0 * c1 + 45.0 * t1 * t1 - 252.0 * ep2 - 3.0 * c1 * c1) * std::pow(d, 6) / 720.0)}; - auto const lambda{(d - (1.0 + 2.0 * t1 + c1) * std::pow(d, 3) / 6.0 + (5.0 - 2.0 * c1 + 28.0 * t1 - 3.0 * c1 * c1 + 8.0 * ep2 + 24.0 * t1 * t1) * std::pow(d, 5) / 120.0) / std::cos(phi1)}; - return {glm::degrees(phi), kCentralMeridian + glm::degrees(lambda)}; -} - bool valid_name(std::string const &Name) { return false == Name.empty() && std::all_of(Name.begin(), Name.end(), [](char const Character) { return (Character >= 'a' && Character <= 'z') || (Character >= '0' && Character <= '9') || Character == '_' || Character == '-'; }); @@ -96,22 +39,16 @@ bool valid_name(std::string const &Name) ui::scenerywizard_panel::scenerywizard_panel() : ui_panel(STR("New scenery"), false) {} -ui::scenerywizard_panel::~scenerywizard_panel() -{ - if (m_request.valid()) - m_request.wait(); - if (m_texture != 0) - glDeleteTextures(1, &m_texture); -} +ui::scenerywizard_panel::~scenerywizard_panel() = default; void ui::scenerywizard_panel::render_contents() { - upload_map(); auto const available{ImGui::GetContentRegionAvail()}; auto const form{360.f * Global.ui_scale}; glm::vec2 const map{std::max(200.f, available.x - form - ImGui::GetStyle().ItemSpacing.x), std::max(200.f, available.y)}; ImGui::BeginChild("##wizardmap", ImVec2(map.x, map.y), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - render_map(map - glm::vec2{16.f}); + if (m_map.render(map - glm::vec2{16.f})) + pick(m_map.point()); ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("##wizardform", ImVec2(0.f, map.y), true); @@ -119,151 +56,10 @@ void ui::scenerywizard_panel::render_contents() ImGui::EndChild(); } -void ui::scenerywizard_panel::render_map(glm::vec2 const &Size) -{ - auto const origin{ImGui::GetCursorScreenPos()}; - if (false == m_fitted) - { - m_fitted = true; - m_centre = (kPolandLow + kPolandHigh) * 0.5; - m_scale = std::max((kPolandHigh.x - kPolandLow.x) / Size.x, (kPolandHigh.y - kPolandLow.y) / Size.y); - } - ImGui::InvisibleButton("##map", ImVec2(Size.x, Size.y)); - auto const hovered{ImGui::IsItemHovered()}; - auto const active{ImGui::IsItemActive()}; - auto const &io{ImGui::GetIO()}; - glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; - glm::vec2 const middle{origin.x + Size.x * 0.5f, origin.y + Size.y * 0.5f}; - auto const to_world = [&](glm::vec2 const &Screen) { return glm::dvec2{m_centre.x + (Screen.x - middle.x) * m_scale, m_centre.y - (Screen.y - middle.y) * m_scale}; }; - auto const to_screen = [&](glm::dvec2 const &World) { return ImVec2(middle.x + static_cast((World.x - m_centre.x) / m_scale), middle.y - static_cast((World.y - m_centre.y) / m_scale)); }; - - if (hovered && io.MouseWheel != 0.f) - { - auto const under{to_world(mouse)}; - m_scale = std::clamp(m_scale * std::pow(0.8, static_cast(io.MouseWheel)), 0.25, 2000.0); - m_centre = {under.x - (mouse.x - middle.x) * m_scale, under.y + (mouse.y - middle.y) * m_scale}; - m_dirty = true; - m_changed = seconds(); - } - if (ImGui::IsItemActivated()) - { - m_pressed = mouse; - m_dragging = false; - } - if (active) - { - if (glm::length(mouse - m_pressed) > 4.f) - m_dragging = true; - if (m_dragging && (io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f)) - { - m_centre.x -= io.MouseDelta.x * m_scale; - m_centre.y += io.MouseDelta.y * m_scale; - m_dirty = true; - m_changed = seconds(); - } - } - if (ImGui::IsItemDeactivated() && false == m_dragging) - pick(to_world(mouse)); - - if (m_dirty && seconds() - m_changed > 0.25 && false == m_request.valid()) - request_map(Size); - - auto *drawlist{ImGui::GetWindowDrawList()}; - drawlist->PushClipRect(origin, ImVec2(origin.x + Size.x, origin.y + Size.y), true); - drawlist->AddRectFilled(origin, ImVec2(origin.x + Size.x, origin.y + Size.y), IM_COL32(200, 214, 222, 255)); - if (m_texture != 0) - drawlist->AddImage((ImTextureID)(static_cast(m_texture)), to_screen({m_image_low.x, m_image_high.y}), to_screen({m_image_high.x, m_image_low.y})); - if (m_picked) - { - auto const at{to_screen(m_point)}; - auto const colour{IM_COL32(220, 30, 30, 255)}; - drawlist->AddCircle(at, 10.f, colour, 24, 2.5f); - drawlist->AddLine(ImVec2(at.x - 16.f, at.y), ImVec2(at.x + 16.f, at.y), colour, 2.f); - drawlist->AddLine(ImVec2(at.x, at.y - 16.f), ImVec2(at.x, at.y + 16.f), colour, 2.f); - } - std::string info; - if (hovered) - { - auto const world{to_world(mouse)}; - auto const degrees{to_degrees(world)}; - info = format(STR_C("E %.3f km, N %.3f km (%.5f N, %.5f E)"), world.x / 1000.0, world.y / 1000.0, degrees.x, degrees.y); - } - if (m_request.valid()) - info += (info.empty() ? "" : " ") + std::string{STR_C("loading the map...")}; - else if (false == m_map_error.empty()) - info += (info.empty() ? "" : " ") + m_map_error; - if (false == info.empty()) - { - auto const size{ImGui::CalcTextSize(info.c_str())}; - drawlist->AddRectFilled(ImVec2(origin.x + 4.f, origin.y + 4.f), ImVec2(origin.x + 12.f + size.x, origin.y + 8.f + size.y), IM_COL32(0, 0, 0, 170), 4.f); - drawlist->AddText(ImVec2(origin.x + 8.f, origin.y + 6.f), IM_COL32(255, 255, 255, 240), info.c_str()); - } - drawlist->PopClipRect(); -} - -void ui::scenerywizard_panel::request_map(glm::vec2 const &Size) -{ - m_dirty = false; - auto const width{std::clamp(static_cast(Size.x), 64, 2048)}; - auto const height{std::clamp(static_cast(Size.y), 64, 2048)}; - glm::dvec2 const half{width * 0.5 * m_scale, height * 0.5 * m_scale}; - auto const low{m_centre - half}; - auto const high{m_centre + half}; - char bbox[160]; - std::snprintf(bbox, sizeof(bbox), "%.1f,%.1f,%.1f,%.1f", low.x, low.y, high.x, high.y); - auto const url{kMapService + "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=osm&STYLES=&SRS=EPSG:2180&BBOX=" + bbox + "&WIDTH=" + std::to_string(width) + "&HEIGHT=" + std::to_string(height) + "&FORMAT=image/png"}; - m_request = std::async(std::launch::async, [url, low, high, width, height]() { - map_result result; - result.low = low; - result.high = high; - std::vector data; - if (false == editor_http_get(url, data, result.error)) - return result; - int w{0}, h{0}, components{0}; - auto *pixels{stbi_load_from_memory(data.data(), static_cast(data.size()), &w, &h, &components, 4)}; - if (pixels == nullptr) - { - result.error = STR("The map service sent no image"); - return result; - } - result.width = w; - result.height = h; - result.pixels.assign(pixels, pixels + static_cast(w) * h * 4); - stbi_image_free(pixels); - return result; - }); -} - -void ui::scenerywizard_panel::upload_map() -{ - if (false == m_request.valid() || m_request.wait_for(std::chrono::seconds(0)) != std::future_status::ready) - return; - auto result{m_request.get()}; - if (false == result.error.empty() || result.pixels.empty()) - { - m_map_error = result.error.empty() ? std::string{STR("The map service sent no image")} : STR("No map: ") + result.error; - return; - } - m_map_error.clear(); - if (m_texture == 0) - glGenTextures(1, &m_texture); - glBindTexture(GL_TEXTURE_2D, m_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_RGBA8, result.width, result.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, result.pixels.data()); - glBindTexture(GL_TEXTURE_2D, 0); - m_image_low = result.low; - m_image_high = result.high; -} - void ui::scenerywizard_panel::pick(glm::dvec2 const &Point) { - m_point = Point; - m_picked = true; - auto const degrees{to_degrees(Point)}; + m_map.pick(Point); + auto const degrees{editor_geo::to_degrees(Point)}; std::snprintf(m_latlon, sizeof(m_latlon), "%.6f, %.6f", degrees.x, degrees.y); } @@ -302,7 +98,7 @@ void ui::scenerywizard_panel::render_form() ImGui::InputTextWithHint("##title", STR_C("title, written in the file"), m_title, sizeof(m_title)); ImGui::Spacing(); - auto const placed{m_picked || false == m_realistic}; + auto const placed{m_map.picked() || false == m_realistic}; auto const ready{named && false == exists && placed}; if (false == ready) { @@ -322,8 +118,8 @@ void ui::scenerywizard_panel::render_form() ImGui::TextUnformatted(STR_C("Centre of the scenery, PUWG 1992")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("PUWG 1992: the coordinate system of the maps of Poland, in km")); - double east{m_point.x / 1000.0}; - double north{m_point.y / 1000.0}; + double east{m_map.point().x / 1000.0}; + double north{m_map.point().y / 1000.0}; ImGui::SetNextItemWidth(130.f); auto changed{ImGui::InputDouble(STR_C("E, km##east"), &east, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)}; ImGui::SetNextItemWidth(130.f); @@ -331,8 +127,7 @@ void ui::scenerywizard_panel::render_form() if (changed) { pick({east * 1000.0, north * 1000.0}); - m_centre = m_point; - m_dirty = true; + m_map.show(m_map.point(), 1000.0); } ImGui::SetNextItemWidth(-90.f); auto const entered{ImGui::InputTextWithHint("##latlon", STR_C("latitude, longitude"), m_latlon, sizeof(m_latlon), ImGuiInputTextFlags_EnterReturnsTrue)}; @@ -345,10 +140,8 @@ void ui::scenerywizard_panel::render_form() double latitude{0.0}, longitude{0.0}; if (words >> latitude >> longitude && latitude > 45.0 && latitude < 58.0 && longitude > 10.0 && longitude < 28.0) { - pick(to_puwg(latitude, longitude)); - m_centre = m_point; - m_scale = std::min(m_scale, 4.0); - m_dirty = true; + pick(editor_geo::to_puwg(latitude, longitude)); + m_map.show(m_map.point()); m_status.clear(); } else @@ -356,9 +149,9 @@ void ui::scenerywizard_panel::render_form() } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Degrees, as given by the online maps; the map moves there")); - if (m_picked) + if (m_map.picked()) { - auto const degrees{to_degrees(m_point)}; + auto const degrees{editor_geo::to_degrees(m_map.point())}; ImGui::TextDisabled(STR_C("%.5f N, %.5f E"), degrees.x, degrees.y); } ImGui::Spacing(); @@ -387,7 +180,7 @@ bool ui::scenerywizard_panel::create_scenery() char date[32]; std::strftime(date, sizeof(date), "%Y-%m-%d", std::localtime(&now)); char reference[96]; - std::snprintf(reference, sizeof(reference), "//$g PUWG1992 %.3f %.3f", m_point.x / 1000.0, m_point.y / 1000.0); + std::snprintf(reference, sizeof(reference), "//$g PUWG1992 %.3f %.3f", m_map.point().x / 1000.0, m_map.point().y / 1000.0); if (m_realistic) { auto const &stored{EditorSettings.orthophoto()}; diff --git a/launcher/scenery_wizard.h b/launcher/scenery_wizard.h index cd22dff7..fcdf74dc 100644 --- a/launcher/scenery_wizard.h +++ b/launcher/scenery_wizard.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "application/uilayer.h" +#include "editor/editorGeoMap.hpp" #include #include @@ -28,40 +29,12 @@ class scenerywizard_panel : public ui_panel void render_contents() override; private: - struct map_result - { - glm::dvec2 low{0.0}; - glm::dvec2 high{0.0}; - int width{0}; - int height{0}; - std::vector pixels; - std::string error; - }; - - void render_map(glm::vec2 const &Size); void render_form(); - void request_map(glm::vec2 const &Size); - void upload_map(); void pick(glm::dvec2 const &Point); bool create_scenery(); - glm::dvec2 m_centre{515000.0, 460000.0}; - double m_scale{1000.0}; - bool m_fitted{false}; - double m_changed{0.0}; - bool m_dirty{true}; - glm::vec2 m_pressed{0.f}; - bool m_dragging{false}; - - std::future m_request; - unsigned int m_texture{0}; - glm::dvec2 m_image_low{0.0}; - glm::dvec2 m_image_high{0.0}; - std::string m_map_error; - - bool m_picked{false}; + editor_map m_map; bool m_realistic{true}; - glm::dvec2 m_point{0.0}; char m_filename[64]{}; char m_title[128]{}; char m_latlon[64]{};