Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,42 @@ state; hosts and renderers read from it.
The map does not reproject; layers in different coordinate reference
systems do not align until coordinate transformations are implemented.

## Viewport

`mwg::Viewport` (`mapwingis/map/Viewport.h`) is the view of map coordinates on
a pixel area. It converts between map and screen coordinates and fits an
extent, typically `Map::GetExtent()`, into the display area. It does not
reference a `Map` and holds no windowing, COM, or rendering state. It is a
small copyable value type, like `Extent`, so renderers and navigation code can
take snapshots.

- **Coordinates.** Map X increases right and map Y increases up. Screen X
increases right and screen Y increases down, in pixels from the top-left
corner. Screen coordinates are continuous `double` values: pixel `(i, j)`
covers `[i, i + 1) x [j, j + 1)`. `ToPixel` rounds down to the containing
pixel; round only there.
- **State.** A size in pixels plus, once set, a view: the map point at the
viewport center and one resolution (map units per pixel) for both axes. The
visible extent is derived from these, so it always has the viewport's
aspect ratio and geometry is never distorted. Resizing keeps the center and
resolution.
- **Validity.** A viewport is valid when both dimensions are positive and it
has a view. On an invalid viewport, transformations return `std::nullopt`
and `VisibleExtent` returns an empty `Extent`. A zero-sized viewport keeps
its view. Non-finite inputs or results give `std::nullopt`; negative sizes,
non-finite views, and invalid padding throw `InvalidArgument`.
- **Fitting.** `FitExtent` centers the extent and picks the smallest
resolution that shows it on both axes, so the other axis expands. Padding
is kept free on every side. Extent edges map to the centers of the
outermost pixels inside the padding, so pixel rounding never pushes
boundary geometry outside the viewport. A zero-span axis does not constrain
the resolution; a single point keeps the current resolution. Invalid
extents and zero-sized viewports return `false` without changing the view.
- **Limits.** Sizes are limited to `kMaxDimension` pixels. Centers are clamped
to `±kMaxCoordinate` and resolutions to `[MinResolution(center),
kMaxResolution]`, where the minimum is relative to the center's magnitude so
that pixels stay many representable doubles wide.

## ActiveX and rendering architecture

The first ActiveX and rendering vertical slice is implemented across the
Expand Down
2 changes: 2 additions & 0 deletions src/MapWinGIS.Core/MapWinGIS.Core.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<ClInclude Include="include\mapwingis\gdal\GdalModern.h" />
<ClInclude Include="include\mapwingis\gdal\GdalUtils.h" />
<ClInclude Include="include\mapwingis\map\Map.h" />
<ClInclude Include="include\mapwingis\map\Viewport.h" />
<ClInclude Include="include\mapwingis\vector\FieldInfo.h" />
<ClInclude Include="include\mapwingis\vector\VectorDataset.h" />
<ClInclude Include="include\mapwingis\rendering\VectorRenderScene.h" />
Expand All @@ -79,6 +80,7 @@
<ClCompile Include="src\gdal\GdalTypedOptions.cpp" />
<ClCompile Include="src\gdal\GdalUtils.cpp" />
<ClCompile Include="src\map\Map.cpp" />
<ClCompile Include="src\map\Viewport.cpp" />
<ClCompile Include="src\vector\VectorDataset.cpp" />
<ClCompile Include="src\rendering\VectorRenderScene.cpp" />
</ItemGroup>
Expand Down
188 changes: 188 additions & 0 deletions src/MapWinGIS.Core/include/mapwingis/map/Viewport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// SPDX-License-Identifier: MPL-2.0

/**
* @file Viewport.h
* @brief Declares the viewport: the view of map coordinates on a pixel area.
*
* The viewport converts between map coordinates and screen coordinates and
* fits a map extent into the available display area. It holds no windowing,
* COM, or rendering-backend state, and it does not reference a Map: callers
* fit the viewport to Map::GetExtent and keep the two side by side.
*
* Coordinate conventions:
* - Map coordinates: X increases to the right, Y increases upwards, in the
* units of the layers' coordinate system.
* - Screen coordinates: X increases to the right, Y increases downwards, in
* pixels, with the origin at the top-left corner of the viewport. Screen
* coordinates are continuous: pixel (i, j) covers [i, i + 1) x [j, j + 1),
* so its center is at (i + 0.5, j + 0.5) and the viewport covers
* [0, Width()] x [0, Height()].
*/
#pragma once

#include <optional>

#include "mapwingis/core/Api.h"
#include "mapwingis/core/Extent.h"

namespace mwg
{
/// A position in map coordinates (X right, Y up).
struct MapPoint
{
double x = 0.0;
double y = 0.0;
};

/// A position in continuous screen coordinates (X right, Y down, pixels).
struct ScreenPoint
{
double x = 0.0;
double y = 0.0;
};

/// The integer pixel that contains a screen position.
struct PixelPoint
{
int x = 0;
int y = 0;
};

/// Pixel that contains a screen position: each coordinate is rounded
/// down, so pixel (i, j) holds every position in [i, i + 1) x [j, j + 1).
/// Rounding happens only here; keep screen coordinates in double until
/// pixels are needed. Returns std::nullopt when a coordinate is not
/// finite or the pixel is outside the int range.
[[nodiscard]] MWG_CORE_API std::optional<PixelPoint> ToPixel(const ScreenPoint& point) noexcept;

/// The view of map coordinates on a rectangular pixel area.
///
/// State: a size in pixels and, once set, a view given by the map
/// coordinate shown at the viewport center and the resolution in map
/// units per pixel. The same resolution applies to both axes, so the view
/// never distorts geometry. The visible extent follows from these values
/// and always has the viewport's aspect ratio.
///
/// Validity: a default-constructed viewport has size 0 x 0 and no view.
/// A viewport is valid when both dimensions are positive and it has a
/// view. Transformations and VisibleExtent need a valid viewport; on an
/// invalid one they return std::nullopt or an empty Extent. A zero-sized
/// viewport (for example, a minimized window) keeps its view, so the view
/// returns when the viewport gets a size again.
///
/// Limits: the center is clamped to +/-kMaxCoordinate on each axis and
/// the resolution to [MinResolution(center), kMaxResolution]. The lower
/// resolution bound keeps every pixel many representable doubles wide,
/// so screen-to-map conversions stay stable at any supported location.
///
/// Viewport is a small copyable value type, like Extent, so render
/// scenes and navigation code can take snapshots of it.
/// Not thread-safe.
class MWG_CORE_API Viewport
{
public:
/// Largest supported width or height, in pixels.
static constexpr int kMaxDimension = 1 << 16;

/// Largest supported absolute center coordinate, in map units.
static constexpr double kMaxCoordinate = 1.0e12;

/// Largest supported resolution, in map units per pixel.
static constexpr double kMaxResolution = 1.0e9;

/// Smallest supported resolution relative to the center's magnitude;
/// see MinResolution.
static constexpr double kMinRelativeResolution = 1.0e-10;

/// Resolution used to fit a single point when there is no view yet.
static constexpr double kDefaultPointResolution = 1.0;

/// Smallest supported resolution for a center: kMinRelativeResolution
/// times the larger of 1 and the center's largest absolute coordinate.
[[nodiscard]] static double MinResolution(const MapPoint& center) noexcept;

/// Size 0 x 0 and no view.
Viewport() noexcept = default;

/// A viewport of this size without a view.
/// \throws InvalidArgument when a dimension is negative or larger than
/// kMaxDimension.
Viewport(int width, int height);

[[nodiscard]] int Width() const noexcept { return _width; }
[[nodiscard]] int Height() const noexcept { return _height; }

/// True when the view has been set by SetView or FitExtent.
[[nodiscard]] bool HasView() const noexcept { return _hasView; }

/// True when both dimensions are positive and the viewport has a view.
[[nodiscard]] bool IsValid() const noexcept
{
return _hasView && _width > 0 && _height > 0;
}

/// Map coordinate at the viewport center, or std::nullopt without a view.
[[nodiscard]] std::optional<MapPoint> Center() const noexcept;

/// Map units per pixel, or std::nullopt without a view.
[[nodiscard]] std::optional<double> Resolution() const noexcept;

/// Changes the size and keeps the center and resolution, so the map
/// stays centered and keeps its scale when the display area resizes.
/// \throws InvalidArgument when a dimension is negative or larger than
/// kMaxDimension.
void SetSize(int width, int height);

/// Sets the view. The center and resolution are clamped to the limits
/// described above.
/// \throws InvalidArgument when a center coordinate is not finite or
/// the resolution is not a finite positive number.
void SetView(const MapPoint& center, double resolution);

/// Removes the view; the size is kept.
void ClearView() noexcept;

/// Fits extent into the viewport, keeping paddingPixels free on every
/// side, and returns true. The extent is centered, and the resolution
/// is the smallest that shows the whole extent on both axes, so the
/// visible extent grows on the other axis to keep the aspect ratio.
///
/// The extent edges map to the centers of the outermost pixels inside
/// the padding, so rounding to pixels never puts boundary geometry
/// outside the viewport. When the padding leaves less than half a
/// pixel between those centers, the extent spans half a pixel around
/// the viewport center.
///
/// Degenerate extents: an axis with zero span does not constrain the
/// resolution. A single point is centered and keeps the current
/// resolution, or kDefaultPointResolution without a view. Results are
/// clamped to the limits; an extent larger than the limits allow is
/// then not fully visible.
///
/// Returns false, and changes nothing, when the extent is not valid
/// (see Extent::IsValid) or a dimension is zero.
/// \throws InvalidArgument when paddingPixels is negative or not finite.
bool FitExtent(const Extent& extent, double paddingPixels = 0.0);

/// Map extent covered by the whole viewport, or an empty Extent when
/// the viewport is not valid.
[[nodiscard]] Extent VisibleExtent() const noexcept;

/// Screen position of a map point. Returns std::nullopt when the
/// viewport is not valid, a coordinate is not finite, or the result
/// is not finite. Points outside the visible extent give positions
/// outside the viewport.
[[nodiscard]] std::optional<ScreenPoint> MapToScreen(const MapPoint& point) const noexcept;

/// Map position of a screen point; the inverse of MapToScreen.
/// Returns std::nullopt under the same conditions.
[[nodiscard]] std::optional<MapPoint> ScreenToMap(const ScreenPoint& point) const noexcept;

private:
int _width = 0;
int _height = 0;
bool _hasView = false;
MapPoint _center{};
double _resolution = 0.0;
};
}
Loading