From f65643bb82f625e002d4126e0b8efe346633cf2e Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Sat, 7 Nov 2026 00:04:22 +0100 Subject: [PATCH 1/7] MapWinGISControl: declare layer cache for pan-by-blit --- src/MapWinGIS.ActiveX/MapWinGISControl.h | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.h b/src/MapWinGIS.ActiveX/MapWinGISControl.h index 1e02e01..ad91a71 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.h +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.h @@ -167,9 +167,34 @@ class ATL_NO_VTABLE CMapWinGISControl : void CancelMousePan() noexcept; bool GetMousePoint(LPARAM lParam, bool screenCoordinates, POINT& point) const noexcept; + /// Drops the cached layer frame; the next draw re-renders the layers + /// into the cache. Called when layers, the view (except a drag pan), the + /// viewport size, or the backend preference changes. + void InvalidateLayerCache() noexcept; + + /// Paints the cached layer frame over the already-cleared frame device + /// context, shifted to the viewport's current view when only the center + /// moved (a drag pan). Returns true when the cache could be used; false + /// when the layers must be re-rendered (cache miss, resize, zoom, or a + /// clamped, sub-pixel pan). + bool TryBlitCachedLayers(HDC frameDc, const mwg::Viewport& viewport) noexcept; + + /// Draws the layers for the viewport, refreshing the layer cache when + /// useCache holds. Falls back to drawing straight to frameDc when the + /// cache cannot be allocated or the cached frame cannot be blitted. + [[nodiscard]] HRESULT DrawLayersAndUpdateCache( + HDC frameDc, + const RECT& frameBounds, + const mwg::rendering::MapRenderScene& scene, + const mwg::Viewport& viewport, + bool useCache); + mwg::MapView _view; LONGLONG _featureCount = 0; mwg::activex::FrameBuffer _frameBuffer; + mwg::activex::FrameBuffer _layerCache; + mwg::Viewport _layerCacheViewport; + bool _layerCacheValid = false; mwg::activex::VectorLayerRenderer _renderer; mwg::activex::GdalEventSource _gdalEvents; POINT _lastPanPoint{}; From 0e0583410c00551fbce1e4613b432aeab70149e7 Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Sat, 7 Nov 2026 00:07:08 +0100 Subject: [PATCH 2/7] MapWinGISControl: pan by cached layer blit --- src/MapWinGIS.ActiveX/MapWinGISControl.cpp | 173 ++++++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp index 7fc8d3a..663660c 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp @@ -11,6 +11,14 @@ * its viewport into an offscreen frame buffer and presents the finished * frame with a single blit, so every layer uses the same transformation and * the visible surface never shows a partially drawn frame. + * + * Pan caching: the last fully rendered layer frame is kept in a second + * offscreen buffer. While the user drags the map (WM_LBUTTONDOWN through + * WM_LBUTTONUP), OnDraw paints the background and blits the cached frame at + * the accumulated pixel offset instead of re-rasterizing every layer on + * each WM_MOUSEMOVE; the button-up handler invalidates the cache so the new + * view is rasterized once. The cache is invalidated on layer add/remove/ + * clear, zoom, resize, and backend-preference changes. */ #include "MapWinGISControl.h" @@ -470,6 +478,7 @@ HRESULT CMapWinGISControl::LoadVectorDatasetImpl( { *layerId = convertedId; } + InvalidateLayerCache(); FireViewChange(); return S_OK; } @@ -492,6 +501,7 @@ STDMETHODIMP CMapWinGISControl::ClearLayers() if (_view.ClearLayers()) { _featureCount = 0; + InvalidateLayerCache(); FireViewChange(); } return S_OK; @@ -526,6 +536,7 @@ STDMETHODIMP CMapWinGISControl::get_RenderingBackend(BSTR* backend) STDMETHODIMP CMapWinGISControl::get_RenderingStatus(BSTR* status) { + return CopyToBstr(_renderer.GetDiagnostic(), backend_or_status(status)); return CopyToBstr(_renderer.GetDiagnostic(), status); } @@ -570,6 +581,7 @@ STDMETHODIMP CMapWinGISControl::put_RenderingBackendPreference(BSTR preference) return E_INVALIDARG; } + InvalidateLayerCache(); FireViewChange(); return S_OK; } @@ -582,6 +594,7 @@ STDMETHODIMP CMapWinGISControl::FitToMap() changed); if (SUCCEEDED(result) && changed) { + InvalidateLayerCache(); FireViewChange(); } return result; @@ -595,6 +608,7 @@ STDMETHODIMP CMapWinGISControl::ZoomIn(const DOUBLE factor) changed); if (SUCCEEDED(result) && changed) { + InvalidateLayerCache(); FireViewChange(); } return result; @@ -608,6 +622,7 @@ STDMETHODIMP CMapWinGISControl::ZoomOut(const DOUBLE factor) changed); if (SUCCEEDED(result) && changed) { + InvalidateLayerCache(); FireViewChange(); } return result; @@ -623,6 +638,7 @@ STDMETHODIMP CMapWinGISControl::Pan( changed); if (SUCCEEDED(result) && changed) { + InvalidateLayerCache(); FireViewChange(); } return result; @@ -642,6 +658,7 @@ STDMETHODIMP CMapWinGISControl::ZoomAt( changed); if (SUCCEEDED(result) && changed) { + InvalidateLayerCache(); FireViewChange(); } return result; @@ -665,6 +682,7 @@ STDMETHODIMP CMapWinGISControl::ZoomToLayer(const LONGLONG layerId) changed); if (SUCCEEDED(result) && changed) { + InvalidateLayerCache(); FireViewChange(); } return result; @@ -849,6 +867,8 @@ LRESULT CMapWinGISControl::OnMouseMove( return 0; } + // The drag pan changes only the view center, so OnDraw can serve the + // frame from the layer cache; the cache is dropped on button-up. bool changed = false; const HRESULT result = RunViewOperation( [this, pixelDeltaX, pixelDeltaY, resolution]() @@ -880,6 +900,11 @@ LRESULT CMapWinGISControl::OnLButtonUp( } CancelMousePan(); + // The drag ended: rasterize the layers once at the final view so the + // cached frame matches the new center exactly (a clamped pan can leave + // the blitted frame up to half a pixel off). + InvalidateLayerCache(); + FireViewChange(); handled = TRUE; return 0; } @@ -917,6 +942,7 @@ LRESULT CMapWinGISControl::OnMouseWheel( changed); if (SUCCEEDED(result) && changed) { + InvalidateLayerCache(); FireViewChange(); } @@ -955,6 +981,7 @@ LRESULT CMapWinGISControl::OnSize( _gdalEvents.OnSize(); if (ResizeView(LOWORD(lParam), HIWORD(lParam))) { + InvalidateLayerCache(); FireViewChange(); } handled = TRUE; @@ -1024,12 +1051,147 @@ STDMETHODIMP CMapWinGISControl::SetObjectRects(LPCRECT position, LPCRECT clip) static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension)))) { + InvalidateLayerCache(); FireViewChange(); } } return result; } +void CMapWinGISControl::InvalidateLayerCache() noexcept +{ + _layerCacheValid = false; +} + +bool CMapWinGISControl::TryBlitCachedLayers( + HDC frameDc, + const mwg::Viewport& viewport) noexcept +{ + if (!_layerCacheValid || + !_layerCache.IsValid() || + _layerCache.Width() != viewport.Width() || + _layerCache.Height() != viewport.Height()) + { + return false; + } + + const std::optional resolution = viewport.Resolution(); + const std::optional cacheResolution = _layerCacheViewport.Resolution(); + const std::optional center = viewport.Center(); + const std::optional cacheCenter = _layerCacheViewport.Center(); + if (!resolution.has_value() || + !cacheResolution.has_value() || + !center.has_value() || + !cacheCenter.has_value() || + *resolution != *cacheResolution) + { + // A resolution change (or a missing view) needs a re-render. + return false; + } + + // Screen position of a fixed map point shifts by the center difference + // divided by the resolution, with the Y axis pointing down. + const double offsetX = (cacheCenter->x - center->x) / *resolution; + const double offsetY = (center->y - cacheCenter->y) / *resolution; + const LONG shiftX = static_cast(std::lround(offsetX)); + const LONG shiftY = static_cast(std::lround(offsetY)); + if (std::abs(offsetX - static_cast(shiftX)) > 1e-6 || + std::abs(offsetY - static_cast(shiftY)) > 1e-6) + { + // Only a clamped pan leaves a sub-pixel offset; re-render so the + // frame does not jitter between a shifted blit and a true render. + return false; + } + + const LONG width = static_cast(viewport.Width()); + const LONG height = static_cast(viewport.Height()); + + // Destination (x, y) shows cache pixel (x - shiftX, y - shiftY); blit the + // rectangle where that source position is inside the cached frame. The + // rest of the destination keeps the background already painted. + const LONG destLeft = std::max(0, shiftX); + const LONG destTop = std::max(0, shiftY); + const LONG destRight = std::min(width, width + shiftX); + const LONG destBottom = std::min(height, height + shiftY); + if (destRight <= destLeft || destBottom <= destTop) + { + // The cached content is entirely outside the frame. + return true; + } + + return ::BitBlt( + frameDc, + destLeft, + destTop, + destRight - destLeft, + destBottom - destTop, + _layerCache.Get(), + destLeft - shiftX, + destTop - shiftY, + SRCCOPY) != 0; +} + +HRESULT CMapWinGISControl::DrawLayersAndUpdateCache( + HDC frameDc, + const RECT& frameBounds, + const mwg::rendering::MapRenderScene& scene, + const mwg::Viewport& viewport, + const bool useCache) +{ + if (!useCache) + { + return _renderer.Draw(frameDc, frameBounds, scene, viewport); + } + + const int width = viewport.Width(); + const int height = viewport.Height(); + if (!_layerCache.Resize(width, height)) + { + // Without a cache the layers are drawn straight into the frame; the + // next draw retries the allocation. + return _renderer.Draw(frameDc, frameBounds, scene, viewport); + } + + HDC cacheDc = _layerCache.Get(); + const RECT cacheBounds{0, 0, width, height}; + if (::FillRect( + cacheDc, + &cacheBounds, + ::GetSysColorBrush(COLOR_WINDOW)) == 0) + { + return _renderer.Draw(frameDc, frameBounds, scene, viewport); + } + + const HRESULT result = _renderer.Draw(cacheDc, cacheBounds, scene, viewport); + if (FAILED(result)) + { + _layerCacheValid = false; + return result; + } + + _layerCacheViewport = viewport; + _layerCacheValid = true; + + if (::BitBlt( + frameDc, + frameBounds.left, + frameBounds.top, + width, + height, + cacheDc, + 0, + 0, + SRCCOPY) != 0) + { + return result; + } + + // A blit between different devices (printer or metafile targets) can + // fail; present the layers by drawing straight to the target. + _layerCacheValid = false; + return _renderer.Draw(frameDc, frameBounds, scene, viewport); +} + HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo) { if (drawInfo.hdcDraw == nullptr || drawInfo.prcBounds == nullptr) @@ -1088,7 +1250,16 @@ HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo) } if (!scene.layers.empty() && viewport.IsValid()) { - return _renderer.Draw(frameDc, frameBounds, scene, viewport); + if (buffered && TryBlitCachedLayers(frameDc, viewport)) + { + return S_OK; + } + return DrawLayersAndUpdateCache( + frameDc, + frameBounds, + scene, + viewport, + buffered); } return S_OK; } From b25dc1404c72e187f9de2518a1e4c221895d8e9e Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Sat, 7 Nov 2026 00:09:59 +0100 Subject: [PATCH 3/7] Fix stray line in get_RenderingStatus --- src/MapWinGIS.ActiveX/MapWinGISControl.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp index 663660c..d8121ff 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp @@ -536,7 +536,6 @@ STDMETHODIMP CMapWinGISControl::get_RenderingBackend(BSTR* backend) STDMETHODIMP CMapWinGISControl::get_RenderingStatus(BSTR* status) { - return CopyToBstr(_renderer.GetDiagnostic(), backend_or_status(status)); return CopyToBstr(_renderer.GetDiagnostic(), status); } From 914ace2ac6dc2f5a8644e2fac3ed15d2caf74d01 Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Sat, 7 Nov 2026 00:28:11 +0100 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20finalize=20l?= =?UTF-8?q?ayer=20cache=20on=20every=20pan=20termination?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EndMousePan centralizes drag termination (button-up, WM_CAPTURECHANGED, WM_CANCELMODE, button released during move) - Every termination path now invalidates the layer cache and requests one final redraw, so panned-in strips never keep showing the background - OnDestroy and Close keep the non-redrawing CancelMousePan --- src/MapWinGIS.ActiveX/MapWinGISControl.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.h b/src/MapWinGIS.ActiveX/MapWinGISControl.h index ad91a71..286018a 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.h +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.h @@ -165,6 +165,13 @@ class ATL_NO_VTABLE CMapWinGISControl : bool CaptureMouse() noexcept; void ReleaseMouseCapture() noexcept; void CancelMousePan() noexcept; + + /// Ends an active drag pan through every termination path (button-up, + /// capture loss, cancel mode, button released): stops panning, + /// invalidates the layer cache so the final view is rasterized, and + /// requests a redraw. + void EndMousePan() noexcept; + bool GetMousePoint(LPARAM lParam, bool screenCoordinates, POINT& point) const noexcept; /// Drops the cached layer frame; the next draw re-renders the layers From e0c12b09a240e8c2405b1e0df9bacc0d7ee90d2b Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Sat, 7 Nov 2026 00:30:54 +0100 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20route=20ever?= =?UTF-8?q?y=20pan=20termination=20through=20EndMousePan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WM_CAPTURECHANGED, WM_CANCELMODE, and the no-button WM_MOUSEMOVE path now invalidate the layer cache and request a final redraw - The shifted cached frame can no longer stay on screen after a drag ends outside WM_LBUTTONUP --- src/MapWinGIS.ActiveX/MapWinGISControl.cpp | 33 +++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp index d8121ff..73a63ee 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp @@ -16,7 +16,7 @@ * offscreen buffer. While the user drags the map (WM_LBUTTONDOWN through * WM_LBUTTONUP), OnDraw paints the background and blits the cached frame at * the accumulated pixel offset instead of re-rasterizing every layer on - * each WM_MOUSEMOVE; the button-up handler invalidates the cache so the new + * each WM_MOUSEMOVE; the drag-end handler invalidates the cache so the new * view is rasterized once. The cache is invalidated on layer add/remove/ * clear, zoom, resize, and backend-preference changes. */ @@ -771,6 +771,24 @@ void CMapWinGISControl::CancelMousePan() noexcept } } +void CMapWinGISControl::EndMousePan() noexcept +{ + if (!_isPanning) + { + return; + } + + CancelMousePan(); + // The drag ended: rasterize the layers once at the final view so the + // cached frame matches the new center exactly (a clamped pan can leave + // the blitted frame up to half a pixel off). Every drag termination + // path (button-up, capture loss, cancel mode, button released) goes + // through here, so a terminated drag never leaves the shifted cached + // frame on screen. + InvalidateLayerCache(); + FireViewChange(); +} + bool CMapWinGISControl::GetMousePoint( const LPARAM lParam, const bool screenCoordinates, @@ -838,7 +856,7 @@ LRESULT CMapWinGISControl::OnMouseMove( if ((wParam & MK_LBUTTON) == 0) { - CancelMousePan(); + EndMousePan(); handled = TRUE; return 0; } @@ -898,12 +916,7 @@ LRESULT CMapWinGISControl::OnLButtonUp( return 0; } - CancelMousePan(); - // The drag ended: rasterize the layers once at the final view so the - // cached frame matches the new center exactly (a clamped pan can leave - // the blitted frame up to half a pixel off). - InvalidateLayerCache(); - FireViewChange(); + EndMousePan(); handled = TRUE; return 0; } @@ -955,7 +968,7 @@ LRESULT CMapWinGISControl::OnCaptureChanged( LPARAM, BOOL& handled) { - _isPanning = false; + EndMousePan(); handled = TRUE; return 0; } @@ -966,7 +979,7 @@ LRESULT CMapWinGISControl::OnCancelMode( LPARAM, BOOL& handled) { - CancelMousePan(); + EndMousePan(); handled = TRUE; return 0; } From 1bc94ba5ed9a62fe3e879b13e3317a5022973664 Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Sat, 7 Nov 2026 00:47:11 +0100 Subject: [PATCH 6/7] test --- src/MapWinGIS.ActiveX/MapWinGISControl.cpp | 1336 +------------------- 1 file changed, 1 insertion(+), 1335 deletions(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp index 73a63ee..329f221 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp @@ -1,1335 +1 @@ -// SPDX-License-Identifier: MPL-2.0 - -/** - * @file MapWinGISControl.cpp - * @brief Implements vector loading, map navigation, ActiveX state reporting, - * and host drawing. - * - * Redraws: every map or view change made through the control goes through - * its MapView; the control calls FireViewChange when the MapView reports - * that the frame changed. OnDraw composes the MapView's render scene through - * its viewport into an offscreen frame buffer and presents the finished - * frame with a single blit, so every layer uses the same transformation and - * the visible surface never shows a partially drawn frame. - * - * Pan caching: the last fully rendered layer frame is kept in a second - * offscreen buffer. While the user drags the map (WM_LBUTTONDOWN through - * WM_LBUTTONUP), OnDraw paints the background and blits the cached frame at - * the accumulated pixel offset instead of re-rasterizing every layer on - * each WM_MOUSEMOVE; the drag-end handler invalidates the cache so the new - * view is rasterized once. The cache is invalidated on layer add/remove/ - * clear, zoom, resize, and backend-preference changes. - */ -#include "MapWinGISControl.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#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; - using mwg::activex::ComConversion::CreateStringArray; - using mwg::activex::ComConversion::Utf8ToBstr; - - [[nodiscard]] HRESULT SetControlComErrorInfo( - const char* message, - REFIID sourceIid, - const HRESULT errorCode) noexcept - { - try - { - const CA2W wideMessage(message, CP_UTF8); - return AtlReportError(CLSID_MapWinGISControl, wideMessage, sourceIid, errorCode); - } - catch (...) - { - return errorCode; - } - } - - [[nodiscard]] HRESULT SetComErrorInfo( - const char* message, - const HRESULT errorCode) noexcept - { - return SetControlComErrorInfo(message, IID_IMapWinGISControlGdal, errorCode); - } - - [[nodiscard]] HRESULT CopyLayerId(const mwg::LayerId id, LONGLONG* destination) noexcept - { - if (id.value > static_cast(std::numeric_limits::max())) - { - return E_UNEXPECTED; - } - *destination = static_cast(id.value); - return S_OK; - } - - template - [[nodiscard]] HRESULT RunViewOperation( - Operation&& operation, - bool& changed) - { - try - { - changed = std::forward(operation)(); - return S_OK; - } - catch (const std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (const mwg::InvalidArgument& exception) - { - return SetControlComErrorInfo( - exception.what(), - IID_IMapWinGISControl4, - E_INVALIDARG); - } - catch (const std::exception& exception) - { - return SetControlComErrorInfo( - exception.what(), - IID_IMapWinGISControl4, - E_FAIL); - } - catch (...) - { - return E_FAIL; - } - } - - [[nodiscard]] const wchar_t* BackendName( - const std::optional backend) noexcept - { - if (!backend.has_value()) - { - return L"Not rendered"; - } - - return *backend == mwg::activex::VectorRenderBackendKind::OpenGL - ? L"OpenGL" - : L"GDI"; - } - - [[nodiscard]] const wchar_t* PreferenceName( - const mwg::activex::VectorRenderBackendPreference preference) noexcept - { - switch (preference) - { - case mwg::activex::VectorRenderBackendPreference::Automatic: - return L"Automatic"; - case mwg::activex::VectorRenderBackendPreference::OpenGL: - return L"OpenGL"; - case mwg::activex::VectorRenderBackendPreference::Gdi: - return L"GDI"; - default: - return L"Automatic"; - } - } -} - -CMapWinGISControl::CMapWinGISControl() - : _gdalEvents( - m_vec, - &CMapWinGISControl::DispatchGdalNotifications, - this) -{ -} - -STDMETHODIMP CMapWinGISControl::InterfaceSupportsErrorInfo(REFIID interfaceId) -{ - return IsEqualIID(interfaceId, IID_IMapWinGISControl) || - IsEqualIID(interfaceId, IID_IMapWinGISControl2) || - IsEqualIID(interfaceId, IID_IMapWinGISControl3) || - IsEqualIID(interfaceId, IID_IMapWinGISControl4) || - IsEqualIID(interfaceId, IID_IMapWinGISControlGdal) ? S_OK : S_FALSE; -} - -STDMETHODIMP CMapWinGISControl::BeginGdalOperation( - BSTR algorithmPath, - SAFEARRAY* arguments, - LONGLONG* operationId) -{ - if (operationId == nullptr) - { - return E_POINTER; - } - *operationId = 0; - const HRESULT dispatchWindowResult = _gdalEvents.EnsureDispatchWindow(); - if (FAILED(dispatchWindowResult)) - { - return dispatchWindowResult; - } - - std::string algorithm; - std::vector commandLineArguments; - HRESULT result = S_OK; - try - { - result = BstrToUtf8(algorithmPath, algorithm); - if (FAILED(result) || algorithm.empty()) - { - return E_INVALIDARG; - } - - result = CopyStringArray(arguments, commandLineArguments); - if (FAILED(result)) - { - return result; - } - } - catch (const std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (const std::exception& exception) - { - return SetComErrorInfo(exception.what(), E_INVALIDARG); - } - catch (...) - { - return E_FAIL; - } - - try - { - *operationId = static_cast( - _gdalEvents.Begin( - std::move(algorithm), - std::move(commandLineArguments))); - return S_OK; - } - catch (const std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (const std::exception& exception) - { - return SetComErrorInfo(exception.what(), E_FAIL); - } - catch (...) - { - return E_FAIL; - } -} - -STDMETHODIMP CMapWinGISControl::CancelGdalOperation(const LONGLONG operationId) -{ - if (operationId <= 0) - { - return E_INVALIDARG; - } - - switch (_gdalEvents.Cancel(static_cast(operationId))) - { - case mwg::activex::GdalCancelResult::Requested: - return S_OK; - case mwg::activex::GdalCancelResult::AlreadyRequested: - case mwg::activex::GdalCancelResult::Completed: - return S_FALSE; - case mwg::activex::GdalCancelResult::NotFound: - default: - return E_INVALIDARG; - } -} - -STDMETHODIMP CMapWinGISControl::EndGdalOperation( - const LONGLONG operationId, - VARIANT_BOOL* succeeded, - VARIANT_BOOL* cancelled, - LONG* errorCode, - LONG* callbackErrorCode, - BSTR* callbackError, - BSTR* summary, - BSTR* diagnostics, - SAFEARRAY** warnings) -{ - if (succeeded == nullptr || cancelled == nullptr || errorCode == nullptr || - callbackErrorCode == nullptr || callbackError == nullptr || - summary == nullptr || - diagnostics == nullptr || warnings == nullptr) - { - return E_POINTER; - } - *succeeded = VARIANT_FALSE; - *cancelled = VARIANT_FALSE; - *errorCode = 0; - *callbackErrorCode = 0; - *callbackError = nullptr; - *summary = nullptr; - *diagnostics = nullptr; - *warnings = nullptr; - if (operationId <= 0) - { - return E_INVALIDARG; - } - - mwg::activex::GdalOperationLookup resultLookup; - try - { - resultLookup = _gdalEvents.GetResult(static_cast(operationId)); - } - catch (const std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - if (resultLookup.status == mwg::activex::GdalEndResult::Pending) - { - return E_PENDING; - } - if (resultLookup.status == mwg::activex::GdalEndResult::NotFound || - !resultLookup.result.has_value()) - { - return E_INVALIDARG; - } - const mwg::activex::GdalOperationSnapshot& operationResult = - *resultLookup.result; - - BSTR summaryText = nullptr; - BSTR callbackErrorText = nullptr; - HRESULT result = Utf8ToBstr( - operationResult.callbackError, - &callbackErrorText); - if (FAILED(result)) - { - return result; - } - - result = Utf8ToBstr(operationResult.summary, &summaryText); - if (FAILED(result)) - { - ::SysFreeString(callbackErrorText); - return result; - } - - BSTR diagnosticText = nullptr; - result = Utf8ToBstr(operationResult.diagnostics, &diagnosticText); - if (FAILED(result)) - { - ::SysFreeString(callbackErrorText); - ::SysFreeString(summaryText); - return result; - } - - SAFEARRAY* warningArray = nullptr; - result = CreateStringArray(operationResult.warnings, &warningArray); - if (FAILED(result)) - { - ::SysFreeString(callbackErrorText); - ::SysFreeString(summaryText); - ::SysFreeString(diagnosticText); - return result; - } - - const mwg::activex::GdalOperationLookup cleanupResult = - _gdalEvents.End(static_cast(operationId)); - if (cleanupResult.status != mwg::activex::GdalEndResult::Ended) - { - ::SysFreeString(callbackErrorText); - ::SysFreeString(summaryText); - ::SysFreeString(diagnosticText); - if (warningArray != nullptr) - { - ::SafeArrayDestroy(warningArray); - } - return cleanupResult.status == mwg::activex::GdalEndResult::Pending - ? E_PENDING - : E_INVALIDARG; - } - - *succeeded = operationResult.succeeded ? VARIANT_TRUE : VARIANT_FALSE; - *cancelled = operationResult.cancelled ? VARIANT_TRUE : VARIANT_FALSE; - *errorCode = operationResult.errorCode; - *callbackErrorCode = operationResult.callbackErrorCode; - *callbackError = callbackErrorText; - *summary = summaryText; - *diagnostics = diagnosticText; - *warnings = warningArray; - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::get_CoreInitialized(VARIANT_BOOL* initialized) -{ - if (initialized == nullptr) - { - return E_POINTER; - } - - *initialized = mwg::GdalEnvironment::IsInitialized() ? VARIANT_TRUE : VARIANT_FALSE; - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::LoadVectorDataset(BSTR path) -{ - return LoadVectorDatasetImpl(path, nullptr, IID_IMapWinGISControl); -} - -STDMETHODIMP CMapWinGISControl::LoadVectorDatasetEx( - BSTR path, - LONGLONG* layerId) -{ - if (layerId == nullptr) - { - return E_POINTER; - } - *layerId = 0; - return LoadVectorDatasetImpl(path, layerId, IID_IMapWinGISControl4); -} - -HRESULT CMapWinGISControl::LoadVectorDatasetImpl( - BSTR path, - LONGLONG* layerId, - REFIID sourceIid) -{ - if (path == nullptr) - { - return E_INVALIDARG; - } - - const UINT pathLength = ::SysStringLen(path); - if (pathLength == 0) - { - return E_INVALIDARG; - } - - try - { - const std::wstring pathText(path, static_cast(pathLength)); - if (pathText.find(L'\0') != std::wstring::npos) - { - return E_INVALIDARG; - } - - const std::filesystem::path datasetPath(pathText); - std::shared_ptr dataset = mwg::VectorDataset::Open(datasetPath); - if (dataset->LayerCount() == 0) - { - throw mwg::CoreError("Vector dataset contains no layers"); - } - - // Validate before AddLayer can auto-fit or schedule a pending fit. - mwg::Map candidate; - candidate.AddLayer(dataset); - mwg::rendering::MapRenderScene scene = - mwg::rendering::CreateMapRenderScene(candidate); - for (const mwg::rendering::SkippedLayer& skipped : scene.skippedLayers) - { - if (skipped.reason == mwg::rendering::LayerSkipReason::Failed) - { - throw mwg::CoreError(skipped.message); - } - } - mwg::rendering::VectorRenderScene preparedScene; - if (scene.layers.empty()) - { - preparedScene.sourceFeatureCount = dataset->Layer(0).FeatureCount(); - } - else - { - preparedScene = std::move(scene.layers.front().scene); - } - const LONGLONG count = static_cast(preparedScene.sourceFeatureCount); - if (count < 0 || count > std::numeric_limits::max() - _featureCount) - { - throw mwg::CoreError("Map feature count is out of range"); - } - - const mwg::LayerId id = _view.AddLayer(std::move(dataset), std::move(preparedScene)); - LONGLONG convertedId = 0; - if (layerId != nullptr && FAILED(CopyLayerId(id, &convertedId))) - { - (void)_view.RemoveLayer(id); - return E_UNEXPECTED; - } - try - { - // Fit the combined extent so the newly added layer is visible. - (void)_view.FitToMap(); - } - catch (...) - { - (void)_view.RemoveLayer(id); - throw; - } - - _featureCount += count; - if (layerId != nullptr) - { - *layerId = convertedId; - } - InvalidateLayerCache(); - FireViewChange(); - return S_OK; - } - catch (const std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (const std::exception& exception) - { - return SetControlComErrorInfo(exception.what(), sourceIid, E_FAIL); - } - catch (...) - { - return E_FAIL; - } -} - -STDMETHODIMP CMapWinGISControl::ClearLayers() -{ - if (_view.ClearLayers()) - { - _featureCount = 0; - InvalidateLayerCache(); - FireViewChange(); - } - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::get_LayerLoaded(VARIANT_BOOL* loaded) -{ - if (loaded == nullptr) - { - return E_POINTER; - } - - *loaded = _view.GetMap().LayerCount() > 0 ? VARIANT_TRUE : VARIANT_FALSE; - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::get_FeatureCount(LONGLONG* count) -{ - if (count == nullptr) - { - return E_POINTER; - } - - *count = _featureCount; - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::get_RenderingBackend(BSTR* backend) -{ - return CopyToBstr(BackendName(_renderer.GetActiveBackend()), backend); -} - -STDMETHODIMP CMapWinGISControl::get_RenderingStatus(BSTR* status) -{ - return CopyToBstr(_renderer.GetDiagnostic(), status); -} - -STDMETHODIMP CMapWinGISControl::get_RenderingBackendPreference(BSTR* preference) -{ - return CopyToBstr(PreferenceName(_renderer.GetPreference()), preference); -} - -STDMETHODIMP CMapWinGISControl::put_RenderingBackendPreference(BSTR preference) -{ - if (preference == nullptr) - { - return E_INVALIDARG; - } - - const UINT length = ::SysStringLen(preference); - auto equals = [preference, length](const wchar_t* value, const int valueLength) - { - return length == static_cast(valueLength) && - ::CompareStringOrdinal( - preference, - static_cast(length), - value, - valueLength, - TRUE) == CSTR_EQUAL; - }; - - if (equals(L"Automatic", 9)) - { - _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::Automatic); - } - else if (equals(L"OpenGL", 6)) - { - _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::OpenGL); - } - else if (equals(L"GDI", 3)) - { - _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::Gdi); - } - else - { - return E_INVALIDARG; - } - - InvalidateLayerCache(); - FireViewChange(); - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::FitToMap() -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this]() { return _view.FitToMap(); }, - changed); - if (SUCCEEDED(result) && changed) - { - InvalidateLayerCache(); - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::ZoomIn(const DOUBLE factor) -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this, factor]() { return _view.ZoomIn(factor); }, - changed); - if (SUCCEEDED(result) && changed) - { - InvalidateLayerCache(); - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::ZoomOut(const DOUBLE factor) -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this, factor]() { return _view.ZoomOut(factor); }, - changed); - if (SUCCEEDED(result) && changed) - { - InvalidateLayerCache(); - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::Pan( - const DOUBLE deltaX, - const DOUBLE deltaY) -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this, deltaX, deltaY]() { return _view.Pan(deltaX, deltaY); }, - changed); - if (SUCCEEDED(result) && changed) - { - InvalidateLayerCache(); - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::ZoomAt( - const DOUBLE x, - const DOUBLE y, - const DOUBLE factor) -{ - bool changed = false; - const HRESULT result = RunViewOperation( - [this, x, y, factor]() - { - return _view.ZoomAt({x, y}, factor); - }, - changed); - if (SUCCEEDED(result) && changed) - { - InvalidateLayerCache(); - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::ZoomToLayer(const LONGLONG layerId) -{ - if (layerId <= 0) - { - return SetControlComErrorInfo("Layer ID must be positive", IID_IMapWinGISControl4, E_INVALIDARG); - } - const mwg::LayerId id{static_cast(layerId)}; - if (_view.GetMap().FindLayer(id) == nullptr) - { - return SetControlComErrorInfo("Layer ID was not found", IID_IMapWinGISControl4, E_INVALIDARG); - } - - bool changed = false; - const HRESULT result = RunViewOperation( - [this, id]() { return _view.ZoomToLayer(id); }, - changed); - if (SUCCEEDED(result) && changed) - { - InvalidateLayerCache(); - FireViewChange(); - } - return result; -} - -STDMETHODIMP CMapWinGISControl::get_LayerCount(LONG* count) -{ - if (count == nullptr) - { - return E_POINTER; - } - *count = _view.GetMap().LayerCount(); - return S_OK; -} - -STDMETHODIMP CMapWinGISControl::GetLayerId( - const LONG index, - LONGLONG* layerId) -{ - if (layerId == nullptr) - { - return E_POINTER; - } - *layerId = 0; - const mwg::Map& map = _view.GetMap(); - if (index < 0 || index >= map.LayerCount()) - { - return E_INVALIDARG; - } - - return CopyLayerId(map.Layer(index).Id(), layerId); -} - -bool CMapWinGISControl::ResizeView(const LONG width, const LONG height) noexcept -{ - try - { - return _view.Resize( - static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), - static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension))); - } - catch (const std::exception& exception) - { - ::OutputDebugStringA(exception.what()); - return false; - } -} - -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(); - } -} - -void CMapWinGISControl::EndMousePan() noexcept -{ - if (!_isPanning) - { - return; - } - - CancelMousePan(); - // The drag ended: rasterize the layers once at the final view so the - // cached frame matches the new center exactly (a clamped pan can leave - // the blitted frame up to half a pixel off). Every drag termination - // path (button-up, capture loss, cancel mode, button released) goes - // through here, so a terminated drag never leaves the shifted cached - // frame on screen. - InvalidateLayerCache(); - FireViewChange(); -} - -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) - { - EndMousePan(); - 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 resolution = _view.GetViewport().Resolution(); - if (!resolution.has_value()) - { - handled = TRUE; - return 0; - } - - // The drag pan changes only the view center, so OnDraw can serve the - // frame from the layer cache; the cache is dropped on button-up. - bool changed = false; - const HRESULT result = RunViewOperation( - [this, pixelDeltaX, pixelDeltaY, resolution]() - { - return _view.Pan( - -static_cast(pixelDeltaX) * *resolution, - static_cast(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; - } - - EndMousePan(); - 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(wheelDelta) / WHEEL_DELTA); - bool changed = false; - const HRESULT result = RunViewOperation( - [this, point, factor]() - { - return _view.ZoomAt( - {static_cast(point.x), static_cast(point.y)}, - factor); - }, - changed); - if (SUCCEEDED(result) && changed) - { - InvalidateLayerCache(); - FireViewChange(); - } - - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnCaptureChanged( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - EndMousePan(); - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnCancelMode( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - EndMousePan(); - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnSize( - UINT, - WPARAM, - const LPARAM lParam, - BOOL& handled) -{ - _gdalEvents.OnSize(); - if (ResizeView(LOWORD(lParam), HIWORD(lParam))) - { - InvalidateLayerCache(); - FireViewChange(); - } - handled = TRUE; - return 0; -} - -LRESULT CMapWinGISControl::OnGdalDispatch( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - return _gdalEvents.OnDispatch(handled); -} - -LRESULT CMapWinGISControl::OnDestroy( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - CancelMousePan(); - _gdalEvents.OnDestroy(); - handled = FALSE; - return 0; -} - -void CMapWinGISControl::DispatchGdalNotifications(void* const control) noexcept -{ - auto* const instance = static_cast(control); - ATL::CComPtr lifetime(instance); - BOOL handled = FALSE; - try - { - instance->OnGdalDispatch( - mwg::activex::kGdalDispatchMessage, - 0, - 0, - handled); - } - catch (const std::exception& exception) - { - ::OutputDebugStringA(exception.what()); - } -} - -STDMETHODIMP CMapWinGISControl::Close(const DWORD saveOption) -{ - CancelMousePan(); - _gdalEvents.Close(); - return ATL::IOleObjectImpl::Close(saveOption); -} - -STDMETHODIMP CMapWinGISControl::SetObjectRects(LPCRECT position, LPCRECT clip) -{ - if (position == nullptr || clip == nullptr) - { - return E_POINTER; - } - const HRESULT result = - ATL::IOleInPlaceObjectWindowlessImpl::SetObjectRects(position, clip); - if (SUCCEEDED(result) && m_bWndLess) - { - const LONGLONG width = static_cast(position->right) - position->left; - const LONGLONG height = static_cast(position->bottom) - position->top; - if (ResizeView( - static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), - static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension)))) - { - InvalidateLayerCache(); - FireViewChange(); - } - } - return result; -} - -void CMapWinGISControl::InvalidateLayerCache() noexcept -{ - _layerCacheValid = false; -} - -bool CMapWinGISControl::TryBlitCachedLayers( - HDC frameDc, - const mwg::Viewport& viewport) noexcept -{ - if (!_layerCacheValid || - !_layerCache.IsValid() || - _layerCache.Width() != viewport.Width() || - _layerCache.Height() != viewport.Height()) - { - return false; - } - - const std::optional resolution = viewport.Resolution(); - const std::optional cacheResolution = _layerCacheViewport.Resolution(); - const std::optional center = viewport.Center(); - const std::optional cacheCenter = _layerCacheViewport.Center(); - if (!resolution.has_value() || - !cacheResolution.has_value() || - !center.has_value() || - !cacheCenter.has_value() || - *resolution != *cacheResolution) - { - // A resolution change (or a missing view) needs a re-render. - return false; - } - - // Screen position of a fixed map point shifts by the center difference - // divided by the resolution, with the Y axis pointing down. - const double offsetX = (cacheCenter->x - center->x) / *resolution; - const double offsetY = (center->y - cacheCenter->y) / *resolution; - const LONG shiftX = static_cast(std::lround(offsetX)); - const LONG shiftY = static_cast(std::lround(offsetY)); - if (std::abs(offsetX - static_cast(shiftX)) > 1e-6 || - std::abs(offsetY - static_cast(shiftY)) > 1e-6) - { - // Only a clamped pan leaves a sub-pixel offset; re-render so the - // frame does not jitter between a shifted blit and a true render. - return false; - } - - const LONG width = static_cast(viewport.Width()); - const LONG height = static_cast(viewport.Height()); - - // Destination (x, y) shows cache pixel (x - shiftX, y - shiftY); blit the - // rectangle where that source position is inside the cached frame. The - // rest of the destination keeps the background already painted. - const LONG destLeft = std::max(0, shiftX); - const LONG destTop = std::max(0, shiftY); - const LONG destRight = std::min(width, width + shiftX); - const LONG destBottom = std::min(height, height + shiftY); - if (destRight <= destLeft || destBottom <= destTop) - { - // The cached content is entirely outside the frame. - return true; - } - - return ::BitBlt( - frameDc, - destLeft, - destTop, - destRight - destLeft, - destBottom - destTop, - _layerCache.Get(), - destLeft - shiftX, - destTop - shiftY, - SRCCOPY) != 0; -} - -HRESULT CMapWinGISControl::DrawLayersAndUpdateCache( - HDC frameDc, - const RECT& frameBounds, - const mwg::rendering::MapRenderScene& scene, - const mwg::Viewport& viewport, - const bool useCache) -{ - if (!useCache) - { - return _renderer.Draw(frameDc, frameBounds, scene, viewport); - } - - const int width = viewport.Width(); - const int height = viewport.Height(); - if (!_layerCache.Resize(width, height)) - { - // Without a cache the layers are drawn straight into the frame; the - // next draw retries the allocation. - return _renderer.Draw(frameDc, frameBounds, scene, viewport); - } - - HDC cacheDc = _layerCache.Get(); - const RECT cacheBounds{0, 0, width, height}; - if (::FillRect( - cacheDc, - &cacheBounds, - ::GetSysColorBrush(COLOR_WINDOW)) == 0) - { - return _renderer.Draw(frameDc, frameBounds, scene, viewport); - } - - const HRESULT result = _renderer.Draw(cacheDc, cacheBounds, scene, viewport); - if (FAILED(result)) - { - _layerCacheValid = false; - return result; - } - - _layerCacheViewport = viewport; - _layerCacheValid = true; - - if (::BitBlt( - frameDc, - frameBounds.left, - frameBounds.top, - width, - height, - cacheDc, - 0, - 0, - SRCCOPY) != 0) - { - return result; - } - - // A blit between different devices (printer or metafile targets) can - // fail; present the layers by drawing straight to the target. - _layerCacheValid = false; - return _renderer.Draw(frameDc, frameBounds, scene, viewport); -} - -HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo) -{ - if (drawInfo.hdcDraw == nullptr || drawInfo.prcBounds == nullptr) - { - return E_POINTER; - } - - const RECT bounds{ - drawInfo.prcBounds->left, - drawInfo.prcBounds->top, - drawInfo.prcBounds->right, - drawInfo.prcBounds->bottom}; - - const LONGLONG width = static_cast(bounds.right) - bounds.left; - const LONGLONG height = static_cast(bounds.bottom) - bounds.top; - const bool drawable = width > 0 && height > 0 && - width <= mwg::Viewport::kMaxDimension && height <= mwg::Viewport::kMaxDimension; - - // Compose the frame in an offscreen buffer and present it with a single - // blit, so the visible surface never shows the background erase or a - // partially drawn frame. Drawing straight to the target device context - // is the fallback for sizes the buffer cannot hold. - const bool buffered = drawable && - _frameBuffer.Resize( - static_cast(width), - static_cast(height)); - HDC frameDc = buffered ? _frameBuffer.Get() : drawInfo.hdcDraw; - RECT frameBounds = buffered - ? RECT{0, 0, static_cast(width), static_cast(height)} - : bounds; - - const auto renderFrame = [&]() -> HRESULT - { - if (::FillRect( - frameDc, - &frameBounds, - ::GetSysColorBrush(COLOR_WINDOW)) == 0) - { - return E_FAIL; - } - - if (!drawable) - { - return S_OK; - } - - try - { - const mwg::rendering::MapRenderScene& scene = _view.Scene(); - // Printing and scaled draws use a snapshot, never the live viewport. - mwg::Viewport viewport = _view.GetViewport(); - viewport.SetSize(static_cast(width), static_cast(height)); - if (!viewport.HasView()) - { - (void)viewport.FitExtent(_view.GetMap().GetExtent(), mwg::MapView::kDefaultPadding); - } - if (!scene.layers.empty() && viewport.IsValid()) - { - if (buffered && TryBlitCachedLayers(frameDc, viewport)) - { - return S_OK; - } - return DrawLayersAndUpdateCache( - frameDc, - frameBounds, - scene, - viewport, - buffered); - } - return S_OK; - } - catch (const std::bad_alloc&) - { - return E_OUTOFMEMORY; - } - catch (const std::exception& exception) - { - ::OutputDebugStringA(exception.what()); - return E_FAIL; - } - }; - - if (buffered) - { - const HRESULT result = renderFrame(); - if (FAILED(result)) - { - return result; - } - if (::BitBlt( - drawInfo.hdcDraw, - bounds.left, - bounds.top, - static_cast(width), - static_cast(height), - frameDc, - 0, - 0, - SRCCOPY) != 0) - { - return result; - } - - // A blit between different devices (printer or metafile targets) - // can fail; present the frame by drawing straight to the target. - frameDc = drawInfo.hdcDraw; - frameBounds = bounds; - } - - return renderFrame(); -} - -LRESULT CMapWinGISControl::OnEraseBkgnd( - UINT, - WPARAM, - LPARAM, - BOOL& handled) -{ - // OnDraw paints an opaque frame over the whole bounds, so erasing the - // background first would only flash the host's color through. - handled = TRUE; - return 1; -} - -CMapWinGISControl::~CMapWinGISControl() -{ - CancelMousePan(); - _gdalEvents.Shutdown(); -} +// placeholder \ No newline at end of file From a1a240dc3407c7ccd27115ebe692bee9bd934592 Mon Sep 17 00:00:00 2001 From: Julian-MW Date: Sat, 7 Nov 2026 00:51:07 +0100 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=90=9B=20fix(activex):=20address=20Co?= =?UTF-8?q?pilot=20review=20on=20pan-by-blit=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Disable the layer cache when the presentation blit fails and the frame is drawn straight to the target DC, whose bounds origin and device differ from the offscreen buffer - CancelMousePan invalidates the layer cache so shutdown paths (Close, OnDestroy, destructor) cannot leave a shifted cached frame for a reactivated control - Accept any cache offset within half a pixel of an integer pixel shift: center floating-point accumulation no longer forces a full layer re-render on ordinary drags, and the mandatory drag-end render still lands the final frame exactly - Record COLOR_WINDOW with the cache and reject the cached frame after a system-color or high-contrast change instead of repainting the old background over the new one --- src/MapWinGIS.ActiveX/MapWinGISControl.cpp | 1361 +++++++++++++++++++- src/MapWinGIS.ActiveX/MapWinGISControl.h | 5 + 2 files changed, 1365 insertions(+), 1 deletion(-) diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp index 329f221..651c6f7 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.cpp +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.cpp @@ -1 +1,1360 @@ -// placeholder \ No newline at end of file +// SPDX-License-Identifier: MPL-2.0 + +/** + * @file MapWinGISControl.cpp + * @brief Implements vector loading, map navigation, ActiveX state reporting, + * and host drawing. + * + * Redraws: every map or view change made through the control goes through + * its MapView; the control calls FireViewChange when the MapView reports + * that the frame changed. OnDraw composes the MapView's render scene through + * its viewport into an offscreen frame buffer and presents the finished + * frame with a single blit, so every layer uses the same transformation and + * the visible surface never shows a partially drawn frame. + * + * Pan caching: the last fully rendered layer frame is kept in a second + * offscreen buffer. While the user drags the map (WM_LBUTTONDOWN through + * WM_LBUTTONUP), OnDraw paints the background and blits the cached frame at + * the accumulated pixel offset instead of re-rasterizing every layer on + * each WM_MOUSEMOVE; the drag-end handler invalidates the cache so the new + * view is rasterized once. The cache is invalidated on layer add/remove/ + * clear, zoom, resize, and backend-preference changes. + */ +#include "MapWinGISControl.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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; + using mwg::activex::ComConversion::CreateStringArray; + using mwg::activex::ComConversion::Utf8ToBstr; + + [[nodiscard]] HRESULT SetControlComErrorInfo( + const char* message, + REFIID sourceIid, + const HRESULT errorCode) noexcept + { + try + { + const CA2W wideMessage(message, CP_UTF8); + return AtlReportError(CLSID_MapWinGISControl, wideMessage, sourceIid, errorCode); + } + catch (...) + { + return errorCode; + } + } + + [[nodiscard]] HRESULT SetComErrorInfo( + const char* message, + const HRESULT errorCode) noexcept + { + return SetControlComErrorInfo(message, IID_IMapWinGISControlGdal, errorCode); + } + + [[nodiscard]] HRESULT CopyLayerId(const mwg::LayerId id, LONGLONG* destination) noexcept + { + if (id.value > static_cast(std::numeric_limits::max())) + { + return E_UNEXPECTED; + } + *destination = static_cast(id.value); + return S_OK; + } + + template + [[nodiscard]] HRESULT RunViewOperation( + Operation&& operation, + bool& changed) + { + try + { + changed = std::forward(operation)(); + return S_OK; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const mwg::InvalidArgument& exception) + { + return SetControlComErrorInfo( + exception.what(), + IID_IMapWinGISControl4, + E_INVALIDARG); + } + catch (const std::exception& exception) + { + return SetControlComErrorInfo( + exception.what(), + IID_IMapWinGISControl4, + E_FAIL); + } + catch (...) + { + return E_FAIL; + } + } + + [[nodiscard]] const wchar_t* BackendName( + const std::optional backend) noexcept + { + if (!backend.has_value()) + { + return L"Not rendered"; + } + + return *backend == mwg::activex::VectorRenderBackendKind::OpenGL + ? L"OpenGL" + : L"GDI"; + } + + [[nodiscard]] const wchar_t* PreferenceName( + const mwg::activex::VectorRenderBackendPreference preference) noexcept + { + switch (preference) + { + case mwg::activex::VectorRenderBackendPreference::Automatic: + return L"Automatic"; + case mwg::activex::VectorRenderBackendPreference::OpenGL: + return L"OpenGL"; + case mwg::activex::VectorRenderBackendPreference::Gdi: + return L"GDI"; + default: + return L"Automatic"; + } + } +} + +CMapWinGISControl::CMapWinGISControl() + : _gdalEvents( + m_vec, + &CMapWinGISControl::DispatchGdalNotifications, + this) +{ +} + +STDMETHODIMP CMapWinGISControl::InterfaceSupportsErrorInfo(REFIID interfaceId) +{ + return IsEqualIID(interfaceId, IID_IMapWinGISControl) || + IsEqualIID(interfaceId, IID_IMapWinGISControl2) || + IsEqualIID(interfaceId, IID_IMapWinGISControl3) || + IsEqualIID(interfaceId, IID_IMapWinGISControl4) || + IsEqualIID(interfaceId, IID_IMapWinGISControlGdal) ? S_OK : S_FALSE; +} + +STDMETHODIMP CMapWinGISControl::BeginGdalOperation( + BSTR algorithmPath, + SAFEARRAY* arguments, + LONGLONG* operationId) +{ + if (operationId == nullptr) + { + return E_POINTER; + } + *operationId = 0; + const HRESULT dispatchWindowResult = _gdalEvents.EnsureDispatchWindow(); + if (FAILED(dispatchWindowResult)) + { + return dispatchWindowResult; + } + + std::string algorithm; + std::vector commandLineArguments; + HRESULT result = S_OK; + try + { + result = BstrToUtf8(algorithmPath, algorithm); + if (FAILED(result) || algorithm.empty()) + { + return E_INVALIDARG; + } + + result = CopyStringArray(arguments, commandLineArguments); + if (FAILED(result)) + { + return result; + } + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::exception& exception) + { + return SetComErrorInfo(exception.what(), E_INVALIDARG); + } + catch (...) + { + return E_FAIL; + } + + try + { + *operationId = static_cast( + _gdalEvents.Begin( + std::move(algorithm), + std::move(commandLineArguments))); + return S_OK; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::exception& exception) + { + return SetComErrorInfo(exception.what(), E_FAIL); + } + catch (...) + { + return E_FAIL; + } +} + +STDMETHODIMP CMapWinGISControl::CancelGdalOperation(const LONGLONG operationId) +{ + if (operationId <= 0) + { + return E_INVALIDARG; + } + + switch (_gdalEvents.Cancel(static_cast(operationId))) + { + case mwg::activex::GdalCancelResult::Requested: + return S_OK; + case mwg::activex::GdalCancelResult::AlreadyRequested: + case mwg::activex::GdalCancelResult::Completed: + return S_FALSE; + case mwg::activex::GdalCancelResult::NotFound: + default: + return E_INVALIDARG; + } +} + +STDMETHODIMP CMapWinGISControl::EndGdalOperation( + const LONGLONG operationId, + VARIANT_BOOL* succeeded, + VARIANT_BOOL* cancelled, + LONG* errorCode, + LONG* callbackErrorCode, + BSTR* callbackError, + BSTR* summary, + BSTR* diagnostics, + SAFEARRAY** warnings) +{ + if (succeeded == nullptr || cancelled == nullptr || errorCode == nullptr || + callbackErrorCode == nullptr || callbackError == nullptr || + summary == nullptr || + diagnostics == nullptr || warnings == nullptr) + { + return E_POINTER; + } + *succeeded = VARIANT_FALSE; + *cancelled = VARIANT_FALSE; + *errorCode = 0; + *callbackErrorCode = 0; + *callbackError = nullptr; + *summary = nullptr; + *diagnostics = nullptr; + *warnings = nullptr; + if (operationId <= 0) + { + return E_INVALIDARG; + } + + mwg::activex::GdalOperationLookup resultLookup; + try + { + resultLookup = _gdalEvents.GetResult(static_cast(operationId)); + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + if (resultLookup.status == mwg::activex::GdalEndResult::Pending) + { + return E_PENDING; + } + if (resultLookup.status == mwg::activex::GdalEndResult::NotFound || + !resultLookup.result.has_value()) + { + return E_INVALIDARG; + } + const mwg::activex::GdalOperationSnapshot& operationResult = + *resultLookup.result; + + BSTR summaryText = nullptr; + BSTR callbackErrorText = nullptr; + HRESULT result = Utf8ToBstr( + operationResult.callbackError, + &callbackErrorText); + if (FAILED(result)) + { + return result; + } + + result = Utf8ToBstr(operationResult.summary, &summaryText); + if (FAILED(result)) + { + ::SysFreeString(callbackErrorText); + return result; + } + + BSTR diagnosticText = nullptr; + result = Utf8ToBstr(operationResult.diagnostics, &diagnosticText); + if (FAILED(result)) + { + ::SysFreeString(callbackErrorText); + ::SysFreeString(summaryText); + return result; + } + + SAFEARRAY* warningArray = nullptr; + result = CreateStringArray(operationResult.warnings, &warningArray); + if (FAILED(result)) + { + ::SysFreeString(callbackErrorText); + ::SysFreeString(summaryText); + ::SysFreeString(diagnosticText); + return result; + } + + const mwg::activex::GdalOperationLookup cleanupResult = + _gdalEvents.End(static_cast(operationId)); + if (cleanupResult.status != mwg::activex::GdalEndResult::Ended) + { + ::SysFreeString(callbackErrorText); + ::SysFreeString(summaryText); + ::SysFreeString(diagnosticText); + if (warningArray != nullptr) + { + ::SafeArrayDestroy(warningArray); + } + return cleanupResult.status == mwg::activex::GdalEndResult::Pending + ? E_PENDING + : E_INVALIDARG; + } + + *succeeded = operationResult.succeeded ? VARIANT_TRUE : VARIANT_FALSE; + *cancelled = operationResult.cancelled ? VARIANT_TRUE : VARIANT_FALSE; + *errorCode = operationResult.errorCode; + *callbackErrorCode = operationResult.callbackErrorCode; + *callbackError = callbackErrorText; + *summary = summaryText; + *diagnostics = diagnosticText; + *warnings = warningArray; + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::get_CoreInitialized(VARIANT_BOOL* initialized) +{ + if (initialized == nullptr) + { + return E_POINTER; + } + + *initialized = mwg::GdalEnvironment::IsInitialized() ? VARIANT_TRUE : VARIANT_FALSE; + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::LoadVectorDataset(BSTR path) +{ + return LoadVectorDatasetImpl(path, nullptr, IID_IMapWinGISControl); +} + +STDMETHODIMP CMapWinGISControl::LoadVectorDatasetEx( + BSTR path, + LONGLONG* layerId) +{ + if (layerId == nullptr) + { + return E_POINTER; + } + *layerId = 0; + return LoadVectorDatasetImpl(path, layerId, IID_IMapWinGISControl4); +} + +HRESULT CMapWinGISControl::LoadVectorDatasetImpl( + BSTR path, + LONGLONG* layerId, + REFIID sourceIid) +{ + if (path == nullptr) + { + return E_INVALIDARG; + } + + const UINT pathLength = ::SysStringLen(path); + if (pathLength == 0) + { + return E_INVALIDARG; + } + + try + { + const std::wstring pathText(path, static_cast(pathLength)); + if (pathText.find(L'\0') != std::wstring::npos) + { + return E_INVALIDARG; + } + + const std::filesystem::path datasetPath(pathText); + std::shared_ptr dataset = mwg::VectorDataset::Open(datasetPath); + if (dataset->LayerCount() == 0) + { + throw mwg::CoreError("Vector dataset contains no layers"); + } + + // Validate before AddLayer can auto-fit or schedule a pending fit. + mwg::Map candidate; + candidate.AddLayer(dataset); + mwg::rendering::MapRenderScene scene = + mwg::rendering::CreateMapRenderScene(candidate); + for (const mwg::rendering::SkippedLayer& skipped : scene.skippedLayers) + { + if (skipped.reason == mwg::rendering::LayerSkipReason::Failed) + { + throw mwg::CoreError(skipped.message); + } + } + mwg::rendering::VectorRenderScene preparedScene; + if (scene.layers.empty()) + { + preparedScene.sourceFeatureCount = dataset->Layer(0).FeatureCount(); + } + else + { + preparedScene = std::move(scene.layers.front().scene); + } + const LONGLONG count = static_cast(preparedScene.sourceFeatureCount); + if (count < 0 || count > std::numeric_limits::max() - _featureCount) + { + throw mwg::CoreError("Map feature count is out of range"); + } + + const mwg::LayerId id = _view.AddLayer(std::move(dataset), std::move(preparedScene)); + LONGLONG convertedId = 0; + if (layerId != nullptr && FAILED(CopyLayerId(id, &convertedId))) + { + (void)_view.RemoveLayer(id); + return E_UNEXPECTED; + } + try + { + // Fit the combined extent so the newly added layer is visible. + (void)_view.FitToMap(); + } + catch (...) + { + (void)_view.RemoveLayer(id); + throw; + } + + _featureCount += count; + if (layerId != nullptr) + { + *layerId = convertedId; + } + InvalidateLayerCache(); + FireViewChange(); + return S_OK; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::exception& exception) + { + return SetControlComErrorInfo(exception.what(), sourceIid, E_FAIL); + } + catch (...) + { + return E_FAIL; + } +} + +STDMETHODIMP CMapWinGISControl::ClearLayers() +{ + if (_view.ClearLayers()) + { + _featureCount = 0; + InvalidateLayerCache(); + FireViewChange(); + } + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::get_LayerLoaded(VARIANT_BOOL* loaded) +{ + if (loaded == nullptr) + { + return E_POINTER; + } + + *loaded = _view.GetMap().LayerCount() > 0 ? VARIANT_TRUE : VARIANT_FALSE; + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::get_FeatureCount(LONGLONG* count) +{ + if (count == nullptr) + { + return E_POINTER; + } + + *count = _featureCount; + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::get_RenderingBackend(BSTR* backend) +{ + return CopyToBstr(BackendName(_renderer.GetActiveBackend()), backend); +} + +STDMETHODIMP CMapWinGISControl::get_RenderingStatus(BSTR* status) +{ + return CopyToBstr(_renderer.GetDiagnostic(), status); +} + +STDMETHODIMP CMapWinGISControl::get_RenderingBackendPreference(BSTR* preference) +{ + return CopyToBstr(PreferenceName(_renderer.GetPreference()), preference); +} + +STDMETHODIMP CMapWinGISControl::put_RenderingBackendPreference(BSTR preference) +{ + if (preference == nullptr) + { + return E_INVALIDARG; + } + + const UINT length = ::SysStringLen(preference); + auto equals = [preference, length](const wchar_t* value, const int valueLength) + { + return length == static_cast(valueLength) && + ::CompareStringOrdinal( + preference, + static_cast(length), + value, + valueLength, + TRUE) == CSTR_EQUAL; + }; + + if (equals(L"Automatic", 9)) + { + _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::Automatic); + } + else if (equals(L"OpenGL", 6)) + { + _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::OpenGL); + } + else if (equals(L"GDI", 3)) + { + _renderer.SetPreference(mwg::activex::VectorRenderBackendPreference::Gdi); + } + else + { + return E_INVALIDARG; + } + + InvalidateLayerCache(); + FireViewChange(); + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::FitToMap() +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this]() { return _view.FitToMap(); }, + changed); + if (SUCCEEDED(result) && changed) + { + InvalidateLayerCache(); + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::ZoomIn(const DOUBLE factor) +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this, factor]() { return _view.ZoomIn(factor); }, + changed); + if (SUCCEEDED(result) && changed) + { + InvalidateLayerCache(); + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::ZoomOut(const DOUBLE factor) +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this, factor]() { return _view.ZoomOut(factor); }, + changed); + if (SUCCEEDED(result) && changed) + { + InvalidateLayerCache(); + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::Pan( + const DOUBLE deltaX, + const DOUBLE deltaY) +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this, deltaX, deltaY]() { return _view.Pan(deltaX, deltaY); }, + changed); + if (SUCCEEDED(result) && changed) + { + InvalidateLayerCache(); + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::ZoomAt( + const DOUBLE x, + const DOUBLE y, + const DOUBLE factor) +{ + bool changed = false; + const HRESULT result = RunViewOperation( + [this, x, y, factor]() + { + return _view.ZoomAt({x, y}, factor); + }, + changed); + if (SUCCEEDED(result) && changed) + { + InvalidateLayerCache(); + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::ZoomToLayer(const LONGLONG layerId) +{ + if (layerId <= 0) + { + return SetControlComErrorInfo("Layer ID must be positive", IID_IMapWinGISControl4, E_INVALIDARG); + } + const mwg::LayerId id{static_cast(layerId)}; + if (_view.GetMap().FindLayer(id) == nullptr) + { + return SetControlComErrorInfo("Layer ID was not found", IID_IMapWinGISControl4, E_INVALIDARG); + } + + bool changed = false; + const HRESULT result = RunViewOperation( + [this, id]() { return _view.ZoomToLayer(id); }, + changed); + if (SUCCEEDED(result) && changed) + { + InvalidateLayerCache(); + FireViewChange(); + } + return result; +} + +STDMETHODIMP CMapWinGISControl::get_LayerCount(LONG* count) +{ + if (count == nullptr) + { + return E_POINTER; + } + *count = _view.GetMap().LayerCount(); + return S_OK; +} + +STDMETHODIMP CMapWinGISControl::GetLayerId( + const LONG index, + LONGLONG* layerId) +{ + if (layerId == nullptr) + { + return E_POINTER; + } + *layerId = 0; + const mwg::Map& map = _view.GetMap(); + if (index < 0 || index >= map.LayerCount()) + { + return E_INVALIDARG; + } + + return CopyLayerId(map.Layer(index).Id(), layerId); +} + +bool CMapWinGISControl::ResizeView(const LONG width, const LONG height) noexcept +{ + try + { + return _view.Resize( + static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), + static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension))); + } + catch (const std::exception& exception) + { + ::OutputDebugStringA(exception.what()); + return false; + } +} + +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; + // Drop the cache even when the control is closing or being destroyed: + // a reactivated control must never present a frame from a drag that + // ended without a final render. + InvalidateLayerCache(); + ReleaseMouseCapture(); + } +} + +void CMapWinGISControl::EndMousePan() noexcept +{ + if (!_isPanning) + { + return; + } + + CancelMousePan(); + // The drag ended: rasterize the layers once at the final view so the + // cached frame matches the new center exactly (a clamped pan can leave + // the blitted frame up to half a pixel off). Every drag termination + // path (button-up, capture loss, cancel mode, button released) goes + // through here, so a terminated drag never leaves the shifted cached + // frame on screen. + InvalidateLayerCache(); + FireViewChange(); +} + +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) + { + EndMousePan(); + 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 resolution = _view.GetViewport().Resolution(); + if (!resolution.has_value()) + { + handled = TRUE; + return 0; + } + + // The drag pan changes only the view center, so OnDraw can serve the + // frame from the layer cache; the cache is dropped on button-up. + bool changed = false; + const HRESULT result = RunViewOperation( + [this, pixelDeltaX, pixelDeltaY, resolution]() + { + return _view.Pan( + -static_cast(pixelDeltaX) * *resolution, + static_cast(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; + } + + EndMousePan(); + 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(wheelDelta) / WHEEL_DELTA); + bool changed = false; + const HRESULT result = RunViewOperation( + [this, point, factor]() + { + return _view.ZoomAt( + {static_cast(point.x), static_cast(point.y)}, + factor); + }, + changed); + if (SUCCEEDED(result) && changed) + { + InvalidateLayerCache(); + FireViewChange(); + } + + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnCaptureChanged( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + EndMousePan(); + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnCancelMode( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + EndMousePan(); + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnSize( + UINT, + WPARAM, + const LPARAM lParam, + BOOL& handled) +{ + _gdalEvents.OnSize(); + if (ResizeView(LOWORD(lParam), HIWORD(lParam))) + { + InvalidateLayerCache(); + FireViewChange(); + } + handled = TRUE; + return 0; +} + +LRESULT CMapWinGISControl::OnGdalDispatch( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + return _gdalEvents.OnDispatch(handled); +} + +LRESULT CMapWinGISControl::OnDestroy( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + CancelMousePan(); + _gdalEvents.OnDestroy(); + handled = FALSE; + return 0; +} + +void CMapWinGISControl::DispatchGdalNotifications(void* const control) noexcept +{ + auto* const instance = static_cast(control); + ATL::CComPtr lifetime(instance); + BOOL handled = FALSE; + try + { + instance->OnGdalDispatch( + mwg::activex::kGdalDispatchMessage, + 0, + 0, + handled); + } + catch (const std::exception& exception) + { + ::OutputDebugStringA(exception.what()); + } +} + +STDMETHODIMP CMapWinGISControl::Close(const DWORD saveOption) +{ + CancelMousePan(); + _gdalEvents.Close(); + return ATL::IOleObjectImpl::Close(saveOption); +} + +STDMETHODIMP CMapWinGISControl::SetObjectRects(LPCRECT position, LPCRECT clip) +{ + if (position == nullptr || clip == nullptr) + { + return E_POINTER; + } + const HRESULT result = + ATL::IOleInPlaceObjectWindowlessImpl::SetObjectRects(position, clip); + if (SUCCEEDED(result) && m_bWndLess) + { + const LONGLONG width = static_cast(position->right) - position->left; + const LONGLONG height = static_cast(position->bottom) - position->top; + if (ResizeView( + static_cast(std::clamp(width, 0, mwg::Viewport::kMaxDimension)), + static_cast(std::clamp(height, 0, mwg::Viewport::kMaxDimension)))) + { + InvalidateLayerCache(); + FireViewChange(); + } + } + return result; +} + +void CMapWinGISControl::InvalidateLayerCache() noexcept +{ + _layerCacheValid = false; +} + +bool CMapWinGISControl::TryBlitCachedLayers( + HDC frameDc, + const mwg::Viewport& viewport) noexcept +{ + if (!_layerCacheValid || + !_layerCache.IsValid() || + _layerCache.Width() != viewport.Width() || + _layerCache.Height() != viewport.Height()) + { + return false; + } + + const std::optional resolution = viewport.Resolution(); + const std::optional cacheResolution = _layerCacheViewport.Resolution(); + const std::optional center = viewport.Center(); + const std::optional cacheCenter = _layerCacheViewport.Center(); + if (!resolution.has_value() || + !cacheResolution.has_value() || + !center.has_value() || + !cacheCenter.has_value() || + *resolution != *cacheResolution) + { + // A resolution change (or a missing view) needs a re-render. + return false; + } + + // The cached background must match the color the frame is cleared with; + // a system-color or high-contrast change would otherwise leave the old + // background showing through an otherwise valid cache. + if (::GetSysColor(COLOR_WINDOW) != _layerCacheBackground) + { + return false; + } + + // Screen position of a fixed map point shifts by the center difference + // divided by the resolution, with the Y axis pointing down. The center + // accumulates rounding through repeated pan operations, so the offset can + // sit a tiny amount off an integer; anything within half a pixel rounds to + // the nearest pixel, which is imperceptible during the drag and corrected + // by the final render when the drag ends. + const double offsetX = (cacheCenter->x - center->x) / *resolution; + const double offsetY = (center->y - cacheCenter->y) / *resolution; + const LONG shiftX = static_cast(std::lround(offsetX)); + const LONG shiftY = static_cast(std::lround(offsetY)); + if (std::abs(offsetX - static_cast(shiftX)) >= 0.5 || + std::abs(offsetY - static_cast(shiftY)) >= 0.5) + { + // A clamped pan (or extreme accumulated drift) leaves a sub-pixel-or- + // worse offset; re-render so the frame does not jitter between a + // shifted blit and a true render. + return false; + } + + const LONG width = static_cast(viewport.Width()); + const LONG height = static_cast(viewport.Height()); + + // Destination (x, y) shows cache pixel (x - shiftX, y - shiftY); blit the + // rectangle where that source position is inside the cached frame. The + // rest of the destination keeps the background already painted. + const LONG destLeft = std::max(0, shiftX); + const LONG destTop = std::max(0, shiftY); + const LONG destRight = std::min(width, width + shiftX); + const LONG destBottom = std::min(height, height + shiftY); + if (destRight <= destLeft || destBottom <= destTop) + { + // The cached content is entirely outside the frame. + return true; + } + + return ::BitBlt( + frameDc, + destLeft, + destTop, + destRight - destLeft, + destBottom - destTop, + _layerCache.Get(), + destLeft - shiftX, + destTop - shiftY, + SRCCOPY) != 0; +} + +HRESULT CMapWinGISControl::DrawLayersAndUpdateCache( + HDC frameDc, + const RECT& frameBounds, + const mwg::rendering::MapRenderScene& scene, + const mwg::Viewport& viewport, + const bool useCache) +{ + if (!useCache) + { + return _renderer.Draw(frameDc, frameBounds, scene, viewport); + } + + const int width = viewport.Width(); + const int height = viewport.Height(); + if (!_layerCache.Resize(width, height)) + { + // Without a cache the layers are drawn straight into the frame; the + // next draw retries the allocation. + return _renderer.Draw(frameDc, frameBounds, scene, viewport); + } + + HDC cacheDc = _layerCache.Get(); + const RECT cacheBounds{0, 0, width, height}; + if (::FillRect( + cacheDc, + &cacheBounds, + ::GetSysColorBrush(COLOR_WINDOW)) == 0) + { + return _renderer.Draw(frameDc, frameBounds, scene, viewport); + } + + const HRESULT result = _renderer.Draw(cacheDc, cacheBounds, scene, viewport); + if (FAILED(result)) + { + _layerCacheValid = false; + return result; + } + + _layerCacheViewport = viewport; + _layerCacheBackground = ::GetSysColor(COLOR_WINDOW); + _layerCacheValid = true; + + if (::BitBlt( + frameDc, + frameBounds.left, + frameBounds.top, + width, + height, + cacheDc, + 0, + 0, + SRCCOPY) != 0) + { + return result; + } + + // A blit between different devices (printer or metafile targets) can + // fail; present the layers by drawing straight to the target. + _layerCacheValid = false; + return _renderer.Draw(frameDc, frameBounds, scene, viewport); +} + +HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo) +{ + if (drawInfo.hdcDraw == nullptr || drawInfo.prcBounds == nullptr) + { + return E_POINTER; + } + + const RECT bounds{ + drawInfo.prcBounds->left, + drawInfo.prcBounds->top, + drawInfo.prcBounds->right, + drawInfo.prcBounds->bottom}; + + const LONGLONG width = static_cast(bounds.right) - bounds.left; + const LONGLONG height = static_cast(bounds.bottom) - bounds.top; + const bool drawable = width > 0 && height > 0 && + width <= mwg::Viewport::kMaxDimension && height <= mwg::Viewport::kMaxDimension; + + // Compose the frame in an offscreen buffer and present it with a single + // blit, so the visible surface never shows the background erase or a + // partially drawn frame. Drawing straight to the target device context + // is the fallback for sizes the buffer cannot hold. + const bool buffered = drawable && + _frameBuffer.Resize( + static_cast(width), + static_cast(height)); + HDC frameDc = buffered ? _frameBuffer.Get() : drawInfo.hdcDraw; + RECT frameBounds = buffered + ? RECT{0, 0, static_cast(width), static_cast(height)} + : bounds; + + // The cache holds a viewport-sized bitmap that is blitted at the frame + // origin; it is only valid while the frame is the offscreen buffer. A + // failed presentation blit switches to the target DC and bounds, and + // must not use the cache. + bool useLayerCache = buffered; + + const auto renderFrame = [&]() -> HRESULT + { + if (::FillRect( + frameDc, + &frameBounds, + ::GetSysColorBrush(COLOR_WINDOW)) == 0) + { + return E_FAIL; + } + + if (!drawable) + { + return S_OK; + } + + try + { + const mwg::rendering::MapRenderScene& scene = _view.Scene(); + // Printing and scaled draws use a snapshot, never the live viewport. + mwg::Viewport viewport = _view.GetViewport(); + viewport.SetSize(static_cast(width), static_cast(height)); + if (!viewport.HasView()) + { + (void)viewport.FitExtent(_view.GetMap().GetExtent(), mwg::MapView::kDefaultPadding); + } + if (!scene.layers.empty() && viewport.IsValid()) + { + if (useLayerCache && TryBlitCachedLayers(frameDc, viewport)) + { + return S_OK; + } + return DrawLayersAndUpdateCache( + frameDc, + frameBounds, + scene, + viewport, + useLayerCache); + } + return S_OK; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const std::exception& exception) + { + ::OutputDebugStringA(exception.what()); + return E_FAIL; + } + }; + + if (buffered) + { + const HRESULT result = renderFrame(); + if (FAILED(result)) + { + return result; + } + if (::BitBlt( + drawInfo.hdcDraw, + bounds.left, + bounds.top, + static_cast(width), + static_cast(height), + frameDc, + 0, + 0, + SRCCOPY) != 0) + { + return result; + } + + // A blit between different devices (printer or metafile targets) + // can fail; present the frame by drawing straight to the target. + frameDc = drawInfo.hdcDraw; + frameBounds = bounds; + useLayerCache = false; + } + + return renderFrame(); +} + +LRESULT CMapWinGISControl::OnEraseBkgnd( + UINT, + WPARAM, + LPARAM, + BOOL& handled) +{ + // OnDraw paints an opaque frame over the whole bounds, so erasing the + // background first would only flash the host's color through. + handled = TRUE; + return 1; +} + +CMapWinGISControl::~CMapWinGISControl() +{ + CancelMousePan(); + _gdalEvents.Shutdown(); +} diff --git a/src/MapWinGIS.ActiveX/MapWinGISControl.h b/src/MapWinGIS.ActiveX/MapWinGISControl.h index 286018a..3a55abe 100644 --- a/src/MapWinGIS.ActiveX/MapWinGISControl.h +++ b/src/MapWinGIS.ActiveX/MapWinGISControl.h @@ -201,6 +201,11 @@ class ATL_NO_VTABLE CMapWinGISControl : mwg::activex::FrameBuffer _frameBuffer; mwg::activex::FrameBuffer _layerCache; mwg::Viewport _layerCacheViewport; + + /// COLOR_WINDOW the cached frame was rendered with; a system-color or + /// high-contrast change makes the cache unusable. + COLORREF _layerCacheBackground = 0; + bool _layerCacheValid = false; mwg::activex::VectorLayerRenderer _renderer; mwg::activex::GdalEventSource _gdalEvents;