• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jumpinjackie / mapguide-react-layout / 22997640305

12 Mar 2026 10:32AM UTC coverage: 36.597% (-0.3%) from 36.937%
22997640305

Pull #1603

github

web-flow
Merge 59c28113e into d20ddfb87
Pull Request #1603: Add declarative map swipe (layer compare) feature

1618 of 2109 branches covered (76.72%)

77 of 431 new or added lines in 17 files covered. (17.87%)

408 existing lines in 7 files now uncovered.

8897 of 24311 relevant lines covered (36.6%)

8.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.64
/src/actions/init-command.ts
1
import { IInitAsyncOptions, normalizeInitPayload } from './init';
1✔
2
import { ReduxDispatch, Dictionary, ActiveMapTool, IMapSwipePair } from '../api/common';
1✔
3
import { IGenericSubjectMapLayer, IInitAppActionPayload, MapInfo } from './defs';
4
import { ToolbarConf, convertFlexLayoutUIItems, parseWidgetsInAppDef, prepareSubMenus } from '../api/registry/command-spec';
1✔
5
import { makeUnique } from '../utils/array';
1✔
6
import { ApplicationDefinition, MapConfiguration } from '../api/contracts/fusion';
7
import { warn, info } from '../utils/logger';
1✔
8
import { registerCommand } from '../api/registry/command';
1✔
9
import { tr, registerStringBundle, DEFAULT_LOCALE } from '../api/i18n';
1✔
10
import { WEBLAYOUT_CONTEXTMENU } from "../constants";
1✔
11
import { Client } from '../api/client';
12
import { ActionType } from '../constants/actions';
1✔
13
import { ensureParameters } from '../utils/url';
1✔
14
import { MgError } from '../api/error';
1✔
15
import { strStartsWith } from '../utils/string';
1✔
16
import { IClusterSettings } from '../api/ol-style-contracts';
17

18
/**
19
 * Parses swipe pair declarations from the application definition's MapSet.
20
 *
21
 * A swipe pair is declared by adding Extension.SwipePairWith (the paired map group id)
22
 * and Extension.SwipePrimary ("true" or "false") to a MapGroup element.
23
 *
24
 * @since 0.15
25
 */
26
export function parseSwipePairs(appDef: ApplicationDefinition): IMapSwipePair[] {
1✔
27
    const pairs: IMapSwipePair[] = [];
6✔
28
    const seen = new Set<string>();
6✔
29
    if (!appDef.MapSet?.MapGroup) {
6✔
30
        return pairs;
1✔
31
    }
1✔
32
    for (const mg of appDef.MapSet.MapGroup) {
6✔
33
        const ext = mg.Extension;
10✔
34
        if (!ext) {
10✔
35
            continue;
5✔
36
        }
5✔
37
        const swipePairWith = ext.SwipePairWith as string | undefined;
5✔
38
        const swipePrimary = ext.SwipePrimary as string | undefined;
5✔
39
        if (swipePairWith && swipePrimary?.toLowerCase() === "true") {
10✔
40
            const primaryId = mg["@id"];
4✔
41
            const pairKey = [primaryId, swipePairWith].sort().join("|");
4✔
42
            if (!seen.has(pairKey)) {
4✔
43
                seen.add(pairKey);
3✔
44
                pairs.push({
3✔
45
                    primaryMapName: primaryId,
3✔
46
                    secondaryMapName: swipePairWith
3✔
47
                });
3✔
48
            }
3✔
49
        }
4✔
50
    }
10✔
51
    return pairs;
5✔
52
}
5✔
53

54
const TYPE_SUBJECT = "SubjectLayer";
1✔
55
const TYPE_EXTERNAL = "External";
1✔
56

57
export type SessionInit = {
58
    session: string;
59
    sessionWasReused: boolean;
60
}
61

62
function getMapGuideConfiguration(appDef: ApplicationDefinition): [string, MapConfiguration][] {
18✔
63
    const configs = [] as [string, MapConfiguration][];
18✔
64
    if (appDef.MapSet) {
18✔
65
        for (const mg of appDef.MapSet.MapGroup) {
18✔
66
            for (const map of mg.Map) {
76✔
67
                if (map.Type == "MapGuide") {
112✔
68
                    configs.push([mg["@id"], map]);
76✔
69
                }
76✔
70
            }
112✔
71
        }
76✔
72
    }
18✔
73
    return configs;
18✔
74
}
18✔
75

76
function tryExtractMapMetadata(extension: any) {
76✔
77
    const ext: any = {};
76✔
78
    for (const k in extension) {
76✔
79
        if (strStartsWith(k, "Meta_")) {
190!
UNCOV
80
            const sk = k.substring("Meta_".length);
×
UNCOV
81
            ext[sk] = extension[k];
×
UNCOV
82
        }
×
83
    }
190✔
84
    return ext;
76✔
85
}
76✔
86

87
export function buildSubjectLayerDefn(name: string, map: MapConfiguration): IGenericSubjectMapLayer {
1✔
88
    const st = map.Extension.source_type;
1✔
89
    const initiallyVisible = map.Extension.initially_visible ?? true;
1!
90
    const sp: any = {};
1✔
91
    const lo: any = {};
1✔
92
    const meta: any = {};
1✔
93
    const keys = Object.keys(map.Extension);
1✔
94
    let popupTemplate = map.Extension.popup_template;
1✔
95
    let selectable: boolean | undefined = map.Extension.is_selectable ?? true;
1!
96
    let disableHover: boolean | undefined = map.Extension.disable_hover ?? false;
1!
97
    for (const k of keys) {
1✔
98
        const spidx = k.indexOf("source_param_");
12✔
99
        const loidx = k.indexOf("layer_opt_");
12✔
100
        const midx = k.indexOf("meta_");
12✔
101
        if (spidx == 0) {
12✔
102
            const kn = k.substring("source_param_".length);
1✔
103
            sp[kn] = map.Extension[k];
1✔
104
        } else if (loidx == 0) {
12✔
105
            const kn = k.substring("layer_opt_".length);
1✔
106
            lo[kn] = map.Extension[k];
1✔
107
        } else if (midx == 0) {
11!
UNCOV
108
            const kn = k.substring("meta_".length);
×
UNCOV
109
            meta[kn] = map.Extension[k];
×
UNCOV
110
        }
×
111
    }
12✔
112
    const sl = {
1✔
113
        name: name,
1✔
114
        description: map.Extension.layer_description,
1✔
115
        displayName: map.Extension.display_name,
1✔
116
        driverName: map.Extension.driver_name,
1✔
117
        type: st,
1✔
118
        layerOptions: lo,
1✔
119
        sourceParams: sp,
1✔
120
        meta: (Object.keys(meta).length > 0 ? meta : undefined),
1!
121
        initiallyVisible,
1✔
122
        selectable,
1✔
123
        disableHover,
1✔
124
        popupTemplate,
1✔
125
        vectorStyle: map.Extension.vector_layer_style
1✔
126
    } as IGenericSubjectMapLayer;
1✔
127

128
    if (map.Extension.cluster) {
1✔
129
        sl.cluster = {
1✔
130
            ...map.Extension.cluster
1✔
131
        } as IClusterSettings;
1✔
132
    }
1✔
133
    return sl;
1✔
134
}
1✔
135

136
export function getMapDefinitionsFromFlexLayout(appDef: ApplicationDefinition): (MapToLoad | IGenericSubjectMapLayer)[] {
1✔
137
    const maps = [] as (MapToLoad | IGenericSubjectMapLayer)[];
18✔
138
    const configs = getMapGuideConfiguration(appDef);
18✔
139
    if (configs.length > 0) {
18✔
140
        for (const c of configs) {
18✔
141
            maps.push({ 
76✔
142
                name: c[0],
76✔
143
                mapDef: c[1].Extension.ResourceId,
76✔
144
                metadata: tryExtractMapMetadata(c[1].Extension)
76✔
145
            });
76✔
146
        }
76✔
147
    }
18✔
148
    if (appDef.MapSet?.MapGroup) {
18✔
149
        for (const mGroup of appDef.MapSet.MapGroup) {
18✔
150
            for (const map of mGroup.Map) {
76✔
151
                if (map.Type == TYPE_SUBJECT) {
112!
UNCOV
152
                    const name = mGroup["@id"];
×
UNCOV
153
                    maps.push(buildSubjectLayerDefn(name, map));
×
UNCOV
154
                }
×
155
            }
112✔
156
        }
76✔
157
    }
18✔
158
    if (maps.length == 0)
18✔
159
        throw new MgError("No Map Definition or subject layer found in Application Definition");
18!
160

161
    return maps;
18✔
162
}
18✔
163

164
export type MapToLoad = { name: string, mapDef: string, metadata: any };
165

166
export function isMapDefinition(arg: MapToLoad | IGenericSubjectMapLayer): arg is MapToLoad {
1✔
167
    return (arg as any).mapDef != null;
20✔
168
}
20✔
169

170
export function isStateless(appDef: ApplicationDefinition) {
1✔
171
    // This appdef is stateless if:
172
    //
173
    //  1. It has a Stateless extension property set to "true" (ie. The author has opted-in to this feature)
174
    //  2. No MapGuide Map Definitions were found in the appdef
175
    if (appDef.Extension?.Stateless == "true")
28✔
176
        return true;
28✔
177

178
    try {
18✔
179
        const maps = getMapDefinitionsFromFlexLayout(appDef);
18✔
180
        for (const m of maps) {
18✔
181
            if (isMapDefinition(m)) {
18✔
182
                return false;
18✔
183
            }
18✔
184
        }
18!
UNCOV
185
        return true;
×
UNCOV
186
    } catch (e) {
×
UNCOV
187
        return true;
×
UNCOV
188
    }
×
189
}
28✔
190

191
export interface IViewerInitCommand {
192
    attachClient(client: Client): void;
193
    runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload>;
194
}
195

196
export abstract class ViewerInitCommand<TSubject> implements IViewerInitCommand {
1✔
197
    constructor(protected readonly dispatch: ReduxDispatch) { }
1✔
198
    public abstract attachClient(client: Client): void;
199
    public abstract runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload>;
200
    protected abstract isArbitraryCoordSys(map: TSubject): boolean;
201
    protected abstract establishInitialMapNameAndSession(mapsByName: Dictionary<TSubject>): [string, string];
202
    protected abstract setupMaps(appDef: ApplicationDefinition, mapsByName: Dictionary<TSubject>, config: any, warnings: string[], locale: string): Dictionary<MapInfo>;
203
    protected async initLocaleAsync(options: IInitAsyncOptions): Promise<void> {
1✔
204
        //English strings are baked into this bundle. For non-en locales, we assume a strings/{locale}.json
205
        //exists for us to fetch
UNCOV
206
        const { locale } = options;
×
UNCOV
207
        if (locale != DEFAULT_LOCALE) {
×
UNCOV
208
            const r = await fetch(`strings/${locale}.json`);
×
UNCOV
209
            if (r.ok) {
×
UNCOV
210
                const res = await r.json();
×
211
                registerStringBundle(locale, res);
×
212
                // Dispatch the SET_LOCALE as it is safe to change UI strings at this point
213
                this.dispatch({
×
214
                    type: ActionType.SET_LOCALE,
×
215
                    payload: locale
×
216
                });
×
UNCOV
217
                info(`Registered string bundle for locale: ${locale}`);
×
218
            } else {
×
219
                //TODO: Push warning to init error/warning reducer when we implement it
220
                warn(`Failed to register string bundle for locale: ${locale}`);
×
221
            }
×
222
        }
×
223
    }
×
224
    protected getExtraProjectionsFromFlexLayout(appDef: ApplicationDefinition): string[] {
1✔
225
        //The only widget we care about is the coordinate tracker
226
        const epsgs: string[] = [];
×
227
        for (const ws of appDef.WidgetSet) {
×
228
            for (const w of ws.Widget) {
×
UNCOV
229
                if (w.Type == "CoordinateTracker") {
×
UNCOV
230
                    const ps = w.Extension.Projection || [];
×
231
                    for (const p of ps) {
×
232
                        epsgs.push(p.split(':')[1]);
×
233
                    }
×
234
                } else if (w.Type == "CursorPosition") {
×
235
                    const dp = w.Extension.DisplayProjection;
×
236
                    if (dp) {
×
237
                        epsgs.push(dp.split(':')[1]);
×
238
                    }
×
239
                }
×
240
            }
×
241
        }
×
242
        return makeUnique(epsgs);
×
243
    }
×
244
    
245
    protected async initFromAppDefCoreAsync(appDef: ApplicationDefinition, options: IInitAsyncOptions, mapsByName: Dictionary<TSubject | IGenericSubjectMapLayer>, warnings: string[]): Promise<IInitAppActionPayload> {
1✔
246
        const {
×
247
            taskPane,
×
248
            hasTaskBar,
×
UNCOV
249
            hasStatus,
×
UNCOV
250
            hasNavigator,
×
251
            hasSelectionPanel,
×
252
            hasLegend,
×
253
            viewSize,
×
254
            widgetsByKey,
×
255
            isStateless,
×
256
            initialTask
×
257
        } = parseWidgetsInAppDef(appDef, registerCommand);
×
258
        const { locale, featureTooltipsEnabled } = options;
×
259
        const config: any = {};
×
260
        config.isStateless = isStateless;
×
261
        const tbConf: Dictionary<ToolbarConf> = {};
×
262
        
263
        //Now build toolbar layouts
264
        for (const widgetSet of appDef.WidgetSet) {
×
265
            for (const cont of widgetSet.Container) {
×
266
                let tbName = cont.Name;
×
UNCOV
267
                tbConf[tbName] = { items: convertFlexLayoutUIItems(isStateless, cont.Item, widgetsByKey, locale) };
×
UNCOV
268
            }
×
269
            for (const w of widgetSet.Widget) {
×
270
                if (w.Type == "CursorPosition") {
×
271
                    config.coordinateProjection = w.Extension.DisplayProjection;
×
272
                    config.coordinateDecimals = w.Extension.Precision;
×
273
                    config.coordinateDisplayFormat = w.Extension.Template;
×
274
                }
×
275
            }
×
276
        }
×
277

278
        const mapsDict: any  = mapsByName; //HACK: TS generics doesn't want to play nice with us
×
279
        const maps = this.setupMaps(appDef, mapsDict, config, warnings, locale);
×
280
        if (appDef.Title) {
×
281
            document.title = appDef.Title || document.title;
×
UNCOV
282
        }
×
283
        const [firstMapName, firstSessionId] = this.establishInitialMapNameAndSession(mapsDict);
×
284
        const [tb, bFoundContextMenu] = prepareSubMenus(tbConf);
×
285
        if (!bFoundContextMenu) {
×
286
            warnings.push(tr("INIT_WARNING_NO_CONTEXT_MENU", locale, { containerName: WEBLAYOUT_CONTEXTMENU }));
×
287
        }
×
288
        const settings: Record<string, string> = {};
×
289
        if (Array.isArray(appDef.Extension?.ViewerSettings?.Setting)) {
×
290
            for (const s of appDef.Extension.ViewerSettings.Setting) {
×
291
                const [sn] = s["@name"];
×
292
                const [sv] = s["@value"];
×
293
                settings[sn] = sv;
×
294
            }
×
295
        }
×
296
        return normalizeInitPayload({
×
297
            appSettings: settings,
×
298
            activeMapName: firstMapName,
×
299
            initialUrl: ensureParameters(initialTask, firstMapName, firstSessionId, locale),
×
300
            featureTooltipsEnabled: featureTooltipsEnabled,
×
301
            locale: locale,
×
302
            maps: maps,
×
303
            config: config,
×
304
            capabilities: {
×
305
                hasTaskPane: (taskPane != null),
×
306
                hasTaskBar: hasTaskBar,
×
307
                hasStatusBar: hasStatus,
×
308
                hasNavigator: hasNavigator,
×
309
                hasSelectionPanel: hasSelectionPanel,
×
310
                hasLegend: hasLegend,
×
311
                hasToolbar: (Object.keys(tbConf).length > 0),
×
312
                hasViewSize: (viewSize != null)
×
313
            },
×
314
            toolbars: tb,
×
315
            warnings: warnings,
×
NEW
316
            initialActiveTool: ActiveMapTool.Pan,
×
NEW
317
            mapSwipePairs: parseSwipePairs(appDef)
×
318
        }, options.layout);
×
319
    }
×
320
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc