The compare feature landed with #622, but only as a playground: CarmaMapCompare in libraries/mapping/core/src/components/compare/ and the route /compare in ng-topicmap-playground. There it compares background-layer presets, and the component owns every map, because the playground has no map of its own.
The geoportal is the opposite case: one map, its content driven by redux, carrying selection, info box, hash and the carma.mapping2D API. The playground component cannot be dropped into it.
What the geoportal needs instead is a mode over the layers already on the map: the user has several layers in the layer bar, opens a comparing control, and assigns each layer a role ("this one left, that one right"). The map is the data source of the comparison, not a panel content config. The purpose is informal, judging visible similarity between layers and getting a feel for their rendering performance.
Entry point
A row at the end of the layer bar that opens a panel, the way measurement mode works. The pattern to copy is the one introduced with #761: the addon library says when the row appears and what it contains, the app supplies the dispatches in a thin bridge hook, and the panel is contributed through ADDON_INTERACTION_COMPONENTS. No addon reaches into the geoportal store.
Modes
| Kind |
Panels |
What it does |
comparing |
|
The control: owns mode and role state, contributes the layer row and its panel |
compareSwipe |
2-4 |
Clip-path split, horizontal or vertical, draggable divider; four panels is the 2x2 grid |
compareSpyglass |
2 |
Clip-path circle, draggable, wheel-resizable |
compareArena |
2-4 |
Full viewports side by side, real layout rather than clipping |
compareBlend |
2 |
Two full-size maps stacked, a slider cross-fades from one to the other |
Spyglass and blend have no meaning above two panels: a single slider cannot cross-fade three maps, and a lens shows one thing under it. The control disables them above two and falls back to swipe, the way the playground already handles spyglass.
How it works
useLibreLayers() already flattens the redux layers and the background into one LibreLayer[], and the app map is already driven purely by that prop. Every entry carries carmaLayerId. A panel's content is therefore that same array, filtered by role. Nothing needs to mutate redux, and the background is simply the first block of the array, which is why it can be assigned to panels like any other layer.
The app's own map stays mounted but hidden, and acts as the camera master. That is what keeps the rest of the app working untouched: the zoom buttons, the home button, the gazetteer and carma.mapping2D.flyTo all move the app map, and the app map syncs the panels. The hash keeps being written by the map that has always written it, and leaving the mode reveals a map already at the compared view. Dragging a panel moves the app map back the same way.
Panels are built on LibreMap rather than CarmaMap, since CarmaMap brings its own HashStateProvider and ControlLayout and nesting several of those inside the geoportal risks the panels fighting the app over the hash.
Camera sync is the off/jump/on dance already in CarmaMapCompare: detach every move listener, jumpTo all targets, reattach. Subscription has to happen when a map arrives rather than in a useEffect, or child-before-parent effect ordering under StrictMode leaves listeners on destroyed maps.
Open point
The addon lives in a library, useLibreLayers() is a geoportal hook, and AddonHost takes no props by design. The LibreLayer[] needs a sanctioned route into the addon, most likely a new method on the carma.mapping2D facade implemented app-side where registerMapping already happens.
Steps
- The stage plus
compareSwipe, with roles taken implicitly from the stack (the last two layers left and right, the background in both). No layer row, no channels, no control UI. This puts two synced panels on screen and shows whether the hidden-master approach behaves.
- The layer row, the control panel and the two addon-state channels (
compareModes, compareState). The implicit roles become what the panel starts from.
compareBlend, then compareSpyglass, then compareArena.
Built against the dev-only addons route, where the highlight addon is already exercised. Nothing goes into DEFAULT_ADDONS.
Later
- Per-panel layer order and per-panel opacity in the control table, once assigning roles is in use.
- A per-panel timing readout: milliseconds from
moveend to maplibre's idle. Note onProgressUpdate is not usable for this, it counts geojson layer loading only.
- Feeding the hidden app map an empty layer set, if the unseen render pass turns out to cost anything.
Housekeeping from #622
libraries/mapping/core/src/hooks/useMapLibreSync.ts came along with the playground and is imported by nothing; it should not be ported and can go. CarmaMapCompare also ships with SYNC_LOG = true, so it logs on every camera event.
The compare feature landed with #622, but only as a playground:
CarmaMapCompareinlibraries/mapping/core/src/components/compare/and the route/compareinng-topicmap-playground. There it compares background-layer presets, and the component owns every map, because the playground has no map of its own.The geoportal is the opposite case: one map, its content driven by redux, carrying selection, info box, hash and the
carma.mapping2DAPI. The playground component cannot be dropped into it.What the geoportal needs instead is a mode over the layers already on the map: the user has several layers in the layer bar, opens a comparing control, and assigns each layer a role ("this one left, that one right"). The map is the data source of the comparison, not a panel content config. The purpose is informal, judging visible similarity between layers and getting a feel for their rendering performance.
Entry point
A row at the end of the layer bar that opens a panel, the way measurement mode works. The pattern to copy is the one introduced with #761: the addon library says when the row appears and what it contains, the app supplies the dispatches in a thin bridge hook, and the panel is contributed through
ADDON_INTERACTION_COMPONENTS. No addon reaches into the geoportal store.Modes
comparingcompareSwipecompareSpyglasscompareArenacompareBlendSpyglass and blend have no meaning above two panels: a single slider cannot cross-fade three maps, and a lens shows one thing under it. The control disables them above two and falls back to swipe, the way the playground already handles spyglass.
How it works
useLibreLayers()already flattens the redux layers and the background into oneLibreLayer[], and the app map is already driven purely by that prop. Every entry carriescarmaLayerId. A panel's content is therefore that same array, filtered by role. Nothing needs to mutate redux, and the background is simply the first block of the array, which is why it can be assigned to panels like any other layer.The app's own map stays mounted but hidden, and acts as the camera master. That is what keeps the rest of the app working untouched: the zoom buttons, the home button, the gazetteer and
carma.mapping2D.flyToall move the app map, and the app map syncs the panels. The hash keeps being written by the map that has always written it, and leaving the mode reveals a map already at the compared view. Dragging a panel moves the app map back the same way.Panels are built on
LibreMaprather thanCarmaMap, sinceCarmaMapbrings its ownHashStateProviderandControlLayoutand nesting several of those inside the geoportal risks the panels fighting the app over the hash.Camera sync is the off/jump/on dance already in
CarmaMapCompare: detach everymovelistener,jumpToall targets, reattach. Subscription has to happen when a map arrives rather than in auseEffect, or child-before-parent effect ordering under StrictMode leaves listeners on destroyed maps.Open point
The addon lives in a library,
useLibreLayers()is a geoportal hook, andAddonHosttakes no props by design. TheLibreLayer[]needs a sanctioned route into the addon, most likely a new method on thecarma.mapping2Dfacade implemented app-side whereregisterMappingalready happens.Steps
compareSwipe, with roles taken implicitly from the stack (the last two layers left and right, the background in both). No layer row, no channels, no control UI. This puts two synced panels on screen and shows whether the hidden-master approach behaves.compareModes,compareState). The implicit roles become what the panel starts from.compareBlend, thencompareSpyglass, thencompareArena.Built against the dev-only
addonsroute, where the highlight addon is already exercised. Nothing goes intoDEFAULT_ADDONS.Later
moveendto maplibre'sidle. NoteonProgressUpdateis not usable for this, it counts geojson layer loading only.Housekeeping from #622
libraries/mapping/core/src/hooks/useMapLibreSync.tscame along with the playground and is imported by nothing; it should not be ported and can go.CarmaMapComparealso ships withSYNC_LOG = true, so it logs on every camera event.