From bf82c35734cbf0f39894ec98ef83ac8f36c99571 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 31 Jul 2026 19:32:00 +0200 Subject: [PATCH] Georeference a new map against a point in Poland "New map" now asks what is being started. A fictional scenery keeps its own frame, where coordinates mean nothing outside it. A real one pins the scenery's zero to a point picked off a map of Poland, so the plan is drawn in EPSG:2180 - the frame the layout library already works in, and the one Geoportal serves imagery in. The projection is the standard transverse Mercator series for PUWG 1992 on GRS80. Checked against known values: the central meridian comes out at easting 500000 exactly, Warszawa at 637382/486757, Krakow at 567614/244588, Gdansk at 477038/720937. The map behind the picker is a schematic border outline plus the larger cities, projected from geographic coordinates on first use. It is coarse on purpose - it is there to point at a region, not to measure against - and the dialog says so. No network is involved. --- CMakeLists.txt | 2 + application/planpanel.cpp | 221 ++++++++++++++++++++++++++++++++++++-- application/planpanel.h | 12 +++ editor/polandmap.cpp | 92 ++++++++++++++++ editor/polandmap.h | 34 ++++++ editor/puwg1992.cpp | 73 +++++++++++++ editor/puwg1992.h | 22 ++++ utilities/Globals.h | 4 + 8 files changed, 454 insertions(+), 6 deletions(-) create mode 100644 editor/polandmap.cpp create mode 100644 editor/polandmap.h create mode 100644 editor/puwg1992.cpp create mode 100644 editor/puwg1992.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e420d41a..27c46871 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,6 +167,8 @@ set(SOURCES "application/driveruipanels.cpp" "input/editorkeyboardinput.cpp" "editor/editorSettings.cpp" +"editor/puwg1992.cpp" +"editor/polandmap.cpp" "editor/editorTerrain.cpp" "editor/editorTerrainStreamer.cpp" "application/editormode.cpp" diff --git a/application/planpanel.cpp b/application/planpanel.cpp index e0bf2b9a..81fd1d60 100644 --- a/application/planpanel.cpp +++ b/application/planpanel.cpp @@ -11,24 +11,26 @@ http://mozilla.org/MPL/2.0/. #include "application/planpanel.h" #include "application/editormode.h" +#include "editor/polandmap.h" #include "rendering/renderer.h" #include "utilities/Globals.h" namespace { -// the plan works in the project's cartesian frame, the scenery in the engine's world space. with no -// georeference set the two share an origin, so the mapping is just the naming of the axes: easting -// runs along world x, northing against world z +// the plan works in EPSG:2180, the scenery in the engine's world space around its own zero. the +// georeference says which point of the projection that zero stands for; a fictional scenery leaves +// it at the origin, so the two frames differ only in the naming of the axes - easting runs along +// world x, northing against world z glm::dvec3 plan_to_world(double const X, double const Y) { - return {X, 0.0, -Y}; + return {X - Global.scenery_origin.x, 0.0, -(Y - Global.scenery_origin.y)}; } void world_to_plan(glm::dvec3 const &World, double &X, double &Y) { - X = World.x; - Y = -World.z; + X = World.x + Global.scenery_origin.x; + Y = -World.z + Global.scenery_origin.y; } // places a world point on screen using the camera of the most recent colour pass. the engine renders @@ -89,6 +91,200 @@ void plan_panel::render_contents() render_toolbar(); render_gaps(); render_storage(); + render_newmap_dialog(); + render_location_dialog(); +} + +void plan_panel::render_newmap_dialog() +{ + if (false == ImGui::BeginPopupModal("New map", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + return; + } + + ImGui::TextUnformatted("What is this scenery going to be?"); + ImGui::Spacing(); + + if (ImGui::Button("Fictional", ImVec2(160.0f, 0.0f))) + { + start_map(false, 0.0, 0.0); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + ImGui::TextDisabled("coordinates mean nothing outside the scenery"); + + if (ImGui::Button("Real place", ImVec2(160.0f, 0.0f))) + { + // start the picker on the middle of the country, showing the whole of it + m_pickingplace = true; + m_pickx = 500000.0; + m_picky = 350000.0; + m_mapviewx = m_pickx; + m_mapviewy = m_picky; + m_mapscale = 0.0; + ImGui::CloseCurrentPopup(); + ImGui::OpenPopup("Scenery location"); + } + ImGui::SameLine(); + ImGui::TextDisabled("pin the scenery's zero to a point in Poland"); + + ImGui::Spacing(); + if (ImGui::Button("Cancel")) + { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); +} + +void plan_panel::render_location_dialog() +{ + if (m_pickingplace) + { + // the popup has to be opened from the same stack level it is drawn on + ImGui::OpenPopup("Scenery location"); + m_pickingplace = false; + } + + if (false == ImGui::BeginPopupModal("Scenery location", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + return; + } + + ImVec2 const mapsize{560.0f, 460.0f}; + 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::max()}; + auto miny{std::numeric_limits::max()}; + auto maxx{std::numeric_limits::lowest()}; + auto maxy{std::numeric_limits::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; + } + + ImVec2 const centre{origin.x + mapsize.x * 0.5f, origin.y + mapsize.y * 0.5f}; + auto const to_screen = [&](double const X, double const Y) { + return ImVec2{centre.x + static_cast((X - m_mapviewx) * m_mapscale), centre.y - static_cast((Y - m_mapviewy) * m_mapscale)}; + }; + auto const to_map = [&](ImVec2 const &Point, double &X, double &Y) { + X = m_mapviewx + (Point.x - centre.x) / m_mapscale; + Y = m_mapviewy - (Point.y - centre.y) / m_mapscale; + }; + + auto const mouse{ImGui::GetIO().MousePos}; + if (hovered && ImGui::GetIO().MouseWheel != 0.0f) + { + double anchorx{0.0}; + double anchory{0.0}; + to_map(mouse, anchorx, anchory); + m_mapscale = std::clamp(m_mapscale * std::pow(1.2, ImGui::GetIO().MouseWheel), 1e-5, 1.0); + m_mapviewx = anchorx - (mouse.x - centre.x) / m_mapscale; + m_mapviewy = anchory + (mouse.y - centre.y) / m_mapscale; + } + if (hovered && ImGui::IsMouseClicked(0)) + { + to_map(mouse, m_pickx, m_picky); + } + + 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)); + + std::vector border; + border.reserve(outline.size()); + for (auto const &point : outline) + { + border.push_back(to_screen(point.x, point.y)); + } + drawlist->AddPolyline(border.data(), static_cast(border.size()), IM_COL32(120, 190, 140, 255), true, 1.5f); + + for (auto const &place : places) + { + auto const point{to_screen(place.position.x, place.position.y)}; + drawlist->AddCircleFilled(point, 3.0f, IM_COL32(200, 200, 200, 255)); + drawlist->AddText(ImVec2(point.x + 5.0f, point.y - 6.0f), IM_COL32(190, 190, 190, 255), place.name.c_str()); + } + + auto const picked{to_screen(m_pickx, m_picky)}; + drawlist->AddCircleFilled(picked, 5.0f, IM_COL32(255, 210, 90, 255)); + drawlist->AddLine(ImVec2(picked.x - 12.0f, picked.y), ImVec2(picked.x + 12.0f, picked.y), IM_COL32(255, 210, 90, 200)); + drawlist->AddLine(ImVec2(picked.x, picked.y - 12.0f), ImVec2(picked.x, picked.y + 12.0f), IM_COL32(255, 210, 90, 200)); + + drawlist->PopClipRect(); + + ImGui::TextDisabled("schematic outline - click to place the scenery's zero, wheel zooms"); + + ImGui::SetNextItemWidth(180.0f); + ImGui::InputDouble("easting", &m_pickx, 100.0, 1000.0, "%.0f"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(180.0f); + ImGui::InputDouble("northing", &m_picky, 100.0, 1000.0, "%.0f"); + + ImGui::SetNextItemWidth(220.0f); + if (ImGui::BeginCombo("go to", "pick a city")) + { + for (auto const &place : places) + { + if (ImGui::Selectable(place.name.c_str())) + { + m_pickx = place.position.x; + m_picky = place.position.y; + m_mapviewx = m_pickx; + m_mapviewy = m_picky; + } + } + ImGui::EndCombo(); + } + + ImGui::Spacing(); + if (ImGui::Button("Use this place", ImVec2(160.0f, 0.0f))) + { + start_map(true, m_pickx, m_picky); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); +} + +void plan_panel::start_map(bool const Georeferenced, double const Originx, double const Originy) +{ + Global.scenery_georeferenced = Georeferenced; + Global.scenery_origin = {Originx, Originy}; + + m_document.niwelety.clear(); + m_document.niwelety.push_back({"niweleta 1", {}, {}}); + m_niweleta = 0; + m_pending = false; + m_draggedvertex = -1; + solve(); + + // the scenery's zero is where the work starts, whichever frame it stands for + auto &camera{editor_mode::get_camera()}; + camera.Pos.x = 0.0; + camera.Pos.z = 0.0; + + m_status = Georeferenced ? "map pinned at " + to_string(Originx, 0) + ", " + to_string(Originy, 0) + " (EPSG:2180)" : "fictional map"; } void plan_panel::handle_scene() @@ -228,8 +424,21 @@ void plan_panel::render_toolbar() ImGui::Text("top-down view, %.0f m across", Global.editor_ortho_extent * 2.0f); ImGui::TextDisabled("click the scenery to lay points, drag a point to move it, wheel zooms"); + if (Global.scenery_georeferenced) + { + ImGui::Text("zero at %.0f, %.0f (EPSG:2180)", Global.scenery_origin.x, Global.scenery_origin.y); + } + else + { + ImGui::TextDisabled("fictional map, no georeference"); + } ImGui::Separator(); + if (ImGui::Button("New map")) + { + ImGui::OpenPopup("New map"); + } + ImGui::SameLine(); if (ImGui::Button("New niweleta")) { m_document.niwelety.push_back({"niweleta " + std::to_string(m_document.niwelety.size() + 1), {}, {}}); diff --git a/application/planpanel.h b/application/planpanel.h index d5976cab..4a579931 100644 --- a/application/planpanel.h +++ b/application/planpanel.h @@ -32,6 +32,11 @@ class plan_panel : public ui_panel void render_toolbar(); void render_gaps(); void render_storage(); + // asks what kind of scenery is being started, and where on the map it sits + void render_newmap_dialog(); + void render_location_dialog(); + // clears the drawing and sets the point of EPSG:2180 that the scenery's zero stands for + void start_map(bool const Georeferenced, double const Originx, double const Originy); // paints the solved plan over the rendered scenery and takes the clicks that build it void handle_scene(); void draw_on_scene(); @@ -64,4 +69,11 @@ class plan_panel : public ui_panel double m_pendingy{0.0}; std::string m_status; char m_path[256]{"editor/plan.m0s"}; + // location picker: the point being considered, and the view over the schematic map of Poland + bool m_pickingplace{false}; + double m_pickx{0.0}; + double m_picky{0.0}; + double m_mapviewx{0.0}; + double m_mapviewy{0.0}; + double m_mapscale{0.0}; }; diff --git a/editor/polandmap.cpp b/editor/polandmap.cpp new file mode 100644 index 00000000..65b97fef --- /dev/null +++ b/editor/polandmap.cpp @@ -0,0 +1,92 @@ +/* +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/polandmap.h" + +#include "editor/puwg1992.h" + +namespace editor +{ + +namespace +{ + +// geographic coordinates, degrees, as (latitude, longitude); projected to EPSG:2180 on first use so +// the numbers stay readable and there is a single place where the frame is decided +struct geo_point +{ + double latitude; + 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}}; + +struct named_geo_point +{ + char const *name; + geo_point position; +}; + +constexpr named_geo_point kPlaces[]{ + {"Warszawa", {52.23, 21.01}}, {"Krakow", {50.06, 19.94}}, {"Lodz", {51.77, 19.46}}, + {"Wroclaw", {51.11, 17.03}}, {"Poznan", {52.41, 16.93}}, {"Gdansk", {54.35, 18.65}}, + {"Szczecin", {53.43, 14.55}}, {"Bydgoszcz", {53.12, 18.01}}, {"Lublin", {51.25, 22.57}}, + {"Bialystok", {53.13, 23.16}}, {"Katowice", {50.26, 19.02}}, {"Rzeszow", {50.04, 22.00}}, + {"Olsztyn", {53.78, 20.49}}, {"Kielce", {50.87, 20.63}}, {"Zielona Gora", {51.94, 15.51}}, + {"Opole", {50.67, 17.93}}, {"Torun", {53.02, 18.61}}, {"Gorzow Wlkp.", {52.73, 15.24}}, + {"Koszalin", {54.19, 16.18}}, {"Czestochowa", {50.81, 19.12}}}; + +} // namespace + +std::vector const &poland_outline() +{ + static std::vector const outline = []() { + std::vector points; + points.reserve(std::size(kOutline)); + for (auto const &point : kOutline) + { + points.push_back(puwg1992(point.latitude, point.longitude)); + } + return points; + }(); + + return outline; +} + +std::vector const &poland_places() +{ + static std::vector const places = []() { + std::vector result; + result.reserve(std::size(kPlaces)); + for (auto const &place : kPlaces) + { + result.push_back({place.name, puwg1992(place.position.latitude, place.position.longitude)}); + } + return result; + }(); + + return places; +} + +} // namespace editor diff --git a/editor/polandmap.h b/editor/polandmap.h new file mode 100644 index 00000000..f237e040 --- /dev/null +++ b/editor/polandmap.h @@ -0,0 +1,34 @@ +/* +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 + +namespace editor +{ + +// a named point to start a scenery from, in EPSG:2180 metres +struct map_place +{ + std::string name; + 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 const &poland_outline(); + +// larger cities, to put the view somewhere recognisable before picking the exact spot +std::vector const &poland_places(); + +} // namespace editor diff --git a/editor/puwg1992.cpp b/editor/puwg1992.cpp new file mode 100644 index 00000000..599e0dbe --- /dev/null +++ b/editor/puwg1992.cpp @@ -0,0 +1,73 @@ +/* +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/puwg1992.h" + +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 diff --git a/editor/puwg1992.h b/editor/puwg1992.h new file mode 100644 index 00000000..a5c38098 --- /dev/null +++ b/editor/puwg1992.h @@ -0,0 +1,22 @@ +/* +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 + +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 diff --git a/utilities/Globals.h b/utilities/Globals.h index 4ad3c0eb..d218d535 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -82,6 +82,10 @@ struct global_settings { // rzut z góry bez perspektywy, do kreślenia planu torowiska na tle wczytanej scenerii bool editor_ortho{ false }; float editor_ortho_extent{ 250.f }; // połowa wysokości widoku w metrach, czyli zoom rzutu + // punkt EPSG:2180 odpowiadający zeru scenerii. Bez tego sceneria jest fikcyjna i jej współrzędne + // nie znaczą nic poza nią samą + bool scenery_georeferenced{ false }; + glm::dvec2 scenery_origin{ 0.0, 0.0 }; std::string local_start_vehicle{ "EU07-424" }; int iConvertModels{ 0 }; // tworzenie plików binarnych int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan