# ArcGIS 300 Symbology Migration Summary Qt5→Qt6 + ArcGIS Runtime 100.x→ArcGIS Maps SDK for Qt **300.0.0** migration audit of the SIDC/Esri symbology subsystem. Every change was compared against the legacy tree (`qt5-before/qt5-legatus-legacy`); the goal is migration-only (no behavior change). ArcGIS 300 SDK at `external/ArcGIS/MapsSDK_Qt_300.0.0/.../qt300.0.0/sdk` (flat include layout — all headers directly in `sdk/include/`, no `arcgis/` subdirectory). ## Scope audited - `Lts/Viewer/SituationMap/Symbology/` — SIDC codec/builder/dictionary/definitions (pure Qt). - `Lts/Viewer/SituationMap/SymbologyImpl/` — `esri_dictionary_processor*.cpp`, `base_symbol_model.*`, `symbol_dictionary_impl.*`, `advanced_symbology_logging.*`. - `Lts/Viewer/SituationMap/SymbologyXML/` — symbol XML read/write/export. - `Lts/Viewer/SituationMap/Map/` — the ArcGIS map layer (57 files). **Excluded from the build** (see Remaining Issues). - `Lts/Plugins/AdvancedSymbology/` — `asb_data_plugin.*`, `asb_foreign_interface_impl.*`, `Gui/SymbolEditor/*`, `Gui/SymbolProxy/*`. - `Lts/Services/CentralEffectSimulation/Plugins/Explosions/expand_polygon.cpp`. ## Migration-only pattern classes found All in-scope changes classified as the following approved adaptations (no semantic change): - Qt include path churn: `#include ` → `#include ` (Gui/Widgets/Network/Sql/…). - `qAsConst(x)` → `std::as_const(x)` (Qt6 removed `qAsConst`). - `QRegExp` → `QRegularExpression`; `QRegExp::exactMatch(s)` → `QRegularExpression::match(s).hasMatch()`. - `QString::count()` → `size()`; `leftRef/rightRef/midRef(n)` → `left/right/mid(n)`; `split()` now returns `QStringList` (Qt6 dropped the `QStringView`/`QStringRef` ref overloads) → `QVector` or `QStringList`; `.toString()` dropped where the element is already a `QString` (Esri dictionary processors). - `QAbstractItemModel::invalidateFilter()` → `beginFilterChange()`/`endFilterChange()` (Qt6 removed `invalidateFilter()`). - `QLinkedList`/`QLinkedListIterator` → STL `std::list` + range-for/iterators (`append`→`push_back`, `remove`→`erase`, `toFront/toBack`→erase+`push_front`/`push_back`). - `LOGFUNCTION` → `Q_FUNC_INFO` (no-op: `INGLLogger.h` defines `LOGFUNCTION` as `Q_FUNC_INFO`). - `const QImage &x = …image()` → `const QImage x = …image()` (Qt6 `QImage::image()` by value). - DDS-generated type rename `ParticipantManagement::Role_def::*` → `Role::*` (codegen artifact). - ArcGIS include path: old 100.x `` → flat 300 `` (see below). - Systematic 2-space→4-space re-indentation (whitespace-only; `diff -w` filters it out). ## The four "verify carefully" items — resolutions 1. **`Role_def` → `Role`** (`symbol_data_to_render_role.h`) — RESOLVED, 1:1. Legacy `ppm_service.xml` and migrated `ppm_enums.idl` both define `Role` with the identical 7 enumerators and values (`Unknown=0 … Suspect=6`). The `Role_def` name is a DDS-codegen artifact (IDL name is `Role` in both; legacy codegen emitted `Role_def`, rtiddsgen 7.7 emits `Role`). `role.underlying()` switch behaves identically; return values unchanged. 2. **`#ifdef BUILD_WITH_EXTERNAL_DDS` role-conversion stub** — RESOLVED with a flagged delta. (a) Real path = legacy 1:1 (constructor + `roleToRole<>(party.role())` identical). (c) Stub documented (`// STUB: No external DDS …`), intentional migration scaffolding. **(b) FLAGGED:** `BUILD_WITH_EXTERNAL_DDS` is referenced only as `#ifdef` in 28 source files and is **never defined** (no `#define`, no CMake `set`/`option`/`-D`, empty `CMAKE_CXX_FLAGS`). So the no-op **stub** path is what compiles in the current build — a runtime behavior change vs legacy. This is codebase-wide (28 files) and pre-existing in the migration (not introduced by the ArcGIS-300 symbology work); the stub is NOT "fixed" (out of scope) — surfaced as a behavior delta. 3. **Logger pattern** — RESOLVED, migration-only. `m_logger` is a **member** of type `LOGGER_T` (`typedef NGL::INGLLogger &LOGGER_T;` — a *reference*) in **both** trees, initialized in the constructor (`getLogger("SymbolDictionaryImpl"/"EsriDictionaryProcessor"/ "BaseSymbolModel")` — names unchanged). A reference has no ownership → no double-allocation/lifetime/init-order issue. (The "local→member" premise was inaccurate; reality: unchanged.) 4. **Filter API** (`base_symbol_model.cpp`) — RESOLVED, 1:1. 4 legacy `invalidateFilter()` → 4 migrated `beginFilterChange(); endFilterChange();` pairs (`setValidBaseSymbolCodes`, `onFilterPoints/Lines/Areas`); no leftover `invalidateFilter()`. `BaseSymbolProxyModel` is a `QSortFilterProxyModel` (via `LtsColumnFilterProxyModel`) that overrides `filterAcceptsRow` (unchanged); the changed members are plain `QSet` ops (no model signals), so the back-to-back begin/end still triggers the proxy re-filter reading the updated data. Behavior preserved. ## Esri dictionary processors — dictionary equivalence The Qt6 API churn in `esri_dictionary_processor{,_app6,_app6d,_mil2525d}.cpp` (`splitRef`→`split`, `rightRef`→`right`, `.toString()` removal) is migration-only. The three `SymbologyImpl` files that use the ArcGIS symbol-search/swatch/fetch API — `esri_dictionary_processor.cpp`, `symbol_dictionary_impl.cpp`, `base_symbol_model.cpp` — were **ported from the 100.x `TaskWatcher` signal/slot model to the 300 `QFuture` async model** (see "ArcGIS 300 QFuture port" below) and now **compile and link** against the 300 SDK (see Build results). ## ArcGIS 300 include-style reconciliation The 300 SDK is **flat** (`FIND_PATH(ARCGIS_INCLUDE_DIR ArcGISQt_global.h …)` in `FindArcGIS.cmake` finds it directly in `sdk/include/`), so the correct form is **bare** (`#include `), not ``. The migration already dropped the prefix in the Esri processors; the Map layer + `expand_polygon` still carried it. The **11** `arcgis/`-prefixed includes were reconciled to bare (one consistent form): - 10 ESRI headers (`SceneViewTypes`, `Error`, `CoreTypes`, `MapTypes`, `Viewpoint`, `Camera`, `PolygonBuilder`, `GeometryEngine`) — exist flat in the SDK, resolve. - 3 `arcgisversion.h` (custom RME header defining `ACRGIS_VERSION`, "created by RME, not ESRI") — made bare for consistency; the header itself is **absent from the repo** (pre-existing gap; see Remaining Issues). Files touched: `Map/situation_map_utils.{h,cpp}`, `Map/map_initializer.cpp`, `Map/layer_factory.h`, `Explosions/expand_polygon.cpp`. ## ArcGIS 300 QFuture port (SymbologyImpl) The 100.x ArcGIS Runtime exposed an async **task** API (`TaskWatcher` + per-task completion signals); the 300 SDK replaced it with **futures** (`QFuture` + `QFutureWatcher`). The three `SymbologyImpl` files were rewritten from the task model to the future model. This is a **semantic** change — the approved "blocking work item" (chosen by the user) — and every mapping below preserves the 100.x behavior (same work, same main-thread delivery, same completion order). | 100.x (task API) | 300 (future API) | |---|---| | `searchSymbols(params) → TaskWatcher` + `searchSymbolsCompleted` | `searchSymbolsAsync(params) → QFuture` | | `SymbolStyleSearchResult::symbol(QObject*) → Symbol*` (sync) | `fetchSymbolAsync(QObject*) → QFuture` | | `Symbol::createSwatch(w,h,scale) → TaskWatcher` + `createSwatchCompleted` | `createSwatchAsync(...) → QFuture` | | `DictionarySymbolStyle::fetchSymbol(map) → TaskWatcher` + `fetchSymbolCompleted` | `fetchSymbolAsync(map, QObject*) → QFuture` | | `QtConcurrent::run(this, &Class::method)` | `QtConcurrent::run(&Class::method, this)` (Qt6: member fn first, receiver second) | Behavior-preserving details (each verified against the 300 headers): - **`QFutureWatcher` `result` signal/getter name collision.** `QFutureWatcher` declares both a **signal** `void result(const T&)` and a **getter** `T result() const` — same name, so `&QFutureWatcher::result` is ambiguous, and `QOverload(…)` / `qOverload(…)` both fail (the getter poisons template deduction). Working form: connect to the unambiguous `void finished()` signal and read the value via the `result()` getter (`connect(w, &QFutureWatcher::finished, w, [w]{ auto v = w->result(); …; w->deleteLater(); });`). `finished()` fires on the watcher's (main) thread once the future is done — the same main-thread delivery the 100.x `TaskWatcher` completion signals gave. - **Non-const `fetchSymbolAsync`.** `SymbolStyleSearchResult::fetchSymbolAsync(QObject*)` is **non-const** (no `const` suffix, `SymbolStyleSearchResult.h`), so it can't be called on the `const` search result stored in the map. Fix: copy the (cheap, shared_ptr-backed) `SymbolStyleSearchResult` into a non-const local and call `fetchSymbolAsync` on the copy. - **Swatch future correlation.** 100.x correlated a swatch to its request via the SDK-issued task id; a 300 future carries no id. Fix: the per-request `QFutureWatcher` lambda captures a caller-generated `QUuid` (`csTaskId`); `onEsriSymbolSwatchCompleted` is otherwise unchanged. `Symbol::symbol()` (100.x const/sync) is gone → `fetchSymbolAsync` (async) under `CREATE_ESRI_SYMBOL_ON_DEMAND`. - **`QtConcurrent::run(renderExtensions, const QImage&, …)` → `QFuture`.** `renderExtensions` takes a **non-const** `QImage&` (it scales / replaces color in place); passing a `const QImage&` hit Qt6's `QPromise` overload → `QFuture` (unassignable to `QFuture`). Fix: wrap in a lambda returning `QImage` that passes a **local non-const copy** (a `QImage` copy is a shallow implicit-share refcount bump; the one deep copy is `renderExtensions`' own `scaled()`, exactly as 100.x mutated `QtConcurrent::run`'s stored copy). - **`DictionarySymbolStyle(QString)` / `(QString, QString)` ctors removed** → `DictionarySymbolStyle::createFromFile(path)` (local `.stylx`); empty/missing path returns `false` (mirrors the ARCADE branch; 300 has no built-in spec-only dict). - **`Error::code()` removed** → `error.arcGISServerErrorCode().value_or(-1)`. - **`LoadStatus` / `Error` incomplete** → `#include ` / `#include ` (300 `LoadStatus` is an `enum class` in `MapTypes.h`; `Loadable.h` only forward-declares it). - **`arcgis/arcgisversion.h` (custom RME header) dropped by 300** → a shim `arcgisversion.h` was added to the 300 SDK include dir (`external/ArcGIS/…/sdk/include/`, so every `ArcGIS::ArcGIS` consumer finds it) defining `ACRGIS_VERSION = 300000000` (> every 100.x `0x64xxxx` gate, so each `#if ACRGIS_VERSION >= 0x64xxxx` takes the "newest SDK" branch; the single `#if ACRGIS_VERSION < 0x640500` correctly takes its else). - **Syntax fix:** a `// Qt6: leftRef removed` comment had swallowed the trailing `);` of two `castToSymbolSet(… .left(2).toUInt())` statements (`base_symbol_model.cpp`); restructured so the parens close before the comment. ## Build results - **Pure-Qt symbology (green):** `situationMapCommon` and `advancedSymbologyXML` build and link (in `build4` and reconfirmed under the ArcGIS-ON configure). These carry no ArcGIS dependency. - **ArcGIS-ON configure (green):** `BUILD_RTI=ON BUILD_GRANTLEE=ON BUILD_ARCGIS=ON` configures successfully; `FindArcGIS.cmake` resolves the 300 SDK (`Found ArcGIS: .../sdk/linux/x64/lib/libEsriRuntimeQt.a`). - **ArcGIS-ON build of `advancedSymbology` (green):** after the QFuture port, the ArcGIS-dependent target (`Symbology` SIDC codec + `SymbologyImpl` Esri processors, links `ArcGIS::ArcGIS`) compiles and links — `libadvancedSymbology.so` built with **0 errors** (build log `/tmp/arcgis300_targeted_build5.log`). - **CTest (green for this change):** the 6 symbology-family `SymbologyXML.*` tests pass (`ctest -R SymbologyXML` → 6/6). Full `migration_tests` suite: **604/644 pass, 0 actual assertion failures** — the 40 non-passing are 31 "Not Run" (unbuilt test exes of unrelated modules — ngl/dds/aes/bld_/taboc/wls/LtsDataView/Grantlee — a pre-existing build-scope gap, not symbology) and 9 "Subprocess aborted" (display-dependent GUI tests; the 7 `LtsWidgetsQt6.*` pass under `QT_QPA_PLATFORM=offscreen`, confirmed 10/10). None touch the symbology family. ## Remaining issues / risks - **RESOLVED — `SymbologyImpl` 100.x ArcGIS task API (was the blocker).** The three `SymbologyImpl` files previously used the 100.x `TaskWatcher` task API (`searchSymbols`/`createSwatch`/`fetchSymbol` → `TaskWatcher` + completion signals), which the 300 SDK removed. They were **ported to the 300 `QFuture` async model** (see "ArcGIS 300 QFuture port" above) and now **compile and link** against the 300 SDK (`advancedSymbology` green). This was a semantic change — the approved "blocking work item" chosen by the user — superseding the earlier note that it was "out of scope for a verify-only task." - **Map layer excluded:** `SituationMap/CMakeLists.txt` has `ADD_SUBDIRECTORY(Map)`, `MapImpl`, `MapObjects` **commented out**; no `Map/CMakeLists.txt`. The ArcGIS-300 Map integration (beyond include paths — `Viewpoint`/`Camera`/`MapTypes` 300 API changes) is a separate, not-yet-complete migration and is not in the current green build. - **`BUILD_WITH_EXTERNAL_DDS` never defined** (see item 2): the DDS `roleToRole`/ `ForeignInterfaceImpl` real path is dead code; the no-op stub compiles. Behavior delta vs legacy, codebase-wide, pre-existing in the migration. - **`arcgisversion.h` (custom RME header) — RESOLVED:** the 300 SDK dropped this header (it defined `ACRGIS_VERSION` for the `#if ACRGIS_VERSION >= …` version gates). A shim `arcgisversion.h` was added to the 300 SDK include dir (`external/ArcGIS/…/sdk/include/`, `ACRGIS_VERSION = 300000000`) so every gate takes the "newest SDK" branch (see the QFuture-port section). It lives in the external SDK tree (not the Legatus repo) because it is a drop-in replacement for the SDK-shipped header.