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

jumpinjackie / mapguide-react-layout / 26033267383

18 May 2026 12:23PM UTC coverage: 60.531% (+0.9%) from 59.676%
26033267383

push

github

web-flow
Dispatchable init action (#1648)

* Update instructions

* Refactor initialization logic and remove viewer command dependency

- Replaced the `initLayout` command with `fetchInitDocument` and `initAppFromDocument` in the App component.
- Updated the `IInitAppLayout` interface to accommodate `WebLayout` alongside `ApplicationDefinition`.
- Enhanced error handling by exporting `processAndDispatchInitError`.
- Removed the `IViewerInitCommand` dependency from various components and tests.
- Adjusted tests to reflect changes in initialization logic and ensure proper mocking of new functions.
- Cleaned up imports and unused code related to the old initialization command.

* Add integration tests for init dispatchable action and snapshot for redux state

- Implemented a new test file `init-dispatchable.spec.ts` to test the `initAppFromDocument` action.
- Created a mock client to simulate API interactions.
- Added a snapshot file `init-dispatchable.spec.ts.snap` to capture the expected redux baseline state after dispatching the action.
- Ensured the legacy app definition is correctly de-arrayified and integrated into the redux store.

3235 of 3962 branches covered (81.65%)

447 of 723 new or added lines in 4 files covered. (61.83%)

9 existing lines in 1 file now uncovered.

15990 of 26416 relevant lines covered (60.53%)

14.06 hits per line

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

67.24
/src/actions/init-mapguide.ts
1
import { ApplicationDefinition } from '../api/contracts/fusion';
2
import { MapGroup, MapLayer, RuntimeMap } from '../api/contracts/runtime-map';
3
import { Dictionary, IExternalBaseLayer, ReduxDispatch, ActiveMapTool, IMapView } from '../api/common';
1✔
4
import { MapInfo, IInitAppActionPayload, IGenericSubjectMapLayer, GenericSubjectLayerType } from './defs';
1✔
5
import { tr, DEFAULT_LOCALE, registerStringBundle } from '../api/i18n';
1✔
6
import { isResourceId, strEndsWith, strIsNullOrEmpty } from '../utils/string';
1✔
7
import { Client } from '../api/client';
1✔
8
import { applyInitialBaseLayerVisibility, IInitAsyncOptions, normalizeInitPayload, processLayerInMapGroup } from './init';
1✔
9
import { ICreateRuntimeMapOptions, IDescribeRuntimeMapOptions, RuntimeMapFeatureFlags } from '../api/request-builder';
1✔
10
import { info, debug, warn } from '../utils/logger';
1✔
11
import { MgError } from '../api/error';
1✔
12
import { resolveProjectionFromEpsgCodeAsync } from '../api/registry/projections';
1✔
13
import { register } from 'ol/proj/proj4';
1✔
14
import proj4 from "proj4";
1✔
15
import { buildSubjectLayerDefn, getExtraProjectionsFromFlexLayout, getMapDefinitionsFromFlexLayout, isMapDefinition, isStateless, parseMapGroupCoordinateFormat, parseSwipePairs, MapToLoad } from './init-command';
1✔
16
import { WebLayout } from '../api/contracts/weblayout';
17
import { convertFlexLayoutUIItems, convertWebLayoutUIItems, parseCommandsInWebLayout, parseWidgetsInAppDef, prepareSubMenus, ToolbarConf } from '../api/registry/command-spec';
1✔
18
import { WEBLAYOUT_CONTEXTMENU, WEBLAYOUT_TASKMENU, WEBLAYOUT_TOOLBAR } from "../constants";
1✔
19
import { registerCommand } from '../api/registry/command';
1✔
20
import { ensureParameters } from '../utils/url';
1✔
21
import { assertIsDefined } from '../utils/assert';
1✔
22
import { MapDefinition } from '../api/contracts/map-definition';
23
import { TileSetDefinition } from '../api/contracts/tile-set-definition';
24
import { AsyncLazy } from '../api/lazy';
1✔
25
import { SiteVersionResponse } from '../api/contracts/common';
26
import { isRuntimeMap } from '../utils/type-guards';
1✔
27
import { tryParseArbitraryCs } from '../utils/units';
1✔
28
import { ScopedId } from '../utils/scoped-id';
1✔
29
import { canUseQueryMapFeaturesV4, parseSiteVersion } from '../utils/site-version';
1✔
30
import { supportsWebGL } from '../utils/browser-support';
1✔
31
import { isAppDef, isWebLayout } from '../api/builders/deArrayify';
1✔
32
import { ActionType } from '../constants/actions';
1✔
33

34
const TYPE_SUBJECT = "SubjectLayer";
1✔
35
const TYPE_EXTERNAL = "External";
1✔
36

37
const scopedId = new ScopedId();
1✔
38

39
/**
40
 * @since 0.14
41
 */
42
export type SubjectLayerType = RuntimeMap | IGenericSubjectMapLayer;
43

44
/**
45
 * Returns whether the supplied runtime map uses an arbitrary coordinate system.
46
 */
47
export function isArbitraryCoordSys(subject: SubjectLayerType | undefined) {
1✔
48
    if (subject) {
13✔
49
        if (isRuntimeMap(subject)) {
8✔
50
            const arbCs = tryParseArbitraryCs(subject.CoordinateSystem.MentorCode);
7✔
51
            return arbCs != null;
7✔
52
        }
7✔
53
    }
8✔
54
    return false;
6✔
55
}
6✔
56
/**
57
 * Finds the first runtime map entry and returns its map name and session id.
58
 */
59
export function establishInitialMapNameAndSession(mapsByName: Dictionary<SubjectLayerType>): [string, string] {
1✔
60
    let firstMapName = "";
3✔
61
    let firstSessionId = "";
3✔
62
    for (const mapName in mapsByName) {
3✔
63
        if (!firstMapName && !firstSessionId) {
4✔
64
            const map = mapsByName[mapName];
4✔
65
            if (isRuntimeMap(map)) {
4✔
66
                firstMapName = map.Name;
2✔
67
                firstSessionId = map.SessionId;
2✔
68
                break;
2✔
69
            }
2✔
70
        }
4✔
71
    }
4✔
72
    return [firstMapName, firstSessionId];
3✔
73
}
3✔
74
/**
75
 * Loads and registers the localized string bundle when a non-default locale is requested.
76
 */
77
export async function initLocaleAsync(dispatch: ReduxDispatch, options: IInitAsyncOptions): Promise<void> {
1✔
78
    const { locale } = options;
1✔
79
    if (locale != DEFAULT_LOCALE) {
1!
NEW
80
        const r = await fetch(`strings/${locale}.json`);
×
NEW
81
        if (r.ok) {
×
NEW
82
            const res = await r.json();
×
NEW
83
            registerStringBundle(locale, res);
×
NEW
84
            dispatch({
×
NEW
85
                type: ActionType.SET_LOCALE,
×
NEW
86
                payload: locale
×
NEW
87
            });
×
NEW
88
            info(`Registered string bundle for locale: ${locale}`);
×
UNCOV
89
        } else {
×
NEW
90
            warn(`Failed to register string bundle for locale: ${locale}`);
×
UNCOV
91
        }
×
UNCOV
92
    }
×
93
}
1✔
94
/**
95
 * Builds the normalized init payload from an ApplicationDefinition and map state.
96
 */
97
async function initFromAppDefCoreAsync(appDef: ApplicationDefinition, options: IInitAsyncOptions, mapsByName: Dictionary<SubjectLayerType | IGenericSubjectMapLayer>, warnings: string[], pendingMapDefs?: Dictionary<MapToLoad>): Promise<IInitAppActionPayload> {
1✔
98
    const {
1✔
99
        taskPane,
1✔
100
        hasTaskBar,
1✔
101
        hasStatus,
1✔
102
        hasNavigator,
1✔
103
        hasSelectionPanel,
1✔
104
        hasLegend,
1✔
105
        viewSize,
1✔
106
        widgetsByKey,
1✔
107
        isStateless,
1✔
108
        initialTask
1✔
109
    } = parseWidgetsInAppDef(appDef, registerCommand);
1✔
110
    const { locale, featureTooltipsEnabled } = options;
1✔
111
    const config: any = {};
1✔
112
    config.isStateless = isStateless;
1✔
113
    const tbConf: Dictionary<ToolbarConf> = {};
1✔
114

115
    for (const widgetSet of appDef.WidgetSet) {
1✔
116
        for (const cont of widgetSet.Container) {
1✔
117
            const tbName = cont.Name;
7✔
118
            tbConf[tbName] = { items: convertFlexLayoutUIItems(isStateless, cont.Item, widgetsByKey, locale) };
7✔
119
        }
7✔
120
        for (const w of widgetSet.Widget) {
1✔
121
            if (w.Type == "CursorPosition") {
70✔
122
                config.coordinateProjection = w.Extension.DisplayProjection;
1✔
123
                config.coordinateDecimals = w.Extension.Precision;
1✔
124
                config.coordinateDisplayFormat = w.Extension.Template;
1✔
125
            }
1✔
126
        }
70✔
127
    }
1✔
128

129
    const mapsDict: any = mapsByName;
1✔
130
    const maps = setupMaps(appDef, mapsDict, config, warnings, locale, pendingMapDefs);
1✔
131
    if (appDef.Title) {
1✔
132
        document.title = appDef.Title || document.title;
1!
133
    }
1✔
134
    const [firstMapName, firstSessionId] = establishInitialMapNameAndSession(mapsDict);
1✔
135
    const [tb, bFoundContextMenu] = prepareSubMenus(tbConf);
1✔
136
    if (!bFoundContextMenu) {
1!
NEW
137
        warnings.push(tr("INIT_WARNING_NO_CONTEXT_MENU", locale, { containerName: WEBLAYOUT_CONTEXTMENU }));
×
NEW
138
    }
×
139
    const settings: Record<string, string> = {};
1✔
140
    if (Array.isArray(appDef.Extension?.ViewerSettings?.Setting)) {
1!
NEW
141
        for (const s of appDef.Extension.ViewerSettings.Setting) {
×
NEW
142
            const [sn] = s["@name"];
×
NEW
143
            const [sv] = s["@value"];
×
NEW
144
            settings[sn] = sv;
×
UNCOV
145
        }
×
NEW
146
    }
×
147
    return normalizeInitPayload({
1✔
148
        appSettings: settings,
1✔
149
        activeMapName: firstMapName,
1✔
150
        initialUrl: ensureParameters(initialTask, firstMapName, firstSessionId, locale),
1✔
151
        featureTooltipsEnabled: featureTooltipsEnabled,
1✔
152
        locale: locale,
1✔
153
        maps: maps,
1✔
154
        config: config,
1✔
155
        capabilities: {
1✔
156
            hasTaskPane: (taskPane != null),
1✔
157
            hasTaskBar: hasTaskBar,
1✔
158
            hasStatusBar: hasStatus,
1✔
159
            hasNavigator: hasNavigator,
1✔
160
            hasSelectionPanel: hasSelectionPanel,
1✔
161
            hasLegend: hasLegend,
1✔
162
            hasToolbar: (Object.keys(tbConf).length > 0),
1✔
163
            hasViewSize: (viewSize != null)
1✔
164
        },
1✔
165
        toolbars: tb,
1✔
166
        warnings: warnings,
1✔
167
        initialActiveTool: ActiveMapTool.Pan,
1✔
168
        mapSwipePairs: parseSwipePairs(appDef)
1✔
169
    }, options.layout);
1✔
170
}
1✔
171
/**
172
 * Derives a stable target runtime map name from a map definition resource id.
173
 */
NEW
174
function getDesiredTargetMapName(mapDef: string) {
×
NEW
175
    const lastSlash = mapDef.lastIndexOf("/");
×
NEW
176
    const lastDot = mapDef.lastIndexOf(".");
×
NEW
177
    if (lastSlash >= 0 && lastDot >= 0 && lastDot > lastSlash) {
×
NEW
178
        return `${mapDef.substring(lastSlash + 1, lastDot)}`;
×
NEW
179
    } else {
×
NEW
180
        return `Map_${scopedId.next()}`;
×
NEW
181
    }
×
NEW
182
}
×
183
/**
184
 * Initializes viewer payload from a WebLayout document.
185
 */
NEW
186
async function initFromWebLayoutAsync(client: Client | undefined, options: IInitAsyncOptions, webLayout: WebLayout, session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload> {
×
NEW
187
    const [mapsByName, , warnings] = await createRuntimeMapsAsync(client, options, session, webLayout, false, wl => [{ name: getDesiredTargetMapName(wl.Map.ResourceId), mapDef: wl.Map.ResourceId, metadata: {} }], () => [], sessionWasReused);
×
NEW
188
    const { locale, featureTooltipsEnabled, externalBaseLayers } = options;
×
NEW
189
    const cmdsByKey = parseCommandsInWebLayout(webLayout, registerCommand);
×
NEW
190
    const mainToolbar = (webLayout.ToolBar.Visible
×
NEW
191
        ? convertWebLayoutUIItems(webLayout.ToolBar.Button, cmdsByKey, locale)
×
NEW
192
        : []);
×
NEW
193
    const taskBar = (webLayout.TaskPane.TaskBar.Visible
×
NEW
194
        ? convertWebLayoutUIItems(webLayout.TaskPane.TaskBar.MenuButton, cmdsByKey, locale, false)
×
NEW
195
        : []);
×
NEW
196
    const contextMenu = (webLayout.ContextMenu.Visible
×
NEW
197
        ? convertWebLayoutUIItems(webLayout.ContextMenu.MenuItem, cmdsByKey, locale, false)
×
NEW
198
        : []);
×
NEW
199
    const config: any = {};
×
NEW
200
    if (webLayout.SelectionColor != null) {
×
NEW
201
        config.selectionColor = webLayout.SelectionColor;
×
NEW
202
    }
×
NEW
203
    if (webLayout.MapImageFormat != null) {
×
NEW
204
        config.imageFormat = webLayout.MapImageFormat;
×
NEW
205
    }
×
NEW
206
    if (webLayout.SelectionImageFormat != null) {
×
NEW
207
        config.selectionImageFormat = webLayout.SelectionImageFormat;
×
NEW
208
    }
×
NEW
209
    if (webLayout.PointSelectionBuffer != null) {
×
NEW
210
        config.pointSelectionBuffer = webLayout.PointSelectionBuffer;
×
NEW
211
    }
×
NEW
212
    let initialView: IMapView | null = null;
×
NEW
213
    if (webLayout.Map.InitialView != null) {
×
NEW
214
        initialView = {
×
NEW
215
            x: webLayout.Map.InitialView.CenterX,
×
NEW
216
            y: webLayout.Map.InitialView.CenterY,
×
NEW
217
            scale: webLayout.Map.InitialView.Scale
×
NEW
218
        };
×
NEW
219
    }
×
220

NEW
221
    if (webLayout.Title != "") {
×
NEW
222
        document.title = webLayout.Title || document.title;
×
NEW
223
    }
×
224

NEW
225
    const maps: any = {};
×
NEW
226
    const [firstMapName, firstSessionId] = establishInitialMapNameAndSession(mapsByName);
×
227

NEW
228
    for (const mapName in mapsByName) {
×
NEW
229
        const map = mapsByName[mapName];
×
NEW
230
        maps[mapName] = {
×
NEW
231
            mapGroupId: mapName,
×
NEW
232
            map: map,
×
NEW
233
            externalBaseLayers: options.externalBaseLayers ?? [],
×
NEW
234
            initialView: initialView
×
UNCOV
235
        };
×
NEW
236
    }
×
237

NEW
238
    const menus: Dictionary<ToolbarConf> = {};
×
NEW
239
    menus[WEBLAYOUT_TOOLBAR] = {
×
NEW
240
        items: mainToolbar
×
NEW
241
    };
×
NEW
242
    menus[WEBLAYOUT_TASKMENU] = {
×
NEW
243
        items: taskBar
×
NEW
244
    };
×
NEW
245
    menus[WEBLAYOUT_CONTEXTMENU] = {
×
NEW
246
        items: contextMenu
×
NEW
247
    };
×
248

NEW
249
    const tb = prepareSubMenus(menus)[0];
×
NEW
250
    return {
×
NEW
251
        activeMapName: firstMapName,
×
NEW
252
        featureTooltipsEnabled: featureTooltipsEnabled,
×
NEW
253
        initialUrl: ensureParameters(webLayout.TaskPane.InitialTask || "server/TaskPane.html", firstMapName, firstSessionId, locale),
×
NEW
254
        initialTaskPaneWidth: webLayout.TaskPane.Width,
×
NEW
255
        initialInfoPaneWidth: webLayout.InformationPane.Width,
×
NEW
256
        maps: maps,
×
NEW
257
        locale: locale,
×
NEW
258
        config: config,
×
NEW
259
        capabilities: {
×
NEW
260
            hasTaskPane: webLayout.TaskPane.Visible,
×
NEW
261
            hasTaskBar: webLayout.TaskPane.TaskBar.Visible,
×
NEW
262
            hasStatusBar: webLayout.StatusBar.Visible,
×
NEW
263
            hasNavigator: webLayout.ZoomControl.Visible,
×
NEW
264
            hasSelectionPanel: webLayout.InformationPane.Visible && webLayout.InformationPane.PropertiesVisible,
×
NEW
265
            hasLegend: webLayout.InformationPane.Visible && webLayout.InformationPane.LegendVisible,
×
NEW
266
            hasToolbar: webLayout.ToolBar.Visible,
×
NEW
267
            hasViewSize: webLayout.StatusBar.Visible
×
NEW
268
        },
×
NEW
269
        toolbars: tb,
×
NEW
270
        warnings: warnings,
×
NEW
271
        initialActiveTool: ActiveMapTool.Pan
×
NEW
272
    };
×
NEW
273
}
×
274
/**
275
 * Creates a runtime map using the API variant supported by the current site version.
276
 */
277
export async function createRuntimeMap(client: Client, options: ICreateRuntimeMapOptions, siteVersion: AsyncLazy<SiteVersionResponse>): Promise<RuntimeMap> {
4✔
278
    let map: RuntimeMap;
4✔
279
    const sv = await siteVersion.getValueAsync();
4✔
280
    if (canUseQueryMapFeaturesV4(parseSiteVersion(sv.Version))) {
4✔
281
        map = await client.createRuntimeMap_v4(options);
2✔
282
    } else {
2✔
283
        map = await client.createRuntimeMap(options);
2✔
284
    }
2✔
285
    return map;
4✔
286
}
4✔
287
/**
288
 * Describes an existing runtime map using the API variant supported by the current site version.
289
 */
290
export async function describeRuntimeMap(client: Client, options: IDescribeRuntimeMapOptions, siteVersion: AsyncLazy<SiteVersionResponse>): Promise<RuntimeMap> {
4✔
291
    let map: RuntimeMap;
4✔
292
    const sv = await siteVersion.getValueAsync();
4✔
293
    if (canUseQueryMapFeaturesV4(parseSiteVersion(sv.Version))) {
4✔
294
        map = await client.describeRuntimeMap_v4(options);
1✔
295
    } else {
4✔
296
        map = await client.describeRuntimeMap(options);
3✔
297
    }
1✔
298
    return map;
2✔
299
}
2✔
300
/**
301
 * Attempts to describe an existing runtime map and creates it if the resource does not exist.
302
 */
303
export async function tryDescribeRuntimeMapAsync(client: Client, mapName: string, session: AsyncLazy<string>, mapDef: string, siteVersion: AsyncLazy<SiteVersionResponse>) {
2✔
304
    try {
2✔
305
        const map = await describeRuntimeMap(client, {
2✔
306
            mapname: mapName,
2✔
307
            requestedFeatures: RuntimeMapFeatureFlags.LayerFeatureSources | RuntimeMapFeatureFlags.LayerIcons | RuntimeMapFeatureFlags.LayersAndGroups,
2✔
308
            session: await session.getValueAsync()
2✔
309
        }, siteVersion);
2!
UNCOV
310
        return map;
×
311
    } catch (e) {
2✔
312
        if ((e as any).message === "MgResourceNotFoundException") {
2✔
313
            const map = await createRuntimeMap(client, {
1✔
314
                mapDefinition: mapDef,
1✔
315
                requestedFeatures: RuntimeMapFeatureFlags.LayerFeatureSources | RuntimeMapFeatureFlags.LayerIcons | RuntimeMapFeatureFlags.LayersAndGroups,
1✔
316
                session: await session.getValueAsync(),
1✔
317
                targetMapName: mapName
1✔
318
            }, siteVersion);
1✔
319
            return map;
1✔
320
        }
1✔
321
        throw e;
1✔
322
    }
1✔
323
}
2✔
324
/**
325
 * Creates and/or recovers runtime maps and collects pending lazy maps for deferred creation.
326
 */
327
async function createRuntimeMapsAsync<TLayout>(client: Client | undefined, options: IInitAsyncOptions, session: AsyncLazy<string>, res: TLayout, isStateless: boolean, mapDefSelector: (res: TLayout) => (MapToLoad | IGenericSubjectMapLayer)[], projectionSelector: (res: TLayout) => string[], sessionWasReused: boolean): Promise<[Dictionary<SubjectLayerType>, Dictionary<MapToLoad>, string[]]> {
1✔
328
    const mapDefs = mapDefSelector(res);
1✔
329
    const mapPromises: Promise<RuntimeMap>[] = [];
1✔
330
    const warnings = [] as string[];
1✔
331
    const { locale } = options;
1✔
332
    const subjectLayers: Dictionary<IGenericSubjectMapLayer> = {};
1✔
333
    const fetchEpsgs: { epsg: string, mapDef: string }[] = [];
1✔
334
    const pendingMapDefs: Dictionary<MapToLoad> = {};
1✔
335
    // We use an AsyncLazy because we only want to fetch the site version *iff* we are required to
336
    const siteVersion = new AsyncLazy<SiteVersionResponse>(async () => {
1✔
337
        assertIsDefined(client);
1✔
338
        const sv = await client.getSiteVersion();
1✔
339
        return sv;
1✔
340
    });
1✔
341
    // Collect only the MapDefinition entries for lazy-load eligibility check
342
    const mapDefItems = mapDefs.filter(isMapDefinition);
1✔
343
    // Lazy creation only applies when: not stateless and there are multiple MapGuide maps.
344
    // Note: We intentionally do NOT exclude sessionWasReused here. Even on a browser refresh
345
    // (where the session is reused), non-active maps should still be deferred because they may
346
    // never have been created in the previous session (the user may not have switched to them).
347
    // These deferred maps will be lazily initialized via activateMap() when the user switches
348
    // to them, which now tries to describe the existing map first before creating a new one.
349
    const canLazyLoad = !isStateless && mapDefItems.length > 1;
1✔
350
    // When the session is reused (browser refresh), use initialActiveMap from the URL (?map=)
351
    // to identify which map to eagerly recover. If the URL param doesn't match any map in the
352
    // appdef (or is absent), fall back to the first map by position.
353
    const initialActiveMapName = options.initialActiveMap;
1✔
354
    const activeMapExistsInAppDef = !!initialActiveMapName && mapDefItems.some(mi => mi.name === initialActiveMapName);
1✔
355
    if (isStateless) {
1!
NEW
356
        for (const m of mapDefs) {
×
NEW
357
            if (isMapDefinition(m)) {
×
NEW
358
                const siteVer = await siteVersion.getValueAsync();
×
NEW
359
                assertIsDefined(client);
×
NEW
360
                mapPromises.push(describeRuntimeMapStateless(client, siteVer.Version, m));
×
NEW
361
            } else {
×
NEW
362
                const proj = m.meta?.projection;
×
NEW
363
                if (!strIsNullOrEmpty(proj)) {
×
364
                    //Must be registered to proj4js if not 4326 or 3857
NEW
365
                    const [_, epsg] = proj.split(':');
×
NEW
366
                    if (!proj4.defs[`EPSG:${epsg}`]) {
×
NEW
367
                        fetchEpsgs.push({ epsg: epsg, mapDef: m.name });
×
368
                    }
×
369
                }
×
370
            }
×
371
        }
×
372
    } else {
1✔
373
        let isFirstMapDef = true;
1✔
374
        for (const m of mapDefs) {
1✔
375
            if (isMapDefinition(m)) {
3✔
376
                // Determine if this is the "primary" map to eagerly load/recover.
377
                // - For new sessions: the primary is always the first map in the appdef.
378
                // - For reused sessions (browser refresh): the primary is the map the user was
379
                //   viewing, identified via initialActiveMap (from the ?map= URL param). If the
380
                //   URL param is absent or does not match any map, fall back to first-by-position.
381
                const isPrimaryMap = (sessionWasReused && activeMapExistsInAppDef)
3!
NEW
382
                    ? m.name === initialActiveMapName
×
383
                    : isFirstMapDef;
3✔
384
                if (canLazyLoad && !isPrimaryMap) {
3✔
385
                    // Defer non-primary maps in a multi-map layout to avoid loading them upfront.
386
                    // This applies regardless of whether the session is being reused.
387
                    info(`Deferring lazy creation of runtime map (${m.name}) for: ${m.mapDef}`);
2✔
388
                    pendingMapDefs[m.name] = m;
2✔
389
                } else if (sessionWasReused) {
3!
390
                    //FIXME: If the map state we're recovering has a selection, we need to re-init the selection client-side
NEW
391
                    info(`Session ID re-used. Attempting recovery of map state of: ${m.name}`);
×
NEW
392
                    assertIsDefined(client);
×
NEW
393
                    mapPromises.push(tryDescribeRuntimeMapAsync(client, m.name, session, m.mapDef, siteVersion));
×
394
                } else {
1✔
395
                    info(`Creating runtime map state (${m.name}) for: ${m.mapDef}`);
1✔
396
                    assertIsDefined(client);
1✔
397
                    mapPromises.push(createRuntimeMap(client, {
1✔
398
                        mapDefinition: m.mapDef,
1✔
399
                        requestedFeatures: RuntimeMapFeatureFlags.LayerFeatureSources | RuntimeMapFeatureFlags.LayerIcons | RuntimeMapFeatureFlags.LayersAndGroups,
1✔
400
                        session: await session.getValueAsync(),
1✔
401
                        targetMapName: m.name
1✔
402
                    }, siteVersion));
1✔
403
                }
1✔
404
                isFirstMapDef = false;
3✔
405
            }
3✔
406
        }
3✔
407
    }
1✔
408
    const maps = await Promise.all(mapPromises);
1✔
409
    //All must be non-zero
410
    for (const m of maps) {
1✔
411
        const epsg = m.CoordinateSystem.EpsgCode;
1✔
412
        const mapDef = m.MapDefinition;
1✔
413
        const arbCs = tryParseArbitraryCs(m.CoordinateSystem.MentorCode);
1✔
414
        if (!arbCs) {
1✔
415
            if (epsg == "0") {
1!
NEW
416
                throw new MgError(tr("INIT_ERROR_UNSUPPORTED_COORD_SYS", locale || DEFAULT_LOCALE, { mapDefinition: mapDef }));
×
417
            }
×
418
            //Must be registered to proj4js if not 4326 or 3857
419
            if (!proj4.defs[`EPSG:${epsg}`]) {
1!
NEW
420
                fetchEpsgs.push({ epsg: epsg, mapDef: mapDef });
×
NEW
421
            }
×
422
        }
1✔
423
    }
1✔
424
    const extraEpsgs = projectionSelector(res);
1✔
425
    for (const e of extraEpsgs) {
1✔
426
        if (!proj4.defs[`EPSG:${e}`]) {
2!
NEW
427
            fetchEpsgs.push({ epsg: e, mapDef: "" });
×
428
        }
×
429
    }
2✔
430
    const epsgs = await Promise.all(fetchEpsgs.filter(fe => !strIsNullOrEmpty(fe.epsg)).map(f => resolveProjectionFromEpsgCodeAsync(f.epsg, locale, f.mapDef)));
1✔
431

432
    //Previously, we register proj4 with OpenLayers on the bootstrap phase way before this init
433
    //process is started. This no longer works for OL6 where it doesn't seem to pick up the extra
434
    //projections we've registered with proj4 after linking proj4 to OpenLayers. So that registration
435
    //step has been relocated here, after all the custom projections have been fetched and registered
436
    //with proj4
437
    debug(`Register proj4 with OpenLayers`);
1✔
438
    register(proj4);
1✔
439

440
    //Build the Dictionary<MgSubjectLayerType> from loaded maps
441
    const mapsByName: Dictionary<SubjectLayerType> = {};
1✔
442
    for (const map of maps) {
1✔
443
        mapsByName[map.Name] = map;
1✔
444
    }
1✔
445
    for (const gs of mapDefs) {
1✔
446
        if (!isMapDefinition(gs)) {
3!
NEW
447
            mapsByName[gs.name] = gs;
×
448
        }
×
449
    }
3✔
450
    return [mapsByName, pendingMapDefs, warnings];
1✔
451
}
1✔
452
/**
453
 * Builds a synthetic runtime map from a MapDefinition for stateless map operation.
454
 */
455
export async function describeRuntimeMapStateless(client: Client, siteVersion: string, m: MapToLoad): Promise<RuntimeMap> {
4✔
456
    const { name, mapDef, metadata } = m;
4✔
457
    const mdf = await client.getResource<MapDefinition>(mapDef, { username: "Anonymous" });
4✔
458
    if (!mdf)
4✔
459
        throw new Error("Failed to fetch map def");
4✔
460

461
    const rt: RuntimeMap = {
3✔
462
        SessionId: "",
3✔
463
        Extents: {
3✔
464
            LowerLeftCoordinate: {
3✔
465
                X: mdf.Extents.MinX,
3✔
466
                Y: mdf.Extents.MinY
3✔
467
            },
3✔
468
            UpperRightCoordinate: {
3✔
469
                X: mdf.Extents.MaxX,
3✔
470
                Y: mdf.Extents.MaxY
3✔
471
            }
3✔
472
        },
3✔
473
        SiteVersion: siteVersion,
3✔
474
        Name: name,
3✔
475
        DisplayDpi: 96,
3✔
476
        BackgroundColor: mdf.BackgroundColor,
3✔
477
        MapDefinition: mapDef,
3✔
478
        CoordinateSystem: {
3✔
479
            // We are assuming the app def specifies this data in each <Map> entry as extension properties
480
            // beginning with "Meta_" (eg. Meta_MentorCode, Meta_EpsgCode, etc)
481
            MentorCode: metadata.MentorCode,
3✔
482
            EpsgCode: metadata.EpsgCode,
3✔
483
            MetersPerUnit: metadata.MetersPerUnit,
3✔
484
            Wkt: mdf.CoordinateSystem
3✔
485
        },
3✔
486
        IconMimeType: "image/png",
3✔
487
    };
3✔
488

489
    const groups = [] as MapGroup[];
3✔
490
    const layers = [] as MapLayer[];
3✔
491

492
    if (mdf.TileSetSource) {
4✔
493
        rt.TileSetDefinition = mdf.TileSetSource.ResourceId;
2✔
494
        const tsd = await client.getResource<TileSetDefinition>(mdf.TileSetSource.ResourceId);
2✔
495
        if (tsd.TileStoreParameters.TileProvider == "Default") {
2✔
496
            const sTileWidth = tsd.TileStoreParameters.Parameter.find(p => p.Name == "TileWidth")?.Value;
1✔
497
            const sTileHeight = tsd.TileStoreParameters.Parameter.find(p => p.Name == "TileHeight")?.Value;
1✔
498
            if (!strIsNullOrEmpty(sTileWidth) && !strIsNullOrEmpty(sTileHeight)) {
1✔
499
                rt.TileWidth = parseInt(sTileWidth, 10);
1✔
500
                rt.TileHeight = parseInt(sTileHeight, 10);
1✔
501
            }
1✔
502
        } else if (tsd.TileStoreParameters.TileProvider == "XYZ") {
1✔
503
            rt.TileHeight = 256;
1✔
504
            rt.TileHeight = 256;
1✔
505
        }
1✔
506

507
        for (const bg of tsd.BaseMapLayerGroup) {
2✔
508
            groups.push({
2✔
509
                Name: bg.Name,
2✔
510
                DisplayInLegend: bg.ShowInLegend,
2✔
511
                LegendLabel: bg.LegendLabel,
2✔
512
                ObjectId: bg.Name,
2✔
513
                ExpandInLegend: bg.ExpandInLegend,
2✔
514
                Visible: bg.Visible,
2✔
515
                ActuallyVisible: bg.Visible,
2✔
516
                Type: 3 /* BaseMapFromTileSet */
2✔
517
            });
2✔
518

519
            for (const lyr of bg.BaseMapLayer) {
2✔
520
                layers.push({
2✔
521
                    Name: lyr.Name,
2✔
522
                    DisplayInLegend: lyr.ShowInLegend,
2✔
523
                    // We don't have stateless QUERYMAPFEATURES (yet), so there is no point actually respecting this flag
524
                    Selectable: false, //lyr.Selectable,
2✔
525
                    LegendLabel: lyr.LegendLabel,
2✔
526
                    ExpandInLegend: lyr.ExpandInLegend,
2✔
527
                    Visible: true,
2✔
528
                    ParentId: bg.Name,
2✔
529
                    ActuallyVisible: true,
2✔
530
                    LayerDefinition: lyr.ResourceId,
2✔
531
                    ObjectId: lyr.Name,
2✔
532
                    Type: 2 /* BaseMap */
2✔
533
                });
2✔
534
            }
2✔
535
        }
2✔
536
    }
2✔
537

538
    for (const grp of mdf.MapLayerGroup) {
3✔
539
        groups.push({
3✔
540
            Name: grp.Name,
3✔
541
            DisplayInLegend: grp.ShowInLegend,
3✔
542
            LegendLabel: grp.LegendLabel,
3✔
543
            ObjectId: grp.Name,
3✔
544
            ExpandInLegend: grp.ExpandInLegend,
3✔
545
            Visible: grp.Visible,
3✔
546
            ActuallyVisible: grp.Visible,
3✔
547
            Type: 1 /* Normal */
3✔
548
        });
3✔
549
    }
3✔
550

551
    for (const lyr of mdf.MapLayer) {
3✔
552
        layers.push({
3✔
553
            Name: lyr.Name,
3✔
554
            DisplayInLegend: lyr.ShowInLegend,
3✔
555
            // We don't have stateless QUERYMAPFEATURES (yet), so there is no point actually respecting this flag
556
            Selectable: false, // lyr.Selectable,
3✔
557
            LegendLabel: lyr.LegendLabel,
3✔
558
            ExpandInLegend: lyr.ExpandInLegend,
3✔
559
            Visible: true,
3✔
560
            ParentId: lyr.Group,
3✔
561
            ActuallyVisible: true,
3✔
562
            LayerDefinition: lyr.ResourceId,
3✔
563
            ObjectId: lyr.Name,
3✔
564
            Type: 1 /* Dynamic */
3✔
565
        })
3✔
566
    }
3✔
567

568
    rt.Group = groups;
3✔
569
    rt.Layer = layers;
3✔
570

571
    return rt;
3✔
572
}
3✔
573
/**
574
 * Converts AppDef map groups into viewer map entries with runtime map and layer metadata.
575
 */
576
export function setupMaps(appDef: ApplicationDefinition, mapsByName: Dictionary<SubjectLayerType>, config: any, warnings: string[], locale: string, pendingMapDefs?: Dictionary<MapToLoad>): Dictionary<MapInfo> {
1✔
577
    const dict: Dictionary<MapInfo> = {};
8✔
578
    if (appDef.MapSet) {
8✔
579
        for (const mGroup of appDef.MapSet.MapGroup) {
7✔
580
            let mapName: string | undefined;
9✔
581
            //Setup external layers
582
            const initExternalLayers = [] as IGenericSubjectMapLayer[];
9✔
583
            const externalBaseLayers = [] as IExternalBaseLayer[];
9✔
584
            let subject: SubjectLayerType | undefined;
9✔
585
            //Need to do this in 2 passes. 1st pass to try and get the MG map
586
            for (const map of mGroup.Map) {
9✔
587
                if (map.Type === "MapGuide") {
14✔
588
                    //TODO: Based on the schema, different MG map groups could have different
589
                    //settings here and our redux tree should reflect that. Currently the first one "wins"
590
                    if (!config.selectionColor && map.Extension.SelectionColor != null) {
9✔
591
                        config.selectionColor = map.Extension.SelectionColor;
1✔
592
                    }
1✔
593
                    if (!config.imageFormat && map.Extension.ImageFormat != null) {
9✔
594
                        config.imageFormat = map.Extension.ImageFormat;
1✔
595
                    }
1✔
596
                    if (!config.selectionImageFormat && map.Extension.SelectionFormat != null) {
9✔
597
                        config.selectionImageFormat = map.Extension.SelectionFormat;
1✔
598
                    }
1✔
599

600
                    //NOTE: Although non-sensical, if the same map definition exists across multiple
601
                    //MapGroups, we might be matching the wrong one. We just assume such non-sensical
602
                    //AppDefs won't exist
603
                    for (const name in mapsByName) {
9✔
604
                        const mapDef = mapsByName[name];
8✔
605
                        if (isRuntimeMap(mapDef) && mapDef.MapDefinition == map.Extension.ResourceId) {
8✔
606
                            mapName = name;
5✔
607
                            subject = mapDef;
5✔
608
                            break;
5✔
609
                        }
5✔
610
                    }
8✔
611
                    // If not found in the eagerly-loaded maps, check if it is a pending lazy map
612
                    if (!mapName && pendingMapDefs) {
9✔
613
                        const groupId = mGroup["@id"];
3✔
614
                        if (pendingMapDefs[groupId]) {
3✔
615
                            mapName = groupId;
3✔
616
                            // subject remains undefined for pending maps
617
                        }
3✔
618
                    }
3✔
619
                }
9✔
620
            }
14✔
621
            const isArbitrary = isArbitraryCoordSys(subject);
9✔
622
            //2nd pass to process non-MG maps
623
            for (const map of mGroup.Map) {
9✔
624
                if (map.Type == "MapGuide") {
14✔
625
                    continue;
9✔
626
                }
9✔
627
                if (map.Type == TYPE_SUBJECT) {
14!
NEW
628
                    mapName = mGroup["@id"];
×
629
                } else {
14✔
630
                    if (isArbitrary) {
5!
NEW
631
                        warnings.push(tr("INIT_WARNING_ARBITRARY_COORDSYS_INCOMPATIBLE_LAYER", locale, { mapId: mGroup["@id"], type: map.Type }));
×
632
                    } else {
5✔
633
                        if (map.Type == TYPE_EXTERNAL) {
5!
NEW
634
                            const layer = buildSubjectLayerDefn(map.Extension.layer_name, map);
×
NEW
635
                            if (layer.type == GenericSubjectLayerType.GeoTIFF && !supportsWebGL()) {
×
NEW
636
                                warnings.push(tr("INIT_WARNING_WEBGL_UNSUPPORTED", locale));
×
UNCOV
637
                            }
×
NEW
638
                            initExternalLayers.push(layer);
×
639
                        } else {
5✔
640
                            processLayerInMapGroup(map, warnings, config, appDef, externalBaseLayers);
5✔
641
                        }
5✔
642
                    }
5✔
643
                }
5✔
644
            }
14✔
645

646
            if (isArbitrary) {
9!
647
                //Check for incompatible widgets
NEW
648
                for (const wset of appDef.WidgetSet) {
×
NEW
649
                    for (const widget of wset.Widget) {
×
NEW
650
                        switch (widget.Type) {
×
NEW
651
                            case "CoordinateTracker":
×
NEW
652
                                warnings.push(tr("INIT_WARNING_ARBITRARY_COORDSYS_UNSUPPORTED_WIDGET", locale, { mapId: mGroup["@id"], widget: widget.Type }));
×
NEW
653
                                break;
×
654
                        }
×
655
                    }
×
656
                }
×
NEW
657
            }
×
658

659
            applyInitialBaseLayerVisibility(externalBaseLayers);
9✔
660

661
            //Setup initial view
662
            let initialView: IMapView | undefined;
9✔
663
            if (mGroup.InitialView) {
9✔
664
                initialView = {
1✔
665
                    x: mGroup.InitialView.CenterX,
1✔
666
                    y: mGroup.InitialView.CenterY,
1✔
667
                    scale: mGroup.InitialView.Scale
1✔
668
                };
1✔
669
            }
1✔
670

671
            if (mapName) {
9✔
672
                const coordinateFormat = parseMapGroupCoordinateFormat(mGroup);
8✔
673
                const pendingEntry = pendingMapDefs?.[mapName];
8✔
674
                dict[mapName] = {
8✔
675
                    mapGroupId: mGroup["@id"],
8✔
676
                    map: mapsByName[mapName],
8✔
677
                    initialView: initialView,
8✔
678
                    externalBaseLayers: externalBaseLayers,
8✔
679
                    initialExternalLayers: initExternalLayers,
8✔
680
                    coordinateFormat: coordinateFormat,
8✔
681
                    // If this map is pending lazy creation, store the mapDef for later use
682
                    ...(pendingEntry ? { mapDef: pendingEntry.mapDef, metadata: pendingEntry.metadata } : {})
8✔
683
                };
8✔
684
            }
8✔
685
        }
9✔
686
    }
7✔
687
    return dict;
8✔
688
}
8✔
689
/**
690
 * Initializes viewer payload from an ApplicationDefinition document.
691
 */
692
async function initFromAppDefAsync(client: Client | undefined, options: IInitAsyncOptions, appDef: ApplicationDefinition, session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload> {
1✔
693
    if (Array.isArray(appDef.Extension?.CustomProjections?.Projection)) {
1!
NEW
694
        for (const pd of appDef.Extension.CustomProjections.Projection) {
×
NEW
695
            let k, v;
×
NEW
696
            if (typeof (pd.epsg) === 'string' && typeof (pd.text) === 'string') { // appdef json form
×
NEW
697
                k = pd.epsg;
×
NEW
698
                v = pd.text;
×
NEW
699
            } else { // appdef xml translated form
×
NEW
700
                const [epsg] = pd["@epsg"];
×
NEW
701
                const [projStr] = pd["#text"];
×
NEW
702
                k = epsg;
×
NEW
703
                v = projStr;
×
NEW
704
            }
×
NEW
705
            if (!strIsNullOrEmpty(k) && !strIsNullOrEmpty(v)) {
×
NEW
706
                proj4.defs(`EPSG:${k}`, v);
×
NEW
707
                debug(`Registered proj4 defn from appdef for EPSG:${k}`, v);
×
708
            }
×
709
        }
×
NEW
710
        register(proj4);
×
711
    }
×
712
    const [mapsByName, pendingMapDefs, warnings] = await createRuntimeMapsAsync(client, options, session, appDef, isStateless(appDef), fl => getMapDefinitionsFromFlexLayout(fl), fl => getExtraProjectionsFromFlexLayout(fl), sessionWasReused);
1✔
713
    return await initFromAppDefCoreAsync(appDef, options, mapsByName, warnings, pendingMapDefs);
1✔
714
}
1✔
715
/**
716
 * Resolves the init document source and returns the normalized viewer init payload.
717
 */
718
export async function sessionAcquiredAsync(client: Client | undefined, options: IInitAsyncOptions, session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload> {
1✔
719
    const { resourceId, locale } = options;
1✔
720
    if (!resourceId) {
1!
721
        //Try assumed default location of appdef.json that we are assuming sits in the same place as the viewer html files
NEW
722
        const cl = new Client("", "mapagent");
×
NEW
723
        try {
×
NEW
724
            const fl = await cl.get<ApplicationDefinition>("appdef.json");
×
NEW
725
            return await initFromAppDefAsync(client, options, fl, session, sessionWasReused);
×
NEW
726
        } catch (e) { //The appdef.json doesn't exist at the assumed default location?
×
NEW
727
            throw new MgError(tr("INIT_ERROR_MISSING_RESOURCE_PARAM", locale));
×
NEW
728
        }
×
729
    } else {
1✔
730
        if (typeof (resourceId) == 'string') {
1!
NEW
731
            if (strEndsWith(resourceId, "WebLayout")) {
×
NEW
732
                assertIsDefined(client);
×
NEW
733
                const wl = await client.getResource<WebLayout>(resourceId, { SESSION: await session.getValueAsync() });
×
NEW
734
                return await initFromWebLayoutAsync(client, options, wl, session, sessionWasReused);
×
NEW
735
            } else if (strEndsWith(resourceId, "ApplicationDefinition")) {
×
NEW
736
                assertIsDefined(client);
×
NEW
737
                const fl = await client.getResource<ApplicationDefinition>(resourceId, { SESSION: await session.getValueAsync() });
×
NEW
738
                return await initFromAppDefAsync(client, options, fl, session, sessionWasReused);
×
NEW
739
            } else {
×
NEW
740
                if (isResourceId(resourceId)) {
×
NEW
741
                    throw new MgError(tr("INIT_ERROR_UNKNOWN_RESOURCE_TYPE", locale, { resourceId: resourceId }));
×
742
                } else {
×
743
                    //Assume URL to a appdef json document
NEW
744
                    let fl: ApplicationDefinition;
×
NEW
745
                    if (!client) {
×
746
                        // This wasn't set up with a mapagent URI (probably a non-MG viewer template), so make a new client on-the-fly
NEW
747
                        const cl = new Client("", "mapagent");
×
NEW
748
                        fl = await cl.get<ApplicationDefinition>(resourceId);
×
UNCOV
749
                    } else {
×
NEW
750
                        fl = await client.get<ApplicationDefinition>(resourceId);
×
751
                    }
×
NEW
752
                    return await initFromAppDefAsync(client, options, fl, session, sessionWasReused);
×
753
                }
×
754
            }
×
755
        } else {
1✔
756
            const doc = await resourceId();
1✔
757
            if (isWebLayout(doc as any)) {
1!
NEW
758
                const wl = doc as WebLayout;
×
NEW
759
                return await initFromWebLayoutAsync(client, options, wl, session, sessionWasReused);
×
760
            }
×
761
            if (isAppDef(doc as any)) {
1✔
762
                const appDef = doc as ApplicationDefinition;
1✔
763
                return await initFromAppDefAsync(client, options, appDef, session, sessionWasReused);
1✔
764
            }
1!
NEW
765
            throw new MgError(tr("INIT_ERROR_UNKNOWN_RESOURCE_TYPE", locale, { resourceId: "[function resource loader]" }));
×
UNCOV
766
        }
×
767
    }
1✔
768
}
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