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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ cancellation events.
The core also supports a shared map viewport with pan, center zoom, anchored
zoom, and fitting to the visible map or a named layer. These navigation calls
do not depend on a window. `IMapWinGISControl4` exposes this programmatic
navigation and stable layer IDs through ActiveX; mouse navigation remains
future work.
navigation and stable layer IDs through ActiveX. The control also supports
left-button drag panning and cursor-anchored mouse-wheel zoom. Resizing keeps
the current map center and resolution.

Vector loads add layers rather than replace them. Call `ClearLayers` to
release the loaded datasets before switching or refreshing them.
Expand Down
23 changes: 15 additions & 8 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,9 @@ has no extent or transform of its own.
the view cancels it. A missing layer or an invalid extent also cancels a
pending fit. Navigation that returns `false` keeps it, including `ZoomAt`
on a zero-sized viewport and center zoom at a resolution limit.
None of this depends on COM, mouse messages, or a
renderer; Phase 5 maps input onto these calls.
None of this depends on COM, mouse messages, or a renderer. The ActiveX
control maps left-button drags and cursor-anchored wheel input onto these
calls.

## ActiveX and rendering architecture

Expand Down Expand Up @@ -261,7 +262,13 @@ viewport. For windowless hosts, `SetObjectRects` supplies the live size.
output, and scaled draws do not change the live viewport or its pending fit.
If no view exists yet, drawing fits only that copy to the visible map extent.
Resizing updates the OpenGL viewport and staging buffers without changing
ownership of map data.
ownership of map data. A left-button drag pans the shared `MapView` by the
Comment thread
pmeems marked this conversation as resolved.
Resolved
pixel delta converted with the viewport's map-units-per-pixel resolution.
Wheel input calls `MapView::ZoomAt` at the cursor. The control acquires and
releases mouse capture through either its window or its windowless in-place
site, and ends a drag on capture loss, cancellation, close, or destruction.
These view changes do not rebuild the render scene, and both backends draw
through the updated viewport.

Rendering consumes backend-neutral input prepared from core data; neither the
coordinator nor a backend loads datasets or implements GIS rules. For each
Expand Down Expand Up @@ -391,9 +398,9 @@ Windows CPU fallback; Core has no independent platform-neutral renderer.
The C# host's pixel assertions, activation, and native tests pass for Debug
and Release on Win32 and x64 in an interactive desktop session.
Core navigation on the shared `MapView` (zoom to map and layer, zoom in and
out, anchored zoom, and pan) is implemented but not yet exposed through COM or
mouse input; that is the next milestone. Deep zoom can push a layer past the
GDI coordinate range until clipping exists. Measure scene preparation with
representative datasets before adding caching, paging, or broader OpenGL
geometry support.
out, anchored zoom, and pan) is exposed through `IMapWinGISControl4`; the
ActiveX control also supports left-button drag panning and cursor-anchored
wheel zoom. Deep zoom can push a layer past the GDI coordinate range until
clipping exists. Measure scene preparation with representative datasets
before adding caching, paging, or broader OpenGL geometry support.
OpenCL and full MapWinGIS 5 API parity remain deferred.
11 changes: 7 additions & 4 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ up automatically).

The native `MapWinGIS.ActiveX.Tests` project covers COM activation, registered
control rendering through GDI and OpenGL, and GDAL progress/completion events.
The registered-control host test is opt-in and runs through
`tests\MapWinGIS.ActiveX.Tests\Verify-ActiveXActivation.ps1`.
Its registered-control host test also sends a wheel zoom and captured drag to
the hosted control and verifies their viewport effects. Run this opt-in test
with `tests\MapWinGIS.ActiveX.Tests\Verify-ActiveXActivation.ps1`.

The .NET 10 WinForms host exercises the real ActiveX control through `AxHost`
using registration-free COM activation from its embedded application
Expand Down Expand Up @@ -156,8 +157,8 @@ unchanged-geometry cache. Total memory includes the OpenGL driver and context.
Map view tests fit point and zero-span extents, and tiny targets with default
padding. Map scene tests isolate standard geometry-read exceptions, reuse
the resulting scene, and let allocation failures propagate.
- Navigation is tested without input events: `ViewportTests.cpp` covers the
pan, center-zoom, and anchored-zoom calculations and their limits, and
- `ViewportTests.cpp` covers pan, center-zoom, and anchored-zoom calculations
and their limits, and
`MapViewTests.cpp` covers zoom to map and layer, zoom in and out, empty and
missing-layer cases, and pending-fit handling on `MapView`. Pending-fit
tests cover replacement in both directions between map and layer fits,
Expand All @@ -166,6 +167,8 @@ unchanged-geometry cache. Total memory includes the OpenGL driver and context.
`ZoomAt` on a zero-sized viewport, which keep the pending fit.
Viewport tests cover anchor movement when the new center raises the
minimum resolution, and direct zoom-out multiplication without a reciprocal.
The opt-in registered-control host test also verifies cursor-anchored wheel
zoom and drag-to-pan pixel scaling and direction.
- Cover for each feature: happy path, empty/missing input, out-of-range
indexes, null values, non-ASCII paths, write-then-reopen round-trips, and
error cases.
Expand Down
241 changes: 241 additions & 0 deletions src/MapWinGIS.ActiveX/MapWinGISControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <atlconv.h>

#include <algorithm>
#include <cmath>
#include <exception>
#include <filesystem>
#include <limits>
Expand All @@ -24,13 +25,17 @@
#include <utility>
#include <vector>

#include <windowsx.h>

#include "ComConversion.h"
#include "mapwingis/core/Error.h"
#include "mapwingis/gdal/GdalEnvironment.h"
#include "mapwingis/vector/VectorDataset.h"

namespace
{
constexpr double kMouseWheelZoomFactor = 1.2;

using mwg::activex::ComConversion::BstrToUtf8;
using mwg::activex::ComConversion::CopyStringArray;
using mwg::activex::ComConversion::CopyToBstr;
Expand Down Expand Up @@ -706,6 +711,239 @@ bool CMapWinGISControl::ResizeView(const LONG width, const LONG height) noexcept
}
}

bool CMapWinGISControl::CaptureMouse() noexcept
{
if (m_bWndLess)
{
return m_spInPlaceSite != nullptr &&
m_spInPlaceSite->SetCapture(TRUE) == S_OK;
}

if (m_hWnd == nullptr)
{
return false;
}

::SetCapture(m_hWnd);
return ::GetCapture() == m_hWnd;
}

void CMapWinGISControl::ReleaseMouseCapture() noexcept
{
if (m_bWndLess)
{
if (m_spInPlaceSite != nullptr)
{
m_spInPlaceSite->SetCapture(FALSE);
}
}
else if (m_hWnd != nullptr && ::GetCapture() == m_hWnd)
{
::ReleaseCapture();
}
}

void CMapWinGISControl::CancelMousePan() noexcept
{
if (_isPanning)
{
_isPanning = false;
ReleaseMouseCapture();
}
}

bool CMapWinGISControl::GetMousePoint(
const LPARAM lParam,
const bool screenCoordinates,
POINT& point) const noexcept
{
point = POINT{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};

HWND coordinateWindow = m_hWnd;
if (screenCoordinates)
{
if (m_bWndLess)
{
if (m_spInPlaceSite == nullptr ||
FAILED(m_spInPlaceSite->GetWindow(&coordinateWindow)))
{
return false;
}
}

if (coordinateWindow == nullptr ||
!::ScreenToClient(coordinateWindow, &point))
{
return false;
}
}

if (m_bWndLess)
{
point.x -= m_rcPos.left;
point.y -= m_rcPos.top;
}
return true;
}

LRESULT CMapWinGISControl::OnLButtonDown(
UINT,
WPARAM,
const LPARAM lParam,
BOOL& handled)
{
POINT point{};
if (!GetMousePoint(lParam, false, point) || !CaptureMouse())
{
handled = FALSE;
return 0;
}

_lastPanPoint = point;
_isPanning = true;
handled = TRUE;
return 0;
}

LRESULT CMapWinGISControl::OnMouseMove(
UINT,
const WPARAM wParam,
const LPARAM lParam,
BOOL& handled)
{
if (!_isPanning)
{
handled = FALSE;
return 0;
}

if ((wParam & MK_LBUTTON) == 0)
{
CancelMousePan();
handled = TRUE;
return 0;
}

POINT point{};
if (!GetMousePoint(lParam, false, point))
{
handled = TRUE;
return 0;
}

const LONG pixelDeltaX = point.x - _lastPanPoint.x;
const LONG pixelDeltaY = point.y - _lastPanPoint.y;
_lastPanPoint = point;
if (pixelDeltaX == 0 && pixelDeltaY == 0)
{
handled = TRUE;
return 0;
}

const std::optional<double> resolution = _view.GetViewport().Resolution();
if (!resolution.has_value())
{
handled = TRUE;
return 0;
}

bool changed = false;
const HRESULT result = RunViewOperation(
[this, pixelDeltaX, pixelDeltaY, resolution]()
{
return _view.Pan(
-static_cast<double>(pixelDeltaX) * *resolution,
static_cast<double>(pixelDeltaY) * *resolution);
},
changed);
if (SUCCEEDED(result) && changed)
{
FireViewChange();
}

handled = TRUE;
return 0;
}

LRESULT CMapWinGISControl::OnLButtonUp(
UINT,
WPARAM,
LPARAM,
BOOL& handled)
{
if (!_isPanning)
{
handled = FALSE;
return 0;
}

CancelMousePan();
handled = TRUE;
return 0;
}

LRESULT CMapWinGISControl::OnMouseWheel(
UINT,
const WPARAM wParam,
const LPARAM lParam,
BOOL& handled)
{
const int wheelDelta = GET_WHEEL_DELTA_WPARAM(wParam);
POINT point{};
const mwg::Viewport& viewport = _view.GetViewport();
if (wheelDelta == 0 ||
!viewport.IsValid() ||
!GetMousePoint(lParam, true, point) ||
point.x < 0 || point.x >= viewport.Width() ||
point.y < 0 || point.y >= viewport.Height())
{
handled = FALSE;
return 0;
}

const double factor = std::pow(
kMouseWheelZoomFactor,
static_cast<double>(wheelDelta) / WHEEL_DELTA);
bool changed = false;
const HRESULT result = RunViewOperation(
[this, point, factor]()
{
return _view.ZoomAt(
{static_cast<double>(point.x), static_cast<double>(point.y)},
factor);
},
changed);
if (SUCCEEDED(result) && changed)
{
FireViewChange();
}

handled = TRUE;
return 0;
}

LRESULT CMapWinGISControl::OnCaptureChanged(
UINT,
WPARAM,
LPARAM,
BOOL& handled)
{
_isPanning = false;
handled = TRUE;
return 0;
}

LRESULT CMapWinGISControl::OnCancelMode(
UINT,
WPARAM,
LPARAM,
BOOL& handled)
{
CancelMousePan();
handled = TRUE;
return 0;
}

LRESULT CMapWinGISControl::OnSize(
UINT,
WPARAM,
Expand Down Expand Up @@ -736,6 +974,7 @@ LRESULT CMapWinGISControl::OnDestroy(
LPARAM,
BOOL& handled)
{
CancelMousePan();
_gdalEvents.OnDestroy();
handled = FALSE;
return 0;
Expand All @@ -762,6 +1001,7 @@ void CMapWinGISControl::DispatchGdalNotifications(void* const control) noexcept

STDMETHODIMP CMapWinGISControl::Close(const DWORD saveOption)
{
CancelMousePan();
_gdalEvents.Close();
return ATL::IOleObjectImpl<CMapWinGISControl>::Close(saveOption);
}
Expand Down Expand Up @@ -845,5 +1085,6 @@ HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo)

CMapWinGISControl::~CMapWinGISControl()
{
CancelMousePan();
_gdalEvents.Shutdown();
}
Loading