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

jumpinjackie / mapguide-react-layout / 25588111761

09 May 2026 01:42AM UTC coverage: 59.808% (+0.1%) from 59.676%
25588111761

Pull #1643

github

web-flow
Merge 0206d728e into ac1f1122c
Pull Request #1643: Refactor init command to be a dispatchable init action

3166 of 3885 branches covered (81.49%)

61 of 96 new or added lines in 3 files covered. (63.54%)

103 existing lines in 4 files now uncovered.

15796 of 26411 relevant lines covered (59.81%)

13.02 hits per line

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

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

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

60
/**
61
 * Parses a map-level mouse coordinate format override from the first map
62
 * configuration inside a MapGroup.
63
 *
64
 * Supported extension key on the first map's Extension object:
65
 * - MouseCoordinatesFormat
66
 *
67
 * @hidden
68
 * @since 0.15
69
 */
70
export function parseMapGroupCoordinateFormat(mapGroup: MapSetGroup): string | undefined {
1✔
71
    const ext = mapGroup.Map?.[0]?.Extension;
11✔
72
    if (!ext) {
11✔
73
        return undefined;
2✔
74
    }
2✔
75
    const candidate = ext.MouseCoordinatesFormat;
9✔
76
    if (typeof candidate === "string" && candidate.trim().length > 0) {
11✔
77
        return candidate;
1✔
78
    }
1✔
79
    return undefined;
8✔
80
}
8✔
81

82
const TYPE_SUBJECT = "SubjectLayer";
1✔
83
const TYPE_EXTERNAL = "External";
1✔
84

85
export type SessionInit = {
86
    session: string;
87
    sessionWasReused: boolean;
88
}
89

90
function getMapGuideConfiguration(appDef: ApplicationDefinition): [string, MapConfiguration][] {
20✔
91
    const configs = [] as [string, MapConfiguration][];
20✔
92
    if (appDef.MapSet) {
20✔
93
        for (const mg of appDef.MapSet.MapGroup) {
20✔
94
            for (const map of mg.Map) {
78✔
95
                if (map.Type == "MapGuide") {
114✔
96
                    configs.push([mg["@id"], map]);
77✔
97
                }
77✔
98
            }
114✔
99
        }
78✔
100
    }
20✔
101
    return configs;
20✔
102
}
20✔
103

104
function tryExtractMapMetadata(extension: any) {
77✔
105
    const ext: any = {};
77✔
106
    for (const k in extension) {
77✔
107
        if (strStartsWith(k, "Meta_")) {
193✔
108
            const sk = k.substring("Meta_".length);
2✔
109
            ext[sk] = extension[k];
2✔
110
        }
2✔
111
    }
193✔
112
    return ext;
77✔
113
}
77✔
114

115
export function buildSubjectLayerDefn(name: string, map: MapConfiguration): IGenericSubjectMapLayer {
1✔
116
    const st = map.Extension.source_type;
2✔
117
    const initiallyVisible = map.Extension.initially_visible ?? true;
2!
118
    const sp: any = {};
2✔
119
    const lo: any = {};
2✔
120
    const meta: any = {};
2✔
121
    const keys = Object.keys(map.Extension);
2✔
122
    let popupTemplate = map.Extension.popup_template;
2✔
123
    let selectable: boolean | undefined = map.Extension.is_selectable ?? true;
2✔
124
    let disableHover: boolean | undefined = map.Extension.disable_hover ?? false;
2✔
125
    for (const k of keys) {
2✔
126
        const spidx = k.indexOf("source_param_");
18✔
127
        const loidx = k.indexOf("layer_opt_");
18✔
128
        const midx = k.indexOf("meta_");
18✔
129
        if (spidx == 0) {
18✔
130
            const kn = k.substring("source_param_".length);
2✔
131
            sp[kn] = map.Extension[k];
2✔
132
        } else if (loidx == 0) {
18✔
133
            const kn = k.substring("layer_opt_".length);
1✔
134
            lo[kn] = map.Extension[k];
1✔
135
        } else if (midx == 0) {
16✔
136
            const kn = k.substring("meta_".length);
1✔
137
            meta[kn] = map.Extension[k];
1✔
138
        }
1✔
139
    }
18✔
140
    const sl = {
2✔
141
        name: name,
2✔
142
        description: map.Extension.layer_description,
2✔
143
        displayName: map.Extension.display_name,
2✔
144
        driverName: map.Extension.driver_name,
2✔
145
        type: st,
2✔
146
        layerOptions: lo,
2✔
147
        sourceParams: sp,
2✔
148
        meta: (Object.keys(meta).length > 0 ? meta : undefined),
2✔
149
        initiallyVisible,
2✔
150
        selectable,
2✔
151
        disableHover,
2✔
152
        popupTemplate,
2✔
153
        vectorStyle: map.Extension.vector_layer_style
2✔
154
    } as IGenericSubjectMapLayer;
2✔
155

156
    if (map.Extension.cluster) {
2✔
157
        sl.cluster = {
1✔
158
            ...map.Extension.cluster
1✔
159
        } as IClusterSettings;
1✔
160
    }
1✔
161
    return sl;
2✔
162
}
2✔
163

164
export function getMapDefinitionsFromFlexLayout(appDef: ApplicationDefinition): (MapToLoad | IGenericSubjectMapLayer)[] {
1✔
165
    const maps = [] as (MapToLoad | IGenericSubjectMapLayer)[];
20✔
166
    const configs = getMapGuideConfiguration(appDef);
20✔
167
    if (configs.length > 0) {
20✔
168
        for (const c of configs) {
19✔
169
            maps.push({ 
77✔
170
                name: c[0],
77✔
171
                mapDef: c[1].Extension.ResourceId,
77✔
172
                metadata: tryExtractMapMetadata(c[1].Extension)
77✔
173
            });
77✔
174
        }
77✔
175
    }
19✔
176
    if (appDef.MapSet?.MapGroup) {
20✔
177
        for (const mGroup of appDef.MapSet.MapGroup) {
20✔
178
            for (const map of mGroup.Map) {
78✔
179
                if (map.Type == TYPE_SUBJECT) {
114✔
180
                    const name = mGroup["@id"];
1✔
181
                    maps.push(buildSubjectLayerDefn(name, map));
1✔
182
                }
1✔
183
            }
114✔
184
        }
78✔
185
    }
20✔
186
    if (maps.length == 0)
20✔
187
        throw new MgError("No Map Definition or subject layer found in Application Definition");
20✔
188

189
    return maps;
19✔
190
}
19✔
191

192
export type MapToLoad = { name: string, mapDef: string, metadata: any };
193

194
export function isMapDefinition(arg: MapToLoad | IGenericSubjectMapLayer): arg is MapToLoad {
1✔
195
    return (arg as any).mapDef != null;
21✔
196
}
21✔
197

198
export function isStateless(appDef: ApplicationDefinition) {
1✔
199
    // This appdef is stateless if:
200
    //
201
    //  1. It has a Stateless extension property set to "true" (ie. The author has opted-in to this feature)
202
    //  2. No MapGuide Map Definitions were found in the appdef
203
    if (appDef.Extension?.Stateless == "true")
28✔
204
        return true;
28✔
205

206
    try {
18✔
207
        const maps = getMapDefinitionsFromFlexLayout(appDef);
18✔
208
        for (const m of maps) {
18✔
209
            if (isMapDefinition(m)) {
18✔
210
                return false;
18✔
211
            }
18✔
212
        }
18!
213
        return true;
×
214
    } catch (e) {
×
215
        return true;
×
UNCOV
216
    }
×
217
}
28✔
218

219
export interface IViewerInitCommand {
220
    attachClient(client: Client): void;
221
    /**
222
     * Optionally sets the map provider context (viewer) on the command. When set, the command
223
     * will dispatch {@link initAppFromAppDef} for application-definition based resources instead
224
     * of building the payload internally, routing the full init flow through the new action.
225
     *
226
     * @since 0.15
227
     */
228
    setViewer?(viewer: IMapProviderContext): void;
229
    runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload | undefined>;
230
}
231

232
export abstract class ViewerInitCommand<TSubject> implements IViewerInitCommand {
1✔
233
    constructor(protected readonly dispatch: ReduxDispatch) { }
1✔
234
    public abstract attachClient(client: Client): void;
235
    public abstract runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload | undefined>;
236
    protected abstract isArbitraryCoordSys(map: TSubject): boolean;
237
    protected abstract establishInitialMapNameAndSession(mapsByName: Dictionary<TSubject>): [string, string];
238
    protected abstract setupMaps(appDef: ApplicationDefinition, mapsByName: Dictionary<TSubject>, config: any, warnings: string[], locale: string, pendingMapDefs?: Dictionary<MapToLoad>): Dictionary<MapInfo>;
239
    protected async initLocaleAsync(options: IInitAsyncOptions): Promise<void> {
1✔
240
        //English strings are baked into this bundle. For non-en locales, we assume a strings/{locale}.json
241
        //exists for us to fetch
242
        const { locale } = options;
1✔
243
        if (locale != DEFAULT_LOCALE) {
1!
244
            const r = await fetch(`strings/${locale}.json`);
×
245
            if (r.ok) {
×
246
                const res = await r.json();
×
UNCOV
247
                registerStringBundle(locale, res);
×
248
                // Dispatch the SET_LOCALE as it is safe to change UI strings at this point
UNCOV
249
                this.dispatch({
×
UNCOV
250
                    type: ActionType.SET_LOCALE,
×
UNCOV
251
                    payload: locale
×
UNCOV
252
                });
×
253
                info(`Registered string bundle for locale: ${locale}`);
×
254
            } else {
×
255
                //TODO: Push warning to init error/warning reducer when we implement it
UNCOV
256
                warn(`Failed to register string bundle for locale: ${locale}`);
×
UNCOV
257
            }
×
UNCOV
258
        }
×
259
    }
1✔
260
    protected getExtraProjectionsFromFlexLayout(appDef: ApplicationDefinition): string[] {
1✔
261
        //The only widget we care about is the coordinate tracker
262
        const epsgs: string[] = [];
×
263
        for (const ws of appDef.WidgetSet) {
×
264
            for (const w of ws.Widget) {
×
265
                if (w.Type == "CoordinateTracker") {
×
266
                    const ps = w.Extension.Projection || [];
×
UNCOV
267
                    for (const p of ps) {
×
268
                        epsgs.push(p.split(':')[1]);
×
269
                    }
×
270
                } else if (w.Type == "CursorPosition") {
×
271
                    const dp = w.Extension.DisplayProjection;
×
272
                    if (dp) {
×
273
                        epsgs.push(dp.split(':')[1]);
×
UNCOV
274
                    }
×
275
                }
×
276
            }
×
277
        }
×
278
        return makeUnique(epsgs);
×
UNCOV
279
    }
×
280
    
281
    protected async initFromAppDefCoreAsync(appDef: ApplicationDefinition, options: IInitAsyncOptions, mapsByName: Dictionary<TSubject | IGenericSubjectMapLayer>, warnings: string[], pendingMapDefs?: Dictionary<MapToLoad>): Promise<IInitAppActionPayload> {
1✔
282
        const {
×
283
            taskPane,
×
284
            hasTaskBar,
×
285
            hasStatus,
×
286
            hasNavigator,
×
287
            hasSelectionPanel,
×
288
            hasLegend,
×
289
            viewSize,
×
290
            widgetsByKey,
×
291
            isStateless,
×
292
            initialTask
×
293
        } = parseWidgetsInAppDef(appDef, registerCommand);
×
294
        const { locale, featureTooltipsEnabled } = options;
×
295
        const config: any = {};
×
296
        config.isStateless = isStateless;
×
297
        const tbConf: Dictionary<ToolbarConf> = {};
×
298
        
299
        //Now build toolbar layouts
UNCOV
300
        for (const widgetSet of appDef.WidgetSet) {
×
301
            for (const cont of widgetSet.Container) {
×
302
                let tbName = cont.Name;
×
303
                tbConf[tbName] = { items: convertFlexLayoutUIItems(isStateless, cont.Item, widgetsByKey, locale) };
×
304
            }
×
305
            for (const w of widgetSet.Widget) {
×
306
                if (w.Type == "CursorPosition") {
×
307
                    config.coordinateProjection = w.Extension.DisplayProjection;
×
308
                    config.coordinateDecimals = w.Extension.Precision;
×
309
                    config.coordinateDisplayFormat = w.Extension.Template;
×
310
                }
×
311
            }
×
312
        }
×
313

314
        const mapsDict: any  = mapsByName; //HACK: TS generics doesn't want to play nice with us
×
315
        const maps = this.setupMaps(appDef, mapsDict, config, warnings, locale, pendingMapDefs);
×
316
        if (appDef.Title) {
×
UNCOV
317
            document.title = appDef.Title || document.title;
×
UNCOV
318
        }
×
319
        const [firstMapName, firstSessionId] = this.establishInitialMapNameAndSession(mapsDict);
×
320
        const [tb, bFoundContextMenu] = prepareSubMenus(tbConf);
×
321
        if (!bFoundContextMenu) {
×
322
            warnings.push(tr("INIT_WARNING_NO_CONTEXT_MENU", locale, { containerName: WEBLAYOUT_CONTEXTMENU }));
×
323
        }
×
324
        const settings: Record<string, string> = {};
×
325
        if (Array.isArray(appDef.Extension?.ViewerSettings?.Setting)) {
×
326
            for (const s of appDef.Extension.ViewerSettings.Setting) {
×
327
                const [sn] = s["@name"];
×
328
                const [sv] = s["@value"];
×
329
                settings[sn] = sv;
×
330
            }
×
331
        }
×
UNCOV
332
        return normalizeInitPayload({
×
333
            appSettings: settings,
×
334
            activeMapName: firstMapName,
×
335
            initialUrl: ensureParameters(initialTask, firstMapName, firstSessionId, locale),
×
336
            featureTooltipsEnabled: featureTooltipsEnabled,
×
337
            locale: locale,
×
338
            maps: maps,
×
339
            config: config,
×
340
            capabilities: {
×
341
                hasTaskPane: (taskPane != null),
×
342
                hasTaskBar: hasTaskBar,
×
343
                hasStatusBar: hasStatus,
×
344
                hasNavigator: hasNavigator,
×
345
                hasSelectionPanel: hasSelectionPanel,
×
346
                hasLegend: hasLegend,
×
347
                hasToolbar: (Object.keys(tbConf).length > 0),
×
348
                hasViewSize: (viewSize != null)
×
349
            },
×
350
            toolbars: tb,
×
351
            warnings: warnings,
×
352
            initialActiveTool: ActiveMapTool.Pan,
×
353
            mapSwipePairs: parseSwipePairs(appDef)
×
354
        }, options.layout);
×
355
    }
×
356
}
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