From svn_mapguide at osgeo.org Mon Oct 19 09:06:15 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Mon, 19 Oct 2026 09:06:15 -0700
Subject: [mapguide-commits] r10219 - in branches/4.0/MgDev:
Common/Foundation/System Web/src/HttpHandler Web/src/IsapiAgent
Web/src/WebSupport
Message-ID: <20261019160615.A826A1C8080@trac.osgeo.org>
Author: jng
Date: 2026-10-19 09:06:14 -0700 (Mon, 19 Oct 2026)
New Revision: 10219
Modified:
branches/4.0/MgDev/Common/Foundation/System/Configuration.cpp
branches/4.0/MgDev/Common/Foundation/System/Configuration.h
branches/4.0/MgDev/Common/Foundation/System/ConfigurationHeap.cpp
branches/4.0/MgDev/Web/src/HttpHandler/HttpRequest.cpp
branches/4.0/MgDev/Web/src/HttpHandler/HttpRequestResponseHandler.cpp
branches/4.0/MgDev/Web/src/IsapiAgent/IsapiAgent.cpp
branches/4.0/MgDev/Web/src/WebSupport/InitializeWebTier.cpp
Log:
Fix web tier initialisation failures leaving the process permanently broken
A failed MgInitializeWebTier() call poisoned the process-wide MgConfiguration
singleton for the life of the process. LoadConfiguration() opens the ACE
configuration heap before importing the file, and ACE_Configuration_Heap::open()
can only succeed once (EBUSY afterwards). When the import failed the heap was
left open with m_fileLoaded still false, and every later attempt took the EBUSY
branch, which LoadConfiguration() merely logged - reporting success without
loading anything. MgInitializeWebTierInternal() then carried on with default
values and latched itself as initialised, so only a process restart could clear
the state.
Symptoms this fixes:
* The MapAgent (ISAPI) failing every request, in MapAgentCommon::LogRequest()
at MgConfiguration::GetBoolValue(), until the application pool was recycled.
* "Failed to load the resource file Resources/mapguide_en.res for locale en"
from every call after a failed first call, because ResourcesPath fell back
to its default relative path.
* Coordinate system and resource failures being reported from a web tier that
claimed to be initialised.
Changes:
* Common/Foundation/System/Configuration.*: report a failure to open the ACE
heap as well as a failure to import the file, and add DiscardInstance() so a
caller can drop an instance whose heap can never be re-opened.
* Common/Foundation/System/ConfigurationHeap.cpp: do not index the section
collection with a stale -1 when ACE resolves a section name that IndexOf()
did not find. ACE compares section names case-insensitively and IndexOf()
does not, so a case-different section name in webconfig.ini threw
MgIndexOutOfRangeException out of MgConfiguration::GetValue().
* Web/src/WebSupport/InitializeWebTier.cpp: discard the configuration instance
when the load throws, so a retry reloads instead of reusing a half-open heap;
discard it from MgUninitializeWebTier() so a re-initialisation reloads
instead of reusing a heap built on ACE state that ACE::fini() has torn down;
and restore the throws for the coordinate system initialisation failures,
which had been reduced to a bare `new MgException(...)` (leaked, not thrown)
and, for a NULL catalog, let execution carry on to dereference that NULL.
* Web/src/IsapiAgent/IsapiAgent.cpp: APPL_PHYSICAL_PATH was read into an
uninitialised buffer with the GetServerVariable() result ignored, so a
missing variable produced a garbage configuration path and a strlen() over
uninitialised stack. Zero and check the buffer, and fall back to the
extension's own directory - the mapagent application directory the variable
would have named.
* Web/src/HttpHandler/*: restore two more throws of the same flavour. An
unknown OPERATION reached a NULL handler factory function pointer, and
disabling WFS/WMS/OGC via AgentProperties was ignored.
Verified on Windows x64 Release (WebTier.sln, v142 toolset): a first
MgInitializeWebTier() with a missing configuration now throws
MgConfigurationLoadFailedException with the tier left uninitialised, and
subsequent calls with a valid configuration load it and initialise successfully.
Previously they returned normally with no configuration loaded, or failed with
MgFileNotFoundException on Resources/mapguide_en.res. LogRequest's
AgentProperties lookup succeeds afterwards, as does a re-initialisation after
MgUninitializeWebTier().
Fixes #2224
Fixes #2196
Fixes #2401
Modified: branches/4.0/MgDev/Common/Foundation/System/Configuration.cpp
===================================================================
--- branches/4.0/MgDev/Common/Foundation/System/Configuration.cpp 2026-09-22 11:16:06 UTC (rev 10218)
+++ branches/4.0/MgDev/Common/Foundation/System/Configuration.cpp 2026-10-19 16:06:14 UTC (rev 10219)
@@ -71,6 +71,23 @@
return MgConfiguration::sm_configuration;
}
+///
+/// Discards the process-wide MgConfiguration, if one exists.
+///
+void MgConfiguration::DiscardInstance()
+{
+ // Serialize with GetInstance(). The lock is recursive, so this is safe to call from
+ // within MgInitializeWebTier(), which holds the same lock.
+ ACE_MT (ACE_GUARD (ACE_Recursive_Thread_Mutex, ace_mon, *ACE_Static_Object_Lock::instance ()));
+
+ MG_CONFIGURATION_TRY()
+
+ // Do not assign the instance to a Ptr anywhere else; this is the only owner.
+ MgConfiguration::sm_configuration = (MgConfiguration*)NULL;
+
+ MG_CONFIGURATION_CATCH_AND_THROW(L"MgConfiguration.DiscardInstance")
+}
+
///----------------------------------------------------------------------------
///
/// Loads the configuration file specified
@@ -97,25 +114,22 @@
{
m_fileName = fileName;
- if(m_config.Open())
+ if (m_config.Open() && m_config.ImportConfig(fileName))
{
- if(m_config.ImportConfig(fileName))
- {
- m_fileLoaded = true;
- }
- else
- {
- MgStringCollection arguments;
- arguments.Add(fileName);
-
- throw new MgException(MgExceptionCodes::MgConfigurationLoadFailedException,
- L"MgConfiguration.LoadConfiguration",
- __LINE__, __WFILE__, &arguments, L"", NULL);
- }
+ m_fileLoaded = true;
}
else
{
- ACE_ERROR((LM_ERROR, ACE_TEXT("%p\n"), ACE_TEXT("MgConfiguration::LoadConfiguration()")));
+ // Report a failure to open the heap as well as a failure to import the file.
+ // The heap stays open after either failure and cannot be re-opened, so this
+ // instance can never be loaded; callers must discard it (see DiscardInstance)
+ // rather than treat this as a successful load.
+ MgStringCollection arguments;
+ arguments.Add(fileName);
+
+ throw new MgException(MgExceptionCodes::MgConfigurationLoadFailedException,
+ L"MgConfiguration.LoadConfiguration",
+ __LINE__, __WFILE__, &arguments, L"", NULL);
}
}
Modified: branches/4.0/MgDev/Common/Foundation/System/Configuration.h
===================================================================
--- branches/4.0/MgDev/Common/Foundation/System/Configuration.h 2026-09-22 11:16:06 UTC (rev 10218)
+++ branches/4.0/MgDev/Common/Foundation/System/Configuration.h 2026-10-19 16:06:14 UTC (rev 10219)
@@ -83,6 +83,19 @@
static MgConfiguration* GetInstance();
/// \brief
+ /// Discards the process-wide instance, if one exists.
+ ///
+ /// The underlying ACE configuration heap can only be opened once per instance,
+ /// so an instance whose LoadConfiguration() failed can never load a file. Callers
+ /// that want to retry a failed load must discard the instance first, so that the
+ /// next GetInstance() starts from a clean heap.
+ ///
+ /// Do not call this while the instance is in use by another thread.
+ ///
+
+ static void DiscardInstance();
+
+ /// \brief
/// Loads the configuration file specified
///
/// \param fileName
Modified: branches/4.0/MgDev/Common/Foundation/System/ConfigurationHeap.cpp
===================================================================
--- branches/4.0/MgDev/Common/Foundation/System/ConfigurationHeap.cpp 2026-09-22 11:16:06 UTC (rev 10218)
+++ branches/4.0/MgDev/Common/Foundation/System/ConfigurationHeap.cpp 2026-10-19 16:06:14 UTC (rev 10219)
@@ -332,6 +332,15 @@
{
return false;
}
+
+ if (index < 0)
+ {
+ // ACE compares section names case-insensitively, whereas IndexOf() does not,
+ // so ACE can resolve a section this collection has no entry for. Do not index
+ // the collection with the stale -1; report the section as not found instead.
+ return false;
+ }
+
section = m_sectionCollection->GetItem(index);
}
Modified: branches/4.0/MgDev/Web/src/HttpHandler/HttpRequest.cpp
===================================================================
--- branches/4.0/MgDev/Web/src/HttpHandler/HttpRequest.cpp 2026-09-22 11:16:06 UTC (rev 10218)
+++ branches/4.0/MgDev/Web/src/HttpHandler/HttpRequest.cpp 2026-10-19 16:06:14 UTC (rev 10219)
@@ -325,7 +325,7 @@
//find in the map the creator for the http mapping class
MgHttpRequestResponseHandler* (*createFunc)(MgHttpRequest*) = httpClassCreators[OperationName];
if(createFunc == NULL)
- new MgException(MgExceptionCodes::MgInvalidOperationException, L"MgHttpRequest::CreateRequestResponseHandler", __LINE__, __WFILE__, NULL, L"", NULL);
+ throw new MgException(MgExceptionCodes::MgInvalidOperationException, L"MgHttpRequest::CreateRequestResponseHandler", __LINE__, __WFILE__, NULL, L"", NULL);
//instantiate the http class
returnHandler = (*createFunc)(this);
Modified: branches/4.0/MgDev/Web/src/HttpHandler/HttpRequestResponseHandler.cpp
===================================================================
--- branches/4.0/MgDev/Web/src/HttpHandler/HttpRequestResponseHandler.cpp 2026-09-22 11:16:06 UTC (rev 10218)
+++ branches/4.0/MgDev/Web/src/HttpHandler/HttpRequestResponseHandler.cpp 2026-10-19 16:06:14 UTC (rev 10219)
@@ -69,7 +69,7 @@
config->GetBoolValue(MgConfigProperties::AgentPropertiesSection, disableProperty, bDisable, false);
if (bDisable)
{
- new MgException(MgExceptionCodes::MgInvalidOperationException, L"HttpRequestResponseHandler.InitializeCommonParamters", __LINE__, __WFILE__, NULL, L"", NULL);
+ throw new MgException(MgExceptionCodes::MgInvalidOperationException, L"HttpRequestResponseHandler.InitializeCommonParamters", __LINE__, __WFILE__, NULL, L"", NULL);
}
}
Modified: branches/4.0/MgDev/Web/src/IsapiAgent/IsapiAgent.cpp
===================================================================
--- branches/4.0/MgDev/Web/src/IsapiAgent/IsapiAgent.cpp 2026-09-22 11:16:06 UTC (rev 10218)
+++ branches/4.0/MgDev/Web/src/IsapiAgent/IsapiAgent.cpp 2026-10-19 16:06:14 UTC (rev 10219)
@@ -21,6 +21,7 @@
#include "stdafx.h"
#include "httpext.h"
#include "stdio.h"
+#include
#include "WebSupport.h"
#include "IsapiPostParser.h"
#include "Base64.h"
@@ -31,10 +32,17 @@
void Initialize(EXTENSION_CONTROL_BLOCK *pECB);
+static STRING GetModuleDirectory();
STRING gConfigPath;
+static HMODULE g_hModule = NULL;
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
+ if (DLL_PROCESS_ATTACH == ul_reason_for_call)
+ {
+ g_hModule = (HMODULE)hModule;
+ }
+
return TRUE;
}
@@ -231,14 +239,23 @@
{
if (!IsWebTierInitialized())
{
+ // GetServerVariable leaves the buffer untouched when the variable is not found,
+ // so it must start out empty and the return value must be checked.
DWORD dwSize = 1024;
- char path[1024];
- pECB->GetServerVariable(pECB->ConnID, (LPSTR)MapAgentStrings::PhysicalPath, path, &dwSize);
+ char path[1024] = {0};
+ BOOL bPhysicalPath = pECB->GetServerVariable(pECB->ConnID, (LPSTR)MapAgentStrings::PhysicalPath, path, &dwSize);
- if (strlen(path) > 0)
+ if (bPhysicalPath && strlen(path) > 0)
{
gConfigPath = MgUtil::MultiByteToWideChar(string(path));
}
+ else
+ {
+ // APPL_PHYSICAL_PATH is not always supplied (IIS 6 does not honour it). The
+ // extension is installed in the mapagent application directory, which is what
+ // the variable would have held, so use this module's own directory instead.
+ gConfigPath = GetModuleDirectory();
+ }
STRING configFile = gConfigPath;
configFile.append(L"\\");
@@ -251,3 +268,31 @@
MG_CATCH_AND_THROW(L"IsapiAgent.Initialize");
}
}
+
+
+// Returns the directory containing this extension, without a trailing separator.
+static STRING GetModuleDirectory()
+{
+ wchar_t modulePath[_MAX_PATH + 1];
+ DWORD length = 0;
+
+ if (NULL != g_hModule)
+ {
+ length = GetModuleFileNameW(g_hModule, modulePath, _MAX_PATH);
+ }
+
+ if (length == 0 || length >= _MAX_PATH)
+ {
+ return L"";
+ }
+
+ wchar_t* lastSeparator = wcsrchr(modulePath, L'\\');
+ if (NULL == lastSeparator)
+ {
+ return L"";
+ }
+
+ *lastSeparator = L'\0';
+
+ return modulePath;
+}
Modified: branches/4.0/MgDev/Web/src/WebSupport/InitializeWebTier.cpp
===================================================================
--- branches/4.0/MgDev/Web/src/WebSupport/InitializeWebTier.cpp 2026-09-22 11:16:06 UTC (rev 10218)
+++ branches/4.0/MgDev/Web/src/WebSupport/InitializeWebTier.cpp 2026-10-19 16:06:14 UTC (rev 10219)
@@ -44,7 +44,19 @@
// Load configuration file
MgConfiguration* config = MgConfiguration::GetInstance();
- config->LoadConfiguration(configFile);
+ try
+ {
+ config->LoadConfiguration(configFile);
+ }
+ catch (...)
+ {
+ // A failed load leaves the ACE configuration heap open, and that heap cannot be
+ // re-opened, so the instance can never be loaded. Discard it so that the next
+ // request retries the initialization instead of running against a configuration
+ // that was never loaded.
+ MgConfiguration::DiscardInstance();
+ throw;
+ }
// Init the Anonymous user check
MgHttpAnonymousCheck* anonCheck = MgHttpAnonymousCheck::GetInstance();
@@ -87,7 +99,7 @@
//Check catalog
if (NULL == csCatalog.p)
{
- new MgException(MgExceptionCodes::MgCoordinateSystemInitializationFailedException,
+ throw new MgException(MgExceptionCodes::MgCoordinateSystemInitializationFailedException,
L"MgInitializeWebTierInternal", __LINE__, __WFILE__, NULL, L"", NULL);
}
@@ -107,7 +119,7 @@
LibraryStatus status = csCatalog->GetLibraryStatus();
if(lsInitialized != status)
{
- new MgException(MgExceptionCodes::MgCoordinateSystemInitializationFailedException,
+ throw new MgException(MgExceptionCodes::MgCoordinateSystemInitializationFailedException,
L"MgInitializeWebTierInternal", __LINE__, __WFILE__, NULL, L"", NULL);
}
@@ -137,6 +149,11 @@
ACE_OS::sleep(2);
}
+ // The configuration heap cannot be re-opened once opened, and it is built on ACE state
+ // that is about to be torn down, so drop it. A later MgInitializeWebTier() then loads
+ // the configuration again, exactly as it does in a fresh process.
+ MgConfiguration::DiscardInstance();
+
// Uninitialize ACE
ACE::fini();
From svn_mapguide at osgeo.org Wed Oct 21 01:23:19 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Wed, 21 Oct 2026 01:23:19 -0700
Subject: [mapguide-commits] r10220 - in branches/4.0/MgDev/Server/src:
Common/Manager UnitTesting
Message-ID: <20261021082319.8AAE0476CB@trac.osgeo.org>
Author: jng
Date: 2026-10-21 01:23:18 -0700 (Wed, 21 Oct 2026)
New Revision: 10220
Modified:
branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.cpp
branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.h
branches/4.0/MgDev/Server/src/UnitTesting/TestServerAdminService.cpp
branches/4.0/MgDev/Server/src/UnitTesting/TestServerManager.cpp
Log:
Make reading CPU utilization numbers more robust on Linux. Fixes #1892
Modified: branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.cpp 2026-10-19 16:06:14 UTC (rev 10219)
+++ branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.cpp 2026-10-21 08:23:18 UTC (rev 10220)
@@ -29,6 +29,7 @@
#include
#include
#include
+#include
#endif
const STRING MgServerManager::DocumentExtension = L".awd";
@@ -1278,12 +1279,93 @@
return nCpuLoad;
}
+#ifndef _WIN32
///////////////////////////////////////////////////////////////////////////////////
///
+/// Reads the aggregate "cpu" line of /proc/stat.
+///
+///
+/// The counters are cumulative jiffies since boot, summed over every CPU and
+/// expressed in USER_HZ (100 Hz), so they are read as 64-bit values. On a
+/// many-core host a 32-bit counter passes 2^31 after anything from a few weeks
+/// to a few months of uptime.
+///
+///
+/// True if all four counters were parsed.
+///
+static bool ReadLinuxCpuTimes(MgServerManager::CpuSample& sample)
+{
+ FILE* stat = fopen(MgUtil::WideCharToMultiByte(MgUtil::LinuxFilesystemStat).c_str(), "r");
+ if (NULL == stat)
+ {
+ MgStringCollection arguments;
+ arguments.Add(MgUtil::LinuxFilesystemStat);
+ throw new MgException(MgExceptionCodes::MgFileNotFoundException, L"MgServerManager.ReadLinuxCpuTimes", __LINE__, __WFILE__, &arguments, L"", NULL);
+ }
+
+ // Sized so that the line cannot be truncated as the counters grow
+ char line[256];
+ bool parsed = (NULL != fgets(line, sizeof(line), stat))
+ && (4 == sscanf(line, "%*3s %" SCNu64 " %" SCNu64 " %" SCNu64 " %" SCNu64,
+ &sample.user, &sample.nice, &sample.sys, &sample.idle));
+
+ fclose(stat);
+
+ return parsed;
+}
+
+///////////////////////////////////////////////////////////////////////////////////
+///
+/// Computes the CPU utilization in percent between two /proc/stat samples.
+///
+///
+/// The counters are cumulative, so only their deltas are meaningful. A sample
+/// that cannot be compared must not be turned into a number: an empty interval
+/// would divide 0 by 0, and the NaN that produces converts to INT_MIN.
+///
+///
+/// The utilization in percent, or -1 if the samples cannot be compared.
+///
+INT32 MgServerManager::ComputeCpuUtilization(const CpuSample& previous, const CpuSample& current)
+{
+ // A counter that moved backwards (a CPU was hot-unplugged, for example)
+ // makes the sample meaningless.
+ if (current.user < previous.user || current.nice < previous.nice
+ || current.sys < previous.sys || current.idle < previous.idle)
+ {
+ return -1;
+ }
+
+ UINT64 total = (current.user - previous.user) + (current.nice - previous.nice)
+ + (current.sys - previous.sys) + (current.idle - previous.idle);
+
+ if (0 == total)
+ {
+ return -1;
+ }
+
+ double load = (double)(current.idle - previous.idle) / (double)total * 100;
+
+ // Keep the conversion to INT32 defined even if the counters are inconsistent
+ if (load < 0.0)
+ {
+ load = 0.0;
+ }
+ else if (load > 100.0)
+ {
+ load = 100.0;
+ }
+
+ return 100 - (INT32)load;
+}
+#endif // _WIN32
+
+///////////////////////////////////////////////////////////////////////////////////
+///
/// Helper method to compute the current CPU load on Linux.
///
///
-/// The total the current CPU load in percent.
+/// The total the current CPU load in percent, or -1 if it could not be determined.
///
INT32 MgServerManager::ComputeLinuxCpuLoad()
{
@@ -1295,49 +1377,34 @@
#else
- FILE* stat;
- char line[80];
- int user;
- int nice;
- int sys;
- int idle;
- int ouser;
- int onice;
- int osys;
- int oidle;
- double load;
+ CpuSample previous;
+ CpuSample current;
// Prime initial values for computing the CPU load over the sample interval
- stat = fopen( ( MgUtil::WideCharToMultiByte(MgUtil::LinuxFilesystemStat) ).c_str(), "r" );
- if (NULL == stat)
+ if (!ReadLinuxCpuTimes(previous))
{
- MgStringCollection arguments;
- arguments.Add(MgUtil::LinuxFilesystemStat);
- throw new MgException(MgExceptionCodes::MgFileNotFoundException, L"MgServerManager.ComputeLinuxCpuLoad", __LINE__, __WFILE__, &arguments, L"", NULL);
+ return nCpuLoad;
}
- fgets(line, 80, stat);
- sscanf(line, "%*3s %i %i %i %i", &ouser, &onice, &osys, &oidle);
- fclose(stat);
- // Wait one interval.
- ACE_OS::sleep(1);
+ // Wait one interval. ACE_OS::sleep() does not restart an interrupted
+ // sleep, so retry: a sample taken over a near-zero interval has no
+ // counter movement to measure.
+ for (int attempts = 0; attempts < 3; ++attempts)
+ {
+ if (0 == ACE_OS::sleep(1))
+ {
+ break;
+ }
+ }
// Get the current data values
- stat = fopen( ( MgUtil::WideCharToMultiByte(MgUtil::LinuxFilesystemStat) ).c_str(), "r");
- if(NULL == stat)
+ if (!ReadLinuxCpuTimes(current))
{
- MgStringCollection arguments;
- arguments.Add(MgUtil::LinuxFilesystemStat);
- throw new MgException(MgExceptionCodes::MgFileNotFoundException, L"MgServerManager.ComputeLinuxCpuLoad", __LINE__, __WFILE__, &arguments, L"", NULL);
+ return nCpuLoad;
}
- fgets(line, 80, stat);
- sscanf(line, "%*3s %i %i %i %i", &user, &nice, &sys, &idle);
- fclose(stat);
- load = (double)(idle-oidle) / (double)( (user-ouser) + (nice-onice) + (sys-osys) + (idle-oidle) ) * 100;
+ nCpuLoad = ComputeCpuUtilization(previous, current);
- nCpuLoad = 100 - (INT32)load;
-
#endif // _WIN32
return nCpuLoad;
Modified: branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.h
===================================================================
--- branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.h 2026-10-19 16:06:14 UTC (rev 10219)
+++ branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.h 2026-10-21 08:23:18 UTC (rev 10220)
@@ -141,6 +141,21 @@
void StartWorkerThread(void (*function)());
void StopWorkerThreads();
+#ifndef _WIN32
+ /// Aggregate CPU counters from the "cpu" line of /proc/stat, in USER_HZ jiffies
+ struct CpuSample
+ {
+ UINT64 user;
+ UINT64 nice;
+ UINT64 sys;
+ UINT64 idle;
+ };
+
+ /// Computes the CPU utilization in percent between two /proc/stat samples.
+ /// Returns -1 if the samples cannot be compared.
+ static INT32 ComputeCpuUtilization(const CpuSample& previous, const CpuSample& current);
+#endif
+
private:
// Constructor
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestServerAdminService.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestServerAdminService.cpp 2026-10-19 16:06:14 UTC (rev 10219)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestServerAdminService.cpp 2026-10-21 08:23:18 UTC (rev 10220)
@@ -504,6 +504,16 @@
Ptr pPropertyCollection = MgServerManager::GetInstance()->GetInformationProperties();
REQUIRE(0 < pPropertyCollection->GetCount());
+
+ // The CPU utilization is derived from /proc/stat, so it is a percentage
+ // or -1 when no sample could be taken (see MgServerManager::ComputeCpuUtilization)
+ Ptr pCpuProperty = pPropertyCollection->GetItem(MgServerInformationProperties::CpuUtilization);
+ REQUIRE(pCpuProperty != nullptr);
+ REQUIRE(MgPropertyType::Int32 == pCpuProperty->GetPropertyType());
+
+ INT32 nCpuUtilization = ((MgInt32Property*)pCpuProperty.p)->GetValue();
+ REQUIRE(-1 <= nCpuUtilization);
+ REQUIRE(100 >= nCpuUtilization);
}
catch(MgException* e)
{
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestServerManager.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestServerManager.cpp 2026-10-19 16:06:14 UTC (rev 10219)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestServerManager.cpp 2026-10-21 08:23:18 UTC (rev 10220)
@@ -174,4 +174,81 @@
{
throw;
}
-}
\ No newline at end of file
+}
+#ifndef _WIN32
+///----------------------------------------------------------------------------
+/// Test Case Description:
+///
+/// This test case checks the CPU utilization derived from two /proc/stat
+/// samples. This is what the performance log's CPU column is built from, and
+/// the values it used to produce after a long uptime (100, then -2147483548)
+/// are the reason the samples are read as 64-bit counters and the ratio is
+/// guarded rather than divided blindly.
+///
+/// Linux only, because Windows takes its CPU load from a PDH counter that
+/// already returns a percentage and never builds cumulative counter samples.
+///----------------------------------------------------------------------------
+TEST_CASE("ComputeCpuUtilization", "[ServerManager]")
+{
+ typedef MgServerManager::CpuSample Sample;
+
+ // One second of a 4-core host, counters well short of any 32-bit limit
+ const Sample before = { 1000, 10, 500, 90000 };
+
+ SECTION("utilization between two samples")
+ {
+ // +300 user, +100 sys, +100 idle: 20% of the interval was idle
+ const Sample after = { 1300, 10, 600, 90100 };
+
+ REQUIRE(80 == MgServerManager::ComputeCpuUtilization(before, after));
+ }
+
+ SECTION("an interval spent entirely busy is 100")
+ {
+ const Sample after = { 1800, 10, 700, 90000 };
+
+ REQUIRE(100 == MgServerManager::ComputeCpuUtilization(before, after));
+ }
+
+ SECTION("an interval spent entirely idle is 0")
+ {
+ const Sample after = { 1000, 10, 500, 90250 };
+
+ REQUIRE(0 == MgServerManager::ComputeCpuUtilization(before, after));
+ }
+
+ SECTION("an interval with no counter movement has no utilization")
+ {
+ // The 32-bit read of these counters clamped at INT_MAX, so an idle
+ // system stopped moving and 0/0 ended up logged as -2147483548.
+ const Sample after = before;
+
+ REQUIRE(-1 == MgServerManager::ComputeCpuUtilization(before, after));
+ }
+
+ SECTION("a counter that moved backwards has no utilization")
+ {
+ // A CPU hot-unplugged between the two samples takes its time with it
+ const Sample lessIdle = { 1000, 10, 500, 89900 };
+ const Sample lessUser = { 900, 10, 500, 90000 };
+
+ REQUIRE(-1 == MgServerManager::ComputeCpuUtilization(before, lessIdle));
+ REQUIRE(-1 == MgServerManager::ComputeCpuUtilization(before, lessUser));
+ }
+
+ SECTION("counters past 2^31 and 2^32 keep their deltas")
+ {
+ // Months of uptime on a many-core host: idle crosses 2^31 inside the
+ // interval, which is the point where a 32-bit read stops advancing.
+ const Sample nearIntMax = { 1000000000, 10, 900000000, 2147483600 };
+ const Sample pastIntMax = { 1000000300, 10, 900000100, 2147483700 };
+ REQUIRE(80 == MgServerManager::ComputeCpuUtilization(nearIntMax, pastIntMax));
+
+ // Every counter here is larger than a 32-bit value can hold: +900 user,
+ // +300 sys, +1800 idle
+ const Sample past4Gb = { 4294967296ULL, 0, 1000000000ULL, 9999999999ULL };
+ const Sample past4GbAfter = { 4294967296ULL + 900, 0, 1000000000ULL + 300, 9999999999ULL + 1800 };
+ REQUIRE(40 == MgServerManager::ComputeCpuUtilization(past4Gb, past4GbAfter));
+ }
+}
+#endif // _WIN32
From svn_mapguide at osgeo.org Wed Oct 21 15:24:20 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Wed, 21 Oct 2026 15:24:20 -0700
Subject: [mapguide-commits] r10221 -
branches/4.0/MgDev/Server/src/UnitTesting
Message-ID: <20261021222420.E15553E5F7@trac.osgeo.org>
Author: jng
Date: 2026-10-21 15:24:19 -0700 (Wed, 21 Oct 2026)
New Revision: 10221
Modified:
branches/4.0/MgDev/Server/src/UnitTesting/TestCoordinateSystem.cpp
Log:
#1835: Add regression test to verify this was indeed fixed. The fix landed in r7097
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestCoordinateSystem.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestCoordinateSystem.cpp 2026-10-21 08:23:18 UTC (rev 10220)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestCoordinateSystem.cpp 2026-10-21 22:24:19 UTC (rev 10221)
@@ -21,6 +21,11 @@
#include "CatchHelperMacros.h"
#include "catch.hpp"
+#include
+#include
+#include
+#include
+
#ifndef _WIN32
#define stricmp strcasecmp
@@ -433,6 +438,208 @@
&MgCoordinateSystemCatalog::GetCategoryDictionary>(L"Category", NULL, NULL);
}
+///----------------------------------------------------------------------------
+/// Test Case Description:
+///
+/// Regression test for #1835: adding a coordinate system to a category that
+/// already contains duplicate entries must not truncate that category.
+///
+/// CCoordinateSystemCategory::CopyFrom() used to rebuild a category by calling
+/// AddCoordinateSystem() for every name it enumerated. AddCoordinateSystem()
+/// throws on a duplicate, so the copy stopped at the first duplicate entry, and
+/// CCoordinateSystemCategoryDictionary::Modify() then wrote that truncated
+/// category back to the dictionary.
+///----------------------------------------------------------------------------
+
+//A category dictionary (.CSD) is a straight concatenation of fixed sized
+//records; see CSwrtCategory()/CSrdCategoryEx() in
+//Oem/CsMap/CsMapDev/Source/CS_category.c:
+// UINT32 magic
+// { char ctName[128]; UINT32 nameCnt; UINT32 allocCnt; char csName[24][allocCnt]; } *
+const size_t CategoryDictNameSize = 128;
+const size_t CategoryDictCsNameSize = 24;
+const size_t CategoryDictAllocCount = 12;
+const UINT32 CategoryDictMagic = 0x3372ED6C;
+
+static size_t CategoryDictStrLen(const unsigned char* buffer, size_t maxLength)
+{
+ size_t length = 0;
+ while (length < maxLength && buffer[length] != 0)
+ ++length;
+ return length;
+}
+
+static UINT32 ReadCategoryDictUInt32(const std::vector& bytes, size_t offset)
+{
+ return (UINT32)bytes[offset]
+ | ((UINT32)bytes[offset + 1] << 8)
+ | ((UINT32)bytes[offset + 2] << 16)
+ | ((UINT32)bytes[offset + 3] << 24);
+}
+
+static void WriteCategoryDictUInt32(std::vector& bytes, size_t offset, UINT32 value)
+{
+ bytes[offset] = (unsigned char)(value & 0xFF);
+ bytes[offset + 1] = (unsigned char)((value >> 8) & 0xFF);
+ bytes[offset + 2] = (unsigned char)((value >> 16) & 0xFF);
+ bytes[offset + 3] = (unsigned char)((value >> 24) & 0xFF);
+}
+
+static bool ReadCategoryDictFile(const STRING& file, std::vector& bytes)
+{
+ std::string mbFile = MgUtil::WideCharToMultiByte(file);
+ std::ifstream stream(mbFile.c_str(), std::ios::in | std::ios::binary);
+ if (!stream.is_open())
+ return false;
+
+ bytes.assign(std::istreambuf_iterator(stream), std::istreambuf_iterator());
+ return true;
+}
+
+static bool WriteCategoryDictFile(const STRING& file, const std::vector& bytes)
+{
+ if (bytes.empty())
+ return false;
+
+ std::string mbFile = MgUtil::WideCharToMultiByte(file);
+ std::ofstream stream(mbFile.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
+ if (!stream.is_open())
+ return false;
+
+ stream.write((const char*)&bytes[0], (std::streamsize)bytes.size());
+ return stream.good();
+}
+
+//Appends a copy of its own first CS name to the first non-empty category in the
+//given category dictionary, i.e. makes that category contain a duplicate entry.
+//Reports which category and CS name were used, plus the category's entry count
+//before the duplicate was added.
+static bool DuplicateCategoryEntry(const STRING& csdFile, STRING& categoryName, STRING& csName, UINT32& originalCount)
+{
+ std::vector bytes;
+ if (!ReadCategoryDictFile(csdFile, bytes))
+ return false;
+
+ if (bytes.size() < 4 || ReadCategoryDictUInt32(bytes, 0) != CategoryDictMagic)
+ return false;
+
+ size_t offset = 4;
+ while (offset + CategoryDictNameSize + 8 <= bytes.size())
+ {
+ const unsigned char* mbCategory = &bytes[offset];
+ std::string mbCategoryName((const char*)mbCategory, CategoryDictStrLen(mbCategory, CategoryDictNameSize));
+ size_t countOffset = offset + CategoryDictNameSize;
+ UINT32 nameCount = ReadCategoryDictUInt32(bytes, countOffset);
+ UINT32 allocCount = ReadCategoryDictUInt32(bytes, countOffset + 4);
+ size_t namesOffset = countOffset + 8;
+
+ if (nameCount > allocCount || namesOffset + allocCount * CategoryDictCsNameSize > bytes.size())
+ return false;
+
+ if (nameCount > 0)
+ {
+ const unsigned char* mbFirst = &bytes[namesOffset];
+ std::string mbDuplicate((const char*)mbFirst, CategoryDictStrLen(mbFirst, CategoryDictCsNameSize));
+
+ if (nameCount == allocCount)
+ {
+ //grow the entry block the same way CS-Map's AddCsName() does
+ bytes.insert(bytes.begin() + (namesOffset + allocCount * CategoryDictCsNameSize),
+ CategoryDictAllocCount * CategoryDictCsNameSize, 0);
+ allocCount += (UINT32)CategoryDictAllocCount;
+ WriteCategoryDictUInt32(bytes, countOffset + 4, allocCount);
+ }
+
+ size_t newEntryOffset = namesOffset + nameCount * CategoryDictCsNameSize;
+ memset(&bytes[newEntryOffset], 0, CategoryDictCsNameSize);
+ memcpy(&bytes[newEntryOffset], mbDuplicate.c_str(), mbDuplicate.length());
+ WriteCategoryDictUInt32(bytes, countOffset, nameCount + 1);
+
+ categoryName = MgUtil::MultiByteToWideChar(mbCategoryName);
+ csName = MgUtil::MultiByteToWideChar(mbDuplicate);
+ originalCount = nameCount;
+
+ return WriteCategoryDictFile(csdFile, bytes);
+ }
+
+ offset = namesOffset + allocCount * CategoryDictCsNameSize;
+ }
+
+ return false;
+}
+
+TEST_CASE("AddCoordinateSystemToCategoryWithDuplicateEntries", "[CoordinateSystem]")
+{
+ try
+ {
+ MgCoordinateSystemFactory csFactory;
+ Ptr pCsCatalog = csFactory.GetCatalog();
+
+ //make sure the category dictionary is written into the system dictionary
+ if (!pCsCatalog->GetUserDictionaryDir().empty())
+ pCsCatalog->SetUserDictionaryDir(L"");
+
+ Ptr pCtDict = pCsCatalog->GetCategoryDictionary();
+ STRING csdFile = pCtDict->GetPath();
+ REQUIRE(MgFileUtil::IsFile(csdFile));
+
+ STRING categoryName;
+ STRING duplicateName;
+ UINT32 originalCount = 0;
+
+ {
+ //backup the current CSD file and automatically restore it after we are done
+ FileAutoBackup csdFileBackup(csdFile, L".UNIT_TEST_SAVE", true);
+
+ REQUIRE(DuplicateCategoryEntry(csdFile, categoryName, duplicateName, originalCount));
+ ACE_DEBUG((LM_INFO, ACE_TEXT("\nInjected duplicate %W into category %W\n"), duplicateName.c_str(), categoryName.c_str()));
+
+ pCtDict->SetFileName(pCtDict->GetFileName()); //force the Mg dictionary to re-read the file
+
+ Ptr pCategory = pCtDict->GetCategory(categoryName);
+ REQUIRE(pCategory);
+
+ Ptr pNamesBefore = pCategory->GetCoordinateSystems();
+ REQUIRE(pNamesBefore->GetCount() == (INT32)(originalCount + 1)); //the duplicate has been picked up
+ REQUIRE(pNamesBefore->Contains(duplicateName));
+
+ STRING newCsName = L"MG_UNIT_TEST_1835";
+ REQUIRE(!pNamesBefore->Contains(newCsName));
+
+ pCategory->AddCoordinateSystem(newCsName);
+ pCtDict->Modify(pCategory.p);
+
+ //now verify what's actually in the dictionary, not just the in-memory object
+ pCtDict->SetFileName(pCtDict->GetFileName());
+
+ pCategory = pCtDict->GetCategory(categoryName);
+ REQUIRE(pCategory);
+
+ Ptr pNamesAfter = pCategory->GetCoordinateSystems();
+ REQUIRE(pNamesAfter->GetCount() == (INT32)(originalCount + 2));
+ REQUIRE(pNamesAfter->Contains(newCsName));
+
+ for (INT32 i = 0; i < pNamesBefore->GetCount(); i++)
+ {
+ REQUIRE(pNamesAfter->Contains(pNamesBefore->GetItem(i)));
+ }
+ }
+
+ //the CSD file has been restored - make sure the Mg dictionary sees that, too
+ pCtDict->SetFileName(pCtDict->GetFileName());
+ }
+ catch(MgException* e)
+ {
+ STRING message = e->GetDetails(TEST_LOCALE);
+ SAFE_RELEASE(e);
+ FAIL(MG_WCHAR_TO_CHAR(message.c_str()));
+ }
+ catch(...)
+ {
+ throw;
+ }
+}
+
/// (5) Update a geodetic transformation
TEST_CASE("UpdateGeodeticTransformations", "[CoordinateSystem]")
{
From svn_mapguide at osgeo.org Wed Oct 21 15:30:54 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Wed, 21 Oct 2026 15:30:54 -0700
Subject: [mapguide-commits] r10222 - in branches/4.0/MgDev: . Oem/LinuxApt
Message-ID: <20261021223054.A6F213F0EC@trac.osgeo.org>
Author: jng
Date: 2026-10-21 15:30:53 -0700 (Wed, 21 Oct 2026)
New Revision: 10222
Added:
branches/4.0/MgDev/Oem/LinuxApt/apache-tomcat-9.0.123.tar.gz
branches/4.0/MgDev/Oem/LinuxApt/apr-util-1.6.5.tar.bz2
branches/4.0/MgDev/Oem/LinuxApt/httpd-2.4.69.tar.bz2
branches/4.0/MgDev/Oem/LinuxApt/php-8.3.36.tar.bz2
Removed:
branches/4.0/MgDev/Oem/LinuxApt/apache-tomcat-9.0.110.tar.gz
branches/4.0/MgDev/Oem/LinuxApt/apr-util-1.6.3.tar.bz2
branches/4.0/MgDev/Oem/LinuxApt/httpd-2.4.65.tar.bz2
branches/4.0/MgDev/Oem/LinuxApt/php-8.3.26.tar.bz2
Modified:
branches/4.0/MgDev/Oem/LinuxApt/CMakeLists.txt
branches/4.0/MgDev/UPDATING_WEBTIER_COMPONENTS.md
branches/4.0/MgDev/cmake_linuxapt.sh
branches/4.0/MgDev/prepare_webtier_components.bat
Log:
MapGuide Web Tier component updates
- PHP 8.3.26 -> 8.3.36 (includes php2shell fix)
- HTTPD 2.4.65 -> 2.4.69
- Tomcat 9.0.110 -> 9.0.123
- APR util 1.6.3 -> 1.6.5
Modified: branches/4.0/MgDev/Oem/LinuxApt/CMakeLists.txt
===================================================================
--- branches/4.0/MgDev/Oem/LinuxApt/CMakeLists.txt 2026-10-21 22:24:19 UTC (rev 10221)
+++ branches/4.0/MgDev/Oem/LinuxApt/CMakeLists.txt 2026-10-21 22:30:53 UTC (rev 10222)
@@ -20,7 +20,7 @@
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tomcat)
# TODO: Offload tomcat version to top-level var
- execute_process(COMMAND tar -zxf ${CMAKE_CURRENT_SOURCE_DIR}/apache-tomcat-9.0.110.tar.gz -C ${CMAKE_CURRENT_BINARY_DIR}/tomcat --strip-components=1)
+ execute_process(COMMAND tar -zxf ${CMAKE_CURRENT_SOURCE_DIR}/apache-tomcat-9.0.123.tar.gz -C ${CMAKE_CURRENT_BINARY_DIR}/tomcat --strip-components=1)
configure_file(${CMAKE_MODULE_PATH}/../configs/mapguide.xml.in ${CMAKE_CURRENT_BINARY_DIR}/tomcat/conf/Catalina/localhost/mapguide.xml)
# Don't overwrite startup.sh in the CMAKE_CURRENT_BINARY_DIR as the subseqent install will do the same thing
@@ -31,4 +31,4 @@
file(GLOB TOMCAT_SCRIPTS "${CMAKE_CURRENT_BINARY_DIR}/tomcat/bin/*.sh")
install(PROGRAMS ${TOMCAT_SCRIPTS} DESTINATION ${MG_INSTALL_WEB_PREFIX}/tomcat/bin)
install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/startup.sh DESTINATION ${MG_INSTALL_WEB_PREFIX}/tomcat/bin)
-endif (WITH_JAVA)
\ No newline at end of file
+endif (WITH_JAVA)
Deleted: branches/4.0/MgDev/Oem/LinuxApt/apache-tomcat-9.0.110.tar.gz
===================================================================
(Binary files differ)
Added: branches/4.0/MgDev/Oem/LinuxApt/apache-tomcat-9.0.123.tar.gz
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/Oem/LinuxApt/apache-tomcat-9.0.123.tar.gz
===================================================================
--- branches/4.0/MgDev/Oem/LinuxApt/apache-tomcat-9.0.123.tar.gz 2026-10-21 22:24:19 UTC (rev 10221)
+++ branches/4.0/MgDev/Oem/LinuxApt/apache-tomcat-9.0.123.tar.gz 2026-10-21 22:30:53 UTC (rev 10222)
Property changes on: branches/4.0/MgDev/Oem/LinuxApt/apache-tomcat-9.0.123.tar.gz
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/gzip
\ No newline at end of property
Deleted: branches/4.0/MgDev/Oem/LinuxApt/apr-util-1.6.3.tar.bz2
===================================================================
(Binary files differ)
Added: branches/4.0/MgDev/Oem/LinuxApt/apr-util-1.6.5.tar.bz2
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/Oem/LinuxApt/apr-util-1.6.5.tar.bz2
===================================================================
--- branches/4.0/MgDev/Oem/LinuxApt/apr-util-1.6.5.tar.bz2 2026-10-21 22:24:19 UTC (rev 10221)
+++ branches/4.0/MgDev/Oem/LinuxApt/apr-util-1.6.5.tar.bz2 2026-10-21 22:30:53 UTC (rev 10222)
Property changes on: branches/4.0/MgDev/Oem/LinuxApt/apr-util-1.6.5.tar.bz2
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/x-bzip2
\ No newline at end of property
Deleted: branches/4.0/MgDev/Oem/LinuxApt/httpd-2.4.65.tar.bz2
===================================================================
(Binary files differ)
Added: branches/4.0/MgDev/Oem/LinuxApt/httpd-2.4.69.tar.bz2
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/Oem/LinuxApt/httpd-2.4.69.tar.bz2
===================================================================
--- branches/4.0/MgDev/Oem/LinuxApt/httpd-2.4.69.tar.bz2 2026-10-21 22:24:19 UTC (rev 10221)
+++ branches/4.0/MgDev/Oem/LinuxApt/httpd-2.4.69.tar.bz2 2026-10-21 22:30:53 UTC (rev 10222)
Property changes on: branches/4.0/MgDev/Oem/LinuxApt/httpd-2.4.69.tar.bz2
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/x-bzip2
\ No newline at end of property
Deleted: branches/4.0/MgDev/Oem/LinuxApt/php-8.3.26.tar.bz2
===================================================================
(Binary files differ)
Added: branches/4.0/MgDev/Oem/LinuxApt/php-8.3.36.tar.bz2
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/Oem/LinuxApt/php-8.3.36.tar.bz2
===================================================================
--- branches/4.0/MgDev/Oem/LinuxApt/php-8.3.36.tar.bz2 2026-10-21 22:24:19 UTC (rev 10221)
+++ branches/4.0/MgDev/Oem/LinuxApt/php-8.3.36.tar.bz2 2026-10-21 22:30:53 UTC (rev 10222)
Property changes on: branches/4.0/MgDev/Oem/LinuxApt/php-8.3.36.tar.bz2
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/x-bzip2
\ No newline at end of property
Modified: branches/4.0/MgDev/UPDATING_WEBTIER_COMPONENTS.md
===================================================================
--- branches/4.0/MgDev/UPDATING_WEBTIER_COMPONENTS.md 2026-10-21 22:24:19 UTC (rev 10221)
+++ branches/4.0/MgDev/UPDATING_WEBTIER_COMPONENTS.md 2026-10-21 22:30:53 UTC (rev 10222)
@@ -39,7 +39,7 @@
* For `tomcat`, `httpd` and `php`, only update to the latest release in that series. Do not update to a new major version unless that is planned for (ie. The current major version has entered EOL). As of writing this document, the current major release versions are:
* `apache-tomcat` - `9.0.x`
* `httpd` - `2.4.x`
- * `php` - `8.1.x`
+ * `php` - `8.3.x`
* Update `CMakeLists.txt`
* Update referenced tomcat version number
@@ -121,4 +121,4 @@
* apr-util [old version] -> [new version]
* tomcat [old version] -> [new version]
* tomcat-connectors [old version] -> [new version]
-```
\ No newline at end of file
+```
Modified: branches/4.0/MgDev/cmake_linuxapt.sh
===================================================================
--- branches/4.0/MgDev/cmake_linuxapt.sh 2026-10-21 22:24:19 UTC (rev 10221)
+++ branches/4.0/MgDev/cmake_linuxapt.sh 2026-10-21 22:30:53 UTC (rev 10222)
@@ -130,11 +130,11 @@
echo "Created LinuxApt working dir at: $LINUXAPT_WORK_DIR"
fi
-PHP_VER=8.3.26
-HTTPD_VER=2.4.65
+PHP_VER=8.3.36
+HTTPD_VER=2.4.69
TC_VER=1.2.50
APR_VER=1.7.6
-APR_UTIL_VER=1.6.3
+APR_UTIL_VER=1.6.5
HTTPD_PORT=8008
TOMCAT_PORT=8009
@@ -299,4 +299,4 @@
fi
echo "$OEM_WORK_DIR/env_vars.sh updated"
-echo "Make sure to run cmake_build.sh with --oem-working-dir set to: $OEM_WORK_DIR"
\ No newline at end of file
+echo "Make sure to run cmake_build.sh with --oem-working-dir set to: $OEM_WORK_DIR"
Modified: branches/4.0/MgDev/prepare_webtier_components.bat
===================================================================
--- branches/4.0/MgDev/prepare_webtier_components.bat 2026-10-21 22:24:19 UTC (rev 10221)
+++ branches/4.0/MgDev/prepare_webtier_components.bat 2026-10-21 22:30:53 UTC (rev 10222)
@@ -1,6 +1,6 @@
@echo off
SET SEVENZ=%CD%\BuildTools\WebTools\7-zip\7z.exe
-SET HTTPD_VER=2.4.65
+SET HTTPD_VER=2.4.69
pushd Oem\LinuxApt
:check_httpd
if not exist httpd-%HTTPD_VER% goto unpack_httpd
From svn_mapguide at osgeo.org Wed Oct 21 15:40:45 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Wed, 21 Oct 2026 15:40:45 -0700
Subject: [mapguide-commits] r10223 - in trunk/Installer: . Support/Web/x64
Message-ID: <20261021224046.4E36A3F744@trac.osgeo.org>
Author: jng
Date: 2026-10-21 15:40:44 -0700 (Wed, 21 Oct 2026)
New Revision: 10223
Added:
trunk/Installer/Support/Web/x64/httpd-2.4.69-261002-Win64-VS18.zip
trunk/Installer/Support/Web/x64/php-8.3.36-nts-Win32-vs16-x64.zip
Removed:
trunk/Installer/Support/Web/x64/httpd-2.4.65-250724-Win64-VS17.zip
trunk/Installer/Support/Web/x64/php-8.3.26-nts-Win32-vs16-x64.zip
Modified:
trunk/Installer/Support/Web/x64/MODULE_VERSIONS.txt
trunk/Installer/Support/Web/x64/mod_fcgid.so
trunk/Installer/Support/Web/x64/mod_jk.so
trunk/Installer/build.bat
Log:
Windows installer updates
- PHP 8.3.26 -> 8.3.36 (includes php2shell fix)
- HTTPD 2.4.65 -> 2.4.69
- mod_fcgid.so (same version, but VS18 build)
- mod_jk.so (same version, but VS18 build)
Modified: trunk/Installer/Support/Web/x64/MODULE_VERSIONS.txt
===================================================================
--- trunk/Installer/Support/Web/x64/MODULE_VERSIONS.txt 2026-10-21 22:30:53 UTC (rev 10222)
+++ trunk/Installer/Support/Web/x64/MODULE_VERSIONS.txt 2026-10-21 22:40:44 UTC (rev 10223)
@@ -1,2 +1,2 @@
-mod_jk.so - 1.2.50
-mod_fcgid.so - 2.3.10
\ No newline at end of file
+mod_jk.so - 1.2.50 (VS18)
+mod_fcgid.so - 2.3.10 (VS18)
\ No newline at end of file
Deleted: trunk/Installer/Support/Web/x64/httpd-2.4.65-250724-Win64-VS17.zip
===================================================================
(Binary files differ)
Added: trunk/Installer/Support/Web/x64/httpd-2.4.69-261002-Win64-VS18.zip
===================================================================
(Binary files differ)
Index: trunk/Installer/Support/Web/x64/httpd-2.4.69-261002-Win64-VS18.zip
===================================================================
--- trunk/Installer/Support/Web/x64/httpd-2.4.69-261002-Win64-VS18.zip 2026-10-21 22:30:53 UTC (rev 10222)
+++ trunk/Installer/Support/Web/x64/httpd-2.4.69-261002-Win64-VS18.zip 2026-10-21 22:40:44 UTC (rev 10223)
Property changes on: trunk/Installer/Support/Web/x64/httpd-2.4.69-261002-Win64-VS18.zip
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Modified: trunk/Installer/Support/Web/x64/mod_fcgid.so
===================================================================
(Binary files differ)
Modified: trunk/Installer/Support/Web/x64/mod_jk.so
===================================================================
(Binary files differ)
Deleted: trunk/Installer/Support/Web/x64/php-8.3.26-nts-Win32-vs16-x64.zip
===================================================================
(Binary files differ)
Added: trunk/Installer/Support/Web/x64/php-8.3.36-nts-Win32-vs16-x64.zip
===================================================================
(Binary files differ)
Index: trunk/Installer/Support/Web/x64/php-8.3.36-nts-Win32-vs16-x64.zip
===================================================================
--- trunk/Installer/Support/Web/x64/php-8.3.36-nts-Win32-vs16-x64.zip 2026-10-21 22:30:53 UTC (rev 10222)
+++ trunk/Installer/Support/Web/x64/php-8.3.36-nts-Win32-vs16-x64.zip 2026-10-21 22:40:44 UTC (rev 10223)
Property changes on: trunk/Installer/Support/Web/x64/php-8.3.36-nts-Win32-vs16-x64.zip
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Modified: trunk/Installer/build.bat
===================================================================
--- trunk/Installer/build.bat 2026-10-21 22:30:53 UTC (rev 10222)
+++ trunk/Installer/build.bat 2026-10-21 22:40:44 UTC (rev 10223)
@@ -81,11 +81,11 @@
rem ==================================================
rem Web dependencies
rem ==================================================
-SET HTTPD_VERSION=2.4.65
-SET HTTPD_BUILD=250724
-SET HTTPD_VCVER=VS17
-SET PHP_VERSION=8.3.26
-SET TOMCAT_VERSION=9.0.110
+SET HTTPD_VERSION=2.4.69
+SET HTTPD_BUILD=261002
+SET HTTPD_VCVER=VS18
+SET PHP_VERSION=8.3.36
+SET TOMCAT_VERSION=9.0.123
SET HTTPD_PACKAGE=httpd-%HTTPD_VERSION%-%HTTPD_BUILD%-%PLATFORM_WIN%-%HTTPD_VCVER%.zip
SET TOMCAT_PACKAGE_NAME=apache-tomcat-%TOMCAT_VERSION%
SET TOMCAT_PACKAGE=%MG_OEM%\LinuxApt\%TOMCAT_PACKAGE_NAME%.tar.gz
From svn_mapguide at osgeo.org Wed Oct 21 15:45:33 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Wed, 21 Oct 2026 15:45:33 -0700
Subject: [mapguide-commits] r10224 - in branches/4.0/MgDev:
Common/Foundation/Data Server/src/UnitTesting
Message-ID: <20261021224533.DBA313FBA4@trac.osgeo.org>
Author: jng
Date: 2026-10-21 15:45:33 -0700 (Wed, 21 Oct 2026)
New Revision: 10224
Modified:
branches/4.0/MgDev/Common/Foundation/Data/NamedCollection.cpp
branches/4.0/MgDev/Server/src/UnitTesting/TestMisc.cpp
Log:
Guard against stale refs (by name) in MgNamedCollection
Fixes #1268
Modified: branches/4.0/MgDev/Common/Foundation/Data/NamedCollection.cpp
===================================================================
--- branches/4.0/MgDev/Common/Foundation/Data/NamedCollection.cpp 2026-10-21 22:40:44 UTC (rev 10223)
+++ branches/4.0/MgDev/Common/Foundation/Data/NamedCollection.cpp 2026-10-21 22:45:33 UTC (rev 10224)
@@ -159,8 +159,12 @@
// the requested object.
if (Compare(obj->GetName(), name) != 0)
{
- // The names do not match so we need to do a linear search
- // to find it because the collection and the map don't match.
+ // The names do not match, so the object was renamed after it
+ // was added to the map and the entry is keyed by a name it no
+ // longer has. Drop it, and do a linear search to find the
+ // requested object because the collection and the map don't
+ // match.
+ RemoveMap(obj);
obj = NULL;
}
}
@@ -184,6 +188,13 @@
}
}
}
+
+ // The object was not indexed under its current name, so index it now.
+ if (m_pNameMap && obj != NULL)
+ {
+ RemoveMap(obj);
+ InsertMap(obj);
+ }
}
return obj.Detach();
@@ -332,9 +343,12 @@
if (m_pNameMap)
{
- // If map is built, use it since it is faster.
+ // If map is built, use it since it is faster. Its entry for a name can
+ // be stale though, because an element's name can be changed after it
+ // was added, so only a hit for the element itself is conclusive.
Ptr item = GetMap( ((MgNamedSerializable*)value)->GetName() );
- return(item != NULL );
+ if (item.p == value)
+ return true;
}
// otherwise, linear search
@@ -551,11 +565,16 @@
// Remove the given element from the map.
void MgNamedCollection::RemoveMap(const MgNamedSerializable* value)
{
- // handle the current case sensitivity of the element name.
- if (m_bCaseSensitive)
- m_pNameMap->erase( ((MgNamedSerializable*)value)->GetName() );
- else
- m_pNameMap->erase( Lower(((MgNamedSerializable*)value)->GetName()) );
+ // The element's name may have changed since it was added to the map, so its
+ // entries have to be located by element rather than by name.
+ std::map::iterator iter = m_pNameMap->begin();
+ while (iter != m_pNameMap->end())
+ {
+ if (iter->second == value)
+ m_pNameMap->erase(iter++);
+ else
+ ++iter;
+ }
}
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestMisc.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestMisc.cpp 2026-10-21 22:40:44 UTC (rev 10223)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestMisc.cpp 2026-10-21 22:45:33 UTC (rev 10224)
@@ -1017,6 +1017,76 @@
}
}
+TEST_CASE("TracTicket1268", "[Misc]")
+{
+ try
+ {
+ // MgNamedCollection indexes its elements by name once the collection
+ // outgrows COLL_MAP_THRESHOLD. An element can be renamed after that,
+ // and removal used to erase the map entry by the element's current
+ // name, so the entry keyed by the old name survived and kept pointing
+ // at the removed element.
+ Ptr coll = new MgNamedCollection(false);
+
+ Ptr renamed;
+ for (INT32 i = 0; i < COLL_MAP_THRESHOLD + 2; i++)
+ {
+ STRING suffix;
+ MgUtil::Int32ToString(i, suffix);
+
+ STRING name = L"Schema" + suffix;
+ Ptr schema = new MgFeatureSchema(name, L"");
+ coll->Add(schema);
+
+ if (i == 0)
+ renamed = schema;
+ }
+
+ // ========================== Begin Test ======================== //
+
+ // Renaming an element does not take it out of the collection.
+ renamed->SetName(L"RenamedSchema");
+
+ CHECK(coll->Contains(renamed));
+
+ Ptr found = coll->FindItem(L"RenamedSchema");
+ CHECK(found.p == renamed.p);
+
+ // Removing the element must remove it completely: neither of the names
+ // it has had may resolve to it afterwards.
+ REQUIRE(coll->Remove(renamed));
+ CHECK(coll->FindItem(L"RenamedSchema") == nullptr);
+ CHECK(coll->FindItem(L"Schema0") == nullptr);
+
+ // An element that merely carries the vacated name is not in the
+ // collection until it is added.
+ Ptr replacement = new MgFeatureSchema(L"Schema0", L"");
+ CHECK(!coll->Contains(replacement));
+
+ // Drop the last reference to the removed element. A lookup by the name
+ // it used to have must not reach the freed object.
+ found = NULL;
+ renamed = NULL;
+ CHECK(coll->FindItem(L"Schema0") == nullptr);
+
+ // The vacated name is free to be used again.
+ coll->Add(replacement);
+ found = coll->FindItem(L"Schema0");
+ REQUIRE(found.p == replacement.p);
+ CHECK(coll->Contains(replacement));
+ }
+ catch (MgException* e)
+ {
+ STRING message = e->GetDetails(TestServiceFactory::TEST_LOCALE);
+ SAFE_RELEASE(e);
+ FAIL(MG_WCHAR_TO_CHAR(message.c_str()));
+ }
+ catch (...)
+ {
+ throw;
+ }
+}
+
TEST_CASE("TracTicket1304", "[Misc]")
{
try
From svn_mapguide at osgeo.org Fri Oct 23 04:26:01 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Fri, 23 Oct 2026 04:26:01 -0700
Subject: [mapguide-commits] r10225 - in branches/4.0/MgDev: Common/Renderers
Server/src/PostBuild Server/src/UnitTesting UnitTest/TestData
UnitTest/TestData/RasterStylization
Message-ID: <20261023112601.AACC3465C0@trac.osgeo.org>
Author: jng
Date: 2026-10-23 04:25:59 -0700 (Fri, 23 Oct 2026)
New Revision: 10225
Added:
branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/make_fixtures.py
Modified:
branches/4.0/MgDev/Common/Renderers/AGGImageIO.h
branches/4.0/MgDev/Server/src/PostBuild/PostBuild.mak
branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj
Log:
Add some raster stylization tests to establish behavioral baselines
Modified: branches/4.0/MgDev/Common/Renderers/AGGImageIO.h
===================================================================
--- branches/4.0/MgDev/Common/Renderers/AGGImageIO.h 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Common/Renderers/AGGImageIO.h 2026-10-23 11:25:59 UTC (rev 10225)
@@ -18,9 +18,14 @@
#ifndef AGGIMAGEIO_H
#define AGGIMAGEIO_H
+#include "Renderers.h"
+
class RS_ByteData;
-class AGGImageIO
+// Exported so the unit tests can decode a rendered PNG. On Windows this class
+// is only reachable from outside Renderers.dll through RENDERERS_API; on other
+// platforms the macro expands to nothing and default visibility applies.
+class RENDERERS_API AGGImageIO
{
public:
static bool Save(const RS_String& filename, const RS_String& format,
Modified: branches/4.0/MgDev/Server/src/PostBuild/PostBuild.mak
===================================================================
--- branches/4.0/MgDev/Server/src/PostBuild/PostBuild.mak 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Server/src/PostBuild/PostBuild.mak 2026-10-23 11:25:59 UTC (rev 10225)
@@ -821,6 +821,7 @@
if EXIST "..\..\..\UnitTest\TestData\KmlService\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\KmlService\*.*" "..\..\bin\UnitTestFiles\"
if EXIST "..\..\..\UnitTest\TestData\MappingService\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\MappingService\*.*" "..\..\bin\UnitTestFiles\"
if EXIST "..\..\..\UnitTest\TestData\MdfModel\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\MdfModel\*.*" "..\..\bin\UnitTestFiles\"
+ if EXIST "..\..\..\UnitTest\TestData\RasterStylization\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\RasterStylization\*.*" "..\..\bin\UnitTestFiles\"
if EXIST "..\..\..\UnitTest\TestData\ResourceService\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\ResourceService\*.*" "..\..\bin\UnitTestFiles\"
if EXIST "..\..\..\UnitTest\TestData\ServerAdmin\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\ServerAdmin\*.*" "..\..\bin\UnitTestFiles\"
if EXIST "..\..\..\UnitTest\TestData\Symbology\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\Symbology\*.*" "..\..\bin\UnitTestFiles\"
Modified: branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-10-23 11:25:59 UTC (rev 10225)
@@ -20,6 +20,7 @@
${MG_SERVER_SERVICES_DIR}/Kml
${MG_SERVER_SERVICES_DIR}/Tile
${FDO_INCLUDE_DIR}
+ ${FDO_INCLUDE_DIR}/ExpressionEngine
)
set(MgUnitTesting_SRCS
@@ -32,6 +33,7 @@
TestLogManagerThread.cpp
TestMappingService.cpp
TestMdfModel.cpp
+ TestRasterStylization.cpp
TestMisc.cpp
TestPerformance.cpp
TestRenderingService.cpp
@@ -42,6 +44,7 @@
TestServiceManager.cpp
TestSiteManager.cpp
TestSiteService.cpp
+ TestStylization.cpp
TestTileService.cpp
TestProfilingService.cpp
TestTransformMesh.cpp
@@ -53,4 +56,6 @@
target_link_libraries(MgUnitTesting
${CPPUNIT_LIBRARIES}
+ MgStylization${MG_VERSION_SUFFIX}
+ MgMdfModel${MG_VERSION_SUFFIX}
)
\ No newline at end of file
Added: branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp (rev 0)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp 2026-10-23 11:25:59 UTC (rev 10225)
@@ -0,0 +1,657 @@
+//
+// Copyright (C) 2004-2026 by Autodesk, Inc.
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of version 2.1 of the GNU Lesser
+// General Public License as published by the Free Software Foundation.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+// End to end raster rendering tests.
+//
+// There are two halves. The first renders the synthetic fixtures below, whose
+// colours are known exactly, so every assertion is on a specific pixel value.
+// The second renders a handful of real rasters taken from the FDO GDAL provider
+// test data, writes the result to ../UnitTestFiles/Results/ and only sanity
+// checks it - real imagery has no expected colour to assert, so it is there for
+// human visual inspection. Both halves use the same pipeline and layer style.
+//
+// Each fixture is a 64x64 GeoTIFF generated by
+// UnitTest/TestData/RasterStylization/make_fixtures.py, split into four 32x32
+// quadrants of one colour. Their provenance and checksums are recorded in
+// UnitTest/TestData/RasterStylization/PROVENANCE.md. CMake copies TestData
+// flat into ../UnitTestFiles.
+// The test publishes a GDAL feature source, a grid layer definition whose single
+// GridColorBand rule passes the band through as the pixel colour, and a map
+// definition; renders the map with MgRenderingService::RenderMap; decodes the
+// PNG; and checks the colour at the centre of each quadrant.
+//
+// That exercises the whole raster path - OSGeo.GDAL -> MgRaster -> RSMgRaster ->
+// RasterAdapter -> GridData::ReadRaster -> Band -> GridStylizer -> AGG renderer -
+// so the band code paths that GridData::ReadRaster and Band implement are
+// covered without a unit test stub.
+//
+// The GDAL provider reports the raster data model from the band layout
+// (FdoRfpBandRaster::GetDataModel), so the fixtures map onto ReadRaster's
+// branches like this:
+//
+// file bands/type model bpp grid band ReadRaster branch
+// rgb24.tif 3 x Byte RGB 24 UnsignedInt32 "24 bpp"
+// rgba32.tif 4 x Byte (+alpha) RGBA 32 UnsignedInt32 else ("32 bpp")
+// rgb48.tif 3 x UInt16 RGB 48 UnsignedInt32 "48 bpp"
+// rgb48_int16.tif 3 x Int16 RGB 48 UnsignedInt32 "48 bpp"
+// palette8.tif 1 x Byte + table Palette 8 UnsignedInt32 palette (>= 8)
+// gray8.tif 1 x Byte Gray 8 UnsignedInt8 else (SetRowValue)
+// gray16.tif 1 x UInt16 Gray 16 UnsignedInt16 else (SetRowValue)
+// gray32i.tif 1 x Int32 Gray 32 UnsignedInt32 else (SetRowValue)
+// gray32f.tif 1 x Float32 Gray 32 UnsignedInt32 else (SetRowValue)
+// gray64f.tif 1 x Float64 Gray 64 UnsignedInt64 else (SetRowValue)
+//
+// "grid band" is what GridData::GetGridDataType derives from the bits per pixel
+// and the FdoRasterDataType the provider reports. The GDAL provider only
+// reports UnsignedInteger, so the signed/float band types (Int32, Double32,
+// Double64) and the FdoRasterDataModelType_Data branch cannot be reached from a
+// file at all - the unit tests in TestStylization.cpp cover those directly.
+//
+// 64 bpp RGBA is deliberately not covered: ReadRaster's "else" branch writes a
+// 32 bpp row into the 64 bpp band, and the band's fill value is taken from a
+// 4 byte local while the band is 8 bytes wide, so what it renders depends on
+// uninitialised stack bytes.
+//
+// An UnsignedInt32/Int32 grid band is taken by the memcpy fast path in
+// GridStyleColorHandler::Visit, which copies the raw band bytes into the ARGB
+// colour band. For the 24/48 bpp and palette branches the band already holds
+// packed opaque ARGB, so the copy reproduces the file exactly; for the single
+// band fixtures the raw samples are read as ARGB (alpha included), which is why
+// their expectations below are bit patterns rather than shades of grey. Narrow
+// (8/16 bit) and 64 bit bands take the per pixel path instead.
+//
+
+#include "MapGuideCommon.h"
+#include "ServiceManager.h"
+#include "ServerSiteService.h"
+#include "Services/FeatureService.h"
+#include "Fdo.h"
+#include "ServerFeatureService.h"
+#include "FoundationDefs.h"
+#include "TestServiceFactory.h"
+#include "FdoConnectionManager.h"
+#include "CatchHelperMacros.h"
+#include "RendererStyles.h"
+#include "AGGImageIO.h"
+#include "catch.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+
+// The map definition coordinate system. The fixtures are written with the same
+// WGS84 geographic CS, so the raster is drawn without a datum transform.
+const STRING LL84_WKT = L"GEOGCS[\"LL84\",DATUM[\"WGS84\",SPHEROID[\"WGS84\",6378137.000,298.25722293]],"
+ L"PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.017453292519943295]]";
+
+// Fixture size in pixels, and the sample points (centre of each quadrant).
+const int FIXTURE_SIZE = 64;
+const int QUADRANT_CENTRE_X[] = { 16, 48, 16, 48 };
+const int QUADRANT_CENTRE_Y[] = { 16, 16, 48, 48 };
+
+struct Fixture
+{
+ const char* file;
+ const char* note;
+ unsigned int rgb[4]; // expected 0x00RRGGBB, top-left, top-right, bottom-left, bottom-right
+};
+
+const Fixture FIXTURES[] =
+{
+ // These fixtures already render correctly from unmodified source on both
+ // Windows and Linux: they only use code paths that are shared between the
+ // platforms (SetRowValue plus the colour handler's memcpy fast path), so
+ // they define the behavioural baseline the band fixes must not disturb.
+ //
+ // The 24/48 bpp RGB and palette fixtures are deliberately absent: their
+ // bands are only filled by the 32 bit Band::SetValue path, which is a no-op
+ // on Linux before the fixes. They are added, with their expected colours,
+ // in the change that fixes those paths.
+
+ // The 32 bpp branch copies the source pixels through unchanged, so the
+ // source alpha survives and the renderer blends the colour over the white
+ // background: opaque pixels are exact, translucent and transparent ones are
+ // not. The quadrants carry alpha 0x80, 0xFF, 0x00 and 0x40.
+ { "RasterStylization_rgba32.tif", "32 bpp RGBA, alpha blended over the background",
+ { 0x87A9CB, 0xFF0000, 0xFFFFFF, 0xDECEC2 } },
+
+ // A narrow single band takes the per pixel path in GridColorBandHandler:
+ // the sample becomes the packed ARGB value with the alpha forced opaque, so
+ // the pixel shows the low 24 bits of the sample.
+ { "RasterStylization_gray8.tif", "8 bpp single band",
+ { 0x000012, 0x000034, 0x000056, 0x000078 } },
+ { "RasterStylization_gray16.tif", "16 bpp single band",
+ { 0x001234, 0x005678, 0x009ABC, 0x000100 } },
+ // A 32 bpp band is copied as raw bytes, so each sample is read as ARGB:
+ // 0x00123456/0x00234567/0x00345678/0x00456789 all have a zero alpha and
+ // therefore draw nothing.
+ { "RasterStylization_gray32i.tif", "32 bpp integer single band, samples read as ARGB",
+ { 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF } },
+ // Same raw byte copy, so the float bit patterns of 4660/22136/39612/256
+ // become ARGB and are blended over the background with the alpha byte the
+ // pattern happens to contain (0x45, 0x46, 0x46, 0x40).
+ { "RasterStylization_gray32f.tif", "32 bpp float single band, samples read as ARGB",
+ { 0xE1E5B9, 0xE8FAB8, 0xBFECB7, 0xDDBBBB } },
+ // Doubles are read as unsigned 64 bit samples, which is far outside the
+ // range a packed ARGB value can hold, so no pixel is coloured.
+ { "RasterStylization_gray64f.tif", "64 bpp float single band, not styled",
+ { 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF } },
+};
+
+// Real rasters derived from the FDO GDAL provider test data
+// (fdo/Providers/GDAL/TestData) - see PROVENANCE.md in
+// UnitTest/TestData/RasterStylization for where each one came from and how it
+// was produced.
+//
+// Every one of them is 64-bit row aligned, because ReadRaster is called with
+// bAlignment = true and returns without reading when the provider's stream size
+// does not match the padded row length. A raster whose width * bpp is not a
+// multiple of 64 falls through to RasterAdapter's legacy decoder instead, which
+// is not the code under test here - and which has no 16 bpp case at all.
+//
+// The palette sheet is absent for the same reason as the palette fixture above:
+// its colour band is only filled through the 32 bit Band::SetValue path, so it
+// renders blank from unmodified source on Linux and is added with the fixes.
+//
+// Nothing here has an expected colour: the point is that genuine imagery and
+// elevation data survive the pipeline unaltered, so each fixture is rendered to
+// ../UnitTestFiles/Results/RealRaster_.png for a human to look at, and the
+// assertions only check that the image was produced, that it is the requested
+// size, and that it has real variation in it rather than being a flat fill.
+//
+// LeakSanitizer note: this test case reports allocations from
+// MdfParser::IOGridColorBands::StartElement (three ChannelBand per
+// colour, leaked by that parser) and from MgGeometryUtil::ComputeLineStringEnvelope
+// (the raster bounds envelope). Both are in MapGuide code this change does not
+// touch; the synthetic fixtures above, which use a plain colour, stay
+// leak free.
+
+// How the grid colour style reads the raster. Band 1 is the packed ARGB value
+// that the RGB and palette branches of GridData::ReadRaster build; a single
+// band raster has to be mapped through all three channels to stay grey.
+enum RealStyle
+{
+ StyleBand, // GridColorBand - the band already holds a packed colour
+ StyleGrayBands // GridColorBands - band 1 drives red, green and blue
+};
+
+struct RealFixture
+{
+ const char* file;
+ const char* note;
+ RealStyle style;
+ double lowBand; // channel scaling, only used by StyleGrayBands
+ double highBand;
+};
+
+const RealFixture REAL_FIXTURES[] =
+{
+ { "RealRaster_ae.tif", "8 bit grey DRG tile, 60 m, custom projection",
+ StyleGrayBands, 0, 0 },
+ { "RealRaster_spif83.tif", "8 bit grey, NAD83 / California zone 6, ~195 m",
+ StyleGrayBands, 0, 0 },
+ // The DEM holds metres (nodata is -32767), so it needs scaling to become a
+ // visible grey ramp; 0-3000 m covers the whole tile.
+ { "RealRaster_mtjefferson.tif", "Int16 USGS DEM as a 0-3000 m elevation ramp",
+ StyleGrayBands, 0, 3000 },
+};
+
+// Long edge of the rendered image. Rasters larger than this are downsampled,
+// which also exercises the resampling the provider does when the requested size
+// does not match the file. The view is set to the raster's own extent, so the
+// raster columns the grid covers stay equal to the file's width and the aligned
+// row length above still holds.
+const int REAL_RENDER_MAX_DIM = 800;
+
+std::string Narrow(CREFSTRING value)
+{
+ return MgUtil::WideCharToMultiByte(value);
+}
+
+// Absolute path of a fixture, resolved from the test working directory
+// (/Server/src/Core) where ../UnitTestFiles is the copied test data.
+STRING FixturePath(const char* file)
+{
+ char cwd[1024] = { 0 };
+ if (NULL == ACE_OS::getcwd(cwd, sizeof(cwd)))
+ return L"";
+
+ // UnitTest/TestData is copied flat into ../UnitTestFiles by CMake. Use the
+ // std::string overload so the conversion result is owned by the STRING.
+ STRING path = MgUtil::MultiByteToWideChar(std::string(cwd));
+ path += L"/../UnitTestFiles/";
+ path += MgUtil::MultiByteToWideChar(std::string(file));
+ return path;
+}
+
+void PublishResource(MgResourceService* svcResource, CREFSTRING resourceId, const std::string& xml)
+{
+ Ptr res = new MgResourceIdentifier(resourceId);
+ Ptr src = new MgByteSource((unsigned char*)xml.data(), (INT32)xml.size());
+ Ptr content = src->GetReader();
+ svcResource->SetResource(res, content, NULL);
+}
+
+std::string FeatureSourceXml(const std::string& filePath)
+{
+ std::ostringstream xml;
+ xml << ""
+ << ""
+ // No version suffix: FDO resolves an unversioned provider name to the
+ // highest registered version, so this survives an FDO 4.x -> 5.x bump.
+ << "OSGeo.Gdal"
+ << "DefaultRasterFileLocation"
+ << "" << filePath << ""
+ << "";
+ return xml.str();
+}
+
+// A grid layer whose single colour rule uses band 1 as the pixel colour, so the
+// rendered image shows exactly what GridData::ReadRaster put in the band.
+// The GridLayerDefinition "Geometry" element is the raster property name: the
+// server builds a RESAMPLE computed property from it (see
+// MgMappingUtil::ExecuteRasterQuery).
+std::string LayerXml(CREFSTRING featureSourceId, CREFSTRING featureName, CREFSTRING rasterProperty,
+ RealStyle style = StyleBand, double lowBand = 0, double highBand = 0)
+{
+ std::ostringstream xml;
+ xml << ""
+ << ""
+ << ""
+ << "" << Narrow(featureSourceId) << ""
+ << "" << Narrow(featureName) << ""
+ << "" << Narrow(rasterProperty) << ""
+ << ""
+ << "0"
+ << "100000000000"
+ << "";
+ switch (style)
+ {
+ case StyleGrayBands:
+ {
+ // A single band raster has to drive all three channels to stay grey.
+ // LowBand/HighBand scale elevation metres down to the 0-255 a channel
+ // can show; omitting them uses the 8 bit samples as they are.
+ xml.precision(17);
+ std::ostringstream scale;
+ if (highBand > lowBand)
+ scale << "" << lowBand << "" << highBand << "";
+ xml << "Band 1"
+ << ""
+ << "1" << scale.str() << ""
+ << "1" << scale.str() << ""
+ << "1" << scale.str() << ""
+ << ""
+ << "";
+ break;
+ }
+ default:
+ xml << "Band 1"
+ << "1"
+ << "";
+ break;
+ }
+ xml << ""
+ << "1"
+ << ""
+ << ""
+ << "";
+ return xml.str();
+}
+
+std::string MapXml(CREFSTRING layerId, CREFSTRING csWkt = LL84_WKT,
+ double minX = 0, double minY = 0, double maxX = FIXTURE_SIZE, double maxY = FIXTURE_SIZE)
+{
+ std::ostringstream xml;
+ xml.precision(17);
+ xml << ""
+ << ""
+ << "RasterStylization"
+ << "" << Narrow(csWkt) << ""
+ << "" << minX << "" << maxX << ""
+ << "" << minY << "" << maxY << ""
+ << "FFFFFFFF"
+ << ""
+ << "Raster"
+ << "" << Narrow(layerId) << ""
+ << "false"
+ << "false"
+ << "Raster"
+ << "false"
+ << "true"
+ << ""
+ << ""
+ << "";
+ return xml.str();
+}
+
+// Publishes the feature source and returns "schema:class" for the grid layer,
+// plus the name of the raster property.
+STRING PublishFeatureSource(MgResourceService* svcResource,
+ MgFeatureService* svcFeature,
+ CREFSTRING featureSourceId,
+ const std::string& filePath,
+ REFSTRING rasterProperty)
+{
+ PublishResource(svcResource, featureSourceId, FeatureSourceXml(filePath));
+
+ Ptr fsRes = new MgResourceIdentifier(featureSourceId);
+ Ptr schemas = svcFeature->DescribeSchema(fsRes, L"", NULL);
+ REQUIRE(schemas->GetCount() > 0);
+
+ Ptr schema = schemas->GetItem(0);
+ Ptr klass = schema->GetClasses()->GetItem(0);
+
+ Ptr properties = klass->GetProperties();
+ for (INT32 i = 0; i < properties->GetCount() && rasterProperty.empty(); ++i)
+ {
+ Ptr property = properties->GetItem(i);
+ if (property->GetPropertyType() == MgFeaturePropertyType::RasterProperty)
+ rasterProperty = property->GetName();
+ }
+ REQUIRE(!rasterProperty.empty());
+
+ STRING featureName = schema->GetName();
+ featureName += L":";
+ featureName += klass->GetName();
+ return featureName;
+}
+
+// Renders the map and returns the decoded image; the caller owns the buffer.
+// If saveTo is not empty the PNG is also written there.
+unsigned int* RenderPng(MgRenderingService* svcRendering, MgMap* map, int& width, int& height,
+ CREFSTRING saveTo = L"")
+{
+ Ptr png = svcRendering->RenderMap(map, NULL, L"PNG");
+ REQUIRE(png.p != NULL);
+
+ if (!saveTo.empty())
+ {
+ // ../UnitTestFiles/Results is created by the test run's setup hook.
+ STRING path = L"../UnitTestFiles/Results/";
+ path += saveTo;
+ png->ToFile(path);
+ }
+
+ Ptr sink = new MgByteSink(png);
+ Ptr bytes = sink->ToBuffer();
+ REQUIRE(bytes->GetLength() > 0);
+
+ return AGGImageIO::DecodePNG(bytes->Bytes(), (size_t)bytes->GetLength(), width, height);
+}
+
+// Absolute path of a result image, for the message that tells a human where to
+// look after the run.
+STRING ResultPath(CREFSTRING fileName)
+{
+ char cwd[1024] = { 0 };
+ if (NULL == ACE_OS::getcwd(cwd, sizeof(cwd)))
+ return fileName;
+
+ STRING path = MgUtil::MultiByteToWideChar(std::string(cwd));
+ path += L"/../UnitTestFiles/Results/";
+ path += fileName;
+ return path;
+}
+
+// Extent and pixel size of the raster in a feature source, taken from the
+// provider through a plain feature select of the raster property.
+bool GetRasterFootprint(MgFeatureService* svcFeature, CREFSTRING featureSourceId,
+ CREFSTRING featureName, CREFSTRING rasterProperty,
+ Ptr& extent, INT32& imageXSize, INT32& imageYSize)
+{
+ bool ok = false;
+
+ Ptr fsRes = new MgResourceIdentifier(featureSourceId);
+ Ptr options = new MgFeatureQueryOptions();
+ options->AddFeatureProperty(rasterProperty);
+ Ptr reader = svcFeature->SelectFeatures(fsRes, featureName, options);
+ if (reader.p != NULL && reader->ReadNext())
+ {
+ Ptr raster = reader->GetRaster(rasterProperty);
+ if (raster.p != NULL)
+ {
+ extent = raster->GetBounds();
+ imageXSize = raster->GetImageXSize();
+ imageYSize = raster->GetImageYSize();
+ ok = (extent.p != NULL && imageXSize > 0 && imageYSize > 0);
+ }
+ reader->Close();
+ }
+ return ok;
+}
+
+// The coordinate system the provider exposes for a raster feature source, or an
+// empty string when the file carries no georeferencing.
+STRING GetRasterCoordinateSystem(MgFeatureService* svcFeature, CREFSTRING featureSourceId)
+{
+ STRING wkt;
+
+ Ptr fsRes = new MgResourceIdentifier(featureSourceId);
+ Ptr reader = svcFeature->GetSpatialContexts(fsRes, false);
+ if (reader.p != NULL && reader->ReadNext())
+ wkt = reader->GetCoordinateSystemWkt();
+ if (reader.p != NULL)
+ reader->Close();
+
+ return wkt;
+}
+
+}
+
+TEST_CASE("Raster grid styling", "[RasterStylization]")
+{
+ Ptr svcSite = TestServiceFactory::CreateSiteService();
+ Ptr siteConn = TestServiceFactory::CreateSiteConnection(svcSite);
+ Ptr svcResource = TestServiceFactory::CreateResourceService();
+ Ptr svcRendering = TestServiceFactory::CreateRenderingService();
+ Ptr svcFeature = dynamic_cast(
+ MgServiceManager::GetInstance()->RequestService(MgServiceType::FeatureService));
+
+ for (const Fixture& fixture : FIXTURES)
+ {
+ INFO("fixture: " << fixture.file << " (" << fixture.note << ")");
+
+ STRING name = MgUtil::MultiByteToWideChar(std::string(fixture.file));
+ STRING::size_type dot = name.find_last_of(L'.');
+ if (dot != STRING::npos)
+ name = name.substr(0, dot);
+
+ STRING fsId = L"Library://RasterStylization/Data/" + name + L".FeatureSource";
+ STRING layerId = L"Library://RasterStylization/Layers/" + name + L".LayerDefinition";
+ STRING mapId = L"Library://RasterStylization/Maps/" + name + L".MapDefinition";
+
+ STRING rasterProperty;
+ STRING featureName = PublishFeatureSource(svcResource, svcFeature, fsId,
+ Narrow(FixturePath(fixture.file)),
+ rasterProperty);
+ PublishResource(svcResource, layerId, LayerXml(fsId, featureName, rasterProperty));
+ PublishResource(svcResource, mapId, MapXml(layerId));
+
+ Ptr mapRes = new MgResourceIdentifier(mapId);
+ Ptr map = new MgMap(siteConn);
+ map->Create(mapRes, L"RasterStylizationMap");
+
+ // 1 raster pixel == 1 image pixel. The renderer derives pixels per map
+ // unit from metersPerUnit * dpi / (METERS_PER_INCH * scale).
+ const double dpi = 96.0;
+ const double metersPerInch = 0.0254;
+ Ptr centre = new MgCoordinateXY(FIXTURE_SIZE / 2.0, FIXTURE_SIZE / 2.0);
+ Ptr centrePoint = new MgPoint(centre);
+ map->SetViewCenter(centrePoint);
+ map->SetDisplayDpi((INT32)dpi);
+ map->SetDisplayWidth(FIXTURE_SIZE);
+ map->SetDisplayHeight(FIXTURE_SIZE);
+ map->SetViewScale(map->GetMetersPerUnit() * dpi / metersPerInch);
+
+ int width = 0;
+ int height = 0;
+ unsigned int* rawPixels = RenderPng(svcRendering, map, width, height);
+ REQUIRE(rawPixels != NULL);
+ // DecodePNG hands back a buffer that has to be released with delete[].
+ std::unique_ptr pixels(reinterpret_cast(rawPixels));
+ REQUIRE(width == FIXTURE_SIZE);
+ REQUIRE(height == FIXTURE_SIZE);
+
+ for (int quadrant = 0; quadrant < 4; ++quadrant)
+ {
+ int x = QUADRANT_CENTRE_X[quadrant];
+ int y = QUADRANT_CENTRE_Y[quadrant];
+ // DecodePNG returns the image bottom up.
+ unsigned int argb = rawPixels[(height - 1 - y) * width + x];
+ unsigned int rgb = argb & 0x00FFFFFF;
+ INFO("quadrant " << quadrant << " at (" << x << "," << y << ") got 0x"
+ << std::hex << rgb << " expected 0x" << fixture.rgb[quadrant]);
+ CHECK(rgb == fixture.rgb[quadrant]);
+ }
+
+ // Same reason as in the real raster case: release this fixture's pooled
+ // connection now rather than leave it to be unwound at server shutdown.
+ MgFdoConnectionManager::GetInstance()->ClearCache();
+ }
+}
+
+TEST_CASE("Real raster rendering", "[RasterStylization]")
+{
+ Ptr svcSite = TestServiceFactory::CreateSiteService();
+ Ptr siteConn = TestServiceFactory::CreateSiteConnection(svcSite);
+ Ptr svcResource = TestServiceFactory::CreateResourceService();
+ Ptr svcRendering = TestServiceFactory::CreateRenderingService();
+ Ptr svcFeature = dynamic_cast(
+ MgServiceManager::GetInstance()->RequestService(MgServiceType::FeatureService));
+
+ const double dpi = 96.0;
+ const double metersPerInch = 0.0254;
+
+ for (const RealFixture& fixture : REAL_FIXTURES)
+ {
+ INFO("real fixture: " << fixture.file << " (" << fixture.note << ")");
+
+ STRING name = MgUtil::MultiByteToWideChar(std::string(fixture.file));
+ STRING::size_type dot = name.find_last_of(L'.');
+ if (dot != STRING::npos)
+ name = name.substr(0, dot);
+
+ STRING fsId = L"Library://RasterStylizationReal/Data/" + name + L".FeatureSource";
+ STRING layerId = L"Library://RasterStylizationReal/Layers/" + name + L".LayerDefinition";
+ STRING mapId = L"Library://RasterStylizationReal/Maps/" + name + L".MapDefinition";
+
+ STRING rasterProperty;
+ STRING featureName = PublishFeatureSource(svcResource, svcFeature, fsId,
+ Narrow(FixturePath(fixture.file)),
+ rasterProperty);
+
+ // The raster's own footprint drives the map view, so the image shows the
+ // whole file with one image pixel per raster pixel wherever possible.
+ Ptr rasterExtent;
+ INT32 imageXSize = 0;
+ INT32 imageYSize = 0;
+ REQUIRE(GetRasterFootprint(svcFeature, fsId, featureName, rasterProperty,
+ rasterExtent, imageXSize, imageYSize));
+
+ double minX = rasterExtent->GetLowerLeftCoordinate()->GetX();
+ double minY = rasterExtent->GetLowerLeftCoordinate()->GetY();
+ double maxX = rasterExtent->GetUpperRightCoordinate()->GetX();
+ double maxY = rasterExtent->GetUpperRightCoordinate()->GetY();
+
+ int factor = (std::max)(imageXSize, imageYSize) / REAL_RENDER_MAX_DIM;
+ if (factor < 1)
+ factor = 1;
+ int renderWidth = imageXSize / factor;
+ int renderHeight = imageYSize / factor;
+ if (renderWidth < 1) renderWidth = 1;
+ if (renderHeight < 1) renderHeight = 1;
+
+ // Rasters written without an SRS have no coordinate system to adopt, so
+ // the map falls back to LL84 and the file's own units become map units.
+ STRING csWkt = GetRasterCoordinateSystem(svcFeature, fsId);
+ if (csWkt.empty())
+ csWkt = LL84_WKT;
+
+ PublishResource(svcResource, layerId,
+ LayerXml(fsId, featureName, rasterProperty, fixture.style,
+ fixture.lowBand, fixture.highBand));
+ PublishResource(svcResource, mapId,
+ MapXml(layerId, csWkt, minX, minY, maxX, maxY));
+
+ Ptr mapRes = new MgResourceIdentifier(mapId);
+ Ptr map = new MgMap(siteConn);
+ map->Create(mapRes, L"RasterStylizationMap");
+
+ Ptr centre = new MgCoordinateXY((minX + maxX) / 2.0, (minY + maxY) / 2.0);
+ Ptr centrePoint = new MgPoint(centre);
+ map->SetViewCenter(centrePoint);
+ map->SetDisplayDpi((INT32)dpi);
+ map->SetDisplayWidth(renderWidth);
+ map->SetDisplayHeight(renderHeight);
+ // One image pixel per (imageXSize / renderWidth) raster pixels.
+ map->SetViewScale(((maxX - minX) / renderWidth) * map->GetMetersPerUnit() * dpi / metersPerInch);
+
+ STRING resultFile = name + L".png";
+ int width = 0;
+ int height = 0;
+ unsigned int* rawPixels = RenderPng(svcRendering, map, width, height, resultFile);
+ REQUIRE(rawPixels != NULL);
+ std::unique_ptr pixels(reinterpret_cast(rawPixels));
+ REQUIRE(width == renderWidth);
+ REQUIRE(height == renderHeight);
+
+ ACE_DEBUG((LM_INFO, ACE_TEXT(" wrote %W (%dx%d from %dx%d)\n"),
+ ResultPath(resultFile).c_str(), width, height, imageXSize, imageYSize));
+
+ // Sanity checks only, since there is no expected image to compare
+ // against. Real imagery has hundreds of colours and a wide spread of
+ // luminance; a raster that failed to read produces a flat fill.
+ std::set colours;
+ double sum = 0.0;
+ double sumSquares = 0.0;
+ for (int i = 0; i < width * height; ++i)
+ {
+ unsigned int rgb = rawPixels[i] & 0x00FFFFFF;
+ colours.insert(rgb);
+ double luminance = 0.299 * ((rgb >> 16) & 0xFF)
+ + 0.587 * ((rgb >> 8) & 0xFF)
+ + 0.114 * (rgb & 0xFF);
+ sum += luminance;
+ sumSquares += luminance * luminance;
+ }
+ const double pixelCount = (double)width * height;
+ const double mean = sum / pixelCount;
+ const double deviation = sqrt((std::max)(0.0, sumSquares / pixelCount - mean * mean));
+
+ INFO("rendered " << colours.size() << " distinct colours, luminance mean "
+ << mean << " stddev " << deviation);
+ CHECK(colours.size() >= 16);
+ CHECK(deviation >= 2.0);
+
+ // Release the pooled raster connection now. Leaving a dozen of them to
+ // be unwound when the server shuts down intermittently segfaults or
+ // hangs after the run has finished (pre-existing teardown behaviour,
+ // reproducible with this suite alone but not with one of its cases).
+ MgFdoConnectionManager::GetInstance()->ClearCache();
+ }
+}
Added: branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp (rev 0)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp 2026-10-23 11:25:59 UTC (rev 10225)
@@ -0,0 +1,324 @@
+//
+// Copyright (C) 2004-2026 by Autodesk, Inc.
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of version 2.1 of the GNU Lesser
+// General Public License as published by the Free Software Foundation.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+#include "GridData.h"
+#include "Band.h"
+#include "Bounds.h"
+#include "Point2D.h"
+#include "RS_Raster.h"
+#include "RS_InputStream.h"
+#include "catch.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace
+{
+
+// Minimal RS_InputStream over a fixed byte buffer.
+class TestRasterStream : public RS_InputStream
+{
+public:
+ explicit TestRasterStream(const std::vector& bytes)
+ : m_bytes(bytes), m_pos(0)
+ {
+ }
+
+ size_t available() const { return m_bytes.size() - m_pos; }
+
+ size_t read(void* pBuffer, size_t nBytesToRead)
+ {
+ size_t n = (nBytesToRead < available())? nBytesToRead : available();
+ if (n > 0)
+ memcpy(pBuffer, &m_bytes[m_pos], n);
+ m_pos += n;
+ return n;
+ }
+
+ off_t seek(int eOrigin, off_t nOffset)
+ {
+ off_t old = (off_t)m_pos;
+ off_t base = (SEEK_CUR == eOrigin)? old : (SEEK_END == eOrigin)? (off_t)m_bytes.size() : 0;
+ m_pos = (size_t)(base + nOffset);
+ return old;
+ }
+
+private:
+ std::vector m_bytes;
+ size_t m_pos;
+};
+
+// An RS_Raster over a fixed buffer, laid out the way OSGeo.GDAL hands a raster
+// to GridData::ReadRaster: pixel interleaved, RGB band order, and samples in
+// the native (host) byte order because the provider asks GDAL for the source
+// data type with GDALDatasetRasterIO.
+class TestRaster : public RS_Raster
+{
+public:
+ TestRaster(int bpp,
+ int dataModelType,
+ int dataType,
+ const std::vector& bytes,
+ const std::vector& palette,
+ unsigned long cols,
+ unsigned long rows,
+ int nullValueType = 0,
+ int64_t nullValueData = 0)
+ : m_bpp(bpp),
+ m_dataModelType(dataModelType),
+ m_dataType(dataType),
+ m_bytes(bytes),
+ m_palette(palette),
+ m_cols(cols),
+ m_rows(rows),
+ m_nullValueType(nullValueType),
+ m_nullValueData(nullValueData)
+ {
+ }
+
+ RS_Bounds GetExtent()
+ {
+ return RS_Bounds(0.0, 0.0, (double)m_cols, (double)m_rows);
+ }
+
+ int GetOriginalWidth() { return (int)m_cols; }
+ int GetOriginalHeight() { return (int)m_rows; }
+ int GetBitsPerPixel() { return m_bpp; }
+ int GetDataModelType() { return m_dataModelType; }
+ int GetDataType() { return m_dataType; }
+ int GetNullValueType() { return m_nullValueType; }
+ int64_t GetNullValueData() { return m_nullValueData; }
+ RS_String GetVerticalUnits() { return RS_String(); }
+
+ RS_InputStream* GetPalette()
+ {
+ return m_palette.empty()? NULL : new TestRasterStream(m_palette);
+ }
+
+ RS_InputStream* GetStream(RS_ImageFormat, int, int)
+ {
+ return new TestRasterStream(m_bytes);
+ }
+
+private:
+ int m_bpp;
+ int m_dataModelType;
+ int m_dataType;
+ std::vector m_bytes;
+ std::vector m_palette;
+ unsigned long m_cols;
+ unsigned long m_rows;
+ int m_nullValueType;
+ int64_t m_nullValueData;
+};
+
+std::vector RepeatPixel(const unsigned char* pixel, size_t size, size_t count)
+{
+ std::vector bytes;
+ bytes.reserve(size * count);
+ for (size_t i = 0; i < count; ++i)
+ bytes.insert(bytes.end(), pixel, pixel + size);
+ return bytes;
+}
+
+// Runs a raster through GridData::ReadRaster and reads back pixel (0, 0) of the
+// colour band, returning false when ReadRaster did not create one.
+bool ReadColourBandPixel(const std::vector& bytes,
+ int bpp,
+ int dataModelType,
+ unsigned long cols,
+ unsigned long rows,
+ bool alignment,
+ const std::vector& palette,
+ unsigned int& color)
+{
+ GridData grid(Point2D(0.0, 0.0), (double)cols, (double)rows, (unsigned int)cols, (unsigned int)rows);
+ TestRaster raster(bpp, dataModelType, FdoRasterDataType_UnsignedInteger, bytes, palette, cols, rows);
+
+ grid.ReadRaster(&raster, L"1", cols, rows, 0.0, 0.0, (double)cols, (double)rows, alignment);
+
+ Band* band = grid.GetBand((unsigned int)0);
+ if (NULL == band)
+ return false;
+
+ return band->GetValue(0, 0, Band::UnsignedInt32, &color);
+}
+
+// The read path rounds each row up to a 64 bit boundary when it is told the
+// provider pads its rows, and gives up when the stream is shorter than that.
+// This is behavioural baseline: it holds on Windows and Linux, before and after
+// the band accessor work.
+TEST_CASE("Grid colour band row alignment", "[Stylization]")
+{
+ const std::vector noPalette;
+ unsigned int color = 0;
+
+ SECTION("48 bpp RGB with a row length the provider does not pad produces no band")
+ {
+ // 6 pixels * 6 bytes = 36 bytes per row, but bAlignment rounds the
+ // buffer GridData expects up to 40 bytes, so it bails out.
+ const unsigned char pixel[] = { 0x34, 0x12, 0x78, 0x56, 0xBC, 0x9A };
+
+ REQUIRE_FALSE(ReadColourBandPixel(RepeatPixel(pixel, 6, 24), 48, FdoRasterDataModelType_RGB,
+ 6, 4, true, noPalette, color));
+ }
+}
+
+// Band creation and the narrow (8 and 16 bit) accessors are implemented the
+// same way on both platforms, so they are baseline behaviour. The 32 and 64 bit
+// accessors are covered by the change that implements them on Linux.
+TEST_CASE("Band creation", "[Stylization]")
+{
+ GridData grid(Point2D(0.0, 0.0), 3.0, 3.0, 3, 3);
+
+ SECTION("UnsignedInt32") { REQUIRE(NULL != grid.CreateBand(Band::UnsignedInt32, L"u32")); }
+ SECTION("Int32") { REQUIRE(NULL != grid.CreateBand(Band::Int32, L"i32")); }
+ SECTION("Double32") { REQUIRE(NULL != grid.CreateBand(Band::Double32, L"f32")); }
+ SECTION("UnsignedInt64") { REQUIRE(NULL != grid.CreateBand(Band::UnsignedInt64, L"u64")); }
+ SECTION("Int64") { REQUIRE(NULL != grid.CreateBand(Band::Int64, L"i64")); }
+ SECTION("Double64") { REQUIRE(NULL != grid.CreateBand(Band::Double64, L"f64")); }
+ SECTION("Boolean") { REQUIRE(NULL != grid.CreateBand(Band::Boolean, L"b")); }
+ SECTION("Bit2") { REQUIRE(NULL != grid.CreateBand(Band::Bit2, L"b2")); }
+ SECTION("Bit4") { REQUIRE(NULL != grid.CreateBand(Band::Bit4, L"b4")); }
+}
+
+TEST_CASE("Band numeric accessors for narrow types", "[Stylization]")
+{
+ GridData grid(Point2D(0.0, 0.0), 3.0, 3.0, 3, 3);
+
+ SECTION("UnsignedInt8")
+ {
+ Band* band = grid.CreateBand(Band::UnsignedInt8, L"u8");
+ REQUIRE(NULL != band);
+
+ INT8 value = 100;
+ REQUIRE(band->SetValue(1, 1, Band::UnsignedInt8, &value));
+
+ INT8 readBack = 0;
+ REQUIRE(band->GetValue(1, 1, Band::UnsignedInt8, &readBack));
+ REQUIRE(value == readBack);
+ REQUIRE(100.0 == band->GetValueAsDouble(1, 1));
+ }
+
+ SECTION("UnsignedInt8 above 0x7F reads back negative")
+ {
+ // Pre-existing behaviour, recorded so a change to it is visible: the
+ // unsigned 8 bit case in Band::GetValueAsDouble loads the sample into an
+ // INT8, so anything from 0x80 up is sign extended. This is why paletted
+ // and grey rasters with high sample values behave oddly.
+ Band* band = grid.CreateBand(Band::UnsignedInt8, L"u8high");
+ REQUIRE(NULL != band);
+
+ INT8 value = (INT8)200;
+ REQUIRE(band->SetValue(1, 1, Band::UnsignedInt8, &value));
+
+ INT8 readBack = 0;
+ REQUIRE(band->GetValue(1, 1, Band::UnsignedInt8, &readBack));
+ REQUIRE(value == readBack);
+ REQUIRE(-56.0 == band->GetValueAsDouble(1, 1));
+ }
+
+ SECTION("Int8")
+ {
+ Band* band = grid.CreateBand(Band::Int8, L"i8");
+ REQUIRE(NULL != band);
+
+ INT8 value = -100;
+ REQUIRE(band->SetValue(2, 0, Band::Int8, &value));
+
+ INT8 readBack = 0;
+ REQUIRE(band->GetValue(2, 0, Band::Int8, &readBack));
+ REQUIRE(value == readBack);
+ REQUIRE(-100.0 == band->GetValueAsDouble(2, 0));
+ }
+
+ SECTION("UnsignedInt16")
+ {
+ Band* band = grid.CreateBand(Band::UnsignedInt16, L"u16");
+ REQUIRE(NULL != band);
+
+ INT16 value = (INT16)60000;
+ REQUIRE(band->SetValue(0, 2, Band::UnsignedInt16, &value));
+
+ INT16 readBack = 0;
+ REQUIRE(band->GetValue(0, 2, Band::UnsignedInt16, &readBack));
+ REQUIRE(value == readBack);
+ REQUIRE(60000.0 == band->GetValueAsDouble(0, 2));
+ }
+
+ SECTION("Int16")
+ {
+ Band* band = grid.CreateBand(Band::Int16, L"i16");
+ REQUIRE(NULL != band);
+
+ INT16 value = -12345;
+ REQUIRE(band->SetValue(1, 0, Band::Int16, &value));
+
+ INT16 readBack = 0;
+ REQUIRE(band->GetValue(1, 0, Band::Int16, &readBack));
+ REQUIRE(value == readBack);
+ REQUIRE(-12345.0 == band->GetValueAsDouble(1, 0));
+ }
+
+ SECTION("Boolean")
+ {
+ Band* band = grid.CreateBand(Band::Boolean, L"bool");
+ REQUIRE(NULL != band);
+
+ INT8 value = 1;
+ REQUIRE(band->SetValue(0, 0, Band::Boolean, &value));
+
+ INT8 readBack = 0;
+ REQUIRE(band->GetValue(0, 0, Band::Boolean, &readBack));
+ REQUIRE(value == readBack);
+ REQUIRE(1.0 == band->GetValueAsDouble(0, 0));
+ }
+
+ SECTION("Bit2")
+ {
+ Band* band = grid.CreateBand(Band::Bit2, L"bit2");
+ REQUIRE(NULL != band);
+
+ INT8 value = 3;
+ REQUIRE(band->SetValue(2, 2, Band::Bit2, &value));
+
+ INT8 readBack = 0;
+ REQUIRE(band->GetValue(2, 2, Band::Bit2, &readBack));
+ REQUIRE(value == readBack);
+ REQUIRE(3.0 == band->GetValueAsDouble(2, 2));
+ }
+
+ SECTION("Bit4")
+ {
+ Band* band = grid.CreateBand(Band::Bit4, L"bit4");
+ REQUIRE(NULL != band);
+
+ INT8 value = 9;
+ REQUIRE(band->SetValue(1, 2, Band::Bit4, &value));
+
+ INT8 readBack = 0;
+ REQUIRE(band->GetValue(1, 2, Band::Bit4, &readBack));
+ REQUIRE(value == readBack);
+ REQUIRE(9.0 == band->GetValueAsDouble(1, 2));
+ }
+}
+}
Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp 2026-10-23 11:25:59 UTC (rev 10225)
@@ -47,6 +47,7 @@
TestServiceFactory::AddSuiteToRun("[Misc]");
TestServiceFactory::AddSuiteToRun("[Performance]");
TestServiceFactory::AddSuiteToRun("[RenderingService]");
+ TestServiceFactory::AddSuiteToRun("[RasterStylization]");
TestServiceFactory::AddSuiteToRun("[ResourceService]");
TestServiceFactory::AddSuiteToRun("[ServerAdminService]");
TestServiceFactory::AddSuiteToRun("[ServerManager]");
@@ -53,6 +54,7 @@
TestServiceFactory::AddSuiteToRun("[ServiceManager]");
TestServiceFactory::AddSuiteToRun("[SiteManager]");
TestServiceFactory::AddSuiteToRun("[SiteService]");
+ TestServiceFactory::AddSuiteToRun("[Stylization]");
TestServiceFactory::AddSuiteToRun("[TileService]");
TestServiceFactory::AddSuiteToRun("[ProfilingService]");
TestServiceFactory::AddSuiteToRun("[TransformMesh]");
@@ -75,6 +77,7 @@
ACE_OS::printf(" Misc\n");
ACE_OS::printf(" Performance\n");
ACE_OS::printf(" RenderingService\n");
+ ACE_OS::printf(" RasterStylization\n");
ACE_OS::printf(" ResourceService\n");
ACE_OS::printf(" ServerAdminService\n");
ACE_OS::printf(" ServerManager\n");
@@ -81,6 +84,7 @@
ACE_OS::printf(" ServiceManager\n");
ACE_OS::printf(" SiteManager\n");
ACE_OS::printf(" SiteService\n");
+ ACE_OS::printf(" Stylization\n");
ACE_OS::printf(" TileService\n");
ACE_OS::printf(" ProfilingService\n");
ACE_OS::printf(" TransformMesh\n");
Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj 2026-10-23 11:25:59 UTC (rev 10225)
@@ -216,6 +216,7 @@
+
@@ -224,6 +225,7 @@
+
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md (rev 0)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md 2026-10-23 11:25:59 UTC (rev 10225)
@@ -0,0 +1,339 @@
+# Provenance of the raster fixtures
+
+This directory holds two kinds of test data, both used by the
+`[RasterStylization]` suite in `Server/src/UnitTesting/TestRasterStylization.cpp`:
+
+| Files | Kind | Where it came from |
+|-------|------|--------------------|
+| `RasterStylization_*.tif` | Synthetic, purpose built | Generated by `make_fixtures.py` in this directory (first section below) |
+| `RealRaster_*` | Real world imagery and elevation | Copied from the FDO GDAL provider test data, one file regenerated with GDAL (second section below) |
+
+This file records where each came from, how to reproduce or re-derive them, and
+their licensing status.
+
+# Part 1 - `RasterStylization_*.tif` (synthetic)
+
+These ten GeoTIFFs are the input data for the first half of the suite. This part
+records where they came from, how to reproduce them byte for byte, and their
+licensing status.
+
+## Summary
+
+| Property | Value |
+|----------|-------|
+| Origin | Original, synthetic data generated for this repository |
+| Author | Authored as part of this change; written out by `make_fixtures.py` in this directory |
+| Third party content | None. No external, sampled, scanned or licensed data set is used or derived from |
+| Geographic accuracy | None. Values are arbitrary constants chosen to be recognisable, not to describe any real place |
+| Format | GeoTIFF, written by GDAL `GTiff` driver, uncompressed (`TILED=NO`) |
+| Size on disk | 4,460 to 33,156 bytes per file, 165,100 bytes total |
+| Reproducible | Yes, byte for byte (see [Reproducing](#reproducing)) |
+| Licence | Same as the rest of the MgDev tree (LGPL-2.1); no attribution required |
+
+The `RasterStylization_` filename prefix exists because `UnitTest/TestData` is
+copied flat into `/Server/src/UnitTestFiles` by CMake, so the fixtures
+share one directory with all the other test data.
+
+## Why they are not derived from the FDO GDAL provider test data
+
+The raster fixtures shipped with the FDO GDAL provider
+(`fdo/Providers/GDAL/TestData`) were inventoried first:
+
+| Directory | What it holds | Why it is not enough |
+|-----------|---------------|----------------------|
+| `RGB/` | `RGB.bmp`, 200x200, 3 x Byte | 24 bpp only, and a BMP carries no georeferencing |
+| `band/` | `ae.tif` (513x516 Byte, Gray), JPEG + world files, band configuration XML | 8 bpp grey only |
+| `GeoTiff/`, `DEM/`, `DTED/`, `Jp2/`, `MrSID/`, `Ecw/`, `VRT/`, `rotated/`, ... | assorted real-world imagery | no 16 bit per channel RGB, no RGBA, no small-index palette, and no per-quadrant constant areas to sample |
+
+None of them can drive the 48 bpp, 32 bpp RGBA or palette branches of
+`GridData::ReadRaster` with a value that can be asserted exactly, and several
+are in formats that need third party codecs. Generating purpose-built fixtures
+was therefore cheaper and more precise than transforming them, and it keeps the
+new test data free of any third party licence obligation.
+
+## What the files contain
+
+Every fixture is 64x64 pixels, split into four 32x32 quadrants of one constant
+value so the rendered map can be sampled at the centre of each quadrant:
+
+| Quadrant | Position | Sample point |
+|----------|----------|--------------|
+| 0 | top-left (north-west) | (16, 16) |
+| 1 | top-right (north-east) | (48, 16) |
+| 2 | bottom-left (south-west) | (16, 48) |
+| 3 | bottom-right (south-east) | (48, 48) |
+
+Georeferencing, identical for all ten files: origin `(0, 64)`, pixel size
+`(1, -1)` degree, `AREA_OR_POINT=Area`, and a WGS84 geographic CRS written
+through the GeoTIFF keys. GDAL reports that CRS as EPSG:4326 and `gdalinfo`
+warns that the GeoTIFF-key definition is not byte-identical to the EPSG registry
+entry; that is harmless here, because the map definition in the test uses the
+same WKT and nothing is reprojected.
+
+The per-channel constants are:
+
+| Quadrant | 16 bit RGB sample | written to 8 bit fixtures as | alpha |
+|----------|-------------------|------------------------------|-------|
+| 0 | `0x1234 0x5678 0x9ABC` | `0x12 0x56 0x9A` | `0x80` |
+| 1 | `0xFFFF 0x0000 0x00FF` | `0xFF 0x00 0x00` | `0xFF` |
+| 2 | `0x0000 0xFFFF 0x0100` | `0x00 0xFF 0x01` | `0x00` |
+| 3 | `0x8080 0x4040 0x1010` | `0x80 0x40 0x10` | `0x40` |
+
+The high and low bytes of every 16 bit sample differ, which is what makes the
+48 bpp packing bug visible: a correct implementation renders the high byte, a
+broken one renders the low byte or nothing at all.
+
+| File | Bands | Sample type | Raster data model / bpp | Single band values (quadrants 0-3) |
+|------|-------|-------------|-------------------------|-----------------------------------|
+| `RasterStylization_rgb24.tif` | 3 | `Byte` | RGB / 24 | - |
+| `RasterStylization_rgba32.tif` | 4 | `Byte` | RGBA / 32 | - |
+| `RasterStylization_rgb48.tif` | 3 | `UInt16` | RGB / 48 | - |
+| `RasterStylization_rgb48_int16.tif` | 3 | `Int16` | RGB / 48 | - |
+| `RasterStylization_palette8.tif` | 1 + 256 entry colour table | `Byte` | Palette / 8 | indices `0,1,2,3` |
+| `RasterStylization_gray8.tif` | 1 | `Byte` | Gray / 8 | `0x12 0x34 0x56 0x78` |
+| `RasterStylization_gray16.tif` | 1 | `UInt16` | Gray / 16 | `0x1234 0x5678 0x9ABC 0x0100` |
+| `RasterStylization_gray32i.tif` | 1 | `Int32` | Gray / 32 | `0x00123456 0x00234567 0x00345678 0x00456789` |
+| `RasterStylization_gray32f.tif` | 1 | `Float32` | Gray / 32 | `4660 22136 39612 256` |
+| `RasterStylization_gray64f.tif` | 1 | `Float64` | Gray / 64 | `4660 22136 39612 256` |
+
+The palette is 256 entries long because GDAL always writes a full table; entries
+0-3 are `(0x12,0x56,0x9A,255)`, `(0x00,0x00,0xFF,255)`, `(0x00,0xFF,0x01,255)`
+and `(0x80,0x40,0x10,255)`, and entries 4-255 are opaque black `(0,0,0,255)`.
+Colour interpretation is `Red/Green/Blue[/Alpha]` for the colour fixtures,
+`PaletteIndex` for the palette fixture and `Gray` for the single band fixtures.
+
+The GDAL provider maps a file onto one of these models from its band layout
+(`FdoRfpBandRaster::GetDataModel`), which is why this particular set of layouts
+was chosen - it covers every branch of `GridData::ReadRaster` that a file can
+reach. `FdoRasterDataModelType_Data` and the signed/float band types cannot be
+reached from a file at all, because the provider only ever reports
+`UnsignedInteger`; the `[Stylization]` unit suite covers those directly.
+
+64 bpp RGBA (4 x `UInt16` with alpha) is deliberately **not** part of this set:
+`ReadRaster` writes a 32 bpp row into the 64 bpp band it creates for it, and the
+band's fill value is taken from a 4 byte local while the band is 8 bytes wide, so
+what such a file renders depends on uninitialised stack bytes.
+
+## Generator
+
+`make_fixtures.py` (in this directory, checked in) writes all ten files with the
+GDAL Python bindings and numpy:
+
+```
+python3 make_fixtures.py
+```
+
+It was run with, and the checked-in files were produced by:
+
+| Tool | Version |
+|------|---------|
+| Python | 3.14.8 |
+| GDAL | 3.12.4 "Chicoutimi" (`gdal.VersionInfo('VERSION_NUM')` = 3120400) |
+| numpy | 2.4.6 |
+| Host | Linux x86_64 |
+
+No other tool touched the files. They are not hand-edited, hand-resampled or
+post-processed by any other program, and they must not be - regenerate them with
+the script instead.
+
+## Reproducing
+
+Regeneration is deterministic: the fixtures are uncompressed, carry no TIFF
+metadata domain (no `DateTime`, no `Software` tag), and every pixel is a
+constant, so a rerun on the toolchain above reproduces them byte for byte. This
+was verified by regenerating the set in a scratch directory and diffing
+`sha256sum` output against the checked-in copies.
+
+To re-verify the checked-in bytes, compare them with the inventory table below:
+
+```
+cd UnitTest/TestData/RasterStylization
+sha256sum RasterStylization_*.tif
+```
+
+To prove the generator is still in step with the binaries, regenerate into a
+scratch directory and compare:
+
+```
+mkdir /tmp/regen && cp make_fixtures.py /tmp/regen && (cd /tmp/regen && python3 make_fixtures.py)
+(cd /tmp/regen && sha256sum *.tif) | diff - <(sha256sum *.tif)
+```
+
+If a future change to the fixtures is intended, regenerate them with the script,
+update the table and the checksum listing here in the same commit, and rerun
+`./mgserver test RasterStylization` in both Release and Debug.
+
+A newer GDAL will not necessarily reproduce these bytes (block/strip sizing and
+the default TIFF writer version are not contractual), so treat the versions above
+as part of the fixture definition. Verifying that a regenerated file still
+renders the expected colours matters more than matching the checksum.
+
+## File inventory
+
+| File | Bytes | SHA-256 |
+|------|-------|---------|
+| `RasterStylization_gray16.tif` | 8,556 | `33822aac7899bf85439d755ed4a6878e114467cf97ec23bbc24da7a483cf11ac` |
+| `RasterStylization_gray32f.tif` | 16,756 | `f16aa5516124b0c14054df5f91849d1ba86a1c50c217e634f7060414c5959fae` |
+| `RasterStylization_gray32i.tif` | 16,756 | `d0c3a1ed88673e58e1d07807d4182b820bb1a21c52eed49601a81e65736fd1b2` |
+| `RasterStylization_gray64f.tif` | 33,156 | `162123d4c8bc924234786e119e1b1af8bfec9ae2cc05cfff02bc8261c2b510e2` |
+| `RasterStylization_gray8.tif` | 4,460 | `bca00976efb56623a1e9cd979f6c6f37fbd3f24c4c10de8d3d65c3cab8686c9c` |
+| `RasterStylization_palette8.tif` | 6,008 | `09b7317933f7e19ba9d2da8cd7cb4f8691773c34e71572934e90c8e75a3cd3fb` |
+| `RasterStylization_rgb24.tif` | 12,672 | `5630faeed0a0e8bc9982dc2f8c2fd830c02e9286fb9df61394a11909495a595c` |
+| `RasterStylization_rgb48.tif` | 24,976 | `6c940ddf2813797bc4dfcda497abe4114bc5766a8791902dc7476ff205611f90` |
+| `RasterStylization_rgb48_int16.tif` | 24,976 | `e25d0a97c13379c833eaad4e13b3348bf253b675567523b1641596f6f2320501` |
+| `RasterStylization_rgba32.tif` | 16,784 | `306283d1fc3aaa54458c28c4468217f9cce749572599e21471912611e806914e` |
+
+`make_fixtures.py` and this file are not test inputs and have no checksum
+requirement beyond normal version control.
+
+# Part 2 - `RealRaster_*` (real world data)
+
+The second half of the suite renders genuine imagery and elevation data through
+the same pipeline so a human can confirm the output is a plausible map rather
+than a corrupted or distorted raster. There is no visual regression system, so
+these files are not compared against a reference image; the test writes each
+render to `/Server/src/UnitTestFiles/Results/RealRaster_.png` for
+inspection and only asserts that the image is the requested size and has real
+variation in it.
+
+## Origin
+
+All four come from the FDO GDAL provider test data in the sibling FDO working
+copy, i.e. the upstream FDO repository
+
+ https://svn.osgeo.org/fdo/branches/4.2/Providers/GDAL/TestData/
+
+which ships as part of FDO under the same LGPL-2.1 terms as the rest of FDO.
+They are not third-party commercial basemaps: the FDO project collected them as
+provider test data, and they have been in that tree since 2006-2013 (SVN
+revisions below).
+
+Each file was re-derived from its upstream original with `gdal_translate`, which
+is lossless for these files - the transformations crop a few columns and, for
+the palette sheet, change container format. Two reasons, both explained under
+[Why the files were re-derived](#why-the-files-were-re-derived): the row width
+has to be 64-bit aligned for the grid stylizer to be reached at all, and the
+original palette file is a PNG the provider cannot select features from.
+
+| Local file | Upstream path | Upstream SHA-256 | Upstream SVN |
+|------------|---------------|------------------|--------------|
+| `RealRaster_ae.tif` | `band/ae.tif` | `9435777dcf7098e53285b8f544d163915598f2fafb7ea15beeadac2f7ba66b65` | r892, gregboone, 2006-06-14 |
+| `RealRaster_spif83.tif` | `band/spif83.tif` | `c287327f8f81df675d9c8c6dbe394b1887bb5773f7bf01a234e840030bda0900` | r892, gregboone, 2006-06-14 |
+| `RealRaster_mtjefferson.tif` | `DEM/MtJefferson.dem` | `c804b81c02740e388eafdf90640745fb40f108157f09ca2d5f2f1dcf05e53091` | r892, gregboone, 2006-06-14 |
+| `RealRaster_cm13ct.tif` | `PNG/CM13ct.png` | `caa341405e2d1c51b392b05b073103607483f02c13597f55786445b48c68074e` | r3622, gregboone, 2013-02-20 |
+
+`UnitTest/TestData` is copied flat into `/Server/src/UnitTestFiles`, so
+these carry the `RealRaster_` prefix to keep them apart from the other test data
+and from the synthetic fixtures above.
+
+## What each file is
+
+| Local file | Size | Content |
+|------------|------|---------|
+| `RealRaster_ae.tif` | 512x516 px, 264,876 B | 8 bit grey scanned map sheet, 60 m pixels, projected in a custom coordinate system ("AE E000") |
+| `RealRaster_spif83.tif` | 512x512 px, 262,871 B | 8 bit grey scanned map sheet, ~195 m pixels, NAD83 / California zone 6 |
+| `RealRaster_cm13ct.tif` | 1944x3109 px, 2,287,461 B | 8 bit palette scanned topographic map, 10 m pixels, UTM zone 13N |
+| `RealRaster_mtjefferson.tif` | 336x469 px, 315,773 B | 16 bit integer USGS DEM (Mt Jefferson), 30 m pixels, UTM |
+
+Between them they cover the read paths a file can actually reach - grey 8 bit,
+grey 16 bit elevation and 8 bit palette - and three different coordinate
+systems, which is also what exercises the reprojection a real deployment does.
+The 8 bit grey pair are rendered with band 1 mapped to all three colour
+channels, the palette sheet as a packed colour band, and the DEM as a 0-3000 m
+elevation ramp, because elevation values are not colours.
+
+## How the files were re-derived
+
+```
+cd fdo/Providers/GDAL/TestData
+
+gdal_translate -of GTiff -srcwin 0 0 512 516 \
+ band/ae.tif /UnitTest/TestData/RasterStylization/RealRaster_ae.tif
+
+gdal_translate -of GTiff -srcwin 0 0 512 512 \
+ band/spif83.tif /UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
+
+gdal_translate -of GTiff -srcwin 0 0 1944 3109 -a_srs EPSG:32613 \
+ -co COMPRESS=DEFLATE -co PREDICTOR=2 \
+ PNG/CM13ct.png /UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
+
+gdal_translate -of GTiff -srcwin 0 0 336 469 \
+ DEM/MtJefferson.dem /UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
+```
+
+The crops keep the north-west corner, so the georeferencing of what remains is
+unchanged. Sample values, the colour table (for the palette sheet) and the DEM's
+-32767 nodata value were all verified unchanged against the originals.
+
+## Why the files were re-derived
+
+### Row alignment
+
+`GridData::ReadRaster` is called with `bAlignment = true`, so it expects each row
+of the provider's stream to be padded to a multiple of 8 bytes, and it gives up
+(`if (reader->available() != bufferLength) return;`) when the stream is shorter.
+The GDAL provider returns packed rows, so a raster whose
+`width * bitsPerPixel` is not a multiple of 64 never reaches the band code:
+`RasterAdapter::Stylize` falls back to its legacy decoder instead. That decoder
+handles 8/24/32 bpp adequately, which is why such rasters still look right, but
+it has no 16 bpp case and leaves its destination buffer unwritten (rendering
+heap contents).
+
+That is why every fixture here has a 64-bit aligned row width: 512, 512, 1944
+(8 bit) and 336 (16 bit). Without the crop these files would not exercise the
+`GridData::ReadRaster`/`Band` code the tests exist to cover. This is a
+pre-existing limitation of the raster read path, not something these fixtures
+introduce; the synthetic fixtures in Part 1 are also deliberately 64 px wide for
+the same reason.
+
+### Palette file format
+
+`PNG/CM13ct.png` has a geotransform but no coordinate system, and the provider
+cannot be used to *select* features from it through MapGuide:
+`MgFeatureService::SelectFeatures` returns a reader with no rows (verified for a
+plain select, a `FeatId`-only select and a raster select), so a raster layer over
+it draws nothing. The same data re-encoded as a GeoTIFF selects and renders
+normally, and declaring EPSG:32613 says what its metre coordinates already are,
+so the map definition does not have to pretend UTM eastings are degrees.
+
+`DEFLATE`/`PREDICTOR=2` are lossless; they only make the checked in file 2.3 MB
+instead of 6.1 MB.
+
+Note that palette entry 0 in this file is `(0,0,0,0)`, a fully transparent
+black covering the sheet margin. The renderer composites it onto the white map
+background, so the rendered image shows a white margin where a plain RGB view of
+the source is black. That is the palette branch honouring the palette's alpha,
+not a rendering defect.
+
+## Adding or changing a real fixture
+
+1. Choose a file from `fdo/Providers/GDAL/TestData` and re-derive it as above,
+ keeping the `RealRaster_` prefix. The row width must satisfy
+ `width * bitsPerPixel % 64 == 0`, so crop to a width that does.
+2. Verify it is usable:
+
+ ```
+ gdalinfo # expect a geotransform and a coordinate system
+ ```
+
+ A file without georeferencing cannot be read by the provider at all, and a
+ file whose CRS cannot be represented in the map definition will not render.
+3. Add an entry to `REAL_FIXTURES` in `TestRasterStylization.cpp` with the style
+ that suits the data (`StyleBand`, `StyleGrayBands` or `StyleHillShade`).
+4. Run the suite and look at the PNG it writes to
+ `/Server/src/UnitTestFiles/Results/`.
+5. Record the upstream path, hash, size, derivation command and what the file is
+ in the tables above.
+
+## File inventory
+
+| File | Bytes | SHA-256 |
+|------|-------|---------|
+| `RealRaster_ae.tif` | 264,876 | `72610ac16168d8f6a33177c681a31044aacac909f72af019e08c64a112ab7350` |
+| `RealRaster_cm13ct.tif` | 2,287,461 | `998430170a82cabf8cad85aa0cf876e7765f35fa01fb4d7f6e252612c5f1fb0f` |
+| `RealRaster_mtjefferson.tif` | 315,773 | `e2b7016d355f3cc17e586cd4c834e63dfe0008253443f8eb54330c4339606f1f` |
+| `RealRaster_spif83.tif` | 262,871 | `49e48c375e721bea9a72d19cfec2a48641b83aa6951b97393d8a4ad992394310` |
+
+The derivation commands use GDAL 3.12.4 (the toolchain in Part 1); a different
+GDAL version produces a different (still lossless) encoding and therefore a
+different checksum.
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif 2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif 2026-10-23 11:25:59 UTC (rev 10225)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/make_fixtures.py
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/make_fixtures.py (rev 0)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/make_fixtures.py 2026-10-23 11:25:59 UTC (rev 10225)
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+"""Generates the raster fixtures used by TestRasterStylization.
+
+Each 64x64 fixture is split into four 32x32 quadrants with a distinct colour so
+the rendered map can be sampled at the centre of each quadrant. The values are
+chosen so that for 16 bit samples the high and low bytes differ.
+
+ quadrant 0 (top-left) R 0x1234 G 0x5678 B 0x9ABC
+ quadrant 1 (top-right) R 0xFFFF G 0x0000 B 0x00FF
+ quadrant 2 (bottom-left) R 0x0000 G 0xFFFF B 0x0100
+ quadrant 3 (bottom-right) R 0x8080 G 0x4040 B 0x1010
+
+"top" is the north edge of the raster, which is the top of the rendered map.
+For 8 bit fixtures each channel is reduced to its high byte, so the four
+quadrants render 0x12569A / 0xFF0000 / 0x00FF01 / 0x804010.
+
+The GDAL provider (FdoRfpBandRaster::GetDataModel) decides the raster data
+model it exposes from the band count and colour interpretation:
+
+ 3 bands -> RGB, BitsPerPixel = 3 * sample size * 8
+ 4 bands (4th has alpha) -> RGBA, BitsPerPixel = 4 * sample size * 8
+ 1 band + colour table -> Palette
+ anything else (1 band) -> Gray
+
+so the fixture set below covers 24/32/48 bpp colour, 8 bpp palette and
+8/16/32/64 bpp single band rasters.
+
+Run with: python3 make_fixtures.py
+
+The files this writes are checked in. Their provenance, the exact tool versions
+that produced them and their checksums are recorded in PROVENANCE.md in this
+directory - update both together.
+"""
+import numpy as np
+from osgeo import gdal
+
+SIZE = 64
+BLOCK = SIZE // 2
+
+# UnitTest/TestData is copied flat into /Server/src/UnitTestFiles, so the
+# fixtures carry this prefix to keep them apart from the other test data.
+PREFIX = "RasterStylization_"
+
+# 16 bit quadrants (R, G, B) and the 8 bit colour they reduce to (high byte)
+QUADRANTS_16 = [(0x1234, 0x5678, 0x9ABC),
+ (0xFFFF, 0x0000, 0x00FF),
+ (0x0000, 0xFFFF, 0x0100),
+ (0x8080, 0x4040, 0x1010)]
+def high_byte(v):
+ return (v >> 8) & 0xFF
+
+
+QUADRANTS_8 = [tuple(high_byte(c) for c in q) for q in QUADRANTS_16]
+ALPHAS = (0x80, 0xFF, 0x00, 0x40)
+NP = {gdal.GDT_Byte: np.uint8, gdal.GDT_UInt16: np.uint16,
+ gdal.GDT_Int16: np.int16, gdal.GDT_Int32: np.int32,
+ gdal.GDT_Float32: np.float32, gdal.GDT_Float64: np.float64}
+
+
+def quadrant(x, y):
+ return (1 if x >= BLOCK else 0) + (2 if y >= BLOCK else 0)
+
+
+def create(name, bands, dtype, fill, options=None, palette=None, interp=None):
+ opts = ["TILED=NO"]
+ if palette:
+ opts.append("PHOTOMETRIC=PALETTE")
+ elif bands >= 3:
+ opts.append("PHOTOMETRIC=RGB")
+ if options:
+ opts.extend(options)
+ ds = gdal.GetDriverByName("GTiff").Create(name, SIZE, SIZE, bands, dtype, options=opts)
+ ds.SetGeoTransform((0.0, 1.0, 0.0, float(SIZE), 0.0, -1.0))
+ # WGS84 geographic: the map definition uses the same CS, so the raster is
+ # drawn without a datum transform and 1 pixel = 1 degree.
+ ds.SetProjection('GEOGCS["LL84",DATUM["WGS84",SPHEROID["WGS84",6378137.000,298.25722293]],'
+ 'PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]')
+ if palette:
+ # must be set before the first write
+ ct = gdal.ColorTable()
+ for i, rgba in enumerate(palette):
+ ct.SetColorEntry(i, rgba)
+ ds.GetRasterBand(1).SetColorTable(ct)
+ ds.GetRasterBand(1).SetColorInterpretation(gdal.GCI_PaletteIndex)
+ for b in range(1, bands + 1):
+ arr = np.empty((SIZE, SIZE), dtype=NP[dtype])
+ for y in range(SIZE):
+ for x in range(SIZE):
+ value = int(fill(x, y, b - 1))
+ if dtype == gdal.GDT_Int16:
+ # keep the 16 bit bit pattern, just as a signed sample
+ value = ((value + 0x8000) & 0xFFFF) - 0x8000
+ elif dtype == gdal.GDT_Int32:
+ value = ((value + 0x80000000) & 0xFFFFFFFF) - 0x80000000
+ arr[y, x] = value
+ band = ds.GetRasterBand(b)
+ band.WriteArray(arr)
+ if interp and interp[b - 1] != "Gray":
+ band.SetColorInterpretation(getattr(gdal, "GCI_" + interp[b - 1] + "Band"))
+ ds = None
+
+
+def colour(dtype_scale):
+ """Per-channel fill for the quadrants, scaled to the sample type."""
+ def fill(x, y, band):
+ value = QUADRANTS_16[quadrant(x, y)][band]
+ return value if dtype_scale == 1 else high_byte(value)
+ return fill
+
+
+def rgba(scale):
+ def fill(x, y, band):
+ if band == 3:
+ return ALPHAS[quadrant(x, y)] * (257 if scale == 1 else 1)
+ value = QUADRANTS_16[quadrant(x, y)][band]
+ return value if scale == 1 else high_byte(value)
+ return fill
+
+
+def gray(values, scale):
+ def fill(x, y, band):
+ value = values[quadrant(x, y)]
+ return value if scale == 1 else high_byte(value)
+ return fill
+
+
+# 24 bpp: 3 bands of Byte -> RGB / 24
+create(PREFIX + "rgb24.tif", 3, gdal.GDT_Byte, colour(0), interp=("Red", "Green", "Blue"))
+# 32 bpp: 4 bands of Byte with an alpha band -> RGBA / 32
+create(PREFIX + "rgba32.tif", 4, gdal.GDT_Byte, rgba(0), interp=("Red", "Green", "Blue", "Alpha"))
+# 48 bpp: 3 bands of UInt16 -> RGB / 48 (the packing fix)
+create(PREFIX + "rgb48.tif", 3, gdal.GDT_UInt16, colour(1), interp=("Red", "Green", "Blue"))
+# 48 bpp signed: same layout, different FdoRasterDataType
+create(PREFIX + "rgb48_int16.tif", 3, gdal.GDT_Int16, colour(1), interp=("Red", "Green", "Blue"))
+# 64 bpp RGBA (4 bands of UInt16 with alpha) is not generated: ReadRaster's
+# "else" branch writes a 32 bpp row into the 64 bpp band it creates for it, so
+# the rendered result depends on uninitialised stack bytes.
+# 8 bpp palette: one band with a colour table -> Palette / 8
+create(PREFIX + "palette8.tif", 1, gdal.GDT_Byte, lambda x, y, b: quadrant(x, y),
+ palette=[(0x12, 0x56, 0x9A, 255), (0x00, 0x00, 0xFF, 255),
+ (0x00, 0xFF, 0x01, 255), (0x80, 0x40, 0x10, 255)])
+# 8 bpp single band -> Gray / 8
+# keep the samples within the positive Int8 range: the colour handler maps the value
+# through a signed conversion, so values >= 0x80 would render as null pixels
+create(PREFIX + "gray8.tif", 1, gdal.GDT_Byte, gray([0x12, 0x34, 0x56, 0x78], 1),
+ interp=("Gray",))
+# 16 bpp single band -> Gray / 16
+create(PREFIX + "gray16.tif", 1, gdal.GDT_UInt16, gray([0x1234, 0x5678, 0x9ABC, 0x0100], 1),
+ interp=("Gray",))
+# 32 bpp single band integer -> Gray / 32, exercises the Int32 band accessors
+create(PREFIX + "gray32i.tif", 1, gdal.GDT_Int32,
+ gray([0x00123456, 0x00234567, 0x00345678, 0x00456789], 1), interp=("Gray",))
+# 32 bpp single band float -> Gray / 32, exercises the Double32 band accessors
+create(PREFIX + "gray32f.tif", 1, gdal.GDT_Float32, gray([4660.0, 22136.0, 39612.0, 256.0], 1),
+ interp=("Gray",))
+# 64 bpp single band float -> Gray / 64, exercises the Double64 band accessors
+create(PREFIX + "gray64f.tif", 1, gdal.GDT_Float64, gray([4660.0, 22136.0, 39612.0, 256.0], 1),
+ interp=("Gray",))
+
+for name in ("rgb24.tif", "rgba32.tif", "rgb48.tif", "rgb48_int16.tif",
+ "palette8.tif", "gray8.tif", "gray16.tif", "gray32i.tif", "gray32f.tif",
+ "gray64f.tif"):
+ name = PREFIX + name
+ ds = gdal.Open(name)
+ b = ds.GetRasterBand(1)
+ print("%-16s bands=%d %-8s bpp=%d ci=%s" % (
+ name, ds.RasterCount, gdal.GetDataTypeName(b.DataType),
+ gdal.GetDataTypeSize(b.DataType) * ds.RasterCount // 8,
+ b.GetColorInterpretation()))
From svn_mapguide at osgeo.org Fri Oct 23 11:47:08 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Fri, 23 Oct 2026 11:47:08 -0700
Subject: [mapguide-commits] r10226 - in branches/4.0/MgDev: Oem/LinuxApt
Server/src/Core
Message-ID: <20261023184708.3841818E6E1@trac.osgeo.org>
Author: jng
Date: 2026-10-23 11:47:06 -0700 (Fri, 23 Oct 2026)
New Revision: 10226
Modified:
branches/4.0/MgDev/Oem/LinuxApt/
branches/4.0/MgDev/Server/src/Core/main.cpp
Log:
Allow mgserver to dump crtdbg memory leak report to stderr if MG_CRT_LEAK_CHECK is set
Index: branches/4.0/MgDev/Oem/LinuxApt
===================================================================
--- branches/4.0/MgDev/Oem/LinuxApt 2026-10-23 11:25:59 UTC (rev 10225)
+++ branches/4.0/MgDev/Oem/LinuxApt 2026-10-23 18:47:06 UTC (rev 10226)
Property changes on: branches/4.0/MgDev/Oem/LinuxApt
___________________________________________________________________
Modified: svn:ignore
## -2,3 +2,4 ##
httpd-2.4.52
php-5.6.40
httpd-2.4.65
+httpd-2.4.69
Modified: branches/4.0/MgDev/Server/src/Core/main.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Core/main.cpp 2026-10-23 11:25:59 UTC (rev 10225)
+++ branches/4.0/MgDev/Server/src/Core/main.cpp 2026-10-23 18:47:06 UTC (rev 10226)
@@ -58,6 +58,16 @@
#ifdef _WIN32
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
+ // The leak report is written to the debugger's output, which a plain
+ // command-line run discards. Set MG_CRT_LEAK_CHECK=1 to send it to stderr
+ // instead, so that it shows up in the run's console output and in the log
+ // file a test run tees to.
+ if (getenv("MG_CRT_LEAK_CHECK") != NULL)
+ {
+ _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
+ _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
+ }
+
#ifdef _DEBUG
// to debug memory leaks, set a breakpoint here and set iBlock
// to the block allocation you want to break on
From svn_mapguide at osgeo.org Fri Oct 23 12:10:52 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Fri, 23 Oct 2026 12:10:52 -0700
Subject: [mapguide-commits] r10227 - branches/4.0/MgDev/Common/Stylization
Message-ID: <20261023191053.0F58718F69D@trac.osgeo.org>
Author: jng
Date: 2026-10-23 12:10:51 -0700 (Fri, 23 Oct 2026)
New Revision: 10227
Modified:
branches/4.0/MgDev/Common/Stylization/Band.h
branches/4.0/MgDev/Common/Stylization/GridData.h
Log:
Some header shuffling so that the new stylization test cases don't need to pull down FDO Expression Engine headers in its include chain
Modified: branches/4.0/MgDev/Common/Stylization/Band.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/Band.h 2026-10-23 18:47:06 UTC (rev 10226)
+++ branches/4.0/MgDev/Common/Stylization/Band.h 2026-10-23 19:10:51 UTC (rev 10227)
@@ -18,8 +18,9 @@
#ifndef BAND_H_
#define BAND_H_
-#include "Stylization.h"
+#include "StylizationAPI.h"
#include "MdfModel.h"
+#include "Box2D.h"
#include "Vector3D.h"
#include "Point2D.h"
#include "Point3D.h"
Modified: branches/4.0/MgDev/Common/Stylization/GridData.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/GridData.h 2026-10-23 18:47:06 UTC (rev 10226)
+++ branches/4.0/MgDev/Common/Stylization/GridData.h 2026-10-23 19:10:51 UTC (rev 10227)
@@ -19,6 +19,8 @@
#define GRIDDATA_H_
#include "MdfModel.h"
+#include "Box2D.h"
+#include "Fdo.h"
#include "Band.h"
#include "HillShade.h"
From svn_mapguide at osgeo.org Fri Oct 23 12:36:50 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Fri, 23 Oct 2026 12:36:50 -0700
Subject: [mapguide-commits] r10228 -
branches/4.0/MgDev/Server/src/UnitTesting
Message-ID: <20261023193650.E20C318FD4B@trac.osgeo.org>
Author: jng
Date: 2026-10-23 12:36:50 -0700 (Fri, 23 Oct 2026)
New Revision: 10228
Modified:
branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
Log:
Theoeretical windows baseline fix
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp 2026-10-23 19:10:51 UTC (rev 10227)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp 2026-10-23 19:36:50 UTC (rev 10228)
@@ -128,6 +128,13 @@
// bands are only filled by the 32 bit Band::SetValue path, which is a no-op
// on Linux before the fixes. They are added, with their expected colours,
// in the change that fixes those paths.
+ //
+ // The 64 bpp float fixture is absent for a different reason: before the
+ // fixes its behaviour depends on the platform. Windows reads one byte of
+ // the eight byte sample and paints an opaque black band, while Linux does
+ // not style the band at all and the background shows through. It cannot be
+ // part of a baseline that must pass on both, and is added by the change
+ // that fixes the 8 byte BandData accessor.
// The 32 bpp branch copies the source pixels through unchanged, so the
// source alpha survives and the renderer blends the colour over the white
@@ -153,10 +160,6 @@
// pattern happens to contain (0x45, 0x46, 0x46, 0x40).
{ "RasterStylization_gray32f.tif", "32 bpp float single band, samples read as ARGB",
{ 0xE1E5B9, 0xE8FAB8, 0xBFECB7, 0xDDBBBB } },
- // Doubles are read as unsigned 64 bit samples, which is far outside the
- // range a packed ARGB value can hold, so no pixel is coloured.
- { "RasterStylization_gray64f.tif", "64 bpp float single band, not styled",
- { 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF } },
};
// Real rasters derived from the FDO GDAL provider test data
From svn_mapguide at osgeo.org Fri Oct 23 13:37:05 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Fri, 23 Oct 2026 13:37:05 -0700
Subject: [mapguide-commits] r10229 - in branches/4.0/MgDev:
Common/Foundation/System Server/src/UnitTesting
Message-ID: <20261023203705.F1FEF19A769@trac.osgeo.org>
Author: jng
Date: 2026-10-23 13:37:05 -0700 (Fri, 23 Oct 2026)
New Revision: 10229
Modified:
branches/4.0/MgDev/Common/Foundation/System/Resources.cpp
branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
Log:
Allow mgserver test to accept a comma-delimited set of suites to run
Fixes #2893
Modified: branches/4.0/MgDev/Common/Foundation/System/Resources.cpp
===================================================================
--- branches/4.0/MgDev/Common/Foundation/System/Resources.cpp 2026-10-23 19:36:50 UTC (rev 10228)
+++ branches/4.0/MgDev/Common/Foundation/System/Resources.cpp 2026-10-23 20:37:05 UTC (rev 10229)
@@ -101,8 +101,10 @@
const STRING MgResources::ServerCmdTestDescription = L" test \n"\
L" Runs the server unit test(s).\n\n"\
L" By default all of the unit tests will be run unless you specify an\n"\
- L" optional \"Test\" to run. If you specify \"List\" as the test to run you\n"\
- L" will get a list of the available unit tests.\n\n"\
+ L" optional \"Test\" to run. \"Test\" may be a comma-delimited list of\n"\
+ L" test names, for example \"Geometry,MdfModel\", which runs all of the\n"\
+ L" named tests in a single server start. If you specify \"List\" as the\n"\
+ L" test to run you will get a list of the available unit tests.\n\n"\
L" Default output of the unit tests will be the console unless you specify\n"\
L" an optional \"Filename\". The output to the \"Filename\" will be in XML.\n\n";
Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp 2026-10-23 19:36:50 UTC (rev 10228)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp 2026-10-23 20:37:05 UTC (rev 10229)
@@ -20,9 +20,110 @@
#include "FoundationDefs.h"
#include "TestServiceFactory.h"
+#include
+
#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
+namespace
+{
+ void AddAllSuitesToRun()
+ {
+ static const char* const allSuites[] =
+ {
+ "[CoordinateSystem]",
+ "[DrawingService]",
+ "[FeatureService]",
+ "[Geometry]",
+ "[KmlService]",
+ "[LogManager]",
+ "[MappingService]",
+ "[MdfModel]",
+ "[Misc]",
+ "[Performance]",
+ "[RenderingService]",
+ "[RasterStylization]",
+ "[ResourceService]",
+ "[ServerAdminService]",
+ "[ServerManager]",
+ "[ServiceManager]",
+ "[SiteManager]",
+ "[SiteService]",
+ "[Stylization]",
+ "[TileService]",
+ "[ProfilingService]",
+ "[TransformMesh]"
+ };
+
+ for (size_t i = 0; i < sizeof(allSuites) / sizeof(allSuites[0]); ++i)
+ {
+ TestServiceFactory::AddSuiteToRun(allSuites[i]);
+ }
+ }
+
+ void PrintAvailableSuites()
+ {
+ // Title
+ ACE_OS::printf("The following test suites are available to run:\n\n");
+
+ // Available tests
+ ACE_OS::printf(" all - Excluding Performance\n");
+ ACE_OS::printf(" CoordinateSystem\n");
+ ACE_OS::printf(" DrawingService\n");
+ ACE_OS::printf(" FeatureService\n");
+ ACE_OS::printf(" Geometry\n");
+ ACE_OS::printf(" KmlService\n");
+ ACE_OS::printf(" LogManager\n");
+ ACE_OS::printf(" MappingService\n");
+ ACE_OS::printf(" MdfModel\n");
+ ACE_OS::printf(" Misc\n");
+ ACE_OS::printf(" Performance\n");
+ ACE_OS::printf(" RenderingService\n");
+ ACE_OS::printf(" RasterStylization\n");
+ ACE_OS::printf(" ResourceService\n");
+ ACE_OS::printf(" ServerAdminService\n");
+ ACE_OS::printf(" ServerManager\n");
+ ACE_OS::printf(" ServiceManager\n");
+ ACE_OS::printf(" SiteManager\n");
+ ACE_OS::printf(" SiteService\n");
+ ACE_OS::printf(" Stylization\n");
+ ACE_OS::printf(" TileService\n");
+ ACE_OS::printf(" ProfilingService\n");
+ ACE_OS::printf(" TransformMesh\n");
+ ACE_OS::printf("\n");
+ }
+
+ // Splits a delimited list of suite names. Commas, semicolons and
+ // whitespace are accepted as delimiters, so both
+ // "FeatureService,Geometry" and "FeatureService Geometry" work.
+ void SplitSuiteList(CREFSTRING test, std::vector& suites)
+ {
+ STRING name;
+ for (size_t i = 0; i < test.length(); ++i)
+ {
+ wchar_t ch = test[i];
+ if (ch == L',' || ch == L';' || ch == L' ' || ch == L'\t' ||
+ ch == L'\r' || ch == L'\n')
+ {
+ if (!name.empty())
+ {
+ suites.push_back(name);
+ name.clear();
+ }
+ }
+ else
+ {
+ name += ch;
+ }
+ }
+
+ if (!name.empty())
+ {
+ suites.push_back(name);
+ }
+ }
+}
+
int Execute(CREFSTRING fileName, CREFSTRING test)
{
// Set the default user information for this test run.
@@ -34,70 +135,41 @@
Catch::Session session;
if (!test.empty())
{
- if (test == L"all")
+ std::vector suites;
+ SplitSuiteList(test, suites);
+
+ // Preserve the previous behavior for a value that has no suite names
+ // in it: it is still bracketed and run as a single (matching nothing)
+ // tag.
+ if (suites.empty())
{
- TestServiceFactory::AddSuiteToRun("[CoordinateSystem]");
- TestServiceFactory::AddSuiteToRun("[DrawingService]");
- TestServiceFactory::AddSuiteToRun("[FeatureService]");
- TestServiceFactory::AddSuiteToRun("[Geometry]");
- TestServiceFactory::AddSuiteToRun("[KmlService]");
- TestServiceFactory::AddSuiteToRun("[LogManager]");
- TestServiceFactory::AddSuiteToRun("[MappingService]");
- TestServiceFactory::AddSuiteToRun("[MdfModel]");
- TestServiceFactory::AddSuiteToRun("[Misc]");
- TestServiceFactory::AddSuiteToRun("[Performance]");
- TestServiceFactory::AddSuiteToRun("[RenderingService]");
- TestServiceFactory::AddSuiteToRun("[RasterStylization]");
- TestServiceFactory::AddSuiteToRun("[ResourceService]");
- TestServiceFactory::AddSuiteToRun("[ServerAdminService]");
- TestServiceFactory::AddSuiteToRun("[ServerManager]");
- TestServiceFactory::AddSuiteToRun("[ServiceManager]");
- TestServiceFactory::AddSuiteToRun("[SiteManager]");
- TestServiceFactory::AddSuiteToRun("[SiteService]");
- TestServiceFactory::AddSuiteToRun("[Stylization]");
- TestServiceFactory::AddSuiteToRun("[TileService]");
- TestServiceFactory::AddSuiteToRun("[ProfilingService]");
- TestServiceFactory::AddSuiteToRun("[TransformMesh]");
+ suites.push_back(test);
}
- else if (test == L"list")
- {
- // Title
- ACE_OS::printf("The following test suites are available to run:\n\n");
- // Available tests
- ACE_OS::printf(" all - Excluding Performance\n");
- ACE_OS::printf(" CoordinateSystem\n");
- ACE_OS::printf(" DrawingService\n");
- ACE_OS::printf(" FeatureService\n");
- ACE_OS::printf(" Geometry\n");
- ACE_OS::printf(" KmlService\n");
- ACE_OS::printf(" LogManager\n");
- ACE_OS::printf(" MappingService\n");
- ACE_OS::printf(" MdfModel\n");
- ACE_OS::printf(" Misc\n");
- ACE_OS::printf(" Performance\n");
- ACE_OS::printf(" RenderingService\n");
- ACE_OS::printf(" RasterStylization\n");
- ACE_OS::printf(" ResourceService\n");
- ACE_OS::printf(" ServerAdminService\n");
- ACE_OS::printf(" ServerManager\n");
- ACE_OS::printf(" ServiceManager\n");
- ACE_OS::printf(" SiteManager\n");
- ACE_OS::printf(" SiteService\n");
- ACE_OS::printf(" Stylization\n");
- ACE_OS::printf(" TileService\n");
- ACE_OS::printf(" ProfilingService\n");
- ACE_OS::printf(" TransformMesh\n");
- ACE_OS::printf("\n");
+ bool bListOnly = (suites.size() == 1 && suites[0] == L"list");
- bRunTests = false;
- }
- else
+ for (size_t i = 0; i < suites.size(); ++i)
{
- std::string testToRun = "[";
- testToRun += MgUtil::WideCharToMultiByte(test);
- testToRun += "]";
- TestServiceFactory::AddSuiteToRun(testToRun);
+ const STRING& suite = suites[i];
+ if (suite == L"all")
+ {
+ AddAllSuitesToRun();
+ }
+ else if (suite == L"list")
+ {
+ PrintAvailableSuites();
+ if (bListOnly)
+ {
+ bRunTests = false;
+ }
+ }
+ else
+ {
+ std::string testToRun = "[";
+ testToRun += MgUtil::WideCharToMultiByte(suite);
+ testToRun += "]";
+ TestServiceFactory::AddSuiteToRun(testToRun);
+ }
}
}
From svn_mapguide at osgeo.org Fri Oct 23 15:11:41 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Fri, 23 Oct 2026 15:11:41 -0700
Subject: [mapguide-commits] r10230 - in branches/4.0/MgDev:
Common/Stylization Server/src/UnitTesting
Message-ID: <20261023221141.6D1F619E02C@trac.osgeo.org>
Author: jng
Date: 2026-10-23 15:11:40 -0700 (Fri, 23 Oct 2026)
New Revision: 10230
Modified:
branches/4.0/MgDev/Common/Stylization/Band.cpp
branches/4.0/MgDev/Common/Stylization/BandData.cpp
branches/4.0/MgDev/Common/Stylization/GridData.cpp
branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp
Log:
Add more tests to harden the Grid band I/O
Modified: branches/4.0/MgDev/Common/Stylization/Band.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/Band.cpp 2026-10-23 20:37:05 UTC (rev 10229)
+++ branches/4.0/MgDev/Common/Stylization/Band.cpp 2026-10-23 22:11:40 UTC (rev 10230)
@@ -164,7 +164,43 @@
}
break;
#else
- // Linux implementation here
+ // The ByteBandData native accessors are Windows only, so use the
+ // BandData virtual API, which copies the band element size in bytes.
+ case UnsignedInt32:
+ case Int32:
+ {
+ assert (Int32 == m_dataType || UnsignedInt32 == m_dataType);
+ success = m_pBandData->GetValue(i, j, *reinterpret_cast(pValue));
+ }
+ break;
+
+ case Double32:
+ {
+ assert (Double32 == m_dataType);
+ INT32 bits;
+ success = m_pBandData->GetValue(i, j, bits);
+ if (success)
+ memcpy(pValue, &bits, sizeof(float));
+ }
+ break;
+
+ case UnsignedInt64:
+ case Int64:
+ {
+ assert (Int64 == m_dataType || UnsignedInt64 == m_dataType);
+ success = m_pBandData->GetValue(i, j, *reinterpret_cast(pValue));
+ }
+ break;
+
+ case Double64:
+ {
+ assert (Double64 == m_dataType);
+ INT64 bits;
+ success = m_pBandData->GetValue(i, j, bits);
+ if (success)
+ memcpy(pValue, &bits, sizeof(double));
+ }
+ break;
#endif
@@ -230,7 +266,41 @@
}
break;
#else
- // Linux implementation
+ // The ByteBandData native accessors are Windows only, so use the
+ // BandData virtual API, which copies the band element size in bytes.
+ case UnsignedInt32:
+ case Int32:
+ {
+ assert (Int32 == m_dataType || UnsignedInt32 == m_dataType);
+ success = m_pBandData->SetValue(i, j, reinterpret_cast(pValue));
+ }
+ break;
+
+ case Double32:
+ {
+ assert (Double32 == m_dataType);
+ INT32 bits;
+ memcpy(&bits, pValue, sizeof(float));
+ success = m_pBandData->SetValue(i, j, &bits);
+ }
+ break;
+
+ case UnsignedInt64:
+ case Int64:
+ {
+ assert (Int64 == m_dataType || UnsignedInt64 == m_dataType);
+ success = m_pBandData->SetValue(i, j, reinterpret_cast(pValue));
+ }
+ break;
+
+ case Double64:
+ {
+ assert (Double64 == m_dataType);
+ INT64 bits;
+ memcpy(&bits, pValue, sizeof(double));
+ success = m_pBandData->SetValue(i, j, &bits);
+ }
+ break;
#endif
@@ -337,7 +407,71 @@
}
break;
#else
- // Linux implementation
+ // The ByteBandData native accessors are Windows only, so use the
+ // BandData virtual API, which copies the band element size in bytes.
+ case UnsignedInt32:
+ {
+ INT32 bits;
+ success = m_pBandData->GetValue(i, j, bits);
+ success = success && (memcmp(&bits, &m_nullValue, 4) != 0); //ensure it is not equal to the null value
+ dvalue = (double)(unsigned int)bits;
+ }
+ break;
+
+ case Int32:
+ {
+ INT32 bits;
+ success = m_pBandData->GetValue(i, j, bits);
+ success = success && (memcmp(&bits, &m_nullValue, 4) != 0); //ensure it is not equal to the null value
+ dvalue = (double)bits;
+ }
+ break;
+
+ case Double32:
+ {
+ INT32 bits;
+ float val = 0.0f;
+ success = m_pBandData->GetValue(i, j, bits);
+ if (success)
+ {
+ memcpy(&val, &bits, sizeof(float));
+ success = (memcmp(&val, &m_nullValue, sizeof(float)) != 0); //ensure it is not equal to the null value
+ dvalue = (double)val;
+ }
+ }
+ break;
+
+ case UnsignedInt64:
+ {
+ INT64 bits;
+ success = m_pBandData->GetValue(i, j, bits);
+ success = success && memcmp(&bits, &m_nullValue, 8) != 0; //ensure it is not equal to the null value
+ dvalue = (double)(UINT64)bits;
+ }
+ break;
+
+ case Int64:
+ {
+ INT64 bits;
+ success = m_pBandData->GetValue(i, j, bits);
+ success = success && (memcmp(&bits, &m_nullValue, 8) != 0); //ensure it is not equal to the null value
+ dvalue = (double)bits;
+ }
+ break;
+
+ case Double64:
+ {
+ INT64 bits;
+ double val = 0.0;
+ success = m_pBandData->GetValue(i, j, bits);
+ if (success)
+ {
+ memcpy(&val, &bits, sizeof(double));
+ success = (memcmp(&val, &m_nullValue, sizeof(double)) != 0); //ensure it is not equal to the null value
+ dvalue = val;
+ }
+ }
+ break;
#endif
default:
@@ -666,6 +800,7 @@
return m_pOwnerGrid->GetXUnitDistance();
}
+ // Ignore any C4723 warning about potenial divide by 0. It's a false positive
return m_nXCount ? m_xExtent / m_nXCount : 0;
}
@@ -842,11 +977,26 @@
if (memcmp(&fbottom, &m_nullValue, 4) == 0)
fbottom = fcenter;
#else
- // TODO: Linux implementation
- fleft = 0.0f;
- fright = 0.0f;
- ftop = 0.0f;
- fbottom = 0.0f;
+ // The ByteBandData native accessors are Windows only, so read the
+ // neighbours through GetValueAsDouble, which applies the same
+ // null-value check the Windows path does inline
+ double dcenter, dleft, dright, dtop, dbottom;
+ if (!GetValueAsDouble(i, j, dcenter))
+ return false;
+ if (!i || !GetValueAsDouble(i - 1, j, dleft))
+ dleft = dcenter;
+ if (i == m_nXCount - 1 || !GetValueAsDouble(i + 1, j, dright))
+ dright = dcenter;
+ if (!j || !GetValueAsDouble(i, j - 1, dtop))
+ dtop = dcenter;
+ if (j == m_nYCount - 1 || !GetValueAsDouble(i, j + 1, dbottom))
+ dbottom = dcenter;
+
+ fcenter = (float)dcenter;
+ fleft = (float)dleft;
+ fright = (float)dright;
+ ftop = (float)dtop;
+ fbottom = (float)dbottom;
#endif
left = fleft;
@@ -887,7 +1037,19 @@
else
bottom = ((ByteBandData*)m_pBandData)->GetNativeDouble64(index+m_nXCount);
#else
- // Linux implementation
+ // The ByteBandData native accessors are Windows only, so read the
+ // neighbours through GetValueAsDouble, which applies the same
+ // null-value check the Windows path does inline
+ if (!GetValueAsDouble(i, j, center))
+ return false;
+ if (!i || !GetValueAsDouble(i - 1, j, left))
+ left = center;
+ if (i == m_nXCount - 1 || !GetValueAsDouble(i + 1, j, right))
+ right = center;
+ if (!j || !GetValueAsDouble(i, j - 1, top))
+ top = center;
+ if (j == m_nYCount - 1 || !GetValueAsDouble(i, j + 1, bottom))
+ bottom = center;
#endif
if (memcmp(&left, &m_nullValue, 8) == 0)
Modified: branches/4.0/MgDev/Common/Stylization/BandData.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/BandData.cpp 2026-10-23 20:37:05 UTC (rev 10229)
+++ branches/4.0/MgDev/Common/Stylization/BandData.cpp 2026-10-23 22:11:40 UTC (rev 10230)
@@ -441,7 +441,7 @@
if (m_size == 8)
{
int pos = (x + y * m_width);
- value = m_pData[pos*8];
+ memcpy(&value, &m_pData[pos*8], sizeof(INT64));
return true;
}
Modified: branches/4.0/MgDev/Common/Stylization/GridData.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/GridData.cpp 2026-10-23 20:37:05 UTC (rev 10229)
+++ branches/4.0/MgDev/Common/Stylization/GridData.cpp 2026-10-23 22:11:40 UTC (rev 10230)
@@ -21,6 +21,8 @@
#include "Bounds.h"
#include "RS_InputStream.h"
+#include
+
#ifdef MG_MAX_PATH
#undef MG_MAX_PATH
#endif
@@ -56,9 +58,13 @@
return false;
}
-#define READELEVATIONDATA(elevationd_band_type, elevation_fdo_type, function_name) \
- FdoPtr val = static_cast(nullValue); \
- elevationd_band_type fdoNullValue= val->function_name(); \
+// null_data_type is the FdoDataType the macro's cast expects. If the raster has
+// no null value, or reports one of a different type, every sample is kept.
+#define READELEVATIONDATA(elevationd_band_type, elevation_fdo_type, function_name, null_data_type) \
+ elevation_fdo_type* val = (nullValue.p && nullValue->GetDataType() == (null_data_type))? \
+ static_cast(nullValue.p) : NULL; \
+ const bool bHasNullValue = (val != NULL); \
+ elevationd_band_type fdoNullValue = bHasNullValue? val->function_name() : (elevationd_band_type)0; \
elevationd_band_type curVal; \
for (unsigned long y = 0; y < actualRows; ++y) \
{ \
@@ -66,10 +72,10 @@
{ \
pCurPos = (unsigned char*)(pRasterData + y*rowLength + x*bitPerPixel/8 ); \
curVal = (*(reinterpret_cast(pCurPos))); \
- if (curVal != fdoNullValue) \
+ if (!bHasNullValue || curVal != fdoNullValue) \
{ \
- float val = (float)curVal; \
- pGisBand->SetValue(colStartPos + x, rowStartPos + y, Band::Double32, &val); \
+ float fValue = (float)curVal; \
+ pGisBand->SetValue(colStartPos + x, rowStartPos + y, Band::Double32, &fValue); \
} \
else \
{ \
@@ -337,373 +343,359 @@
)
{
Band* pGisBand = NULL;
- RS_InputStream* reader = NULL;
- FdoByte* pRasterData = NULL;
- FdoByte* pPaletteBuf = NULL;
wchar_t pUniqueBandName[MG_MAX_PATH] = { 0 };
- try
+
+ //FdoPtr dataModel = pRaster->GetDataModel();
+ //FdoPtr byteArray = pRaster->GetBounds();
+ //FdoPtr pAwkbFactory = FdoFgfGeometryFactory::GetInstance();
+ //FdoPtr pGeometry = pAwkbFactory->CreateGeometryFromFgf(byteArray);
+ //FdoPtr envelope = pGeometry->GetEnvelope();
+ FdoInt32 bitPerPixel = pRaster->GetBitsPerPixel();
+ FdoRasterDataModelType dataModelType = (FdoRasterDataModelType)pRaster->GetDataModelType();
+ RS_Bounds env(pRaster->GetExtent());
+ // Compute the acutal pixel size of returned raster
+ unsigned long actualCols = (unsigned long) (((env.maxx - env.minx) / (xMax - xMin)) * cols + 0.5);
+ unsigned long actualRows = (unsigned long) (((env.maxy - env.miny) / (yMax - yMin)) * rows + 0.5);
+ unsigned long colStartPos = (unsigned long) (((env.minx - xMin) / (xMax - xMin)) * cols + 0.5);
+ unsigned long rowStartPos = (unsigned long) (((yMax - env.maxy) / (yMax - yMin)) * rows + 0.5);
+
+ //pRaster->SetImageXSize(actualCols);
+ //pRaster->SetImageYSize(actualRows);
+ std::unique_ptr reader(pRaster->GetStream(/*this value is ignored*/RS_ImageFormat_ABGR, actualCols, actualRows));
+
+ if (reader)
{
- //FdoPtr dataModel = pRaster->GetDataModel();
- //FdoPtr byteArray = pRaster->GetBounds();
- //FdoPtr pAwkbFactory = FdoFgfGeometryFactory::GetInstance();
- //FdoPtr pGeometry = pAwkbFactory->CreateGeometryFromFgf(byteArray);
- //FdoPtr envelope = pGeometry->GetEnvelope();
- FdoInt32 bitPerPixel = pRaster->GetBitsPerPixel();
- FdoRasterDataModelType dataModelType = (FdoRasterDataModelType)pRaster->GetDataModelType();
- RS_Bounds env(pRaster->GetExtent());
- // Compute the acutal pixel size of returned raster
- unsigned long actualCols = (unsigned long) (((env.maxx - env.minx) / (xMax - xMin)) * cols + 0.5);
- unsigned long actualRows = (unsigned long) (((env.maxy - env.miny) / (yMax - yMin)) * rows + 0.5);
- unsigned long colStartPos = (unsigned long) (((env.minx - xMin) / (xMax - xMin)) * cols + 0.5);
- unsigned long rowStartPos = (unsigned long) (((yMax - env.maxy) / (yMax - yMin)) * rows + 0.5);
+ //FireOnStepQuery();
+ // Compute the row length of image. If bAlignment is true, bits size of each row must be a multiple of 8 byte.
+ unsigned long rowBitSize = (actualCols)*bitPerPixel;
+ unsigned long rowLength;
+ if (bAlignment)
+ rowLength = (rowBitSize%64)? ((rowBitSize + 64 - rowBitSize%64) / 8):(rowBitSize/8);
+ else
+ rowLength = rowBitSize/8;
+ unsigned long bufferLength = rowLength * actualRows;
- //pRaster->SetImageXSize(actualCols);
- //pRaster->SetImageYSize(actualRows);
- reader = pRaster->GetStream(/*this value is ignored*/RS_ImageFormat_ABGR, actualCols, actualRows);
+ if (reader->available() != bufferLength)
+ return;
- if (reader)
+ //Create an buffer to hold data
+ FdoByte* pRasterData = new FdoByte[bufferLength];
+ std::unique_ptr rasterData(pRasterData);
+ if (NULL == pRasterData)
{
- //FireOnStepQuery();
- // Compute the row length of image. If bAlignment is true, bits size of each row must be a multiple of 8 byte.
- unsigned long rowBitSize = (actualCols)*bitPerPixel;
- unsigned long rowLength;
- if (bAlignment)
- rowLength = (rowBitSize%64)? ((rowBitSize + 64 - rowBitSize%64) / 8):(rowBitSize/8);
- else
- rowLength = rowBitSize/8;
- unsigned long bufferLength = rowLength * actualRows;
+ throw FdoException::Create(FdoException::NLSGetMessage(FDO_NLSID(CLNT_5_OUTOFMEMORY)));
+ }
- if (reader->available() != bufferLength)
- {
- delete reader;
- return;
- }
+ reader->read(pRasterData, bufferLength);
- //Create an buffer to hold data
- pRasterData = new FdoByte[bufferLength];
- if (NULL == pRasterData)
- {
- throw FdoException::Create(FdoException::NLSGetMessage(FDO_NLSID(CLNT_5_OUTOFMEMORY)));
- }
+ // Get grid data type
+ Band::BandDataType gridDataType = GetGridDataType(pRaster);
+ GetUniqueBandName(pUniqueBandName, this, pBandName);
- reader->read(pRasterData, bufferLength);
+ pGisBand = CreateBand(gridDataType, pUniqueBandName);
+ FdoString* pVerticalUnit = pRaster->GetVerticalUnits().c_str();
+ pGisBand->SetUnitName(pVerticalUnit);
- // Get grid data type
- Band::BandDataType gridDataType = GetGridDataType(pRaster);
- GetUniqueBandName(pUniqueBandName, this, pBandName);
-
- pGisBand = CreateBand(gridDataType, pUniqueBandName);
- FdoString* pVerticalUnit = pRaster->GetVerticalUnits().c_str();
- pGisBand->SetUnitName(pVerticalUnit);
-
- // Set NULL value for Band
- if (dataModelType == FdoRasterDataModelType_Data)
+ // Set NULL value for Band
+ if (dataModelType == FdoRasterDataModelType_Data)
+ {
+ //TODO: Are we guaranteed
+ //that FLT_MAX will always indicate the NODATA value for all grids?
+ if (gridDataType == Band::Double32)
{
- //TODO: Are we guaranteed
- //that FLT_MAX will always indicate the NODATA value for all grids?
- if (gridDataType == Band::Double32)
- {
- pGisBand->SetNullValue(Band::Double32, (void*)&MG_NODATA_VALUE);
- }
- else
- {
- double nullval = DBL_MAX;
- pGisBand->SetNullValue(Band::Double64, (void*)&nullval);
- }
+ pGisBand->SetNullValue(Band::Double32, (void*)&MG_NODATA_VALUE);
}
- else if ((dataModelType == FdoRasterDataModelType_RGB)
- || (dataModelType == FdoRasterDataModelType_RGBA))
- {
- unsigned int nullValue = 0x00000000;
- pGisBand->SetAllToValue(gridDataType, (char*)&nullValue);
- }
else
{
- unsigned long nullValue(0);
- pGisBand->SetAllToValue(gridDataType, (char*)(&nullValue));
+ double nullval = DBL_MAX;
+ pGisBand->SetNullValue(Band::Double64, (void*)&nullval);
}
+ }
+ else if ((dataModelType == FdoRasterDataModelType_RGB)
+ || (dataModelType == FdoRasterDataModelType_RGBA))
+ {
+ unsigned int nullValue = 0x00000000;
+ pGisBand->SetAllToValue(gridDataType, (char*)&nullValue);
+ }
+ else
+ {
+ unsigned long nullValue(0);
+ pGisBand->SetAllToValue(gridDataType, (char*)(&nullValue));
+ }
- unsigned char* pCurPos;
- FdoRasterDataType rasterDataType = (FdoRasterDataType)pRaster->GetDataType();
- if ((dataModelType == FdoRasterDataModelType_RGB)
- || (dataModelType == FdoRasterDataModelType_RGBA))
+ unsigned char* pCurPos;
+ FdoRasterDataType rasterDataType = (FdoRasterDataType)pRaster->GetDataType();
+ if ((dataModelType == FdoRasterDataModelType_RGB)
+ || (dataModelType == FdoRasterDataModelType_RGBA))
+ {
+ unsigned int color;
+ if (24 == bitPerPixel)
{
- unsigned int color;
- if (24 == bitPerPixel)
+ for (unsigned long y = 0; y < actualRows; ++y)
{
- for (unsigned long y = 0; y < actualRows; ++y)
+ //FireOnStepQuery();
+ for (unsigned long x = 0; x < actualCols; ++x)
{
- //FireOnStepQuery();
- for (unsigned long x = 0; x < actualCols; ++x)
- {
- pCurPos = (unsigned char*)(pRasterData + y*rowLength + x*bitPerPixel/8 );
- color = ( 0xFF000000 | ((*(pCurPos)) << 16) | ((*(pCurPos + 1) )<< 8) | (*(pCurPos + 2)));
- pGisBand->SetValue(colStartPos + x, rowStartPos + y, Band::UnsignedInt32, &color );
- }
+ pCurPos = (unsigned char*)(pRasterData + y*rowLength + x*bitPerPixel/8 );
+ color = ( 0xFF000000 | ((*(pCurPos)) << 16) | ((*(pCurPos + 1) )<< 8) | (*(pCurPos + 2)));
+ pGisBand->SetValue(colStartPos + x, rowStartPos + y, Band::UnsignedInt32, &color );
}
}
- else if (48 == bitPerPixel) //BUG: 48 bpp should not be using a 32 bit grid size!
+ }
+ else if (48 == bitPerPixel) //BUG: 48 bpp should not be using a 32 bit grid size!
+ {
+ for (unsigned long y = 0; y < actualRows; ++y)
{
- for (unsigned long y = 0; y < actualRows; ++y)
+ //FireOnStepQuery();
+ for (unsigned long x = 0; x < actualCols; ++x)
{
- //FireOnStepQuery();
- for (unsigned long x = 0; x < actualCols; ++x)
- {
- pCurPos = (unsigned char*)(pRasterData + y*rowLength + x*bitPerPixel/8 );
- color = ( 0xFF000000 | ((*(pCurPos)) << 32) | ((*(pCurPos + 2)) << 16) | (*(pCurPos + 4)) << 8 );
- pGisBand->SetValue(colStartPos + x, rowStartPos + y, Band::UnsignedInt32, &color );//BUG: 48 bpp should not be using a 32 bit grid size!
- }
+ pCurPos = (unsigned char*)(pRasterData + y*rowLength + x*bitPerPixel/8 );
+ //The provider hands us 16 bit samples in native byte order, so read
+ //each RGB channel as a 16 bit value and reduce it to its high byte
+ UINT16 red = *(reinterpret_cast(pCurPos));
+ UINT16 green = *(reinterpret_cast(pCurPos + 2));
+ UINT16 blue = *(reinterpret_cast(pCurPos + 4));
+ color = ( 0xFF000000 | ((red >> 8) << 16) | ((green >> 8) << 8) | (blue >> 8) );
+ pGisBand->SetValue(colStartPos + x, rowStartPos + y, Band::UnsignedInt32, &color );//BUG: 48 bpp should not be using a 32 bit grid size!
}
}
- else //32 bpp
+ }
+ else //32 bpp
+ {
+ //allocate a row where we will do pixel conversion
+ std::unique_ptr row(new unsigned char[actualCols * 4]);
+
+ for (unsigned long y=0; ySetRowValue(0, y, Band::UnsignedInt32, (char*)row, actualCols);
+ pCurPos += 4;
+ pix += 4;
}
- delete [] row;
+ //set the whole row into the grid
+ pGisBand->SetRowValue(0, y, Band::UnsignedInt32, (char*)row.get(), actualCols);
}
}
- else if (dataModelType == FdoRasterDataModelType_Palette)
+ }
+ else if (dataModelType == FdoRasterDataModelType_Palette)
+ {
+ //FdoPtr propDict = pRaster->GetAuxiliaryProperties();
+ //FdoPtr pal = propDict->GetProperty(L"Palette");
+ //FdoLOBValue* palLOB = static_cast(pal.p);
+ struct RgbColor
{
- //FdoPtr propDict = pRaster->GetAuxiliaryProperties();
- //FdoPtr pal = propDict->GetProperty(L"Palette");
- //FdoLOBValue* palLOB = static_cast(pal.p);
- struct RgbColor
- {
- union {
- struct { FdoByte red; FdoByte green; FdoByte blue; FdoByte alpha; } rgba;
- unsigned int packed;
- };
+ union {
+ struct { FdoByte red; FdoByte green; FdoByte blue; FdoByte alpha; } rgba;
+ unsigned int packed;
};
+ };
- unsigned int color;
- unsigned int pos;
- if (bitPerPixel >= 8)
+ unsigned int color;
+ unsigned int pos;
+ if (bitPerPixel >= 8)
+ {
+ std::unique_ptr paletteStream(pRaster->GetPalette());
+ FdoByte* pPaletteBuf = new FdoByte[paletteStream->available()];
+ std::unique_ptr paletteBuf(pPaletteBuf);
+ if (NULL == pPaletteBuf)
{
- RS_InputStream* pStream = pRaster->GetPalette();
- pPaletteBuf = new FdoByte[pStream->available()];
- if (NULL == pPaletteBuf)
- {
- throw FdoException::Create(FdoException::NLSGetMessage(FDO_NLSID(CLNT_5_OUTOFMEMORY)));
- }
+ throw FdoException::Create(FdoException::NLSGetMessage(FDO_NLSID(CLNT_5_OUTOFMEMORY)));
+ }
- pStream->read(pPaletteBuf, pStream->available());
- RgbColor* palette = reinterpret_cast(pPaletteBuf);
+ paletteStream->read(pPaletteBuf, paletteStream->available());
+ RgbColor* palette = reinterpret_cast(pPaletteBuf);
- for (unsigned long y = 0; y < actualRows; ++y)
+ for (unsigned long y = 0; y < actualRows; ++y)
+ {
+ //FireOnStepQuery();
+ for (unsigned long x = 0; x< actualCols; ++x)
{
- //FireOnStepQuery();
- for (unsigned long x = 0; x< actualCols; ++x)
+ pCurPos = (unsigned char*)(pRasterData + y*rowLength + x*bitPerPixel/8 );
+ switch(bitPerPixel)
{
- pCurPos = (unsigned char*)(pRasterData + y*rowLength + x*bitPerPixel/8 );
- switch(bitPerPixel)
- {
- case 8:
- pos = *(reinterpret_cast(pCurPos));
- break;
- case 16:
- pos = *(reinterpret_cast(pCurPos));
- break;
- case 32:
- pos = *(reinterpret_cast(pCurPos));
- break;
- default:
- pos = 0;
- break;
- }
- color = 0xFF000000 | palette[pos].rgba.red << 16 | palette[pos].rgba.green << 8 | palette[pos].rgba.blue;
- pGisBand->SetValue(colStartPos + x, rowStartPos + y, Band::UnsignedInt32, &color );
+ case 8:
+ pos = *(reinterpret_cast(pCurPos));
+ break;
+ case 16:
+ pos = *(reinterpret_cast(pCurPos));
+ break;
+ case 32:
+ pos = *(reinterpret_cast(pCurPos));
+ break;
+ default:
+ pos = 0;
+ break;
}
+ color = 0xFF000000 | palette[pos].rgba.red << 16 | palette[pos].rgba.green << 8 | palette[pos].rgba.blue;
+ pGisBand->SetValue(colStartPos + x, rowStartPos + y, Band::UnsignedInt32, &color );
}
}
- else
+ }
+ else
+ {
+ char value, bitNum, temp;
+ unsigned long byteNum;
+
+ for (unsigned long y = 0; y < actualRows; ++y)
{
- char value, bitNum, temp;
- unsigned long byteNum;
-
- for (unsigned long y = 0; y < actualRows; ++y)
+ bitNum = 0;
+ value = 0;
+ temp = 0;
+ byteNum = 0;
+ //FireOnStepQuery();
+ for (unsigned long x = 0; x < actualCols; ++x)
{
- bitNum = 0;
- value = 0;
- temp = 0;
- byteNum = 0;
- //FireOnStepQuery();
- for (unsigned long x = 0; x < actualCols; ++x)
+ if (bitNum >= 8)
{
- if (bitNum >= 8)
- {
- bitNum = 0;
- byteNum++;
- }
- if (bitNum == 0)
- temp = (*((char*)(pRasterData + y*rowLength + byteNum)));
+ bitNum = 0;
+ byteNum++;
+ }
+ if (bitNum == 0)
+ temp = (*((char*)(pRasterData + y*rowLength + byteNum)));
- value = (char)((temp << (8 - bitNum - bitPerPixel)) >> (8 - bitPerPixel));
- pGisBand->SetValue(x, y, gridDataType, &value);
- bitNum += bitPerPixel;
- }
+ value = (char)((temp << (8 - bitNum - bitPerPixel)) >> (8 - bitPerPixel));
+ pGisBand->SetValue(x, y, gridDataType, &value);
+ bitNum += bitPerPixel;
}
}
}
- else if ((dataModelType == FdoRasterDataModelType_Data) &&
- ((rasterDataType == FdoRasterDataType_Integer) ||
- (rasterDataType == FdoRasterDataType_UnsignedInteger)))
+ }
+ else if ((dataModelType == FdoRasterDataModelType_Data) &&
+ ((rasterDataType == FdoRasterDataType_Integer) ||
+ (rasterDataType == FdoRasterDataType_UnsignedInteger)))
+ {
+ Band::BandDataType bandDataType = GetGridDataType(pRaster, false);
+ FdoDataType type = (FdoDataType)pRaster->GetNullValueType();
+ FdoPtr nullValue;
+ switch(type)
{
- Band::BandDataType bandDataType = GetGridDataType(pRaster, false);
- FdoDataType type = (FdoDataType)pRaster->GetNullValueType();
- FdoDataValue* nullValue = NULL;
- switch(type)
+ case FdoDataType_Byte:
+ nullValue = FdoByteValue::Create((FdoByte)pRaster->GetNullValueData());
+ break;
+ case FdoDataType_Double:
+ nullValue = FdoDoubleValue::Create((double)pRaster->GetNullValueData());
+ break;
+ case FdoDataType_Single:
+ nullValue = FdoSingleValue::Create((float)pRaster->GetNullValueData());
+ break;
+ case FdoDataType_Int16:
+ nullValue = FdoInt16Value::Create((FdoInt16)pRaster->GetNullValueData());
+ break;
+ case FdoDataType_Int32:
+ nullValue = FdoInt32Value::Create((FdoInt32)pRaster->GetNullValueData());
+ break;
+ case FdoDataType_Int64:
+ nullValue = FdoInt64Value::Create(pRaster->GetNullValueData());
+ break;
+ default: break;
+ }
+
+ switch (bandDataType)
+ {
+ case Band::UnsignedInt8:
{
- case FdoDataType_Double:
- nullValue = FdoDoubleValue::Create((double)pRaster->GetNullValueData());
+ READELEVATIONDATA(UINT8, FdoByteValue, GetByte, FdoDataType_Byte);
break;
- case FdoDataType_Single:
- nullValue = FdoSingleValue::Create((float)pRaster->GetNullValueData());
+ }
+ case Band::UnsignedInt16:
+ {
+ READELEVATIONDATA(UINT16, FdoInt16Value, GetInt16, FdoDataType_Int16);
break;
- case FdoDataType_Int16:
- nullValue = FdoInt16Value::Create((FdoInt16)pRaster->GetNullValueData());
+ }
+ case Band::UnsignedInt32:
+ {
+ READELEVATIONDATA(UINT32, FdoInt32Value, GetInt32, FdoDataType_Int32);
break;
- case FdoDataType_Int32:
- nullValue = FdoInt32Value::Create((FdoInt32)pRaster->GetNullValueData());
+ }
+ case Band::UnsignedInt64:
+ {
+ READELEVATIONDATA(UINT64, FdoInt64Value, GetInt64, FdoDataType_Int64);
break;
- case FdoDataType_Int64:
- nullValue = FdoInt64Value::Create(pRaster->GetNullValueData());
+ }
+ case Band::Int8:
+ {
+ READELEVATIONDATA(INT8, FdoByteValue, GetByte, FdoDataType_Byte);
break;
- default: break;
}
-
- switch (bandDataType)
+ case Band::Int16:
{
- case Band::UnsignedInt8:
- {
- READELEVATIONDATA(UINT8, FdoByteValue, GetByte);
- break;
- }
- case Band::UnsignedInt16:
- {
- READELEVATIONDATA(UINT16, FdoInt16Value, GetInt16);
- break;
- }
- case Band::UnsignedInt32:
- {
- READELEVATIONDATA(UINT32, FdoInt32Value, GetInt32);
- break;
- }
- case Band::UnsignedInt64:
- {
- READELEVATIONDATA(UINT64, FdoInt64Value, GetInt64);
- break;
- }
- case Band::Int8:
- {
- READELEVATIONDATA(INT8, FdoByteValue, GetByte);
- break;
- }
- case Band::Int16:
- {
- READELEVATIONDATA(INT16, FdoInt16Value, GetInt16);
- break;
- }
- case Band::Int32:
- {
- READELEVATIONDATA(INT32, FdoInt32Value, GetInt32);
- break;
- }
- case Band::Int64:
- {
- READELEVATIONDATA(INT64, FdoInt64Value, GetInt64);
- break;
- }
- case Band::Double32:
- {
- READELEVATIONDATA(float, FdoSingleValue, GetSingle);
- break;
- }
- case Band::Double64:
- {
- READELEVATIONDATA(double, FdoSingleValue, GetSingle);
- break;
- }
+ READELEVATIONDATA(INT16, FdoInt16Value, GetInt16, FdoDataType_Int16);
+ break;
}
- if(nullValue)
- nullValue->Release();
- }
- else
- {
- for (unsigned long y = 0; y < actualRows; ++y)
+ case Band::Int32:
{
- //FireOnStepQuery();
- pGisBand->SetRowValue(colStartPos, rowStartPos + y, gridDataType,
- (char*)(pRasterData + y*rowLength), actualCols);
+ READELEVATIONDATA(INT32, FdoInt32Value, GetInt32, FdoDataType_Int32);
+ break;
}
+ case Band::Int64:
+ {
+ READELEVATIONDATA(INT64, FdoInt64Value, GetInt64, FdoDataType_Int64);
+ break;
+ }
+ case Band::Double32:
+ {
+ READELEVATIONDATA(float, FdoSingleValue, GetSingle, FdoDataType_Single);
+ break;
+ }
+ case Band::Double64:
+ {
+ READELEVATIONDATA(double, FdoSingleValue, GetSingle, FdoDataType_Single);
+ break;
+ }
}
+ }
+ else
+ {
+ for (unsigned long y = 0; y < actualRows; ++y)
+ {
+ //FireOnStepQuery();
+ pGisBand->SetRowValue(colStartPos, rowStartPos + y, gridDataType,
+ (char*)(pRasterData + y*rowLength), actualCols);
+ }
+ }
- // TODO: When we get past the Mako crunch, we should revisit and probably remove this code.
- // Leaf, Brett and I agree that we should not completely remove this code at this
- // time. I changed it from 25000 to 30000, so we can map anything above the earth's
- // surface in feet (Mt Everest is 28717 feet in elevation). - lap - 3/10/06
- // When new version of FDO RFP, which modifies resampling algorithm for dem file to BSpline, is avaiable,
- // we will remove code to smooth elevation and use the following code to recover modified NULL value to its original value.
- // NOTE: The smoothing code has been removed.
- if (dataModelType == FdoRasterDataModelType_Data)
+ // TODO: When we get past the Mako crunch, we should revisit and probably remove this code.
+ // Leaf, Brett and I agree that we should not completely remove this code at this
+ // time. I changed it from 25000 to 30000, so we can map anything above the earth's
+ // surface in feet (Mt Everest is 28717 feet in elevation). - lap - 3/10/06
+ // When new version of FDO RFP, which modifies resampling algorithm for dem file to BSpline, is avaiable,
+ // we will remove code to smooth elevation and use the following code to recover modified NULL value to its original value.
+ // NOTE: The smoothing code has been removed.
+ if (dataModelType == FdoRasterDataModelType_Data)
+ {
+ float fNullValue = MG_NODATA_VALUE;
+ double dCurValue(0.0);
+ for (unsigned long y = rowStartPos; (y < actualRows + rowStartPos); ++y)
{
- float fNullValue = MG_NODATA_VALUE;
- double dCurValue(0.0);
- for (unsigned long y = rowStartPos; (y < actualRows + rowStartPos); ++y)
+ //FireOnStepQuery();
+ for (unsigned long x = colStartPos; (x < actualCols + colStartPos); ++x)
{
- //FireOnStepQuery();
- for (unsigned long x = colStartPos; (x < actualCols + colStartPos); ++x)
+ // If elevation value is greater than 30000, we will consider it a NULL value
+ bool ret = pGisBand->GetValueAsDouble(x, y, dCurValue);
+ if (!ret || (dCurValue > 30000.0)||(dCurValue < -30000.0))
{
- // If elevation value is greater than 30000, we will consider it a NULL value
- bool ret = pGisBand->GetValueAsDouble(x, y, dCurValue);
- if (!ret || (dCurValue > 30000.0)||(dCurValue < -30000.0))
- {
- pGisBand->SetValue(x, y, gridDataType, (char*)(&fNullValue));
- }
+ pGisBand->SetValue(x, y, gridDataType, (char*)(&fNullValue));
}
}
}
- else if (dataModelType == FdoRasterDataModelType_Bitonal)
- {
- //Fix DID 885517
- }
}
+ else if (dataModelType == FdoRasterDataModelType_Bitonal)
+ {
+ //Fix DID 885517
+ }
}
- catch(...)
- {
- delete [] pRasterData;
- delete [] pPaletteBuf;
- delete reader;
-
- throw;
- }
-
- delete [] pRasterData;
- delete [] pPaletteBuf;
- delete reader;
}
Band::BandDataType
Modified: branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-10-23 20:37:05 UTC (rev 10229)
+++ branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt 2026-10-23 22:11:40 UTC (rev 10230)
@@ -20,7 +20,6 @@
${MG_SERVER_SERVICES_DIR}/Kml
${MG_SERVER_SERVICES_DIR}/Tile
${FDO_INCLUDE_DIR}
- ${FDO_INCLUDE_DIR}/ExpressionEngine
)
set(MgUnitTesting_SRCS
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp 2026-10-23 20:37:05 UTC (rev 10229)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp 2026-10-23 22:11:40 UTC (rev 10230)
@@ -119,22 +119,16 @@
const Fixture FIXTURES[] =
{
- // These fixtures already render correctly from unmodified source on both
- // Windows and Linux: they only use code paths that are shared between the
- // platforms (SetRowValue plus the colour handler's memcpy fast path), so
- // they define the behavioural baseline the band fixes must not disturb.
- //
- // The 24/48 bpp RGB and palette fixtures are deliberately absent: their
- // bands are only filled by the 32 bit Band::SetValue path, which is a no-op
- // on Linux before the fixes. They are added, with their expected colours,
- // in the change that fixes those paths.
- //
- // The 64 bpp float fixture is absent for a different reason: before the
- // fixes its behaviour depends on the platform. Windows reads one byte of
- // the eight byte sample and paints an opaque black band, while Linux does
- // not style the band at all and the background shows through. It cannot be
- // part of a baseline that must pass on both, and is added by the change
- // that fixes the 8 byte BandData accessor.
+ // The 24/48 bpp branches and the palette branch pack an opaque ARGB value
+ // per pixel, so these fixtures render exactly the colour in the file.
+ { "RasterStylization_rgb24.tif", "24 bpp RGB",
+ { 0x12569A, 0xFF0000, 0x00FF01, 0x804010 } },
+ { "RasterStylization_rgb48.tif", "48 bpp RGB, high byte of each 16 bit sample",
+ { 0x12569A, 0xFF0000, 0x00FF01, 0x804010 } },
+ { "RasterStylization_rgb48_int16.tif", "48 bpp signed RGB, same bit patterns",
+ { 0x12569A, 0xFF0000, 0x00FF01, 0x804010 } },
+ { "RasterStylization_palette8.tif", "8 bpp palette",
+ { 0x12569A, 0x0000FF, 0x00FF01, 0x804010 } },
// The 32 bpp branch copies the source pixels through unchanged, so the
// source alpha survives and the renderer blends the colour over the white
@@ -160,6 +154,10 @@
// pattern happens to contain (0x45, 0x46, 0x46, 0x40).
{ "RasterStylization_gray32f.tif", "32 bpp float single band, samples read as ARGB",
{ 0xE1E5B9, 0xE8FAB8, 0xBFECB7, 0xDDBBBB } },
+ // Doubles are read as unsigned 64 bit samples, which is far outside the
+ // range a packed ARGB value can hold, so no pixel is coloured.
+ { "RasterStylization_gray64f.tif", "64 bpp float single band, not styled",
+ { 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF } },
};
// Real rasters derived from the FDO GDAL provider test data
@@ -173,10 +171,6 @@
// multiple of 64 falls through to RasterAdapter's legacy decoder instead, which
// is not the code under test here - and which has no 16 bpp case at all.
//
-// The palette sheet is absent for the same reason as the palette fixture above:
-// its colour band is only filled through the 32 bit Band::SetValue path, so it
-// renders blank from unmodified source on Linux and is added with the fixes.
-//
// Nothing here has an expected colour: the point is that genuine imagery and
// elevation data survive the pipeline unaltered, so each fixture is rendered to
// ../UnitTestFiles/Results/RealRaster_.png for a human to look at, and the
@@ -214,6 +208,8 @@
StyleGrayBands, 0, 0 },
{ "RealRaster_spif83.tif", "8 bit grey, NAD83 / California zone 6, ~195 m",
StyleGrayBands, 0, 0 },
+ { "RealRaster_cm13ct.tif", "8 bit palette scanned map, UTM 13N",
+ StyleBand, 0, 0 },
// The DEM holds metres (nodata is -32767), so it needs scaling to become a
// visible grey ramp; 0-3000 m covers the whole tile.
{ "RealRaster_mtjefferson.tif", "Int16 USGS DEM as a 0-3000 m elevation ramp",
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp 2026-10-23 20:37:05 UTC (rev 10229)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp 2026-10-23 22:11:40 UTC (rev 10230)
@@ -163,15 +163,65 @@
return band->GetValue(0, 0, Band::UnsignedInt32, &color);
}
-// The read path rounds each row up to a 64 bit boundary when it is told the
-// provider pads its rows, and gives up when the stream is shorter than that.
-// This is behavioural baseline: it holds on Windows and Linux, before and after
-// the band accessor work.
-TEST_CASE("Grid colour band row alignment", "[Stylization]")
+// Runs a 16 bpp single component Data raster through ReadRaster and reads back
+// pixel (0, 0) of the elevation band. Returns false when no Double32 band was
+// created.
+bool ReadElevationBandPixel(const std::vector& bytes,
+ int nullValueType,
+ int64_t nullValueData,
+ double& value)
{
const std::vector noPalette;
+ GridData grid(Point2D(0.0, 0.0), 4.0, 4.0, 4, 4);
+ TestRaster raster(16, FdoRasterDataModelType_Data, FdoRasterDataType_UnsignedInteger,
+ bytes, noPalette, 4, 4, nullValueType, nullValueData);
+
+ grid.ReadRaster(&raster, L"1", 4, 4, 0.0, 0.0, 4.0, 4.0, false);
+
+ Band* band = grid.GetBand((unsigned int)0);
+ if (NULL == band || Band::Double32 != band->GetDataType())
+ return false;
+
+ // GetValue, not GetValueAsDouble: a Data model raster sets the band's null
+ // value to MG_NODATA, so MG_NODATA samples read back as "no value".
+ float sample = 0.0f;
+ if (!band->GetValue(0, 0, Band::Double32, &sample))
+ return false;
+
+ value = (double)sample;
+ return true;
+}
+
+}
+
+TEST_CASE("Grid colour band packing", "[Stylization]")
+{
+ const std::vector noPalette;
unsigned int color = 0;
+ SECTION("48 bpp RGB reduces 16 bit samples to their high byte")
+ {
+ // R = 0x1234, G = 0x5678, B = 0x9ABC, little endian
+ const unsigned char pixel[] = { 0x34, 0x12, 0x78, 0x56, 0xBC, 0x9A };
+
+ REQUIRE(ReadColourBandPixel(RepeatPixel(pixel, 6, 16), 48, FdoRasterDataModelType_RGB,
+ 4, 4, false, noPalette, color));
+ REQUIRE(0xFF12569Au == color);
+ }
+
+ SECTION("48 bpp RGB discards the low byte of each channel")
+ {
+ const unsigned char white[] = { 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 }; // 0x00FF per channel
+ REQUIRE(ReadColourBandPixel(RepeatPixel(white, 6, 16), 48, FdoRasterDataModelType_RGB,
+ 4, 4, false, noPalette, color));
+ REQUIRE(0xFF000000u == color);
+
+ const unsigned char almostBlack[] = { 0x00, 0x01, 0x00, 0x01, 0x00, 0x01 }; // 0x0100 per channel
+ REQUIRE(ReadColourBandPixel(RepeatPixel(almostBlack, 6, 16), 48, FdoRasterDataModelType_RGB,
+ 4, 4, false, noPalette, color));
+ REQUIRE(0xFF010101u == color);
+ }
+
SECTION("48 bpp RGB with a row length the provider does not pad produces no band")
{
// 6 pixels * 6 bytes = 36 bytes per row, but bAlignment rounds the
@@ -181,144 +231,175 @@
REQUIRE_FALSE(ReadColourBandPixel(RepeatPixel(pixel, 6, 24), 48, FdoRasterDataModelType_RGB,
6, 4, true, noPalette, color));
}
-}
-// Band creation and the narrow (8 and 16 bit) accessors are implemented the
-// same way on both platforms, so they are baseline behaviour. The 32 and 64 bit
-// accessors are covered by the change that implements them on Linux.
-TEST_CASE("Band creation", "[Stylization]")
-{
- GridData grid(Point2D(0.0, 0.0), 3.0, 3.0, 3, 3);
+ SECTION("24 bpp RGB")
+ {
+ const unsigned char pixel[] = { 0x11, 0x22, 0x33 };
- SECTION("UnsignedInt32") { REQUIRE(NULL != grid.CreateBand(Band::UnsignedInt32, L"u32")); }
- SECTION("Int32") { REQUIRE(NULL != grid.CreateBand(Band::Int32, L"i32")); }
- SECTION("Double32") { REQUIRE(NULL != grid.CreateBand(Band::Double32, L"f32")); }
- SECTION("UnsignedInt64") { REQUIRE(NULL != grid.CreateBand(Band::UnsignedInt64, L"u64")); }
- SECTION("Int64") { REQUIRE(NULL != grid.CreateBand(Band::Int64, L"i64")); }
- SECTION("Double64") { REQUIRE(NULL != grid.CreateBand(Band::Double64, L"f64")); }
- SECTION("Boolean") { REQUIRE(NULL != grid.CreateBand(Band::Boolean, L"b")); }
- SECTION("Bit2") { REQUIRE(NULL != grid.CreateBand(Band::Bit2, L"b2")); }
- SECTION("Bit4") { REQUIRE(NULL != grid.CreateBand(Band::Bit4, L"b4")); }
+ REQUIRE(ReadColourBandPixel(RepeatPixel(pixel, 3, 16), 24, FdoRasterDataModelType_RGB,
+ 4, 4, false, noPalette, color));
+ REQUIRE(0xFF112233u == color);
+ }
+
+ SECTION("32 bpp RGBA keeps the source alpha")
+ {
+ const unsigned char pixel[] = { 0x11, 0x22, 0x33, 0x80 };
+
+ REQUIRE(ReadColourBandPixel(RepeatPixel(pixel, 4, 16), 32, FdoRasterDataModelType_RGBA,
+ 4, 4, false, noPalette, color));
+ REQUIRE(0x80112233u == color);
+ }
+
+ SECTION("8 bpp palette")
+ {
+ const std::vector palette =
+ {
+ 255, 0, 0, 255, // 0 red
+ 0, 255, 0, 255, // 1 green
+ 0, 0, 255, 255, // 2 blue
+ 255, 255, 255, 255 // 3 white
+ };
+ const unsigned char index = 2;
+
+ REQUIRE(ReadColourBandPixel(RepeatPixel(&index, 1, 16), 8, FdoRasterDataModelType_Palette,
+ 4, 4, false, palette, color));
+ REQUIRE(0xFF0000FFu == color);
+ }
}
-TEST_CASE("Band numeric accessors for narrow types", "[Stylization]")
+TEST_CASE("Grid elevation band", "[Stylization]")
{
- GridData grid(Point2D(0.0, 0.0), 3.0, 3.0, 3, 3);
-
- SECTION("UnsignedInt8")
+ // 16 bpp single component Data model: exercises the READELEVATIONDATA path,
+ // including ownership and validation of the null value it is given.
+ std::vector bytes;
+ for (int i = 0; i < 16; ++i)
{
- Band* band = grid.CreateBand(Band::UnsignedInt8, L"u8");
- REQUIRE(NULL != band);
+ bytes.push_back(0x34);
+ bytes.push_back(0x12); // 0x1234, little endian
+ }
- INT8 value = 100;
- REQUIRE(band->SetValue(1, 1, Band::UnsignedInt8, &value));
+ double value = 0.0;
- INT8 readBack = 0;
- REQUIRE(band->GetValue(1, 1, Band::UnsignedInt8, &readBack));
- REQUIRE(value == readBack);
- REQUIRE(100.0 == band->GetValueAsDouble(1, 1));
+ SECTION("supported null value type")
+ {
+ REQUIRE(ReadElevationBandPixel(bytes, FdoDataType_Int16, 0, value));
+ REQUIRE(4660.0 == value);
}
- SECTION("UnsignedInt8 above 0x7F reads back negative")
+ SECTION("sample equal to the null value becomes MG_NODATA")
{
- // Pre-existing behaviour, recorded so a change to it is visible: the
- // unsigned 8 bit case in Band::GetValueAsDouble loads the sample into an
- // INT8, so anything from 0x80 up is sign extended. This is why paletted
- // and grey rasters with high sample values behave oddly.
- Band* band = grid.CreateBand(Band::UnsignedInt8, L"u8high");
- REQUIRE(NULL != band);
+ REQUIRE(ReadElevationBandPixel(bytes, FdoDataType_Int16, 4660, value));
+ REQUIRE(FLT_MAX == (float)value);
+ }
- INT8 value = (INT8)200;
- REQUIRE(band->SetValue(1, 1, Band::UnsignedInt8, &value));
+ SECTION("null value of an unsupported type keeps every sample")
+ {
+ REQUIRE(ReadElevationBandPixel(bytes, FdoDataType_DateTime, 0, value));
+ REQUIRE(4660.0 == value);
+ }
- INT8 readBack = 0;
- REQUIRE(band->GetValue(1, 1, Band::UnsignedInt8, &readBack));
- REQUIRE(value == readBack);
- REQUIRE(-56.0 == band->GetValueAsDouble(1, 1));
+ SECTION("null value type that does not match the reader keeps every sample")
+ {
+ REQUIRE(ReadElevationBandPixel(bytes, FdoDataType_Double, 0, value));
+ REQUIRE(4660.0 == value);
}
+}
- SECTION("Int8")
+TEST_CASE("Band numeric accessors", "[Stylization]")
+{
+ GridData grid(Point2D(0.0, 0.0), 3.0, 3.0, 3, 3);
+
+ SECTION("UnsignedInt32")
{
- Band* band = grid.CreateBand(Band::Int8, L"i8");
+ Band* band = grid.CreateBand(Band::UnsignedInt32, L"color");
REQUIRE(NULL != band);
- INT8 value = -100;
- REQUIRE(band->SetValue(2, 0, Band::Int8, &value));
+ unsigned int value = 0xFF12569A;
+ REQUIRE(band->SetValue(1, 1, Band::UnsignedInt32, &value));
- INT8 readBack = 0;
- REQUIRE(band->GetValue(2, 0, Band::Int8, &readBack));
+ unsigned int readBack = 0;
+ REQUIRE(band->GetValue(1, 1, Band::UnsignedInt32, &readBack));
REQUIRE(value == readBack);
- REQUIRE(-100.0 == band->GetValueAsDouble(2, 0));
+ REQUIRE(4279391898.0 == band->GetValueAsDouble(1, 1));
}
- SECTION("UnsignedInt16")
+ SECTION("Int32")
{
- Band* band = grid.CreateBand(Band::UnsignedInt16, L"u16");
+ Band* band = grid.CreateBand(Band::Int32, L"int");
REQUIRE(NULL != band);
- INT16 value = (INT16)60000;
- REQUIRE(band->SetValue(0, 2, Band::UnsignedInt16, &value));
+ int value = -123456;
+ REQUIRE(band->SetValue(2, 0, Band::Int32, &value));
- INT16 readBack = 0;
- REQUIRE(band->GetValue(0, 2, Band::UnsignedInt16, &readBack));
+ int readBack = 0;
+ REQUIRE(band->GetValue(2, 0, Band::Int32, &readBack));
REQUIRE(value == readBack);
- REQUIRE(60000.0 == band->GetValueAsDouble(0, 2));
+ REQUIRE(-123456.0 == band->GetValueAsDouble(2, 0));
}
- SECTION("Int16")
+ SECTION("Double32")
{
- Band* band = grid.CreateBand(Band::Int16, L"i16");
+ Band* band = grid.CreateBand(Band::Double32, L"float");
REQUIRE(NULL != band);
- INT16 value = -12345;
- REQUIRE(band->SetValue(1, 0, Band::Int16, &value));
+ float value = 1234.5f;
+ REQUIRE(band->SetValue(0, 2, Band::Double32, &value));
- INT16 readBack = 0;
- REQUIRE(band->GetValue(1, 0, Band::Int16, &readBack));
+ float readBack = 0.0f;
+ REQUIRE(band->GetValue(0, 2, Band::Double32, &readBack));
REQUIRE(value == readBack);
- REQUIRE(-12345.0 == band->GetValueAsDouble(1, 0));
+ REQUIRE(1234.5 == band->GetValueAsDouble(0, 2));
}
- SECTION("Boolean")
+ SECTION("Double64")
{
- Band* band = grid.CreateBand(Band::Boolean, L"bool");
+ Band* band = grid.CreateBand(Band::Double64, L"double");
REQUIRE(NULL != band);
- INT8 value = 1;
- REQUIRE(band->SetValue(0, 0, Band::Boolean, &value));
+ double value = 1.0 / 3.0;
+ REQUIRE(band->SetValue(1, 0, Band::Double64, &value));
- INT8 readBack = 0;
- REQUIRE(band->GetValue(0, 0, Band::Boolean, &readBack));
+ double readBack = 0.0;
+ REQUIRE(band->GetValue(1, 0, Band::Double64, &readBack));
REQUIRE(value == readBack);
- REQUIRE(1.0 == band->GetValueAsDouble(0, 0));
+ REQUIRE(value == band->GetValueAsDouble(1, 0));
}
- SECTION("Bit2")
+ SECTION("Int64")
{
- Band* band = grid.CreateBand(Band::Bit2, L"bit2");
+ Band* band = grid.CreateBand(Band::Int64, L"long");
REQUIRE(NULL != band);
- INT8 value = 3;
- REQUIRE(band->SetValue(2, 2, Band::Bit2, &value));
+ INT64 value = 0x0102030405060708LL;
+ REQUIRE(band->SetValue(0, 1, Band::Int64, &value));
- INT8 readBack = 0;
- REQUIRE(band->GetValue(2, 2, Band::Bit2, &readBack));
+ INT64 readBack = 0;
+ REQUIRE(band->GetValue(0, 1, Band::Int64, &readBack));
REQUIRE(value == readBack);
- REQUIRE(3.0 == band->GetValueAsDouble(2, 2));
}
+}
- SECTION("Bit4")
+TEST_CASE("Band slope from neighbouring values", "[Stylization]")
+{
+ GridData grid(Point2D(0.0, 0.0), 3.0, 3.0, 3, 3);
+ Band* elevation = grid.CreateBand(Band::Double32, L"elevation");
+ REQUIRE(NULL != elevation);
+
+ // A ramp of one unit per cell: at the centre pixel the neighbours are
+ // left = 0, right = 2, top = bottom = 1, so the slope is 45 degrees.
+ for (unsigned int y = 0; y < 3; ++y)
{
- Band* band = grid.CreateBand(Band::Bit4, L"bit4");
- REQUIRE(NULL != band);
+ for (unsigned int x = 0; x < 3; ++x)
+ {
+ float z = (float)x;
+ REQUIRE(elevation->SetValue(x, y, Band::Double32, &z));
+ }
+ }
- INT8 value = 9;
- REQUIRE(band->SetValue(1, 2, Band::Bit4, &value));
+ const Band* slope = elevation->GetSlopeBand();
+ REQUIRE(NULL != slope);
- INT8 readBack = 0;
- REQUIRE(band->GetValue(1, 2, Band::Bit4, &readBack));
- REQUIRE(value == readBack);
- REQUIRE(9.0 == band->GetValueAsDouble(1, 2));
- }
+ double angle = slope->GetValueAsDouble(1, 1);
+ REQUIRE(44.9 < angle);
+ REQUIRE(45.1 > angle);
}
-}
From svn_mapguide at osgeo.org Fri Oct 23 18:07:41 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Fri, 23 Oct 2026 18:07:41 -0700
Subject: [mapguide-commits] r10231 - in branches/4.0/MgDev:
Server/src/UnitTesting UnitTest/TestData/RasterStylization
Message-ID: <20261024010742.164DB3D16E@trac.osgeo.org>
Author: jng
Date: 2026-10-23 18:07:41 -0700 (Fri, 23 Oct 2026)
New Revision: 10231
Added:
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif
Modified:
branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
Log:
#2174: Add failing test for striped raster rendering. Test raster from TrueMarble
Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp 2026-10-23 22:11:40 UTC (rev 10230)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp 2026-10-24 01:07:41 UTC (rev 10231)
@@ -460,6 +460,65 @@
return wkt;
}
+// RAII guard that raises the max raster image size for the lifetime of the
+// object and restores the previous values on destruction. The rendering service
+// reads the setting from configuration when it is constructed, so this must be
+// created before the service. The destructor restores the values even when a
+// REQUIRE aborts the test case early, so later cases keep their defaults.
+struct MaxRasterImageGuard
+{
+ MaxRasterImageGuard()
+ : m_oldWidth(MgConfigProperties::DefaultRenderingServicePropertyMaxRasterImageWidth),
+ m_oldHeight(MgConfigProperties::DefaultRenderingServicePropertyMaxRasterImageHeight)
+ {
+ MgConfiguration* config = MgConfiguration::GetInstance();
+ if (NULL != config)
+ {
+ config->GetIntValue(MgConfigProperties::RenderingServicePropertiesSection,
+ MgConfigProperties::RenderingServicePropertyMaxRasterImageWidth,
+ m_oldWidth, MgConfigProperties::DefaultRenderingServicePropertyMaxRasterImageWidth);
+ config->GetIntValue(MgConfigProperties::RenderingServicePropertiesSection,
+ MgConfigProperties::RenderingServicePropertyMaxRasterImageHeight,
+ m_oldHeight, MgConfigProperties::DefaultRenderingServicePropertyMaxRasterImageHeight);
+
+ Set(config, 8192, 8192);
+ }
+ }
+
+ ~MaxRasterImageGuard()
+ {
+ MgConfiguration* config = MgConfiguration::GetInstance();
+ if (NULL != config)
+ Set(config, m_oldWidth, m_oldHeight);
+ }
+
+ static void Set(MgConfiguration* config, INT32 width, INT32 height)
+ {
+ // MgConfiguration::SetProperties only accepts string properties: every
+ // entry in the ini file is stored as a string, and an Int32 property
+ // makes it throw MgInvalidPropertyTypeException.
+ STRING widthValue;
+ STRING heightValue;
+ MgUtil::Int32ToString(width, widthValue);
+ MgUtil::Int32ToString(height, heightValue);
+
+ Ptr widthProp = new MgStringProperty();
+ widthProp->SetName(MgConfigProperties::RenderingServicePropertyMaxRasterImageWidth);
+ widthProp->SetValue(widthValue);
+ Ptr heightProp = new MgStringProperty();
+ heightProp->SetName(MgConfigProperties::RenderingServicePropertyMaxRasterImageHeight);
+ heightProp->SetValue(heightValue);
+
+ Ptr props = new MgPropertyCollection();
+ props->Add(widthProp);
+ props->Add(heightProp);
+ config->SetProperties(MgConfigProperties::RenderingServicePropertiesSection, props);
+ }
+
+ INT32 m_oldWidth;
+ INT32 m_oldHeight;
+};
+
}
TEST_CASE("Raster grid styling", "[RasterStylization]")
@@ -654,3 +713,139 @@
MgFdoConnectionManager::GetInstance()->ClearCache();
}
}
+
+TEST_CASE("TrueMarble striped rendering", "[RasterStylization]")
+{
+ // Reproducer for the GDAL provider "striped" rendering defect, using a
+ // single checked-in tile of the free "Global True Marble" dataset (see
+ // UnitTest/TestData/RasterStylization/PROVENANCE.md). The tile itself is a
+ // 4.5 MB GeoTIFF; the full dataset is ~70 GB, which is why only this one
+ // tile is checked in.
+ //
+ // It requests the small 32 km overview tile (1350x675, 3 x Byte) at
+ // 4000x2000, an upscale that crosses the provider's hard-coded 4 MP
+ // scanline-tiling threshold (FdoRfpBandRaster::GetDataModel). Above that
+ // threshold the provider reads one scanline per tile, and the per-row
+ // source window in FdoRfpStreamReaderByTile::_getTile rounds to zero
+ // height for some rows when upsampling. Those rows are never overwritten,
+ // so they keep the tile's 0xFF prefill and survive the pipeline as whole
+ // rows of pure white: the stripes.
+ //
+ // The defect is in the provider's output stream, so it can be detected
+ // objectively without a human looking at the render: real imagery never
+ // contains a full-width, exactly-white row. For this 4000x2000 upscale the
+ // per-row source window rounds to zero height for a large fraction of the
+ // output rows (653 of 2000 in the actual Release run), and GDAL treats a
+ // zero-height window as a no-op, leaving the 0xFF prefill. The PNG is
+ // still written to Results/ for visual confirmation.
+
+ // Raise the max raster image size so the 4000x2000 request is not halved by
+ // RasterAdapter before it reaches the provider (the halving loop halves any
+ // image >= MaxRasterImageWidth/Height). Each rendering service reads this
+ // from configuration when it is constructed, so the guard is created before
+ // the service and restores the values when it goes out of scope.
+ MaxRasterImageGuard rasterGuard;
+
+ Ptr svcSite = TestServiceFactory::CreateSiteService();
+ Ptr siteConn = TestServiceFactory::CreateSiteConnection(svcSite);
+ Ptr svcResource = TestServiceFactory::CreateResourceService();
+ Ptr svcRendering = TestServiceFactory::CreateRenderingService();
+ Ptr svcFeature = dynamic_cast(
+ MgServiceManager::GetInstance()->RequestService(MgServiceType::FeatureService));
+
+ const double dpi = 96.0;
+ const double metersPerInch = 0.0254;
+ const int renderWidth = 4000;
+ const int renderHeight = 2000;
+
+ const STRING name = L"TrueMarble_striped";
+ STRING fsId = L"Library://RasterStylizationReal/Data/" + name + L".FeatureSource";
+ STRING layerId = L"Library://RasterStylizationReal/Layers/" + name + L".LayerDefinition";
+ STRING mapId = L"Library://RasterStylizationReal/Maps/" + name + L".MapDefinition";
+
+ STRING rasterProperty;
+ STRING featureName = PublishFeatureSource(svcResource, svcFeature, fsId,
+ Narrow(FixturePath("RealRaster_truemarble32.tif")),
+ rasterProperty);
+
+ // The raster's own footprint drives the map view, so the image shows the
+ // whole file (stretched to the 4000x2000 request, which is the upscale that
+ // triggers the defect).
+ Ptr rasterExtent;
+ INT32 imageXSize = 0;
+ INT32 imageYSize = 0;
+ REQUIRE(GetRasterFootprint(svcFeature, fsId, featureName, rasterProperty,
+ rasterExtent, imageXSize, imageYSize));
+
+ double minX = rasterExtent->GetLowerLeftCoordinate()->GetX();
+ double minY = rasterExtent->GetLowerLeftCoordinate()->GetY();
+ double maxX = rasterExtent->GetUpperRightCoordinate()->GetX();
+ double maxY = rasterExtent->GetUpperRightCoordinate()->GetY();
+
+ STRING csWkt = GetRasterCoordinateSystem(svcFeature, fsId);
+ if (csWkt.empty())
+ csWkt = LL84_WKT;
+
+ // The 3 x Byte tile reads through GridData::ReadRaster's 24 bpp branch,
+ // which packs the RGB samples into one ARGB value, so the plain
+ // colour style passes it through unchanged.
+ PublishResource(svcResource, layerId, LayerXml(fsId, featureName, rasterProperty, StyleBand));
+ PublishResource(svcResource, mapId, MapXml(layerId, csWkt, minX, minY, maxX, maxY));
+
+ Ptr mapRes = new MgResourceIdentifier(mapId);
+ Ptr map = new MgMap(siteConn);
+ map->Create(mapRes, L"RasterStylizationMap");
+
+ Ptr centre = new MgCoordinateXY((minX + maxX) / 2.0, (minY + maxY) / 2.0);
+ Ptr centrePoint = new MgPoint(centre);
+ map->SetViewCenter(centrePoint);
+ map->SetDisplayDpi((INT32)dpi);
+ map->SetDisplayWidth(renderWidth);
+ map->SetDisplayHeight(renderHeight);
+ // One image pixel spans (maxX - minX) / renderWidth map units, so the
+ // requested raster size comes out at renderWidth x renderHeight.
+ map->SetViewScale(((maxX - minX) / renderWidth) * map->GetMetersPerUnit() * dpi / metersPerInch);
+
+ int width = 0;
+ int height = 0;
+ unsigned int* rawPixels = RenderPng(svcRendering, map, width, height, name + L".png");
+ REQUIRE(rawPixels != NULL);
+ // DecodePNG hands back a buffer that has to be released with delete[].
+ std::unique_ptr pixels(reinterpret_cast(rawPixels));
+ REQUIRE(width == renderWidth);
+ REQUIRE(height == renderHeight);
+
+ // Count rows that are exactly opaque white. A striped row is the provider's
+ // 0xFF prefill passed through the 24 bpp branch, which packs it to
+ // 0xFFFFFFFF. DecodePNG returns the image bottom up, which does not matter
+ // for a per-row count.
+ int whiteRows = 0;
+ for (int y = 0; y < height; ++y)
+ {
+ bool allWhite = true;
+ for (int x = 0; x < width; ++x)
+ {
+ if ((rawPixels[y * width + x] & 0x00FFFFFF) != 0xFFFFFF)
+ {
+ allWhite = false;
+ break;
+ }
+ }
+ if (allWhite)
+ ++whiteRows;
+ }
+
+ ACE_DEBUG((LM_INFO, ACE_TEXT(" wrote %W (%dx%d from %dx%d), %d uniformly white rows\n"),
+ ResultPath(name + L".png").c_str(), width, height, imageXSize, imageYSize, whiteRows));
+
+ INFO("rendered " << width << "x" << height << " with " << whiteRows
+ << " uniformly white rows out of " << height);
+ // Real imagery never contains a full-width, exactly-white row. Allow a tiny
+ // tolerance for safety; the defect produces on the order of a third of the
+ // image height in white rows, so this fails hard while the bug is present.
+ CHECK(whiteRows <= height / 100);
+
+ // Release the pooled raster connection now, for the same reason as the
+ // other cases in this suite.
+ MgFdoConnectionManager::GetInstance()->ClearCache();
+}
Modified: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md 2026-10-23 22:11:40 UTC (rev 10230)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md 2026-10-24 01:07:41 UTC (rev 10231)
@@ -325,6 +325,30 @@
5. Record the upstream path, hash, size, derivation command and what the file is
in the tables above.
+## The True Marble tile (`RealRaster_truemarble32.tif`)
+
+This one is not derived from the FDO provider test data. It is a single tile of
+"Global True Marble", the free reduced-resolution release of Unearthed
+Outdoors' True Marble global colour imagery. It drives the `[RasterStylization]`
+"TrueMarble striped rendering" case, which reproduces the GDAL provider's
+striped upsampling defect: that case renders the 1350x675 tile at 4000x2000, an
+upscale that crosses the provider's 4 MP scanline-tiling threshold and leaves
+whole rows as the provider's 0xFF prefill.
+
+| Property | Value |
+|----------|-------|
+| Origin | Unearthed Outdoors, LLC - "Global True Marble", 32 km resolution global tile |
+| Source file | `TrueMarble.32km.1350x675.tif` from the free Global True Marble download |
+| Licence | Creative Commons Attribution 3.0 United States (`License.html` in the download) |
+| Attribution | "We only ask that copyright be attributed to Unearthed Outdoors when reproduced." |
+| Size | 1350x675 px, 4,719,372 bytes, 3 x Byte (RGB), WGS84 geographic |
+| Reprocessing | None - checked in byte for byte as downloaded |
+
+The requested render width is 4000 pixels, so `width * bitsPerPixel` is
+`4000 * 24 = 96000`, a multiple of 64, and the tile still reaches the
+`GridData::ReadRaster` 24 bpp branch this suite exists to cover - the 64-bit
+alignment constraint is on the *requested* width, not the file's native width.
+
## File inventory
| File | Bytes | SHA-256 |
@@ -333,6 +357,7 @@
| `RealRaster_cm13ct.tif` | 2,287,461 | `998430170a82cabf8cad85aa0cf876e7765f35fa01fb4d7f6e252612c5f1fb0f` |
| `RealRaster_mtjefferson.tif` | 315,773 | `e2b7016d355f3cc17e586cd4c834e63dfe0008253443f8eb54330c4339606f1f` |
| `RealRaster_spif83.tif` | 262,871 | `49e48c375e721bea9a72d19cfec2a48641b83aa6951b97393d8a4ad992394310` |
+| `RealRaster_truemarble32.tif` | 4,719,372 | `dd9de7a8486350c795b81a350f202c649c4b40727af1a3cd391f93b33af1c72f` |
The derivation commands use GDAL 3.12.4 (the toolchain in Part 1); a different
GDAL version produces a different (still lossless) encoding and therefore a
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif
===================================================================
(Binary files differ)
Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif 2026-10-23 22:11:40 UTC (rev 10230)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif 2026-10-24 01:07:41 UTC (rev 10231)
Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
From svn_mapguide at osgeo.org Sun Oct 25 02:29:00 2026
From: svn_mapguide at osgeo.org (svn_mapguide at osgeo.org)
Date: Sun, 25 Oct 2026 02:29:00 -0700
Subject: [mapguide-commits] r10232 - in branches/4.0/MgDev:
Common/Stylization Portable/MgPortable/Services/Feature
Server/src/Services/Feature Server/src/UnitTesting
UnitTest/TestData/RasterStylization
Message-ID: <20261025092901.91F1047F9E@trac.osgeo.org>
Author: jng
Date: 2026-10-25 02:28:58 -0700 (Sun, 25 Oct 2026)
New Revision: 10232
Modified:
branches/4.0/MgDev/Common/Stylization/DefaultStylizer.cpp
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionArgb.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionDecap.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureClass.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureId.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureSource.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionHtmlColor.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionIf.cpp
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionIf.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLayerDefinition.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLayerId.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLookup.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapCenterX.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapCenterY.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapName.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapScale.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionRange.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionSession.h
branches/4.0/MgDev/Common/Stylization/ExpressionFunctionUrlEncode.h
branches/4.0/MgDev/Common/Stylization/ExpressionHelper.cpp
branches/4.0/MgDev/Common/Stylization/GeometryAdapter.cpp
branches/4.0/MgDev/Common/Stylization/RasterAdapter.cpp
branches/4.0/MgDev/Common/Stylization/RasterAdapter.h
branches/4.0/MgDev/Common/Stylization/RichTextEngine.cpp
branches/4.0/MgDev/Common/Stylization/SE_ExpressionBase.h
branches/4.0/MgDev/Common/Stylization/Stylization.h
branches/4.0/MgDev/Common/Stylization/StylizationEngine.cpp
branches/4.0/MgDev/Common/Stylization/StylizationUtil.cpp
branches/4.0/MgDev/Portable/MgPortable/Services/Feature/GwsFeatureReader.cpp
branches/4.0/MgDev/Server/src/Services/Feature/ServerGwsFeatureReader.cpp
branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp
branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
Log:
Treat MaxRasterImageWidth/Height as an upper bound in RasterAdapter
RasterAdapter::Stylize() halved the requested raster image until both dimensions
fell below MaxRasterImageWidth/MaxRasterImageHeight, comparing with ">=". Those
two settings are upper bounds, so an image that was exactly at the configured
size was halved as well, which made the effective limit half of what the
configuration says: with the default 2048x2048, a 2048-wide request came out at
1024.
Changes:
* Common/Stylization/RasterAdapter.cpp: move the halving loop into the new
ClampToMaxRasterImageSize() and compare with ">", so a dimension exactly at
the maximum is kept.
* Common/Stylization/RasterAdapter.h: declare it, with STYLIZATION_API - the
loop is now callable from outside MgStylization.dll, which is where the unit
test lives.
* Common/Stylization/Stylization.h: replace the three FdoExpressionEngine*
includes with forward declarations, so translation units that only use the
geometry adapters no longer need the FDO Expression Engine headers in their
include chain. The units that need the definitions include them directly:
DefaultStylizer.cpp, ExpressionFunctionIf.cpp, ExpressionHelper.cpp,
GeometryAdapter.cpp, RichTextEngine.cpp, StylizationEngine.cpp and
StylizationUtil.cpp (Fdo.h + FdoExpressionEngine.h, plus
FdoExpressionEngineFunctionCollection.h in ExpressionHelper.cpp),
SE_ExpressionBase.h (FdoExpressionEngine.h) and the 17 ExpressionFunction*.h
headers (FdoExpressionEngineINonAggregateFunction.h).
* Portable/MgPortable/Services/Feature/GwsFeatureReader.cpp and
Server/src/Services/Feature/ServerGwsFeatureReader.cpp: both call
FdoExpressionEngine::Create() and had the definition only through
Stylization.h; include FdoExpressionEngine.h directly, which is what keeps
MgPortable and the Feature Service compiling once the umbrella header no
longer supplies it.
* Server/src/UnitTesting/TestStylization.cpp: add the "Raster max image size"
case, covering dimensions at the maximum, over it, where one dimension is
already within the limit, and where one halving is enough.
* Server/src/UnitTesting/TestRasterStylization.cpp and
UnitTest/TestData/RasterStylization/PROVENANCE.md: the TrueMarble case no
longer reproduces the striped upsampling defect, now that it is fixed in the
provider (FDO r8422), so rename it "TrueMarble upsampled rendering" and
describe it as the regression test it has become. It still renders the
1350x675 tile at 4000x2000, across the provider's 4 MP scanline-tiling
threshold, and asserts that no output row is uniformly white.
Fixes #2174
Modified: branches/4.0/MgDev/Common/Stylization/DefaultStylizer.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/DefaultStylizer.cpp 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/DefaultStylizer.cpp 2026-10-25 09:28:58 UTC (rev 10232)
@@ -16,6 +16,8 @@
//
#include "stdafx.h"
+#include "Fdo.h"
+#include "FdoExpressionEngine.h"
#include "DefaultStylizer.h"
#include "GeometryAdapter.h"
#include "PolylineAdapter.h"
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionArgb.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionArgb.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionArgb.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_ARGB_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionArgb : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionDecap.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionDecap.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionDecap.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_DECAP_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionDecap : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureClass.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureClass.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureClass.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_FEATURECLASS_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionFeatureClass : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureId.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureId.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureId.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_FEATUREID_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
#include "KeyEncode.h"
class ExpressionFunctionFeatureId : public FdoExpressionEngineINonAggregateFunction
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureSource.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureSource.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionFeatureSource.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_FEATURESOURCE_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionFeatureSource : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionHtmlColor.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionHtmlColor.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionHtmlColor.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_HTMLCOLOR_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionHtmlColor : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionIf.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionIf.cpp 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionIf.cpp 2026-10-25 09:28:58 UTC (rev 10232)
@@ -16,6 +16,8 @@
//
#include "stdafx.h"
+#include "Fdo.h"
+#include "FdoExpressionEngine.h"
#include "ExpressionFunctionIf.h"
#include "ExpressionHelper.h"
#include "Foundation.h"
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionIf.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionIf.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionIf.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_IF_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionIf : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLayerDefinition.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLayerDefinition.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLayerDefinition.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_LAYERDEFINITION_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionLayerDefinition : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLayerId.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLayerId.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLayerId.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_LAYERID_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionLayerId : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLookup.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLookup.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionLookup.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_LOOKUP_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionLookup : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapCenterX.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapCenterX.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapCenterX.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_MAPCENTERX_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionMapCenterX : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapCenterY.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapCenterY.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapCenterY.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_MAPCENTERY_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionMapCenterY : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapName.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapName.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapName.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_MAPNAME_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionMapName : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapScale.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapScale.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionMapScale.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_MAPCENTERSCALE_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionMapScale : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionRange.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionRange.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionRange.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_RANGE_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionRange : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionSession.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionSession.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionSession.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -20,6 +20,7 @@
#define EXPRESSION_FUNCTION_SESSION_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionSession : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionFunctionUrlEncode.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionFunctionUrlEncode.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionFunctionUrlEncode.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -19,6 +19,7 @@
#define EXPRESSION_FUNCTION_URLENCODE_H_
#include "Stylization.h"
+#include "FdoExpressionEngineINonAggregateFunction.h"
class ExpressionFunctionUrlEncode : public FdoExpressionEngineINonAggregateFunction
{
Modified: branches/4.0/MgDev/Common/Stylization/ExpressionHelper.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/ExpressionHelper.cpp 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/ExpressionHelper.cpp 2026-10-25 09:28:58 UTC (rev 10232)
@@ -16,6 +16,9 @@
//
#include "stdafx.h"
+#include "Fdo.h"
+#include "FdoExpressionEngine.h"
+#include "FdoExpressionEngineFunctionCollection.h"
#include "ExpressionHelper.h"
#include "ExpressionFunctionArgb.h"
#include "ExpressionFunctionDecap.h"
Modified: branches/4.0/MgDev/Common/Stylization/GeometryAdapter.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/GeometryAdapter.cpp 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/GeometryAdapter.cpp 2026-10-25 09:28:58 UTC (rev 10232)
@@ -16,6 +16,8 @@
//
#include "stdafx.h"
+#include "Fdo.h"
+#include "FdoExpressionEngine.h"
#include "Renderer.h"
#include "GeometryAdapter.h"
#include "SymbolVisitor.h"
Modified: branches/4.0/MgDev/Common/Stylization/RasterAdapter.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/RasterAdapter.cpp 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/RasterAdapter.cpp 2026-10-25 09:28:58 UTC (rev 10232)
@@ -34,6 +34,21 @@
{
}
+
+//////////////////////////////////////////////////////////////////////////////
+// Halves both dimensions until neither exceeds the configured maximum. A
+// dimension exactly at the maximum is kept: MaxRasterImageWidth/Height are an
+// upper bound.
+void RasterAdapter::ClampToMaxRasterImageSize(int& imgW, int& imgH, double maxW, double maxH)
+{
+ while (imgW > maxW || imgH > maxH)
+ {
+ imgW >>= 1;
+ imgH >>= 1;
+ }
+}
+
+
#define PRINT_RS_BOUNDS( ext ) \
//printf(" minx = %6.4f miny = %6.4f maxx = %6.4f maxy = %6.4f\n", ext.minx, ext.miny, ext.maxx, ext.maxy); \
//printf(" width = %6.4f height = %6.4f\n", ext.width(), ext.height());
@@ -99,11 +114,13 @@
int devW = (int)(mapExt.width() * pixelsPerMapUnit);
int devH = (int)(mapExt.height() * pixelsPerMapUnit);
- while (imgW >= renderer->GetMaxRasterImageWidth() || imgH >= renderer->GetMaxRasterImageHeight())
- {
- imgW >>= 1;
- imgH >>= 1;
- }
+ // MaxRasterImageWidth/Height are an upper bound, not a value to stay
+ // strictly below, so an image that is exactly the configured maximum is
+ // not halved. The old ">=" halved it too, which made the effective
+ // limit half of what the configuration says.
+ ClampToMaxRasterImageSize(imgW, imgH,
+ renderer->GetMaxRasterImageWidth(),
+ renderer->GetMaxRasterImageHeight());
#ifdef _DEBUG
//printf(" -------------------------\n");
Modified: branches/4.0/MgDev/Common/Stylization/RasterAdapter.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/RasterAdapter.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/RasterAdapter.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -41,6 +41,11 @@
RS_ElevationSettings* elevSettings = NULL,
CSysTransformer* layer2mapxformer = NULL);
+ /// Halves the requested image dimensions until neither exceeds the
+ /// configured maximum. The maximum is an upper bound, so a dimension that
+ /// is exactly at it is left alone.
+ STYLIZATION_API static void ClampToMaxRasterImageSize(int& imgW, int& imgH, double maxW, double maxH);
+
void DecodeRGBA(RS_InputStream* is, unsigned char* dst, int w, int h);
void DecodeRGB(RS_InputStream* is, unsigned char* dst, int w, int h);
void DecodeMapped(RS_InputStream* is, RS_InputStream* pal, unsigned char* dst, int w, int h);
Modified: branches/4.0/MgDev/Common/Stylization/RichTextEngine.cpp
===================================================================
--- branches/4.0/MgDev/Common/Stylization/RichTextEngine.cpp 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/RichTextEngine.cpp 2026-10-25 09:28:58 UTC (rev 10232)
@@ -16,6 +16,8 @@
//
#include "stdafx.h"
+#include "Fdo.h"
+#include "FdoExpressionEngine.h"
#include "RichTextEngine.h"
#include "SE_Renderer.h"
#include "RS_FontEngine.h"
Modified: branches/4.0/MgDev/Common/Stylization/SE_ExpressionBase.h
===================================================================
--- branches/4.0/MgDev/Common/Stylization/SE_ExpressionBase.h 2026-10-24 01:07:41 UTC (rev 10231)
+++ branches/4.0/MgDev/Common/Stylization/SE_ExpressionBase.h 2026-10-25 09:28:58 UTC (rev 10232)
@@ -20,6 +20,7 @@
#include "Stylization.h"
#include "ExpressionHelper.h"
+#include "FdoExpressionEngine.h"
#include