Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ 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. ActiveX exposes map/layer zoom operations and
supports cursor-anchored mouse-wheel zoom and left-button drag-to-pan.
The ActiveX control also accepts supported vector datasets through file
drag-and-drop.

Vector loads add layers rather than replace them. The first layer fits the
initial view; later loads preserve it. Call `ZoomToMap` to include new layers
in the visible extent, or `ClearLayers` to release loaded datasets before
switching or refreshing them.

Dropped files append as layers and the control fits the combined visible
extent after processing the batch. Unsupported or invalid files are skipped;
valid files in the same drop are still loaded.

## Build and test

### Prerequisites
Expand Down
18 changes: 18 additions & 0 deletions docs/PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ dataset, and reports whether a layer is loaded and its feature count.
Loads add layers, including duplicate loads of one dataset. Call `ClearLayers`
before switching or refreshing datasets. It releases all layers and resets
the cached feature total while keeping the viewport.
The windowless control accepts Explorer file drops through its OLE drop-target
hook. Dropped supported vector datasets append as layers, then the control
fits the combined visible extent once. Unsupported, missing, or invalid paths
are skipped without discarding other files that loaded successfully. A partial
drop returns `S_FALSE` with a copy effect; a drop with no successful files
returns a failing HRESULT and no effect. Failed paths and HRESULTs are written
to the debugger output.
New members belong on a new derived interface; do not change a published
interface IID or silently add vtable members to it.

Expand All @@ -69,6 +76,17 @@ in source coordinates without reprojection. OpenGL only handles convex
single-ring polygons; GDI handles the supported polygon scene as the CPU
fallback.

## Map and viewport behavior

Core `Map` owns layers in insertion order. Each layer has a stable ID that is
independent of its display name and current list index. `MapView` owns the
viewport and the visible extent shared by every rendered layer. Map X increases
right and map Y increases up; screen X increases right and screen Y increases
down. Resizing keeps the view center and scale where possible. Loading through
`LoadVectorDataset` appends layers and preserves an existing view; a file-drop
batch appends layers and then fits the combined visible extent. Source
coordinates are not reprojected, so layers must use compatible coordinates.

Keep these gaps visible when extending the COM surface. Reuse Core behavior,
add only the automation members required by a concrete use case, and retain a
legacy identifier only after verifying that its full signature and behavior
Expand Down
7 changes: 7 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ run exceeded 30 seconds); rerun it before treating a timeout as a hang. These
integration checks require Windows and an interactive desktop session; they
are not currently part of CI.

The native ActiveX integration tests also exercise the control's windowless
OLE drop target with `CF_HDROP` data. They verify that multiple supported
datasets append as layers and fit to their combined extent, and that an
unsupported file does not prevent other files in the same drop from loading.
Dragging files from Explorer onto the visible C# host remains a manual desktop
check.

The host executable accepts `--automated`, `--results <path>`, and
`--timeout-seconds <5-300>`. The PowerShell launcher supplies these options
and starts the host without changing COM registration.
Expand Down
306 changes: 305 additions & 1 deletion src/MapWinGIS.ActiveX/MapWinGISControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/**
* @file MapWinGISControl.cpp
* @brief Implements vector loading, ActiveX state reporting, and host drawing.
* @brief Implements vector loading, file drop, ActiveX state, 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
Expand All @@ -12,6 +12,7 @@
#include "MapWinGISControl.h"

#include <atlconv.h>
#include <shellapi.h>
#include <windowsx.h>

#include <algorithm>
Expand Down Expand Up @@ -90,6 +91,117 @@ namespace
return L"Automatic";
}
}

[[nodiscard]] bool HasFileDropFormat(IDataObject* data) noexcept
{
if (data == nullptr)
{
return false;
}

FORMATETC format{
CF_HDROP,
nullptr,
DVASPECT_CONTENT,
-1,
TYMED_HGLOBAL};
return SUCCEEDED(data->QueryGetData(&format));
}

class StgMediumGuard final
{
public:
explicit StgMediumGuard(STGMEDIUM& medium) noexcept
: _medium(medium)
{
}

~StgMediumGuard()
{
::ReleaseStgMedium(&_medium);
}

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

private:
STGMEDIUM& _medium;
};

[[nodiscard]] HRESULT GetDroppedPaths(
IDataObject* data,
std::vector<std::filesystem::path>& paths)
{
if (data == nullptr)
{
return E_POINTER;
}

FORMATETC format{
CF_HDROP,
nullptr,
DVASPECT_CONTENT,
-1,
TYMED_HGLOBAL};
STGMEDIUM medium{};
const HRESULT result = data->GetData(&format, &medium);
if (FAILED(result))
{
return result;
}
const StgMediumGuard mediumGuard(medium);
if (medium.tymed != TYMED_HGLOBAL || medium.hGlobal == nullptr)
{
return DV_E_TYMED;
}

const HDROP drop = static_cast<HDROP>(medium.hGlobal);
const UINT count = ::DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0);
if (count == 0)
{
return DV_E_FORMATETC;
}

paths.clear();
paths.reserve(count);
for (UINT index = 0; index < count; ++index)
{
const UINT length = ::DragQueryFileW(drop, index, nullptr, 0);
if (length == 0 || length == std::numeric_limits<UINT>::max())
{
return DV_E_FORMATETC;
}

std::wstring path(length + 1, L'\0');
const UINT copied = ::DragQueryFileW(
drop,
index,
path.data(),
static_cast<UINT>(path.size()));
if (copied != length)
{
return DV_E_FORMATETC;
}
path.resize(copied);
paths.emplace_back(std::move(path));
}
return S_OK;
}

void LogDropFailure(
const std::filesystem::path& path,
const HRESULT result) noexcept
{
wchar_t message[64]{};
(void)::swprintf_s(
message,
64,
L"MapWinGIS could not load dropped file (HRESULT 0x%08lX): ",
static_cast<unsigned long>(result));
::OutputDebugStringW(message);
::OutputDebugStringW(path.c_str());
::OutputDebugStringW(L"\n");
}
}

CMapWinGISControl::CMapWinGISControl()
Expand Down Expand Up @@ -685,6 +797,198 @@ STDMETHODIMP CMapWinGISControl::put_RenderingBackendPreference(BSTR preference)
return S_OK;
}

STDMETHODIMP CMapWinGISControl::GetDropTarget(IDropTarget** dropTarget)
{
if (dropTarget == nullptr)
{
return E_POINTER;
}

*dropTarget = static_cast<IDropTarget*>(this);
AddRef();
return S_OK;
}

STDMETHODIMP CMapWinGISControl::DragEnter(
IDataObject* data,
DWORD,
POINTL,
DWORD* effect)
{
if (effect == nullptr)
{
return E_POINTER;
}

const DWORD allowedEffects = *effect;
*effect = DROPEFFECT_NONE;
_dropAcceptsFiles =
(allowedEffects & DROPEFFECT_COPY) != 0 && HasFileDropFormat(data);
if (_dropAcceptsFiles)
{
*effect = DROPEFFECT_COPY;
}
return S_OK;
}

STDMETHODIMP CMapWinGISControl::DragOver(DWORD, POINTL, DWORD* effect)
{
if (effect == nullptr)
{
return E_POINTER;
}

if (_dropAcceptsFiles && (*effect & DROPEFFECT_COPY) != 0)
{
*effect = DROPEFFECT_COPY;
}
else
{
*effect = DROPEFFECT_NONE;
}
return S_OK;
}

STDMETHODIMP CMapWinGISControl::DragLeave()
{
_dropAcceptsFiles = false;
return S_OK;
}

STDMETHODIMP CMapWinGISControl::Drop(
IDataObject* data,
DWORD,
POINTL,
DWORD* effect)
{
if (effect == nullptr)
{
return E_POINTER;
}

const DWORD allowedEffects = *effect;
*effect = DROPEFFECT_NONE;
_dropAcceptsFiles = false;
if ((allowedEffects & DROPEFFECT_COPY) == 0)
{
return S_OK;
}

std::vector<std::filesystem::path> paths;
try
{
const HRESULT extractResult = GetDroppedPaths(data, paths);
if (FAILED(extractResult))
{
::OutputDebugStringA("MapWinGIS rejected dropped data without usable file paths.\n");
return extractResult;
}
}
catch (const std::bad_alloc&)
{
return E_OUTOFMEMORY;
}
catch (const std::exception& exception)
{
::OutputDebugStringA(exception.what());
return E_FAIL;
}

bool loadedAny = false;
HRESULT firstFailure = S_OK;
for (const std::filesystem::path& path : paths)
{
std::error_code fileError;
if (!std::filesystem::exists(path, fileError))
{
const HRESULT pathResult = fileError
? HRESULT_FROM_WIN32(static_cast<DWORD>(fileError.value()))
: HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
LogDropFailure(path, pathResult);
if (SUCCEEDED(firstFailure))
{
firstFailure = pathResult;
}
continue;
}

HRESULT loadResult = E_FAIL;
try
{
const std::wstring pathText = path.native();
if (pathText.size() > std::numeric_limits<UINT>::max())
{
loadResult = E_INVALIDARG;
}
else
{
BSTR pathBstr = ::SysAllocStringLen(
pathText.data(),
static_cast<UINT>(pathText.size()));
if (pathBstr == nullptr)
{
loadResult = E_OUTOFMEMORY;
}
else
{
loadResult = LoadVectorDataset(pathBstr);
::SysFreeString(pathBstr);
}
}
}
catch (const std::bad_alloc&)
{
loadResult = E_OUTOFMEMORY;
}
catch (const std::exception& exception)
{
::OutputDebugStringA(exception.what());
if (SUCCEEDED(firstFailure))
{
firstFailure = E_FAIL;
}
}

if (FAILED(loadResult))
{
LogDropFailure(path, loadResult);
if (SUCCEEDED(firstFailure))
{
firstFailure = loadResult;
}
if (loadResult == E_OUTOFMEMORY)
{
break;
}
continue;
}
loadedAny = true;
}

if (loadedAny)
{
try
{
if (_view.FitToMap())
{
FireViewChange();
}
}
catch (const std::exception& exception)
{
::OutputDebugStringA(exception.what());
if (SUCCEEDED(firstFailure))
{
firstFailure = E_FAIL;
}
}
*effect = DROPEFFECT_COPY;
return FAILED(firstFailure) ? S_FALSE : S_OK;
}

return FAILED(firstFailure) ? firstFailure : E_FAIL;
}

bool CMapWinGISControl::ResizeView(const LONG width, const LONG height) noexcept
{
try
Expand Down
Loading