Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MapGuide React Layout

This context describes the viewer initialization language used to turn a fetched layout document into a ready-to-run viewer state.
This context describes the viewer initialization and map comparison language used to turn a fetched layout document into a ready-to-run viewer state and to describe how paired maps are visually compared.

## Language

Expand All @@ -20,17 +20,52 @@ _Avoid_: document fetch, session bootstrap
Initialization metadata indicating that an existing MapGuide session was reused instead of creating a new one.
_Avoid_: warm start, cached login

### Map comparison

**Comparison pair**:
A primary map and secondary map that are intentionally compared within one viewer interaction.
_Avoid_: swipe pair when the concept is not specific to swipe

**Comparison mode**:
The active rendering mode used for a **Comparison pair**.
_Avoid_: swipe state, compare flag

**Swipe mode**:
A comparison mode that reveals the secondary map on one side of a movable divider while the primary map remains on the other side.
_Avoid_: generic compare mode, spy mode

**Spy mode**:
A comparison mode that shows the primary map normally and reveals the secondary map only inside a movable spy cursor.
_Avoid_: swipe mode, overlay mode

**Spy cursor**:
The movable circular reveal area used by **Spy mode** to show the secondary map over the primary map.
_Avoid_: divider, swipe handle

**Spy cursor radius**:
The size of the **Spy cursor** measured from its center to the edge of the circular reveal area.
_Avoid_: swipe position, divider position

## Relationships

- A **Document fetch stage** produces one **Init document** and one **Session reuse** flag
- An **Init payload stage** consumes one **Init document** and emits one INIT_APP payload
- A **Comparison pair** contains exactly one primary map and one secondary map
- A **Comparison pair** may be rendered using **Swipe mode** or **Spy mode**
- Exactly one **Comparison mode** may be active at a time
- **Spy mode** uses exactly one **Spy cursor**
- A **Spy cursor** has exactly one **Spy cursor radius**

## Example dialogue

> **Dev:** "Can we skip de-arrayification for newer servers without changing payload behavior?"
> **Domain expert:** "Yes, because the **Document fetch stage** may vary normalization, while the **Init payload stage** must always produce the same INIT_APP payload shape."

> **Dev:** "Does this map support swipe and spy?"
> **Domain expert:** "It supports a **Comparison pair**. **Swipe mode** and **Spy mode** are just two ways to render that same pair."

## Flagged ambiguities

- "init" was used to mean both fetching documents and building payloads — resolved: split into **Document fetch stage** and **Init payload stage**.
- "DefaultViewerInitCommand class" was used to describe the init payload stage implementation — resolved: the class holds no meaningful state (all three fields are implicit parameter threading); the `protected` extension surface has no subclasses; the class boundary causes a circular dependency between `init.ts` and `init-mapguide.ts`. The **Init payload stage** is implemented as free functions in `init-mapguide.ts` orchestrated directly from the `initAppFromDocument` thunk in `init.ts`, using the thunk closure for `dispatch` and `client`.
- "swipe pair" was used to mean both the paired-map relationship and the side-by-side rendering style — resolved: the paired-map relationship is a **Comparison pair**; **Swipe mode** and **Spy mode** are rendering modes over that pair.
3 changes: 3 additions & 0 deletions docs/adr/0002-generalize-map-comparison-beyond-swipe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Generalize map comparison beyond swipe

We will model paired-map comparison as a general **Comparison pair** with one active **Comparison mode** rather than treating swipe as the umbrella concept. ApplicationDefinition authoring will use `Comparison*` keys, and the viewer will expose **Swipe mode** and **Spy mode** as two mutually exclusive renderers over the same primary/secondary pair. We chose this because swipe-specific naming became misleading as soon as spy was introduced, and this is still early enough in the dev cycle to prefer a clean shared model over compatibility layers.
36 changes: 25 additions & 11 deletions src/actions/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Redux action definitions
*/

import type { IDOMElementMetrics, IMapView, Dictionary, IExternalBaseLayer, IModalComponentDisplayOptions, IModalDisplayOptions, UnitOfMeasure, ActiveMapTool, ILayerInfo, Bounds, INameValuePair, IMapSwipePair } from '../api/common';
import type { IDOMElementMetrics, IMapView, Dictionary, IExternalBaseLayer, IModalComponentDisplayOptions, IModalDisplayOptions, UnitOfMeasure, ActiveMapTool, ILayerInfo, Bounds, INameValuePair, IComparisonPair, ComparisonMode } from '../api/common';
import { ActionType } from '../constants/actions';
import { PreparedSubMenuSet } from '../api/registry/command-spec';
import { RuntimeMap } from '../api/contracts/runtime-map';
Expand Down Expand Up @@ -376,10 +376,11 @@ export interface IInitAppActionPayload {
*/
appSettings?: Dictionary<string> | undefined;
/**
* Swipe pairs declared in the application definition
* Comparison pairs declared in the application definition
* @since 0.15
*/
mapSwipePairs?: IMapSwipePair[];
comparisonPairs?: IComparisonPair[];
mapSwipePairs?: IComparisonPair[];
/**
* Indicates whether initialization reused an existing MapGuide session.
*
Expand Down Expand Up @@ -994,27 +995,39 @@ export interface IClearClientSelectionAction {
}

/**
* Sets/toggles the map swipe mode
* Sets the active comparison mode
* @since 0.15
*/
export interface ISetMapSwipeModeAction {
type: ActionType.MAP_SET_SWIPE_MODE;
export interface ISetComparisonModeAction {
type: ActionType.MAP_SET_COMPARISON_MODE;
payload: {
active: boolean;
mode?: ComparisonMode;
active?: boolean;
}
}

/**
* Updates the map swipe position
* @since 0.15
*/
export interface IUpdateMapSwipePositionAction {
type: ActionType.MAP_UPDATE_SWIPE_POSITION;
export interface ISetSwipePositionAction {
type: ActionType.MAP_SET_SWIPE_POSITION;
payload: {
position: number;
}
}

/**
* Updates the active spy cursor radius
* @since 0.15
*/
export interface ISetSpyCursorRadiusAction {
type: ActionType.MAP_SET_SPY_CURSOR_RADIUS;
payload: {
radius: number;
}
}

/**
* @since 0.12
*/
Expand Down Expand Up @@ -1080,5 +1093,6 @@ export type ViewerAction = IOpenFlyoutAction
| ISetAppSettingAction //@since 0.14.8
| IUpdateModalDimensionsAction //@since 0.14.8
| ITemplateSetCustomDataAction //@since 0.14.8
| ISetMapSwipeModeAction //@since 0.15
| IUpdateMapSwipePositionAction; //@since 0.15
| ISetComparisonModeAction //@since 0.15
| ISetSwipePositionAction //@since 0.15
| ISetSpyCursorRadiusAction; //@since 0.15
28 changes: 15 additions & 13 deletions src/actions/init-command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IMapSwipePair } from '../api/common';
import type { IComparisonPair } from '../api/common';
import { IGenericSubjectMapLayer } from './defs';
import { makeUnique } from '../utils/array';
import { ApplicationDefinition, MapConfiguration, MapSetGroup } from '../api/contracts/fusion';
Expand All @@ -7,15 +7,15 @@ import { strStartsWith } from '../utils/string';
import { IClusterSettings } from '../api/ol-style-contracts';

/**
* Parses swipe pair declarations from the application definition's MapSet.
* Parses comparison pair declarations from the application definition's MapSet.
*
* A swipe pair is declared by adding Extension.SwipePairWith (the paired map group id)
* and Extension.SwipePrimary ("true" or "false") to a MapGroup element.
* A comparison pair is declared by adding Extension.ComparisonPairWith (the paired map group id)
* and Extension.ComparisonPrimary ("true" or "false") to a MapGroup element.
*
* @since 0.15
*/
export function parseSwipePairs(appDef: ApplicationDefinition): IMapSwipePair[] {
const pairs: IMapSwipePair[] = [];
export function parseComparisonPairs(appDef: ApplicationDefinition): IComparisonPair[] {
const pairs: IComparisonPair[] = [];
const seen = new Set<string>();
if (!appDef.MapSet?.MapGroup) {
return pairs;
Expand All @@ -25,18 +25,18 @@ export function parseSwipePairs(appDef: ApplicationDefinition): IMapSwipePair[]
if (!ext) {
continue;
}
const swipePairWith = ext.SwipePairWith as string | undefined;
const swipePrimary = ext.SwipePrimary as string | undefined;
if (swipePairWith && swipePrimary?.toLowerCase() === "true") {
const comparisonPairWith = ext.ComparisonPairWith as string | undefined;
const comparisonPrimary = ext.ComparisonPrimary as string | undefined;
if (comparisonPairWith && comparisonPrimary?.toLowerCase() === "true") {
const primaryId = mg["@id"];
const pairKey = [primaryId, swipePairWith].sort().join("|");
const pairKey = [primaryId, comparisonPairWith].sort().join("|");
if (!seen.has(pairKey)) {
seen.add(pairKey);
const primaryLabel = ext.SwipePrimaryLabel as string | undefined;
const secondaryLabel = ext.SwipeSecondaryLabel as string | undefined;
const primaryLabel = ext.ComparisonPrimaryLabel as string | undefined;
const secondaryLabel = ext.ComparisonSecondaryLabel as string | undefined;
pairs.push({
primaryMapName: primaryId,
secondaryMapName: swipePairWith,
secondaryMapName: comparisonPairWith,
...(primaryLabel ? { primaryLabel } : {}),
...(secondaryLabel ? { secondaryLabel } : {})
});
Expand All @@ -46,6 +46,8 @@ export function parseSwipePairs(appDef: ApplicationDefinition): IMapSwipePair[]
return pairs;
}

export const parseSwipePairs = parseComparisonPairs;

/**
* Parses a map-level mouse coordinate format override from the first map
* configuration inside a MapGroup.
Expand Down
6 changes: 4 additions & 2 deletions src/actions/init-mapguide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MgError } from '../api/error';
import { resolveProjectionFromEpsgCodeAsync } from '../api/registry/projections';
import { register } from 'ol/proj/proj4';
import proj4 from "proj4";
import { buildSubjectLayerDefn, getExtraProjectionsFromFlexLayout, getMapDefinitionsFromFlexLayout, isMapDefinition, isStateless, parseMapGroupCoordinateFormat, parseSwipePairs, MapToLoad } from './init-command';
import { buildSubjectLayerDefn, getExtraProjectionsFromFlexLayout, getMapDefinitionsFromFlexLayout, isMapDefinition, isStateless, parseComparisonPairs, parseMapGroupCoordinateFormat, MapToLoad } from './init-command';
import { WebLayout } from '../api/contracts/weblayout';
import { convertFlexLayoutUIItems, convertWebLayoutUIItems, parseCommandsInWebLayout, parseWidgetsInAppDef, prepareSubMenus, ToolbarConf } from '../api/registry/command-spec';
import { WEBLAYOUT_CONTEXTMENU, WEBLAYOUT_TASKMENU, WEBLAYOUT_TOOLBAR } from "../constants";
Expand Down Expand Up @@ -144,6 +144,7 @@ async function initFromAppDefCoreAsync(appDef: ApplicationDefinition, options: I
settings[sn] = sv;
}
}
const comparisonPairs = parseComparisonPairs(appDef);
return normalizeInitPayload({
appSettings: settings,
activeMapName: firstMapName,
Expand All @@ -165,7 +166,8 @@ async function initFromAppDefCoreAsync(appDef: ApplicationDefinition, options: I
toolbars: tb,
warnings: warnings,
initialActiveTool: ActiveMapTool.Pan,
mapSwipePairs: parseSwipePairs(appDef)
comparisonPairs,
mapSwipePairs: comparisonPairs
}, options.layout);
}
/**
Expand Down
57 changes: 45 additions & 12 deletions src/actions/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ import {
ISetHeatmapLayerBlurAction,
ISetHeatmapLayerRadiusAction,
IEnableSelectDragPanAction,
ISetMapSwipeModeAction,
IUpdateMapSwipePositionAction,
ISetComparisonModeAction,
ISetSwipePositionAction,
ISetSpyCursorRadiusAction,
IExternalLayersReadyAction
} from './defs';
import { persistSelectionSetToLocalStorage } from '../api/session-store';
Expand All @@ -67,6 +68,7 @@ import proj4 from "proj4";
import { AsyncLazy } from '../api/lazy';
import type { SiteVersionResponse } from '../api/contracts/common';
import { tryParseArbitraryCs } from '../utils/units';
import type { ComparisonMode } from "../api/common";

function combineSelectedFeatures(oldRes: SelectedFeature[], newRes: SelectedFeature[]): SelectedFeature[] {
// This function won't be called if we're using QUERYMAPFEATURES older than v4.0.0 (because we won't request
Expand Down Expand Up @@ -963,33 +965,64 @@ export function clearClientSelection(mapName: string): IClearClientSelectionActi
}

/**
* Sets the map swipe mode active or inactive
* Sets the active comparison mode.
*
* @param {boolean} active
* @returns {ISetMapSwipeModeAction}
* @param {ComparisonMode} mode
* @returns {ISetComparisonModeAction}
* @since 0.15
*/
export function setMapSwipeMode(active: boolean): ISetMapSwipeModeAction {
export function setComparisonMode(mode: ComparisonMode): ISetComparisonModeAction {
return {
type: ActionType.MAP_SET_SWIPE_MODE,
type: ActionType.MAP_SET_COMPARISON_MODE,
payload: {
mode,
active: mode !== "none"
}
};
}

export function setMapSwipeMode(active: boolean): ISetComparisonModeAction {
return {
type: ActionType.MAP_SET_COMPARISON_MODE,
payload: {
mode: active ? "swipe" : "none",
active
}
}
};
}

/**
* Updates the swipe position
*
* @param {number} position A value between 0 and 100 representing the swipe slider position
* @returns {IUpdateMapSwipePositionAction}
* @returns {ISetSwipePositionAction}
* @since 0.15
*/
export function updateMapSwipePosition(position: number): IUpdateMapSwipePositionAction {
export function setSwipePosition(position: number): ISetSwipePositionAction {
return {
type: ActionType.MAP_UPDATE_SWIPE_POSITION,
type: ActionType.MAP_SET_SWIPE_POSITION,
payload: {
position
}
}
};
}

export function updateMapSwipePosition(position: number): ISetSwipePositionAction {
return setSwipePosition(position);
}

/**
* Updates the spy cursor radius.
*
* @param {number} radius
* @returns {ISetSpyCursorRadiusAction}
* @since 0.15
*/
export function setSpyCursorRadius(radius: number): ISetSpyCursorRadiusAction {
return {
type: ActionType.MAP_SET_SPY_CURSOR_RADIUS,
payload: {
radius
}
};
}
Loading
Loading