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