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

jumpinjackie / mapguide-react-layout / 23978816404

04 Apr 2026 12:23PM UTC coverage: 40.912% (-0.02%) from 40.934%
23978816404

Pull #1598

github

web-flow
Merge a7bb589a0 into a72c84739
Pull Request #1598: Mouse coordinates format is now multi-map aware

1901 of 2432 branches covered (78.17%)

4 of 24 new or added lines in 4 files covered. (16.67%)

112 existing lines in 5 files now uncovered.

10187 of 24900 relevant lines covered (40.91%)

10.03 hits per line

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

56.3
/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 { IGenericSubjectMapLayer, IInitAppActionPayload, MapInfo } from './defs';
5
import { ToolbarConf, convertFlexLayoutUIItems, parseWidgetsInAppDef, prepareSubMenus } from '../api/registry/command-spec';
1✔
6
import { makeUnique } from '../utils/array';
1✔
7
import { ApplicationDefinition, MapConfiguration } from '../api/contracts/fusion';
8
import { warn, info } from '../utils/logger';
1✔
9
import { registerCommand } from '../api/registry/command';
1✔
10
import { tr, registerStringBundle, DEFAULT_LOCALE } from '../api/i18n';
1✔
11
import { WEBLAYOUT_CONTEXTMENU } from "../constants";
1✔
12
import { Client } from '../api/client';
13
import { ActionType } from '../constants/actions';
1✔
14
import { ensureParameters } from '../utils/url';
1✔
15
import { MgError } from '../api/error';
1✔
16
import { strStartsWith } from '../utils/string';
1✔
17
import { IClusterSettings } from '../api/ol-style-contracts';
18

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

59
const TYPE_SUBJECT = "SubjectLayer";
1✔
60
const TYPE_EXTERNAL = "External";
1✔
61

62
export type SessionInit = {
63
    session: string;
64
    sessionWasReused: boolean;
65
}
66

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

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

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

133
    if (map.Extension.cluster) {
1✔
134
        sl.cluster = {
1✔
135
            ...map.Extension.cluster
1✔
136
        } as IClusterSettings;
1✔
137
    }
1✔
138
    return sl;
1✔
139
}
1✔
140

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

166
    return maps;
18✔
167
}
18✔
168

169
export type MapToLoad = { name: string, mapDef: string, metadata: any };
170

171
export function isMapDefinition(arg: MapToLoad | IGenericSubjectMapLayer): arg is MapToLoad {
1✔
172
    return (arg as any).mapDef != null;
20✔
173
}
20✔
174

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

183
    try {
18✔
184
        const maps = getMapDefinitionsFromFlexLayout(appDef);
18✔
185
        for (const m of maps) {
18✔
186
            if (isMapDefinition(m)) {
18✔
187
                return false;
18✔
188
            }
18✔
189
        }
18!
UNCOV
190
        return true;
×
UNCOV
191
    } catch (e) {
×
UNCOV
192
        return true;
×
UNCOV
193
    }
×
194
}
28✔
195

196
export interface IViewerInitCommand {
197
    attachClient(client: Client): void;
198
    runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload>;
199
}
200

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

283
        const mapsDict: any  = mapsByName; //HACK: TS generics doesn't want to play nice with us
×
284
        const maps = this.setupMaps(appDef, mapsDict, config, warnings, locale, pendingMapDefs);
×
285
        if (appDef.Title) {
×
286
            document.title = appDef.Title || document.title;
×
287
        }
×
288
        const [firstMapName, firstSessionId] = this.establishInitialMapNameAndSession(mapsDict);
×
UNCOV
289
        const [tb, bFoundContextMenu] = prepareSubMenus(tbConf);
×
UNCOV
290
        if (!bFoundContextMenu) {
×
291
            warnings.push(tr("INIT_WARNING_NO_CONTEXT_MENU", locale, { containerName: WEBLAYOUT_CONTEXTMENU }));
×
292
        }
×
293
        const settings: Record<string, string> = {};
×
294
        if (Array.isArray(appDef.Extension?.ViewerSettings?.Setting)) {
×
295
            for (const s of appDef.Extension.ViewerSettings.Setting) {
×
296
                const [sn] = s["@name"];
×
297
                const [sv] = s["@value"];
×
298
                settings[sn] = sv;
×
299
            }
×
300
        }
×
301
        return normalizeInitPayload({
×
302
            appSettings: settings,
×
303
            activeMapName: firstMapName,
×
UNCOV
304
            initialUrl: ensureParameters(initialTask, firstMapName, firstSessionId, locale),
×
305
            featureTooltipsEnabled: featureTooltipsEnabled,
×
306
            locale: locale,
×
307
            maps: maps,
×
308
            config: config,
×
309
            capabilities: {
×
310
                hasTaskPane: (taskPane != null),
×
311
                hasTaskBar: hasTaskBar,
×
312
                hasStatusBar: hasStatus,
×
313
                hasNavigator: hasNavigator,
×
314
                hasSelectionPanel: hasSelectionPanel,
×
315
                hasLegend: hasLegend,
×
316
                hasToolbar: (Object.keys(tbConf).length > 0),
×
317
                hasViewSize: (viewSize != null)
×
318
            },
×
319
            toolbars: tb,
×
320
            warnings: warnings,
×
321
            initialActiveTool: ActiveMapTool.Pan,
×
322
            mapSwipePairs: parseSwipePairs(appDef)
×
323
        }, options.layout);
×
324
    }
×
325
}
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