mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-23 06:31:11 +02:00
feat(editor): heightmap terrain from the terrain model of GUGiK, fitted to the tracks of the scenery
- editorNmt: squares of 500 m of the NMT at 1 m from the WCS service of geoportal.gov.pl, as GeoTIFF, fetched in the background, kept in editor/cache/nmt and sampled in the coordinates of the scenery, placed as its orthophoto is - the terrain panel's Terrain model section: Fit to the tracks moves the model up or down by the median of the differences to the formation of the tracks (Biała Podlaska, built at 0: -145.73 m, 0.28 m around it); Along the tracks and Around the camera make the chunks as the model comes in; each path gets its formation and slopes 1:1.5 as a modifier of the terrain, laid over each other into one corridor, bridges and tunnels keep the model; the orthophoto is turned on and laid onto the new ground - the http client of the orthophoto in editorHttp, shared; fill_from_nearest in editormodeshared.h - after the generator of Hirek (MaszynaTerrainGenerator): the WCS requests, the cache of squares, the corridor by envelopes - self-tests nmtfit, nmtmake, nmtwait
This commit is contained in:
@@ -200,6 +200,8 @@ set(SOURCES
|
||||
"editor/editorInfra.cpp"
|
||||
"editor/editorTerrainStreamer.cpp"
|
||||
"editor/editorOrthophoto.cpp"
|
||||
"editor/editorHttp.cpp"
|
||||
"editor/editorNmt.cpp"
|
||||
"editor/editorPreviews.cpp"
|
||||
"editor/editorIcons.cpp"
|
||||
"editor/editorClips.cpp"
|
||||
@@ -213,6 +215,7 @@ set(SOURCES
|
||||
"application/editormoderecords.cpp"
|
||||
"application/editormodeswitchdrive.cpp"
|
||||
"application/editormodeswitchtool.cpp"
|
||||
"application/editormodenmt.cpp"
|
||||
"application/editormodeswitchslide.cpp"
|
||||
"application/editormodeswitchdesign.cpp"
|
||||
"application/editormodetrackbed.cpp"
|
||||
|
||||
@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include "application/editormode.h"
|
||||
#include "application/editormodeshared.h"
|
||||
#include "scene/scenenodegroups.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#ifndef _WIN32
|
||||
@@ -1396,6 +1397,7 @@ bool editor_mode::update()
|
||||
|
||||
// --- geoportal orthophoto: streamed around the camera, drawn beneath the other overlays ---
|
||||
m_orthophoto.update(Camera.Pos);
|
||||
nmt_update();
|
||||
draw_orthophoto();
|
||||
|
||||
// --- ImGuizmo: in-viewport transform gizmo for the selected node ---
|
||||
@@ -1561,35 +1563,6 @@ namespace
|
||||
return changed;
|
||||
}
|
||||
|
||||
// grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid)
|
||||
template <typename Value_> void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
|
||||
{
|
||||
std::deque<std::size_t> queue;
|
||||
for (std::size_t index = 0; index < Found.size(); ++index)
|
||||
if (Found[index] != 0)
|
||||
queue.push_back(index);
|
||||
while (false == queue.empty())
|
||||
{
|
||||
auto const index = queue.front();
|
||||
queue.pop_front();
|
||||
auto const ix = index % Side, iz = index / Side;
|
||||
auto const visit = [&](std::size_t const Next) {
|
||||
if (Found[Next] != 0)
|
||||
return;
|
||||
Found[Next] = 1;
|
||||
Heights[Next] = Heights[index];
|
||||
queue.push_back(Next);
|
||||
};
|
||||
if (ix > 0)
|
||||
visit(index - 1);
|
||||
if (ix + 1 < Side)
|
||||
visit(index + 1);
|
||||
if (iz > 0)
|
||||
visit(index - Side);
|
||||
if (iz + 1 < Side)
|
||||
visit(index + Side);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void editor_mode::render_terrain_ui()
|
||||
@@ -1665,6 +1638,9 @@ void editor_mode::render_terrain_ui()
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"), ImGuiTreeNodeFlags_DefaultOpen))
|
||||
render_nmt_ui();
|
||||
|
||||
if (ImGui::CollapsingHeader(STR_C("Chunk resolution"), m_terrain_tool == terrain_tool::spacing ? ImGuiTreeNodeFlags_DefaultOpen : 0))
|
||||
{
|
||||
// chunks removed since are no longer selected
|
||||
|
||||
@@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "editor/editorTerrain.hpp"
|
||||
#include "editor/editorTerrainStreamer.hpp"
|
||||
#include "editor/editorOrthophoto.hpp"
|
||||
#include "editor/editorNmt.hpp"
|
||||
#include "editor/editorTrack.hpp"
|
||||
#include "editor/editorConnections.hpp"
|
||||
#include "editor/editorAlignment.hpp"
|
||||
@@ -319,6 +320,48 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
// adds a chunk of the terrain shaped as the ground under it; where there's no ground, carried on level with the nearest point
|
||||
// of it, or at the base height. returns: false if there's a chunk already
|
||||
bool add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer = 0);
|
||||
|
||||
// the terrain from the digital terrain model of GUGiK (editormodenmt.cpp)
|
||||
struct nmt_place
|
||||
{
|
||||
glm::dvec3 position{0.0}; // on the axis of the path
|
||||
double formation{0.0}; // the height of the formation under it
|
||||
double half_width{3.0}; // from the axis to the foot of the ballast
|
||||
TTrack *track{nullptr};
|
||||
int path{0};
|
||||
bool shapes{true}; // not a bridge nor a tunnel
|
||||
};
|
||||
struct nmt_state
|
||||
{
|
||||
double offset{0.0}; // added to the heights of the model, so they meet the tracks of the scenery
|
||||
bool fitted{false};
|
||||
bool fitting{false}; // waiting for the model under the places of the tracks the offset is worked out from
|
||||
std::vector<nmt_place> fit;
|
||||
double spread{0.0}; // how much the tracks differ from the model about the offset, m
|
||||
std::size_t samples{0};
|
||||
double band{300.0}; // m from the tracks the chunks made along them reach
|
||||
bool shape{true}; // the formation and the slopes of the tracks as modifiers of the terrain
|
||||
double slope{1.5};
|
||||
double reach{60.0};
|
||||
std::vector<heightmap::chunk_key> pending; // chunks to make once their model is in
|
||||
std::size_t made{0};
|
||||
std::size_t shaped{0};
|
||||
std::string status;
|
||||
std::string error;
|
||||
};
|
||||
editor_nmt m_nmt;
|
||||
nmt_state m_nmt_state;
|
||||
std::vector<nmt_place> nmt_track_places(double const Step) const;
|
||||
void nmt_locate();
|
||||
void nmt_fit();
|
||||
void nmt_finish_fit();
|
||||
std::vector<heightmap::chunk_key> nmt_keys_around() const;
|
||||
std::vector<heightmap::chunk_key> nmt_keys_along_tracks() const;
|
||||
void nmt_make(std::vector<heightmap::chunk_key> const &Keys);
|
||||
bool add_chunk_from_nmt(heightmap::chunk_key const &Key);
|
||||
void nmt_shape_tracks();
|
||||
void nmt_update();
|
||||
void render_nmt_ui();
|
||||
// the tool of the terrain the left mouse button works
|
||||
enum class terrain_tool
|
||||
{
|
||||
|
||||
337
application/editormodenmt.cpp
Normal file
337
application/editormodenmt.cpp
Normal file
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
// the heightmap terrain made from the digital terrain model of GUGiK: the scenery placed as its orthophoto is, the heights
|
||||
// of the model moved up or down to meet the tracks it has already, the chunks around the camera or along the tracks made as
|
||||
// the model comes in, and the formation of each track with its slopes kept as a modifier of the terrain
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "application/editormode.h"
|
||||
#include "application/editormodeshared.h"
|
||||
#include "editor/editorFormat.hpp"
|
||||
#include "editor/editorGeometry.hpp"
|
||||
#include "simulation/simulation.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "utilities/translation.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// how many places of the tracks the offset of the model is worked out from at most, each needing its square of the model
|
||||
std::size_t constexpr fit_places{48};
|
||||
|
||||
bool rail(TTrack const &Track)
|
||||
{
|
||||
return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && false == Track.m_paths.empty();
|
||||
}
|
||||
|
||||
// a bridge or a tunnel keeps the model under it as it is
|
||||
bool shapes_ground(TTrack const &Track)
|
||||
{
|
||||
return Track.eEnvironment != e_bridge && Track.eEnvironment != e_tunnel;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<editor_mode::nmt_place> editor_mode::nmt_track_places(double const Step) const
|
||||
{
|
||||
std::vector<nmt_place> result;
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
{
|
||||
if (track == nullptr || false == rail(*track))
|
||||
continue;
|
||||
auto const ballast{profile_ballast(*track)};
|
||||
for (std::size_t path = 0; path < track->m_paths.size(); ++path)
|
||||
{
|
||||
geometry::bezier const curve{track->m_paths[path]};
|
||||
auto const &points{track->m_paths[path].points};
|
||||
auto const chord{glm::distance(points[segment_data::point::start], points[segment_data::point::end])};
|
||||
auto const count{std::max(1, static_cast<int>(std::ceil(chord * 1.1 / Step)))};
|
||||
for (int i = 0; i <= count; ++i)
|
||||
{
|
||||
auto const at{curve.point(static_cast<double>(i) / count)};
|
||||
result.push_back({at, at.y - ballast.first, ballast.second, track, static_cast<int>(path), shapes_ground(*track)});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void editor_mode::nmt_locate()
|
||||
{
|
||||
auto const &config{m_orthophoto.settings()};
|
||||
m_nmt.place(config.north, config.east);
|
||||
}
|
||||
|
||||
void editor_mode::nmt_fit()
|
||||
{
|
||||
nmt_locate();
|
||||
auto &state{m_nmt_state};
|
||||
state.fit.clear();
|
||||
auto const places{nmt_track_places(25.0)};
|
||||
std::vector<nmt_place> shaped;
|
||||
std::copy_if(places.begin(), places.end(), std::back_inserter(shaped), [](nmt_place const &Place) { return Place.shapes; });
|
||||
if (shaped.empty())
|
||||
{
|
||||
state.offset = 0.0;
|
||||
state.fitted = true;
|
||||
state.status = STR("No tracks to fit the terrain model to: its heights are taken as they are");
|
||||
return;
|
||||
}
|
||||
// spread over the whole scenery, each place asking for its square of the model
|
||||
auto const every{std::max<std::size_t>(1, shaped.size() / fit_places)};
|
||||
for (std::size_t i = 0; i < shaped.size(); i += every)
|
||||
state.fit.push_back(shaped[i]);
|
||||
state.fitting = true;
|
||||
state.status = STR("Fetching the terrain model under the tracks...");
|
||||
}
|
||||
|
||||
void editor_mode::nmt_finish_fit()
|
||||
{
|
||||
auto &state{m_nmt_state};
|
||||
std::vector<double> differences;
|
||||
for (auto const &place : state.fit)
|
||||
if (double height; m_nmt.height(place.position.x, place.position.z, height))
|
||||
differences.push_back(place.formation - height);
|
||||
state.fitting = false;
|
||||
if (differences.empty())
|
||||
{
|
||||
state.status = STR("The terrain model has no data under the tracks: is the scenery placed where it is?");
|
||||
return;
|
||||
}
|
||||
std::sort(differences.begin(), differences.end());
|
||||
state.offset = differences[differences.size() / 2];
|
||||
std::vector<double> deviations;
|
||||
for (auto const difference : differences)
|
||||
deviations.push_back(std::abs(difference - state.offset));
|
||||
std::sort(deviations.begin(), deviations.end());
|
||||
state.spread = deviations[deviations.size() / 2];
|
||||
state.fitted = true;
|
||||
state.samples = differences.size();
|
||||
state.status = format(STR_C("The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)"), state.offset, state.samples, state.spread);
|
||||
if (state.spread > 10.0)
|
||||
state.status += STR(". That's far too much: the place of the scenery is likely wrong, check the orthophoto");
|
||||
WriteLog("Editor: NMT - " + state.status, logtype::generic);
|
||||
}
|
||||
|
||||
std::vector<heightmap::chunk_key> editor_mode::nmt_keys_around() const
|
||||
{
|
||||
std::vector<heightmap::chunk_key> result;
|
||||
auto const centre{heightmap::chunk_at(Camera.Pos.x, Camera.Pos.z)};
|
||||
auto const first{-(m_terrain_add_count - 1) / 2};
|
||||
for (int dz = 0; dz < m_terrain_add_count; ++dz)
|
||||
for (int dx = 0; dx < m_terrain_add_count; ++dx)
|
||||
result.push_back({centre.first + first + dx, centre.second + first + dz});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<heightmap::chunk_key> editor_mode::nmt_keys_along_tracks() const
|
||||
{
|
||||
std::set<heightmap::chunk_key> keys;
|
||||
auto const reach{static_cast<int>(std::ceil(m_nmt_state.band / heightmap::chunk_size))};
|
||||
for (auto const &place : nmt_track_places(heightmap::chunk_size * 0.5))
|
||||
{
|
||||
auto const at{heightmap::chunk_at(place.position.x, place.position.z)};
|
||||
for (int dz = -reach; dz <= reach; ++dz)
|
||||
for (int dx = -reach; dx <= reach; ++dx)
|
||||
{
|
||||
heightmap::chunk_key const key{at.first + dx, at.second + dz};
|
||||
auto const low{heightmap::chunk_corner(key)};
|
||||
// the chunk reaches within the band of the track
|
||||
glm::dvec2 const nearest{std::clamp(place.position.x, low.x, low.x + heightmap::chunk_size), std::clamp(place.position.z, low.y, low.y + heightmap::chunk_size)};
|
||||
if (glm::distance(nearest, glm::dvec2{place.position.x, place.position.z}) <= m_nmt_state.band)
|
||||
keys.insert(key);
|
||||
}
|
||||
}
|
||||
return {keys.begin(), keys.end()};
|
||||
}
|
||||
|
||||
void editor_mode::nmt_make(std::vector<heightmap::chunk_key> const &Keys)
|
||||
{
|
||||
auto &state{m_nmt_state};
|
||||
if (false == ensure_terrain())
|
||||
{
|
||||
state.status = STR("The heightmap terrain can't be opened");
|
||||
return;
|
||||
}
|
||||
nmt_locate();
|
||||
std::size_t queued{0};
|
||||
for (auto const &key : Keys)
|
||||
if (false == m_streamer.exists(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end())
|
||||
{
|
||||
state.pending.push_back(key);
|
||||
++queued;
|
||||
}
|
||||
state.made = 0;
|
||||
// the terrain comes with its photos
|
||||
if (queued > 0 && false == m_orthophoto.enabled())
|
||||
m_orthophoto.enabled(true);
|
||||
state.status = queued > 0 ? format(STR_C("%zu chunks to make from the terrain model"), queued) : STR("There's terrain there already");
|
||||
}
|
||||
|
||||
bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key)
|
||||
{
|
||||
auto const low{heightmap::chunk_corner(Key)};
|
||||
auto const high{low + glm::dvec2{heightmap::chunk_size}};
|
||||
if (false == m_nmt.request(low - glm::dvec2{2.0}, high + glm::dvec2{2.0}))
|
||||
return false;
|
||||
auto const cells{heightmap::cells_for(m_terrain_spacing)};
|
||||
auto const side{static_cast<std::size_t>(cells) + 1};
|
||||
auto const step{heightmap::chunk_size / cells};
|
||||
std::vector<double> heights(side * side, m_terrain_baseheight);
|
||||
std::vector<char> found(side * side, 0);
|
||||
bool any{false};
|
||||
for (std::size_t iz = 0; iz < side; ++iz)
|
||||
for (std::size_t ix = 0; ix < side; ++ix)
|
||||
{
|
||||
auto const index{iz * side + ix};
|
||||
if (double height; m_nmt.height(low.x + ix * step, low.y + iz * step, height))
|
||||
{
|
||||
heights[index] = height + m_nmt_state.offset;
|
||||
found[index] = 1;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
// where the model has no data the chunk goes on level with its nearest point
|
||||
if (any)
|
||||
fill_from_nearest(heights, found, side);
|
||||
m_streamer.add_chunk(Key.first, Key.second, m_terrain_spacing, [&](double const X, double const Z) {
|
||||
auto const ix{static_cast<std::size_t>(std::clamp<long>(std::lround((X - low.x) / step), 0, cells))};
|
||||
auto const iz{static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / step), 0, cells))};
|
||||
return static_cast<float>(heights[iz * side + ix]);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void editor_mode::nmt_shape_tracks()
|
||||
{
|
||||
auto &state{m_nmt_state};
|
||||
if (false == ensure_terrain())
|
||||
return;
|
||||
// one modifier per path: the formation under its axis and the slopes from its edges to the ground. laid over each other the
|
||||
// modifiers keep the highest of the slopes going down and the lowest of those going up, so tracks side by side share one
|
||||
// formation instead of a saw of their own ones
|
||||
std::map<std::pair<TTrack *, int>, heightmap::modifier> modifiers;
|
||||
double constexpr step{5.0};
|
||||
for (auto const &place : nmt_track_places(step))
|
||||
{
|
||||
if (false == place.shapes)
|
||||
continue;
|
||||
auto &modifier{modifiers[{place.track, place.path}]};
|
||||
if (modifier.name.empty())
|
||||
{
|
||||
modifier.name = "nmt " + (place.track->name().empty() ? std::string{"track"} : place.track->name()) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{});
|
||||
modifier.slope = state.slope;
|
||||
modifier.reach = state.reach;
|
||||
}
|
||||
modifier.points.push_back({{place.position.x, place.position.z}, place.formation, place.half_width});
|
||||
}
|
||||
for (auto &[path, modifier] : modifiers)
|
||||
if (modifier.points.size() >= 2)
|
||||
m_streamer.modifier(std::move(modifier));
|
||||
forget_ground();
|
||||
state.shaped = modifiers.size();
|
||||
}
|
||||
|
||||
void editor_mode::nmt_update()
|
||||
{
|
||||
auto &state{m_nmt_state};
|
||||
if (state.fitting)
|
||||
{
|
||||
bool all{true};
|
||||
for (auto const &place : state.fit)
|
||||
all = m_nmt.request(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all;
|
||||
if (all)
|
||||
nmt_finish_fit();
|
||||
return;
|
||||
}
|
||||
if (state.pending.empty())
|
||||
return;
|
||||
// a few chunks a frame, those whose model is in
|
||||
int made{0};
|
||||
for (auto it{state.pending.begin()}; it != state.pending.end() && made < 4;)
|
||||
{
|
||||
if (m_streamer.exists(*it))
|
||||
{
|
||||
it = state.pending.erase(it);
|
||||
continue;
|
||||
}
|
||||
if (add_chunk_from_nmt(*it))
|
||||
{
|
||||
it = state.pending.erase(it);
|
||||
++made;
|
||||
++state.made;
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
if (made > 0)
|
||||
{
|
||||
forget_ground();
|
||||
state.status = format(STR_C("Chunks made from the terrain model: %zu, waiting for it: %zu"), state.made, state.pending.size());
|
||||
if (state.pending.empty())
|
||||
{
|
||||
if (state.shape)
|
||||
{
|
||||
nmt_shape_tracks();
|
||||
state.status += format(STR_C("; the formation of %zu paths shaped"), state.shaped);
|
||||
}
|
||||
// the photos laid onto the new ground
|
||||
m_orthophoto.refit();
|
||||
}
|
||||
}
|
||||
if (auto const error{m_nmt.error()}; false == error.empty())
|
||||
state.error = error;
|
||||
}
|
||||
|
||||
void editor_mode::render_nmt_ui()
|
||||
{
|
||||
auto &state{m_nmt_state};
|
||||
nmt_locate();
|
||||
if (false == m_nmt.placed())
|
||||
{
|
||||
ImGui::TextWrapped("%s", STR_C("The scenery isn't placed anywhere: give its place in the settings of the orthophoto first"));
|
||||
if (ImGui::Button(STR_C("Orthophoto settings...")))
|
||||
m_orthophoto_window = true;
|
||||
return;
|
||||
}
|
||||
auto const &config{m_orthophoto.settings()};
|
||||
ImGui::TextDisabled(STR_C("Place: N %.0f, E %.0f (PUWG 1992), as the orthophoto"), config.north, config.east);
|
||||
if (ImGui::Button(STR_C("Fit to the tracks")))
|
||||
nmt_fit();
|
||||
item_tooltip("Compares the formation of the tracks with the terrain model and moves the model up or down to meet them, as a scenery built at height 0 needs");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
ImGui::InputDouble(STR_C("Height offset (m)"), &state.offset, 0.0, 0.0, "%.2f");
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
ImGui::InputDouble(STR_C("Band along the tracks (m)"), &state.band, 0.0, 0.0, "%.0f");
|
||||
state.band = std::clamp(state.band, 50.0, 3000.0);
|
||||
ImGui::Checkbox(STR_C("Formation and slopes along the tracks"), &state.shape);
|
||||
item_tooltip("Each track gets its formation with slopes 1:1.5 down or up to the terrain, as a modifier of the terrain; bridges and tunnels keep the model under them");
|
||||
ImGui::BeginDisabled(state.fitting);
|
||||
if (ImGui::Button(STR_C("Along the tracks")))
|
||||
nmt_make(nmt_keys_along_tracks());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(STR_C("Around the camera")))
|
||||
nmt_make(nmt_keys_around());
|
||||
ImGui::EndDisabled();
|
||||
if (false == state.pending.empty() || m_nmt.waiting() > 0)
|
||||
ImGui::TextDisabled(STR_C("Squares of the model being fetched: %zu"), m_nmt.waiting());
|
||||
if (false == state.status.empty())
|
||||
ImGui::TextWrapped("%s", state.status.c_str());
|
||||
if (false == state.error.empty())
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", state.error.c_str());
|
||||
ImGui::TextDisabled("%s", STR_C("Terrain model: GUGiK, geoportal.gov.pl"));
|
||||
}
|
||||
@@ -63,6 +63,36 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co
|
||||
WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : ""));
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "nmtfit")
|
||||
{
|
||||
// the heights of the terrain model fitted to the tracks, as the button of the terrain panel does
|
||||
nmt_fit();
|
||||
WriteLog("SELFTEST nmtfit: " + m_nmt_state.status);
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "nmtmake")
|
||||
{
|
||||
// chunks from the terrain model along the tracks (with the band in metres) or around the camera
|
||||
std::string where;
|
||||
double band{0.0};
|
||||
words >> where >> band;
|
||||
if (band > 0.0)
|
||||
m_nmt_state.band = band;
|
||||
nmt_make(where == "around" ? nmt_keys_around() : nmt_keys_along_tracks());
|
||||
WriteLog("SELFTEST nmtmake " + where + ": " + m_nmt_state.status);
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "nmtwait")
|
||||
{
|
||||
// till the model is in and the chunks waiting for it are made
|
||||
if (m_nmt_state.fitting || false == m_nmt_state.pending.empty())
|
||||
{
|
||||
--m_selftest.next;
|
||||
return selftest_result::pause;
|
||||
}
|
||||
WriteLog("SELFTEST nmtwait: " + m_nmt_state.status + (m_nmt_state.error.empty() ? std::string{} : " (" + m_nmt_state.error + ")"));
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "terrainmode")
|
||||
{
|
||||
m_terrain_open = true;
|
||||
|
||||
@@ -9,6 +9,9 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "utilities/translation.h"
|
||||
|
||||
@@ -19,3 +22,33 @@ inline void item_tooltip(char const *Text)
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C(Text));
|
||||
}
|
||||
|
||||
// grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid)
|
||||
template <typename Value_> inline void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
|
||||
{
|
||||
std::deque<std::size_t> queue;
|
||||
for (std::size_t index = 0; index < Found.size(); ++index)
|
||||
if (Found[index] != 0)
|
||||
queue.push_back(index);
|
||||
while (false == queue.empty())
|
||||
{
|
||||
auto const index = queue.front();
|
||||
queue.pop_front();
|
||||
auto const ix = index % Side, iz = index / Side;
|
||||
auto const visit = [&](std::size_t const Next) {
|
||||
if (Found[Next] != 0)
|
||||
return;
|
||||
Found[Next] = 1;
|
||||
Heights[Next] = Heights[index];
|
||||
queue.push_back(Next);
|
||||
};
|
||||
if (ix > 0)
|
||||
visit(index - 1);
|
||||
if (ix + 1 < Side)
|
||||
visit(index + 1);
|
||||
if (iz > 0)
|
||||
visit(index - Side);
|
||||
if (iz + 1 < Side)
|
||||
visit(index + Side);
|
||||
}
|
||||
}
|
||||
|
||||
224
editor/editorHttp.cpp
Normal file
224
editor/editorHttp.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
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/editorHttp.hpp"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <winhttp.h>
|
||||
#elif defined(EU07_WITH_CURL)
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace editor_http
|
||||
{
|
||||
|
||||
struct client::platform
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
HINTERNET session{nullptr};
|
||||
#elif defined(EU07_WITH_CURL)
|
||||
CURL *curl{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
client::client() :
|
||||
m_platform(std::make_unique<platform>())
|
||||
{
|
||||
wchar_t const *agent = L"MaSzyna-editor";
|
||||
#ifdef WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY
|
||||
m_platform->session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
#endif
|
||||
if (m_platform->session == nullptr) // automatic proxy needs windows 8.1+
|
||||
m_platform->session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
if (m_platform->session != nullptr)
|
||||
::WinHttpSetTimeouts(m_platform->session, 10000, 10000, 15000, 20000);
|
||||
}
|
||||
|
||||
client::~client()
|
||||
{
|
||||
if (m_platform->session != nullptr)
|
||||
::WinHttpCloseHandle(m_platform->session);
|
||||
}
|
||||
|
||||
bool client::get(std::string const &Url, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &Stop)
|
||||
{
|
||||
Out.clear();
|
||||
if (m_platform->session == nullptr)
|
||||
{
|
||||
Error = "WinHttpOpen failed (" + std::to_string(::GetLastError()) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring const url(Url.begin(), Url.end()); // the requests are plain ascii
|
||||
URL_COMPONENTS parts{};
|
||||
parts.dwStructSize = sizeof(parts);
|
||||
parts.dwSchemeLength = static_cast<DWORD>(-1);
|
||||
parts.dwHostNameLength = static_cast<DWORD>(-1);
|
||||
parts.dwUrlPathLength = static_cast<DWORD>(-1);
|
||||
parts.dwExtraInfoLength = static_cast<DWORD>(-1);
|
||||
if (!::WinHttpCrackUrl(url.c_str(), 0, 0, &parts))
|
||||
{
|
||||
Error = "invalid url";
|
||||
return false;
|
||||
}
|
||||
std::wstring const host(parts.lpszHostName, parts.dwHostNameLength);
|
||||
std::wstring const path = std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) + std::wstring(parts.lpszExtraInfo, parts.dwExtraInfoLength);
|
||||
|
||||
HINTERNET connection = ::WinHttpConnect(m_platform->session, host.c_str(), parts.nPort, 0);
|
||||
HINTERNET request = connection ? ::WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES,
|
||||
parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0)
|
||||
: nullptr;
|
||||
auto const close = [&]() {
|
||||
if (request != nullptr)
|
||||
::WinHttpCloseHandle(request);
|
||||
if (connection != nullptr)
|
||||
::WinHttpCloseHandle(connection);
|
||||
};
|
||||
|
||||
if (request == nullptr || !::WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || !::WinHttpReceiveResponse(request, nullptr))
|
||||
{
|
||||
Error = "WinHTTP error " + std::to_string(::GetLastError());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (Stop())
|
||||
{
|
||||
Error = "cancelled";
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
DWORD available{0};
|
||||
if (!::WinHttpQueryDataAvailable(request, &available))
|
||||
{
|
||||
Error = "WinHTTP error " + std::to_string(::GetLastError());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if (available == 0)
|
||||
break;
|
||||
std::size_t const offset = Out.size();
|
||||
Out.resize(offset + available);
|
||||
DWORD received{0};
|
||||
if (!::WinHttpReadData(request, Out.data() + offset, available, &received))
|
||||
{
|
||||
Error = "WinHTTP error " + std::to_string(::GetLastError());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
Out.resize(offset + received);
|
||||
}
|
||||
close();
|
||||
|
||||
if (status != 200)
|
||||
{
|
||||
Error = "HTTP " + std::to_string(status);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#elif defined(EU07_WITH_CURL)
|
||||
|
||||
client::client() :
|
||||
m_platform(std::make_unique<platform>())
|
||||
{
|
||||
static std::once_flag initialized;
|
||||
std::call_once(initialized, []() { ::curl_global_init(CURL_GLOBAL_DEFAULT); });
|
||||
m_platform->curl = ::curl_easy_init();
|
||||
}
|
||||
|
||||
client::~client()
|
||||
{
|
||||
if (m_platform->curl != nullptr)
|
||||
::curl_easy_cleanup(m_platform->curl);
|
||||
}
|
||||
|
||||
bool client::get(std::string const &Url, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &Stop)
|
||||
{
|
||||
Out.clear();
|
||||
if (m_platform->curl == nullptr)
|
||||
{
|
||||
Error = "curl_easy_init failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const write = +[](char *Data, std::size_t Size, std::size_t Count, void *User) -> std::size_t {
|
||||
auto *out = static_cast<std::vector<std::uint8_t> *>(User);
|
||||
out->insert(out->end(), Data, Data + Size * Count);
|
||||
return Size * Count;
|
||||
};
|
||||
auto const progress = +[](void *User, curl_off_t, curl_off_t, curl_off_t, curl_off_t) -> int { return (*static_cast<stop_check const *>(User))() ? 1 : 0; };
|
||||
|
||||
::curl_easy_reset(m_platform->curl);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_URL, Url.c_str());
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_USERAGENT, "MaSzyna-editor");
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_NOSIGNAL, 1L);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_LOW_SPEED_LIMIT, 1L);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_LOW_SPEED_TIME, 20L);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_WRITEFUNCTION, write);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_WRITEDATA, &Out);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_NOPROGRESS, 0L);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_XFERINFOFUNCTION, progress);
|
||||
::curl_easy_setopt(m_platform->curl, CURLOPT_XFERINFODATA, const_cast<stop_check *>(&Stop));
|
||||
|
||||
CURLcode const code = ::curl_easy_perform(m_platform->curl);
|
||||
if (code != CURLE_OK)
|
||||
{
|
||||
Error = ::curl_easy_strerror(code);
|
||||
return false;
|
||||
}
|
||||
long status{0};
|
||||
::curl_easy_getinfo(m_platform->curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
if (status != 200)
|
||||
{
|
||||
Error = "HTTP " + std::to_string(status);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
client::client() = default;
|
||||
client::~client() = default;
|
||||
|
||||
bool client::get(std::string const &, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &)
|
||||
{
|
||||
Out.clear();
|
||||
Error = "no http client in this build";
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool available()
|
||||
{
|
||||
#if defined(_WIN32) || defined(EU07_WITH_CURL)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace editor_http
|
||||
44
editor/editorHttp.hpp
Normal file
44
editor/editorHttp.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// a minimal blocking https GET for the services the editor fetches its aids from (orthophoto, terrain model)
|
||||
namespace editor_http
|
||||
{
|
||||
|
||||
// true to give the transfer up
|
||||
using stop_check = std::function<bool()>;
|
||||
|
||||
// one instance per worker thread
|
||||
class client
|
||||
{
|
||||
public:
|
||||
client();
|
||||
~client();
|
||||
client(client const &) = delete;
|
||||
client &operator=(client const &) = delete;
|
||||
|
||||
bool get(std::string const &Url, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &Stop);
|
||||
|
||||
private:
|
||||
struct platform;
|
||||
std::unique_ptr<platform> m_platform;
|
||||
};
|
||||
|
||||
// whether this build can fetch anything
|
||||
bool available();
|
||||
|
||||
} // namespace editor_http
|
||||
373
editor/editorNmt.cpp
Normal file
373
editor/editorNmt.cpp
Normal file
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
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/editorNmt.hpp"
|
||||
#include "editor/editorHttp.hpp"
|
||||
#include "utilities/Logs.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// the model at 1 m of the WCS service of geoportal.gov.pl; the name of the model is read from its capabilities
|
||||
char const *const service{"https://mapy.geoportal.gov.pl/wss/service/PZGIK/NMT/GRID1/WCS/DigitalTerrainModelFormatTIFF"};
|
||||
char const *const fallback_coverage{"DTM_PL-KRON86-NH_TIFF"};
|
||||
char const *const cache_folder{"editor/cache/nmt/grid1/"};
|
||||
int constexpr worker_count{2}; // the service is quick, but isn't to be hammered
|
||||
int constexpr border{1}; // pixels of the neighbouring squares fetched around a square, so the heights interpolate across its edges
|
||||
float constexpr no_data{-1000.f}; // the model gives far lower values outside of the country
|
||||
|
||||
// the name of the model among the names the capabilities of the service list: the first which says it's a terrain model
|
||||
std::string coverage_of(std::string const &Capabilities)
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
for (std::size_t at{0}; (at = Capabilities.find("name>", at)) != std::string::npos;)
|
||||
{
|
||||
at += 5;
|
||||
auto const end{Capabilities.find('<', at)};
|
||||
if (end == std::string::npos)
|
||||
break;
|
||||
names.emplace_back(Capabilities.substr(at, end - at));
|
||||
}
|
||||
for (auto const &name : names)
|
||||
{
|
||||
std::string lower{name};
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char const Letter) { return static_cast<char>(std::tolower(Letter)); });
|
||||
if (lower.find("dtm") != std::string::npos || lower.find("nmt") != std::string::npos || lower.find("terrain") != std::string::npos)
|
||||
return name;
|
||||
}
|
||||
return fallback_coverage;
|
||||
}
|
||||
|
||||
// a GeoTIFF as the service gives it: one band of 32-bit floats in strips, not compressed, placed on the grid by its tie point
|
||||
// and pixel scale. false for anything else
|
||||
template <typename Square>
|
||||
bool read_geotiff(std::vector<std::uint8_t> const &Data, Square &Out)
|
||||
{
|
||||
if (Data.size() < 8)
|
||||
return false;
|
||||
bool const little{Data[0] == 'I' && Data[1] == 'I'};
|
||||
if (false == little && false == (Data[0] == 'M' && Data[1] == 'M'))
|
||||
return false;
|
||||
auto const u16 = [&](std::size_t const At) -> std::uint32_t {
|
||||
if (At + 2 > Data.size())
|
||||
return 0;
|
||||
return little ? Data[At] | (Data[At + 1] << 8) : (Data[At] << 8) | Data[At + 1];
|
||||
};
|
||||
auto const u32 = [&](std::size_t const At) -> std::uint32_t {
|
||||
if (At + 4 > Data.size())
|
||||
return 0;
|
||||
return little ? Data[At] | (Data[At + 1] << 8) | (Data[At + 2] << 16) | (static_cast<std::uint32_t>(Data[At + 3]) << 24) :
|
||||
(static_cast<std::uint32_t>(Data[At]) << 24) | (Data[At + 1] << 16) | (Data[At + 2] << 8) | Data[At + 3];
|
||||
};
|
||||
auto const f64 = [&](std::size_t const At) {
|
||||
std::uint64_t bits{0};
|
||||
for (int i = 0; i < 8; ++i)
|
||||
bits |= static_cast<std::uint64_t>(At + i < Data.size() ? Data[At + (little ? i : 7 - i)] : 0) << (8 * i);
|
||||
double value;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
};
|
||||
if (u16(2) != 42)
|
||||
return false;
|
||||
struct entry
|
||||
{
|
||||
std::uint32_t type{0};
|
||||
std::uint32_t count{0};
|
||||
std::size_t at{0}; // of the values: in the entry when they fit in four bytes
|
||||
};
|
||||
std::map<std::uint32_t, entry> entries;
|
||||
auto const ifd{static_cast<std::size_t>(u32(4))};
|
||||
auto const count{u16(ifd)};
|
||||
for (std::uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
auto const at{ifd + 2 + i * 12};
|
||||
entry value{u16(at + 2), u32(at + 4), at + 8};
|
||||
auto const size{(value.type == 3 ? 2u : value.type == 12 ? 8u : 4u) * value.count};
|
||||
if (size > 4)
|
||||
value.at = u32(at + 8);
|
||||
entries[u16(at)] = value;
|
||||
}
|
||||
auto const number = [&](std::uint32_t const Tag, std::uint32_t const Index) -> std::uint32_t {
|
||||
auto const found{entries.find(Tag)};
|
||||
if (found == entries.end() || Index >= found->second.count)
|
||||
return 0;
|
||||
return found->second.type == 3 ? u16(found->second.at + Index * 2) : u32(found->second.at + Index * 4);
|
||||
};
|
||||
auto const real = [&](std::uint32_t const Tag, std::uint32_t const Index) {
|
||||
auto const found{entries.find(Tag)};
|
||||
return found == entries.end() || Index >= found->second.count ? 0.0 : f64(found->second.at + Index * 8);
|
||||
};
|
||||
auto const width{static_cast<int>(number(256, 0))};
|
||||
auto const height{static_cast<int>(number(257, 0))};
|
||||
if (width < 2 || height < 2 || number(258, 0) != 32 || number(339, 0) != 3 || std::max(1u, number(259, 0)) != 1 || false == entries.contains(273) || false == entries.contains(33550) ||
|
||||
false == entries.contains(33922))
|
||||
return false;
|
||||
auto const rows{std::max(1u, number(278, 0))};
|
||||
Out.width = width;
|
||||
Out.height = height;
|
||||
Out.heights.assign(static_cast<std::size_t>(width) * height, no_data - 1.f);
|
||||
auto const strips{entries[273].count};
|
||||
for (std::uint32_t strip = 0; strip < strips; ++strip)
|
||||
{
|
||||
auto const offset{static_cast<std::size_t>(number(273, strip))};
|
||||
auto const first{strip * rows};
|
||||
for (std::uint32_t row = first; row < std::min<std::uint32_t>(first + rows, height); ++row)
|
||||
for (int column = 0; column < width; ++column)
|
||||
{
|
||||
auto const bits{u32(offset + ((row - first) * width + column) * 4)};
|
||||
float value;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
Out.heights[static_cast<std::size_t>(row) * width + column] = value;
|
||||
}
|
||||
}
|
||||
Out.pixel = real(33550, 0);
|
||||
Out.east = real(33922, 3) - real(33922, 0) * Out.pixel;
|
||||
Out.north = real(33922, 4) + real(33922, 1) * Out.pixel;
|
||||
return Out.pixel > 0.0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
editor_nmt::editor_nmt() = default;
|
||||
|
||||
editor_nmt::~editor_nmt()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
m_stop = true;
|
||||
m_queue.clear();
|
||||
}
|
||||
m_wake.notify_all();
|
||||
for (auto &worker : m_workers)
|
||||
if (worker.joinable())
|
||||
worker.join();
|
||||
}
|
||||
|
||||
bool editor_nmt::can_download()
|
||||
{
|
||||
return editor_http::available();
|
||||
}
|
||||
|
||||
void editor_nmt::place(double const North, double const East)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
m_north = North;
|
||||
m_east = East;
|
||||
}
|
||||
|
||||
bool editor_nmt::placed() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
return m_north != 0.0 || m_east != 0.0;
|
||||
}
|
||||
|
||||
editor_nmt::key editor_nmt::key_of(double const East, double const North) const
|
||||
{
|
||||
return {static_cast<int>(std::floor(East / square_size)), static_cast<int>(std::floor(North / square_size))};
|
||||
}
|
||||
|
||||
bool editor_nmt::height(double const X, double const Z, double &Height) const
|
||||
{
|
||||
std::shared_ptr<square const> found;
|
||||
double east, north;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
east = m_east - X;
|
||||
north = m_north + Z;
|
||||
auto const it{m_squares.find(key_of(east, north))};
|
||||
if (it == m_squares.end())
|
||||
return false;
|
||||
found = it->second;
|
||||
}
|
||||
auto const &grid{*found};
|
||||
if (grid.failed)
|
||||
return false;
|
||||
// the values lie in the middles of the pixels
|
||||
auto const column{(east - grid.east) / grid.pixel - 0.5};
|
||||
auto const row{(grid.north - north) / grid.pixel - 0.5};
|
||||
auto const c0{std::clamp(static_cast<int>(std::floor(column)), 0, grid.width - 2)};
|
||||
auto const r0{std::clamp(static_cast<int>(std::floor(row)), 0, grid.height - 2)};
|
||||
auto const fc{std::clamp(column - c0, 0.0, 1.0)};
|
||||
auto const fr{std::clamp(row - r0, 0.0, 1.0)};
|
||||
auto const at = [&](int const C, int const R) { return grid.heights[static_cast<std::size_t>(R) * grid.width + C]; };
|
||||
float const corners[4]{at(c0, r0), at(c0 + 1, r0), at(c0, r0 + 1), at(c0 + 1, r0 + 1)};
|
||||
if (std::all_of(std::begin(corners), std::end(corners), [](float const Value) { return std::isfinite(Value) && Value > no_data; }))
|
||||
{
|
||||
Height = (corners[0] * (1.0 - fc) + corners[1] * fc) * (1.0 - fr) + (corners[2] * (1.0 - fc) + corners[3] * fc) * fr;
|
||||
return true;
|
||||
}
|
||||
// at the edge of the data: the nearest value there is
|
||||
auto const nearest{corners[(fr > 0.5 ? 2 : 0) + (fc > 0.5 ? 1 : 0)]};
|
||||
if (false == std::isfinite(nearest) || nearest <= no_data)
|
||||
return false;
|
||||
Height = nearest;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool editor_nmt::request(glm::dvec2 const &Min, glm::dvec2 const &Max)
|
||||
{
|
||||
std::vector<key> wanted;
|
||||
bool all{true};
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
auto const low{key_of(m_east - Max.x, m_north + Min.y)};
|
||||
auto const high{key_of(m_east - Min.x, m_north + Max.y)};
|
||||
auto const middle{key_of(m_east - 0.5 * (Min.x + Max.x), m_north + 0.5 * (Min.y + Max.y))};
|
||||
for (auto e = low.first; e <= high.first; ++e)
|
||||
for (auto n = low.second; n <= high.second; ++n)
|
||||
{
|
||||
key const square{e, n};
|
||||
if (m_squares.contains(square))
|
||||
continue;
|
||||
all = false;
|
||||
if (false == m_queued.contains(square))
|
||||
wanted.push_back(square);
|
||||
}
|
||||
std::sort(wanted.begin(), wanted.end(), [&](key const &Left, key const &Right) {
|
||||
auto const distance = [&](key const &Square) { return std::abs(Square.first - middle.first) + std::abs(Square.second - middle.second); };
|
||||
return distance(Left) < distance(Right);
|
||||
});
|
||||
for (auto const &square : wanted)
|
||||
{
|
||||
m_queued.insert(square);
|
||||
m_queue.push_back(square);
|
||||
}
|
||||
if (false == wanted.empty() && m_workers.empty())
|
||||
for (int i = 0; i < worker_count; ++i)
|
||||
m_workers.emplace_back([this]() { work(); });
|
||||
}
|
||||
if (false == wanted.empty())
|
||||
m_wake.notify_all();
|
||||
return all;
|
||||
}
|
||||
|
||||
std::size_t editor_nmt::waiting() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
return m_queued.size();
|
||||
}
|
||||
|
||||
std::string editor_nmt::error() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
return m_error;
|
||||
}
|
||||
|
||||
void editor_nmt::clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
m_queue.clear();
|
||||
m_queued.clear();
|
||||
m_squares.clear();
|
||||
}
|
||||
|
||||
void editor_nmt::work()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
key square;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock{m_mutex};
|
||||
m_wake.wait(lock, [this]() { return m_stop || false == m_queue.empty(); });
|
||||
if (m_stop)
|
||||
return;
|
||||
square = m_queue.front();
|
||||
m_queue.pop_front();
|
||||
}
|
||||
std::string error;
|
||||
auto fetched{fetch(square, error)};
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
if (false == m_queued.contains(square))
|
||||
continue; // cleared meanwhile
|
||||
m_queued.erase(square);
|
||||
m_squares[square] = std::move(fetched);
|
||||
m_error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<editor_nmt::square const> editor_nmt::fetch(key const &Key, std::string &Error)
|
||||
{
|
||||
auto result{std::make_shared<square>()};
|
||||
std::error_code fault;
|
||||
fs::create_directories(cache_folder, fault);
|
||||
auto const file{std::string{cache_folder} + std::to_string(Key.first) + "_" + std::to_string(Key.second) + ".tif"};
|
||||
std::vector<std::uint8_t> data;
|
||||
if (std::ifstream cached{file, std::ios::binary}; cached.is_open())
|
||||
{
|
||||
data.assign(std::istreambuf_iterator<char>(cached), std::istreambuf_iterator<char>());
|
||||
if (read_geotiff(data, *result))
|
||||
return result;
|
||||
// damaged by a fetch broken off: fetched again
|
||||
fs::remove(file, fault);
|
||||
}
|
||||
if (false == can_download())
|
||||
{
|
||||
Error = "this build can't fetch from the internet";
|
||||
result->failed = true;
|
||||
return result;
|
||||
}
|
||||
editor_http::client client;
|
||||
auto const stop = [this]() {
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
return m_stop;
|
||||
};
|
||||
std::string coverage;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
coverage = m_coverage;
|
||||
}
|
||||
if (coverage.empty())
|
||||
{
|
||||
std::vector<std::uint8_t> capabilities;
|
||||
std::string unused;
|
||||
coverage = client.get(std::string{service} + "?SERVICE=WCS&VERSION=1.0.0&REQUEST=GetCapabilities", capabilities, unused, stop) ?
|
||||
coverage_of(std::string(capabilities.begin(), capabilities.end())) :
|
||||
fallback_coverage;
|
||||
std::lock_guard<std::mutex> lock{m_mutex};
|
||||
m_coverage = coverage;
|
||||
}
|
||||
// the square with a pixel of each neighbour around it
|
||||
auto const pixels{static_cast<int>(square_size) + 2 * border};
|
||||
auto const east{Key.first * square_size - border};
|
||||
auto const north{Key.second * square_size - border};
|
||||
char bbox[128];
|
||||
std::snprintf(bbox, sizeof(bbox), "%.0f,%.0f,%.0f,%.0f", east, north, east + pixels, north + pixels);
|
||||
auto const url{std::string{service} + "?SERVICE=WCS&VERSION=1.0.0&REQUEST=GetCoverage&COVERAGE=" + coverage + "&CRS=EPSG:2180&RESPONSE_CRS=EPSG:2180&BBOX=" + bbox +
|
||||
"&WIDTH=" + std::to_string(pixels) + "&HEIGHT=" + std::to_string(pixels) + "&FORMAT=GEOTIFF"};
|
||||
for (int attempt = 0; attempt < 3; ++attempt)
|
||||
{
|
||||
if (client.get(url, data, Error, stop) && read_geotiff(data, *result))
|
||||
{
|
||||
if (std::ofstream out{file, std::ios::binary}; out.is_open())
|
||||
out.write(reinterpret_cast<char const *>(data.data()), static_cast<std::streamsize>(data.size()));
|
||||
Error.clear();
|
||||
return result;
|
||||
}
|
||||
if (stop())
|
||||
break;
|
||||
if (Error.empty())
|
||||
Error = "the service gave no terrain model";
|
||||
}
|
||||
WriteLog("Editor: NMT square " + std::to_string(Key.first) + "," + std::to_string(Key.second) + " not fetched: " + Error, logtype::generic);
|
||||
result->failed = true;
|
||||
return result;
|
||||
}
|
||||
87
editor/editorNmt.hpp
Normal file
87
editor/editorNmt.hpp
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
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 <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
// heights of the ground from the digital terrain model of GUGiK (NMT, the WCS service of geoportal.gov.pl): squares of the
|
||||
// EPSG:2180 grid fetched in the background at 1 m, kept on the disk and sampled in the coordinates of the scenery. the
|
||||
// scenery is placed on the grid by the PUWG 1992 coordinates of its point (0,0,0), as the orthophoto is: easting = east - x,
|
||||
// northing = north + z. the heights are those of the model, metres over the sea
|
||||
class editor_nmt
|
||||
{
|
||||
public:
|
||||
editor_nmt();
|
||||
~editor_nmt();
|
||||
editor_nmt(editor_nmt const &) = delete;
|
||||
editor_nmt &operator=(editor_nmt const &) = delete;
|
||||
|
||||
// where the point (0,0,0) of the scenery lies in PUWG 1992; the squares fetched stay, only the mapping changes
|
||||
void place(double const North, double const East);
|
||||
// a scenery at (0,0) isn't placed anywhere
|
||||
bool placed() const;
|
||||
// the height of the model at the point of the scenery; false where its square isn't in yet or the model has no data
|
||||
bool height(double const X, double const Z, double &Height) const;
|
||||
// the squares under the rectangle of the scenery (x, z) asked for in the background, the ones nearest its middle first;
|
||||
// true if all of them are in already, those the service couldn't give included
|
||||
bool request(glm::dvec2 const &Min, glm::dvec2 const &Max);
|
||||
// squares asked for and not in yet
|
||||
std::size_t waiting() const;
|
||||
// what the last fetch said went wrong, empty when it went right
|
||||
std::string error() const;
|
||||
// the squares dropped from the memory, and those waiting; the files on the disk stay
|
||||
void clear();
|
||||
// whether this build can fetch from the service at all
|
||||
static bool can_download();
|
||||
|
||||
// side of a square of the grid, m
|
||||
static double constexpr square_size{500.0};
|
||||
|
||||
private:
|
||||
using key = std::pair<int, int>; // easting and northing index of the square on the grid
|
||||
struct square
|
||||
{
|
||||
double east{0.0}; // of the outer edge of the first column
|
||||
double north{0.0}; // of the outer edge of the first row, the northernmost
|
||||
double pixel{1.0};
|
||||
int width{0};
|
||||
int height{0};
|
||||
std::vector<float> heights; // rows from the north, columns from the west
|
||||
bool failed{false}; // the service gave nothing usable: the square has no data
|
||||
};
|
||||
|
||||
key key_of(double const East, double const North) const;
|
||||
void work();
|
||||
std::shared_ptr<square const> fetch(key const &Key, std::string &Error);
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::condition_variable m_wake;
|
||||
std::vector<std::thread> m_workers;
|
||||
std::deque<key> m_queue;
|
||||
std::set<key> m_queued;
|
||||
std::map<key, std::shared_ptr<square const>> m_squares;
|
||||
std::string m_coverage; // the name of the model in the service, read from its capabilities once
|
||||
std::string m_error;
|
||||
double m_north{0.0};
|
||||
double m_east{0.0};
|
||||
bool m_stop{false};
|
||||
};
|
||||
@@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "editor/editorOrthophoto.hpp"
|
||||
#include "editor/editorHttp.hpp"
|
||||
|
||||
#include "rendering/renderer.h"
|
||||
#include "scene/scene.h"
|
||||
@@ -19,11 +20,6 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "imgui/imgui.h"
|
||||
#include "stb/stb_image.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <winhttp.h>
|
||||
#elif defined(EU07_WITH_CURL)
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -49,212 +45,14 @@ constexpr std::size_t upload_budget{16u << 20}; // bytes of texture data uploade
|
||||
constexpr std::size_t vertex_budget{60000}; // ImGui uses 16-bit indices and not every backend supports vertex offsets
|
||||
constexpr float near_w{0.5f};
|
||||
|
||||
using stop_check = std::function<bool()>;
|
||||
using stop_check = editor_http::stop_check;
|
||||
using http_client = editor_http::client;
|
||||
|
||||
double steady_seconds()
|
||||
{
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// minimal blocking https GET; one instance per worker thread
|
||||
|
||||
class http_client
|
||||
{
|
||||
public:
|
||||
http_client();
|
||||
~http_client();
|
||||
http_client(http_client const &) = delete;
|
||||
http_client &operator=(http_client const &) = delete;
|
||||
|
||||
bool get(std::string const &Url, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &Stop);
|
||||
|
||||
private:
|
||||
#if defined(_WIN32)
|
||||
HINTERNET m_session{nullptr};
|
||||
#elif defined(EU07_WITH_CURL)
|
||||
CURL *m_curl{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
http_client::http_client()
|
||||
{
|
||||
wchar_t const *agent = L"MaSzyna-editor";
|
||||
#ifdef WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY
|
||||
m_session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
#endif
|
||||
if (m_session == nullptr) // automatic proxy needs windows 8.1+
|
||||
m_session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
if (m_session != nullptr)
|
||||
::WinHttpSetTimeouts(m_session, 10000, 10000, 15000, 20000);
|
||||
}
|
||||
|
||||
http_client::~http_client()
|
||||
{
|
||||
if (m_session != nullptr)
|
||||
::WinHttpCloseHandle(m_session);
|
||||
}
|
||||
|
||||
bool http_client::get(std::string const &Url, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &Stop)
|
||||
{
|
||||
Out.clear();
|
||||
if (m_session == nullptr)
|
||||
{
|
||||
Error = "WinHttpOpen failed (" + std::to_string(::GetLastError()) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring const url(Url.begin(), Url.end()); // the requests are plain ascii
|
||||
URL_COMPONENTS parts{};
|
||||
parts.dwStructSize = sizeof(parts);
|
||||
parts.dwSchemeLength = static_cast<DWORD>(-1);
|
||||
parts.dwHostNameLength = static_cast<DWORD>(-1);
|
||||
parts.dwUrlPathLength = static_cast<DWORD>(-1);
|
||||
parts.dwExtraInfoLength = static_cast<DWORD>(-1);
|
||||
if (!::WinHttpCrackUrl(url.c_str(), 0, 0, &parts))
|
||||
{
|
||||
Error = "invalid url";
|
||||
return false;
|
||||
}
|
||||
std::wstring const host(parts.lpszHostName, parts.dwHostNameLength);
|
||||
std::wstring const path = std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) + std::wstring(parts.lpszExtraInfo, parts.dwExtraInfoLength);
|
||||
|
||||
HINTERNET connection = ::WinHttpConnect(m_session, host.c_str(), parts.nPort, 0);
|
||||
HINTERNET request = connection ? ::WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES,
|
||||
parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0)
|
||||
: nullptr;
|
||||
auto const close = [&]() {
|
||||
if (request != nullptr)
|
||||
::WinHttpCloseHandle(request);
|
||||
if (connection != nullptr)
|
||||
::WinHttpCloseHandle(connection);
|
||||
};
|
||||
|
||||
if (request == nullptr || !::WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || !::WinHttpReceiveResponse(request, nullptr))
|
||||
{
|
||||
Error = "WinHTTP error " + std::to_string(::GetLastError());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (Stop())
|
||||
{
|
||||
Error = "cancelled";
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
DWORD available{0};
|
||||
if (!::WinHttpQueryDataAvailable(request, &available))
|
||||
{
|
||||
Error = "WinHTTP error " + std::to_string(::GetLastError());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if (available == 0)
|
||||
break;
|
||||
std::size_t const offset = Out.size();
|
||||
Out.resize(offset + available);
|
||||
DWORD received{0};
|
||||
if (!::WinHttpReadData(request, Out.data() + offset, available, &received))
|
||||
{
|
||||
Error = "WinHTTP error " + std::to_string(::GetLastError());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
Out.resize(offset + received);
|
||||
}
|
||||
close();
|
||||
|
||||
if (status != 200)
|
||||
{
|
||||
Error = "HTTP " + std::to_string(status);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#elif defined(EU07_WITH_CURL)
|
||||
|
||||
http_client::http_client()
|
||||
{
|
||||
static std::once_flag initialized;
|
||||
std::call_once(initialized, []() { ::curl_global_init(CURL_GLOBAL_DEFAULT); });
|
||||
m_curl = ::curl_easy_init();
|
||||
}
|
||||
|
||||
http_client::~http_client()
|
||||
{
|
||||
if (m_curl != nullptr)
|
||||
::curl_easy_cleanup(m_curl);
|
||||
}
|
||||
|
||||
bool http_client::get(std::string const &Url, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &Stop)
|
||||
{
|
||||
Out.clear();
|
||||
if (m_curl == nullptr)
|
||||
{
|
||||
Error = "curl_easy_init failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const write = +[](char *Data, std::size_t Size, std::size_t Count, void *User) -> std::size_t {
|
||||
auto *out = static_cast<std::vector<std::uint8_t> *>(User);
|
||||
out->insert(out->end(), Data, Data + Size * Count);
|
||||
return Size * Count;
|
||||
};
|
||||
auto const progress = +[](void *User, curl_off_t, curl_off_t, curl_off_t, curl_off_t) -> int { return (*static_cast<stop_check const *>(User))() ? 1 : 0; };
|
||||
|
||||
::curl_easy_reset(m_curl);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_URL, Url.c_str());
|
||||
::curl_easy_setopt(m_curl, CURLOPT_USERAGENT, "MaSzyna-editor");
|
||||
::curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1L);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_LOW_SPEED_LIMIT, 1L);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_LOW_SPEED_TIME, 20L);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, write);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &Out);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, 0L);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_XFERINFOFUNCTION, progress);
|
||||
::curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, const_cast<stop_check *>(&Stop));
|
||||
|
||||
CURLcode const code = ::curl_easy_perform(m_curl);
|
||||
if (code != CURLE_OK)
|
||||
{
|
||||
Error = ::curl_easy_strerror(code);
|
||||
return false;
|
||||
}
|
||||
long status{0};
|
||||
::curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
if (status != 200)
|
||||
{
|
||||
Error = "HTTP " + std::to_string(status);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
http_client::http_client() = default;
|
||||
http_client::~http_client() = default;
|
||||
|
||||
bool http_client::get(std::string const &, std::vector<std::uint8_t> &Out, std::string &Error, stop_check const &)
|
||||
{
|
||||
Out.clear();
|
||||
Error = "no http client in this build";
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -7058,3 +7058,66 @@ msgstr "Upuść sygnalizator przy torze"
|
||||
|
||||
msgid "This signal is in a file the editor doesn't change, it can't be moved"
|
||||
msgstr "Ten sygnalizator jest w pliku, którego edytor nie zmienia, nie da się go przesunąć"
|
||||
|
||||
msgid "%zu chunks to make from the terrain model"
|
||||
msgstr "%zu kafli do zrobienia z NMT"
|
||||
|
||||
msgid "; the formation of %zu paths shaped"
|
||||
msgstr "; torowisko %zu torów ukształtowane"
|
||||
|
||||
msgid "Along the tracks"
|
||||
msgstr "Wzdłuż torów"
|
||||
|
||||
msgid "Band along the tracks (m)"
|
||||
msgstr "Pas wzdłuż torów (m)"
|
||||
|
||||
msgid "Chunks made from the terrain model: %zu, waiting for it: %zu"
|
||||
msgstr "Kafle z NMT: zrobione %zu, czeka %zu"
|
||||
|
||||
msgid "Compares the formation of the tracks with the terrain model and moves the model up or down to meet them, as a scenery built at height 0 needs"
|
||||
msgstr "Porównuje torowisko z NMT i przesuwa NMT w pionie, żeby się spotkały, np. dla scenerii zbudowanej na wysokości 0"
|
||||
|
||||
msgid "Each track gets its formation with slopes 1:1.5 down or up to the terrain, as a modifier of the terrain; bridges and tunnels keep the model under them"
|
||||
msgstr "Każdy tor dostaje torowisko ze skarpami 1:1,5 w dół albo w górę do terenu, jako modyfikator terenu; pod mostami i w tunelach zostaje NMT"
|
||||
|
||||
msgid "Fit to the tracks"
|
||||
msgstr "Dopasuj do torów"
|
||||
|
||||
msgid "Formation and slopes along the tracks"
|
||||
msgstr "Torowisko i skarpy wzdłuż torów"
|
||||
|
||||
msgid "Height offset (m)"
|
||||
msgstr "Przesunięcie wysokości (m)"
|
||||
|
||||
msgid "Place: N %.0f, E %.0f (PUWG 1992), as the orthophoto"
|
||||
msgstr "Położenie: N %.0f, E %.0f (PUWG 1992), jak ortofotomapa"
|
||||
|
||||
msgid "Squares of the model being fetched: %zu"
|
||||
msgstr "Pobierane kafle NMT: %zu"
|
||||
|
||||
msgid "Terrain model: GUGiK, geoportal.gov.pl"
|
||||
msgstr "NMT: GUGiK, geoportal.gov.pl"
|
||||
|
||||
msgid "The scenery isn't placed anywhere: give its place in the settings of the orthophoto first"
|
||||
msgstr "Sceneria nie ma położenia: najpierw podaj je w ustawieniach ortofotomapy"
|
||||
|
||||
msgid "The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)"
|
||||
msgstr "Tory leżą %+.2f m względem NMT (%zu miejsc, rozrzut %.2f m)"
|
||||
|
||||
msgid "Terrain model (NMT)"
|
||||
msgstr "Teren z NMT"
|
||||
|
||||
msgid "No tracks to fit the terrain model to: its heights are taken as they are"
|
||||
msgstr "Brak torów do dopasowania: wysokości NMT bez przesunięcia"
|
||||
|
||||
msgid "Fetching the terrain model under the tracks..."
|
||||
msgstr "Pobieranie NMT pod torami..."
|
||||
|
||||
msgid "The terrain model has no data under the tracks: is the scenery placed where it is?"
|
||||
msgstr "NMT nie ma danych pod torami: czy położenie scenerii jest dobre?"
|
||||
|
||||
msgid ". That's far too much: the place of the scenery is likely wrong, check the orthophoto"
|
||||
msgstr ". To za dużo: położenie scenerii jest pewnie złe, sprawdź ortofotomapę"
|
||||
|
||||
msgid "There's terrain there already"
|
||||
msgstr "Tam już jest teren"
|
||||
|
||||
Reference in New Issue
Block a user