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
32 changes: 32 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,38 @@ pattern is: one dataset object per thread, or serialized access.
`GdalEnvironment` initialization is the exception — safe to call from
anywhere.

## Map model

`mwg::Map` (`mapwingis/map/Map.h`) is the authoritative owner of the layer
collection and map-level layer state. It has no windowing, COM, or rendering
state; hosts and renderers read from it.

- **Identity.** `Map::AddLayer` returns a `LayerId`. Identities start at 1,
increase, and are never reused by the same map, even after `RemoveLayer` or
`Clear`. `LayerId{}` (0) identifies no layer. Neither display names nor
positions identify a layer.
- **Ordering.** Position 0 is the bottom layer and is drawn first; new layers
are appended on top. Positions shift on removal; identities do not.
- **Ownership.** The map owns its `MapLayer` objects. A `MapLayer` reference
stays valid until that layer is removed, the map is cleared, or the map is
destroyed; changes to other layers do not invalidate it. Each `MapLayer`
shares ownership of its `VectorDataset` (`std::shared_ptr`), so the dataset
lives as long as any map layer or other owner uses it.
- **Duplicates.** The same source layer may be added more than once; each
addition is an independent map layer with its own identity and visibility.
- **Visibility.** Layers are visible when added. `Map::SetLayerVisible`
changes visibility. Hidden layers are excluded from
`GetExtent(ExtentScope::VisibleLayers)`, the default and the extent used for
zoom-to-map, and included in `GetExtent(ExtentScope::AllLayers)`.
- **Extents.** Map extents are the union of valid layer extents
(`Extent::IsValid`: non-empty and finite). They are computed on each call
rather than cached, so visibility changes, added or removed layers, and
source edits are reflected without invalidation. A map with no valid layer
extent reports an empty `Extent`.
- **Coordinates.** Layers are assumed to share compatible source coordinates.
The map does not reproject; layers in different coordinate reference
systems do not align until coordinate transformations are implemented.

## ActiveX and rendering architecture

The first ActiveX and rendering vertical slice is implemented across the
Expand Down
6 changes: 4 additions & 2 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
default through `MwgVcpkgFeatures`).
- Test project: `tests/MapWinGIS.Core.Tests`, built with the solution.
- Shared fixtures and helpers live in `TestData.h/.cpp` (`TempDirectory`,
`MakeSampleShapefile`, `MakeProvincesDataset`, and layer constants). Extend
these instead of writing per-file fixture code.
`MakeSampleShapefile`, `MakeProvincesDataset`, `MakeRectanglesDataset`, and
layer constants). Extend these instead of writing per-file fixture code.
`MakeRectanglesDataset` writes one rectangle per given extent, so map tests
can place layers at chosen coordinates or create an empty layer.

## Running

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 @@ -60,6 +60,7 @@
<ClInclude Include="include\mapwingis\gdal\GdalTypedOptions.h" />
<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\vector\FieldInfo.h" />
<ClInclude Include="include\mapwingis\vector\VectorDataset.h" />
<ClInclude Include="include\mapwingis\rendering\VectorRenderScene.h" />
Expand All @@ -77,6 +78,7 @@
<ClCompile Include="src\gdal\GdalRuntime.cpp" />
<ClCompile Include="src\gdal\GdalTypedOptions.cpp" />
<ClCompile Include="src\gdal\GdalUtils.cpp" />
<ClCompile Include="src\map\Map.cpp" />
<ClCompile Include="src\vector\VectorDataset.cpp" />
<ClCompile Include="src\rendering\VectorRenderScene.cpp" />
</ItemGroup>
Expand Down
9 changes: 9 additions & 0 deletions src/MapWinGIS.Core/include/mapwingis/core/Extent.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#pragma once

#include <algorithm>
#include <cmath>
#include <limits>

#include "mapwingis/core/Api.h"
Expand Down Expand Up @@ -37,6 +38,14 @@ namespace mwg
return !(xMin <= xMax && yMin <= yMax);
}

/// True when the extent is not empty and every bound is finite.
/// Map-level calculations ignore extents that are not valid.
[[nodiscard]] bool IsValid() const noexcept
{
return !IsEmpty() && std::isfinite(xMin) && std::isfinite(yMin) &&
std::isfinite(xMax) && std::isfinite(yMax);
}

[[nodiscard]] double Width() const noexcept { return IsEmpty() ? 0.0 : xMax - xMin; }
[[nodiscard]] double Height() const noexcept { return IsEmpty() ? 0.0 : yMax - yMin; }
[[nodiscard]] double CenterX() const noexcept { return (xMin + xMax) / 2.0; }
Expand Down
171 changes: 171 additions & 0 deletions src/MapWinGIS.Core/include/mapwingis/map/Map.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: MPL-2.0

/**
* @file Map.h
* @brief Declares the map model: an ordered collection of map layers.
*
* The map owns its layer collection and map-level layer state, such as
* visibility, and calculates the combined extent of its layers. It holds no
* windowing, COM, or rendering-backend state.
*
* Layer coordinates are used as stored in their source. The map assumes all
* layers use compatible coordinates; it does not reproject, so layers in
* different coordinate reference systems do not align.
*/
#pragma once

#include <compare>
#include <cstdint>
#include <memory>
#include <optional>

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

namespace mwg
{
class Map;
class VectorDataset;
class VectorLayer;

/// Stable identity of a layer within one Map.
///
/// Assigned by Map::AddLayer, starting at 1 and increasing. An identity is
/// never reused by the same map, not even after the layer is removed or
/// the map is cleared, so a stale identity can never select another layer.
/// The default value (0) identifies no layer.
struct LayerId
{
std::uint64_t value = 0;

[[nodiscard]] constexpr bool IsValid() const noexcept { return value != 0; }

friend constexpr auto operator<=>(const LayerId&, const LayerId&) noexcept = default;
};

/// Which layers contribute to a map-level extent.
enum class ExtentScope : int
{
/// Only visible layers. This is the extent used for zoom-to-map.
VisibleLayers,
/// All layers, including hidden ones.
AllLayers,
};

/// One layer of a Map: a vector layer of a dataset plus map-level state.
///
/// Owned by its Map. A MapLayer reference stays valid until that layer is
/// removed, the map is cleared, or the map is destroyed; adding, removing,
/// or clearing other layers does not invalidate it. The MapLayer shares
/// ownership of its dataset, so the source layer stays valid for the
/// MapLayer's lifetime even when the caller releases the dataset.
///
/// Not thread-safe, consistent with VectorLayer.
class MWG_CORE_API MapLayer
{
public:
~MapLayer();

MapLayer(const MapLayer&) = delete;
MapLayer& operator=(const MapLayer&) = delete;

[[nodiscard]] LayerId Id() const noexcept;

/// The vector layer displayed by this map layer.
[[nodiscard]] VectorLayer& Source() const noexcept;

/// The dataset that owns Source().
[[nodiscard]] VectorDataset& Dataset() const noexcept;

/// 0-based index of Source() within Dataset().
[[nodiscard]] int SourceLayerIndex() const noexcept;

/// Layers are visible when added. Change visibility through
/// Map::SetLayerVisible.
[[nodiscard]] bool IsVisible() const noexcept;

/// Extent of the source layer, read from the layer on every call so
/// that edits to the source are reflected. Empty when the layer has no
/// geometry or its extent is not valid (see Extent::IsValid).
/// May read the whole layer for formats that don't store an extent;
/// the source layer caches the result.
[[nodiscard]] Extent GetExtent() const;

private:
friend class Map;

class Impl;
explicit MapLayer(std::unique_ptr<Impl> impl);
std::unique_ptr<Impl> _impl;
};

/// The map model: an ordered collection of layers and map-level state.
///
/// Ordering: position 0 is the bottom layer, drawn first; the last
/// position is the top layer, drawn last. AddLayer appends a layer on top.
/// Positions shift when layers are removed, so use LayerId to refer to a
/// layer over time.
///
/// Duplicates: the same source layer may be added more than once. Each
/// addition is a separate map layer with its own identity and state.
///
/// Extents: map-level extents are computed from the layers on every call,
/// so they reflect visibility changes, added and removed layers, and edits
/// to source geometry without explicit invalidation. Layers whose extent
/// is empty or not valid are ignored. A map without any valid layer extent
/// reports an empty Extent.
///
/// Not thread-safe; see VectorLayer.
class MWG_CORE_API Map
{
public:
Map();
~Map();

Map(const Map&) = delete;
Map& operator=(const Map&) = delete;

/// Adds layer layerIndex of dataset on top of the map and returns its
/// new identity. The map shares ownership of the dataset; the dataset
/// is released when its last map layer is removed and no other owner
/// remains.
/// \throws InvalidArgument when dataset is null or layerIndex is out of
/// range.
/// \throws CoreError when the map has exhausted its layer identity space.
LayerId AddLayer(std::shared_ptr<VectorDataset> dataset, int layerIndex = 0);

/// Removes the layer with this identity. Returns false, and changes
/// nothing, when the map has no such layer.
bool RemoveLayer(LayerId id) noexcept;

/// Removes all layers. Identities of removed layers are not reused.
void Clear() noexcept;

[[nodiscard]] int LayerCount() const noexcept;

/// Layer at a 0-based position; see the ordering rules above.
/// \throws InvalidArgument when position is out of range.
[[nodiscard]] const MapLayer& Layer(int position) const;

/// nullptr when the map has no layer with this identity.
[[nodiscard]] const MapLayer* FindLayer(LayerId id) const noexcept;

/// Current 0-based position of a layer, or std::nullopt when the map
/// has no layer with this identity.
[[nodiscard]] std::optional<int> LayerPosition(LayerId id) const noexcept;

/// Shows or hides a layer. Returns false when the map has no layer
/// with this identity.
bool SetLayerVisible(LayerId id, bool visible) noexcept;

/// Union of the valid extents of the layers in scope, or an empty
/// Extent when none of them has a valid extent. Deterministic for a
/// given set of layer extents. Reads each layer's extent; see
/// MapLayer::GetExtent for the cost.
[[nodiscard]] Extent GetExtent(ExtentScope scope = ExtentScope::VisibleLayers) const;

private:
class Impl;
std::unique_ptr<Impl> _impl;
};
}
Loading