• 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

52.02
/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, ReduxThunkedAction } from '../api/common';
1✔
4
import type { IMapProviderContext } from '../components/map-providers/base';
5
import { MapInfo, IInitAppActionPayload, IRestoredSelectionSets, IGenericSubjectMapLayer, GenericSubjectLayerType } from './defs';
1✔
6
import { tr, DEFAULT_LOCALE } from '../api/i18n';
1✔
7
import { isResourceId, strEndsWith, strIsNullOrEmpty } from '../utils/string';
1✔
8
import { Client } from '../api/client';
1✔
9
import { applyInitialBaseLayerVisibility, applyInitPayloadOverrides, processAndDispatchInitError, IInitAsyncOptions, IInitAppLayout, processLayerInMapGroup } from './init';
1✔
10
import { ICreateRuntimeMapOptions, IDescribeRuntimeMapOptions, RuntimeMapFeatureFlags } from '../api/request-builder';
1✔
11
import { info, debug } from '../utils/logger';
1✔
12
import { MgError } from '../api/error';
1✔
13
import { resolveProjectionFromEpsgCodeAsync } from '../api/registry/projections';
1✔
14
import { register } from 'ol/proj/proj4';
1✔
15
import proj4 from "proj4";
1✔
16
import { buildSubjectLayerDefn, getMapDefinitionsFromFlexLayout, isMapDefinition, isStateless, parseMapGroupCoordinateFormat, MapToLoad, ViewerInitCommand } from './init-command';
1✔
17
import { WebLayout } from '../api/contracts/weblayout';
18
import { convertWebLayoutUIItems, parseCommandsInWebLayout, prepareSubMenus, ToolbarConf } from '../api/registry/command-spec';
1✔
19
import { clearSessionStore, retrieveSelectionSetFromLocalStorage } from '../api/session-store';
1✔
20
import { WEBLAYOUT_CONTEXTMENU, WEBLAYOUT_TASKMENU, WEBLAYOUT_TOOLBAR } from "../constants";
1✔
21
import { registerCommand } from '../api/registry/command';
1✔
22
import { ensureParameters } from '../utils/url';
1✔
23
import { assertIsDefined } from '../utils/assert';
1✔
24
import { MapDefinition } from '../api/contracts/map-definition';
25
import { TileSetDefinition } from '../api/contracts/tile-set-definition';
26
import { AsyncLazy } from '../api/lazy';
1✔
27
import { SiteVersionResponse } from '../api/contracts/common';
28
import { isRuntimeMap } from '../utils/type-guards';
1✔
29
import { tryParseArbitraryCs } from '../utils/units';
1✔
30
import { ScopedId } from '../utils/scoped-id';
1✔
31
import { canUseQueryMapFeaturesV4, parseSiteVersion } from '../utils/site-version';
1✔
32
import { supportsWebGL } from '../utils/browser-support';
1✔
33
import { ActionType } from '../constants/actions';
1✔
34

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

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

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

45
/**
46
 * Default viewer init commmand
47
 * 
48
 * @since 0.14
49
 */
50
export class DefaultViewerInitCommand extends ViewerInitCommand<SubjectLayerType> {
1✔
51
    private client: Client | undefined;
52
    private options: IInitAsyncOptions;
53
    private _viewer: IMapProviderContext | undefined;
54
    constructor(dispatch: ReduxDispatch) {
1✔
55
        super(dispatch);
32✔
56
    }
32✔
57
    public attachClient(client: Client): void {
1✔
58
        this.client = client;
9✔
59
    }
9✔
60
    public setViewer(viewer: IMapProviderContext): void {
1✔
NEW
UNCOV
61
        this._viewer = viewer;
×
NEW
UNCOV
62
    }
×
63
    protected isArbitraryCoordSys(subject: SubjectLayerType | undefined) {
1✔
64
        if (subject) {
10✔
65
            if (isRuntimeMap(subject)) {
7✔
66
                const arbCs = tryParseArbitraryCs(subject.CoordinateSystem.MentorCode);
6✔
67
                return arbCs != null;
6✔
68
            }
6✔
69
        }
7✔
70
        return false;
4✔
71
    }
10✔
72
    /**
73
     * @override
74
     * @protected
75
     * @param {Dictionary<RuntimeMap>} mapsByName
76
     *
77
     */
78
    protected establishInitialMapNameAndSession(mapsByName: Dictionary<SubjectLayerType>): [string, string] {
1✔
79
        let firstMapName = "";
2✔
80
        let firstSessionId = "";
2✔
81
        for (const mapName in mapsByName) {
2✔
82
            if (!firstMapName && !firstSessionId) {
3✔
83
                const map = mapsByName[mapName];
3✔
84
                if (isRuntimeMap(map)) {
3✔
85
                    firstMapName = map.Name;
1✔
86
                    firstSessionId = map.SessionId;
1✔
87
                    break;
1✔
88
                }
1✔
89
            }
3✔
90
        }
3✔
91
        return [firstMapName, firstSessionId];
2✔
92
    }
2✔
93
    private getDesiredTargetMapName(mapDef: string) {
1✔
94
        const lastSlash = mapDef.lastIndexOf("/");
2✔
95
        const lastDot = mapDef.lastIndexOf(".");
2✔
96
        if (lastSlash >= 0 && lastDot >= 0 && lastDot > lastSlash) {
2✔
97
            return `${mapDef.substring(lastSlash + 1, lastDot)}`;
1✔
98
        } else {
1✔
99
            return `Map_${scopedId.next()}`;
1✔
100
        }
1✔
101
    }
2✔
102
    private async initFromWebLayoutAsync(webLayout: WebLayout, session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload> {
1✔
103
        const [mapsByName, , warnings] = await this.createRuntimeMapsAsync(session, webLayout, false, wl => [{ name: this.getDesiredTargetMapName(wl.Map.ResourceId), mapDef: wl.Map.ResourceId, metadata: {} }], () => [], sessionWasReused);
×
104
        const { locale, featureTooltipsEnabled, externalBaseLayers } = this.options;
×
105
        const cmdsByKey = parseCommandsInWebLayout(webLayout, registerCommand);
×
106
        const mainToolbar = (webLayout.ToolBar.Visible
×
107
            ? convertWebLayoutUIItems(webLayout.ToolBar.Button, cmdsByKey, locale)
×
108
            : []);
×
109
        const taskBar = (webLayout.TaskPane.TaskBar.Visible
×
110
            ? convertWebLayoutUIItems(webLayout.TaskPane.TaskBar.MenuButton, cmdsByKey, locale, false)
×
111
            : []);
×
112
        const contextMenu = (webLayout.ContextMenu.Visible
×
113
            ? convertWebLayoutUIItems(webLayout.ContextMenu.MenuItem, cmdsByKey, locale, false)
×
114
            : []);
×
115
        const config: any = {};
×
116
        if (webLayout.SelectionColor != null) {
×
117
            config.selectionColor = webLayout.SelectionColor;
×
118
        }
×
119
        if (webLayout.MapImageFormat != null) {
×
120
            config.imageFormat = webLayout.MapImageFormat;
×
121
        }
×
122
        if (webLayout.SelectionImageFormat != null) {
×
123
            config.selectionImageFormat = webLayout.SelectionImageFormat;
×
124
        }
×
125
        if (webLayout.PointSelectionBuffer != null) {
×
126
            config.pointSelectionBuffer = webLayout.PointSelectionBuffer;
×
127
        }
×
128
        let initialView: IMapView | null = null;
×
129
        if (webLayout.Map.InitialView != null) {
×
UNCOV
130
            initialView = {
×
131
                x: webLayout.Map.InitialView.CenterX,
×
132
                y: webLayout.Map.InitialView.CenterY,
×
133
                scale: webLayout.Map.InitialView.Scale
×
UNCOV
134
            };
×
135
        }
×
136

UNCOV
137
        if (webLayout.Title != "") {
×
138
            document.title = webLayout.Title || document.title;
×
139
        }
×
140

141
        const maps: any = {};
×
142
        const [firstMapName, firstSessionId] = this.establishInitialMapNameAndSession(mapsByName);
×
143

144
        for (const mapName in mapsByName) {
×
145
            const map = mapsByName[mapName];
×
146
            maps[mapName] = {
×
UNCOV
147
                mapGroupId: mapName,
×
148
                map: map,
×
149
                externalBaseLayers: this.options.externalBaseLayers ?? [],
×
150
                initialView: initialView
×
151
            };
×
152
        }
×
153

154
        const menus: Dictionary<ToolbarConf> = {};
×
155
        menus[WEBLAYOUT_TOOLBAR] = {
×
156
            items: mainToolbar
×
157
        };
×
UNCOV
158
        menus[WEBLAYOUT_TASKMENU] = {
×
159
            items: taskBar
×
160
        };
×
161
        menus[WEBLAYOUT_CONTEXTMENU] = {
×
162
            items: contextMenu
×
163
        };
×
164

165
        const tb = prepareSubMenus(menus)[0];
×
166
        return {
×
167
            activeMapName: firstMapName,
×
168
            featureTooltipsEnabled: featureTooltipsEnabled,
×
169
            initialUrl: ensureParameters(webLayout.TaskPane.InitialTask || "server/TaskPane.html", firstMapName, firstSessionId, locale),
×
170
            initialTaskPaneWidth: webLayout.TaskPane.Width,
×
171
            initialInfoPaneWidth: webLayout.InformationPane.Width,
×
172
            maps: maps,
×
173
            locale: locale,
×
174
            config: config,
×
175
            capabilities: {
×
176
                hasTaskPane: webLayout.TaskPane.Visible,
×
177
                hasTaskBar: webLayout.TaskPane.TaskBar.Visible,
×
178
                hasStatusBar: webLayout.StatusBar.Visible,
×
179
                hasNavigator: webLayout.ZoomControl.Visible,
×
180
                hasSelectionPanel: webLayout.InformationPane.Visible && webLayout.InformationPane.PropertiesVisible,
×
181
                hasLegend: webLayout.InformationPane.Visible && webLayout.InformationPane.LegendVisible,
×
182
                hasToolbar: webLayout.ToolBar.Visible,
×
183
                hasViewSize: webLayout.StatusBar.Visible
×
UNCOV
184
            },
×
UNCOV
185
            toolbars: tb,
×
UNCOV
186
            warnings: warnings,
×
UNCOV
187
            initialActiveTool: ActiveMapTool.Pan
×
UNCOV
188
        };
×
UNCOV
189
    }
×
190
    private async createRuntimeMap(options: ICreateRuntimeMapOptions, siteVersion: AsyncLazy<SiteVersionResponse>): Promise<RuntimeMap> {
1✔
191
        assertIsDefined(this.client);
3✔
192
        let map: RuntimeMap;
3✔
193
        const sv = await siteVersion.getValueAsync();
3✔
194
        if (canUseQueryMapFeaturesV4(parseSiteVersion(sv.Version))) {
3✔
195
            map = await this.client.createRuntimeMap_v4(options);
1✔
196
        } else {
3✔
197
            map = await this.client.createRuntimeMap(options);
2✔
198
        }
2✔
199
        return map;
3✔
200
    }
3✔
201
    private async describeRuntimeMap(options: IDescribeRuntimeMapOptions, siteVersion: AsyncLazy<SiteVersionResponse>): Promise<RuntimeMap> {
1✔
202
        assertIsDefined(this.client);
4✔
203
        let map: RuntimeMap;
4✔
204
        const sv = await siteVersion.getValueAsync();
4✔
205
        if (canUseQueryMapFeaturesV4(parseSiteVersion(sv.Version))) {
4✔
206
            map = await this.client.describeRuntimeMap_v4(options);
1✔
207
        } else {
4✔
208
            map = await this.client.describeRuntimeMap(options);
3✔
209
        }
1✔
210
        return map;
2✔
211
    }
4✔
212
    private async tryDescribeRuntimeMapAsync(mapName: string, session: AsyncLazy<string>, mapDef: string, siteVersion: AsyncLazy<SiteVersionResponse>) {
1✔
213
        assertIsDefined(this.client);
2✔
214
        try {
2✔
215
            const map = await this.describeRuntimeMap({
2✔
216
                mapname: mapName,
2✔
217
                requestedFeatures: RuntimeMapFeatureFlags.LayerFeatureSources | RuntimeMapFeatureFlags.LayerIcons | RuntimeMapFeatureFlags.LayersAndGroups,
2✔
218
                session: await session.getValueAsync()
2✔
219
            }, siteVersion);
2!
UNCOV
220
            return map;
×
221
        } catch (e) {
2✔
222
            if (e.message === "MgResourceNotFoundException") {
2✔
223
                const map = await this.createRuntimeMap({
1✔
224
                    mapDefinition: mapDef,
1✔
225
                    requestedFeatures: RuntimeMapFeatureFlags.LayerFeatureSources | RuntimeMapFeatureFlags.LayerIcons | RuntimeMapFeatureFlags.LayersAndGroups,
1✔
226
                    session: await session.getValueAsync(),
1✔
227
                    targetMapName: mapName
1✔
228
                }, siteVersion);
1✔
229
                return map;
1✔
230
            }
1✔
231
            throw e;
1✔
232
        }
1✔
233
    }
2✔
234
    private async createRuntimeMapsAsync<TLayout>(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✔
235
        const mapDefs = mapDefSelector(res);
×
UNCOV
236
        const mapPromises: Promise<RuntimeMap>[] = [];
×
237
        const warnings = [] as string[];
×
238
        const { locale } = this.options;
×
239
        const subjectLayers: Dictionary<IGenericSubjectMapLayer> = {};
×
240
        const fetchEpsgs: { epsg: string, mapDef: string }[] = [];
×
241
        const pendingMapDefs: Dictionary<MapToLoad> = {};
×
242
        // We use an AsyncLazy because we only want to fetch the site version *iff* we are required to
243
        const siteVersion = new AsyncLazy<SiteVersionResponse>(async () => {
×
UNCOV
244
            assertIsDefined(this.client);
×
UNCOV
245
            const sv = await this.client.getSiteVersion();
×
UNCOV
246
            return sv;
×
UNCOV
247
        });
×
248
        // Collect only the MapDefinition entries for lazy-load eligibility check
UNCOV
249
        const mapDefItems = mapDefs.filter(isMapDefinition);
×
250
        // Lazy creation only applies when: not stateless and there are multiple MapGuide maps.
251
        // Note: We intentionally do NOT exclude sessionWasReused here. Even on a browser refresh
252
        // (where the session is reused), non-active maps should still be deferred because they may
253
        // never have been created in the previous session (the user may not have switched to them).
254
        // These deferred maps will be lazily initialized via activateMap() when the user switches
255
        // to them, which now tries to describe the existing map first before creating a new one.
256
        const canLazyLoad = !isStateless && mapDefItems.length > 1;
×
257
        // When the session is reused (browser refresh), use initialActiveMap from the URL (?map=)
258
        // to identify which map to eagerly recover. If the URL param doesn't match any map in the
259
        // appdef (or is absent), fall back to the first map by position.
260
        const initialActiveMapName = this.options.initialActiveMap;
×
261
        const activeMapExistsInAppDef = !!initialActiveMapName && mapDefItems.some(mi => mi.name === initialActiveMapName);
×
262
        if (isStateless) { 
×
263
            for (const m of mapDefs) {
×
264
                if (isMapDefinition(m)) {
×
UNCOV
265
                    const siteVer = await siteVersion.getValueAsync();
×
266
                    assertIsDefined(this.client);
×
267
                    mapPromises.push(this.describeRuntimeMapStateless(this.client, siteVer.Version, m));
×
268
                } else {
×
269
                    const proj = m.meta?.projection;
×
270
                    if (!strIsNullOrEmpty(proj)) {
×
271
                        //Must be registered to proj4js if not 4326 or 3857
272
                        const [_, epsg] = proj.split(':');
×
273
                        if (!proj4.defs[`EPSG:${epsg}`]) {
×
274
                            fetchEpsgs.push({ epsg: epsg, mapDef: m.name });
×
275
                        }
×
276
                    }
×
UNCOV
277
                }
×
UNCOV
278
            }
×
UNCOV
279
        } else {
×
UNCOV
280
            let isFirstMapDef = true;
×
UNCOV
281
            for (const m of mapDefs) {
×
282
                if (isMapDefinition(m)) {
×
283
                    // Determine if this is the "primary" map to eagerly load/recover.
284
                    // - For new sessions: the primary is always the first map in the appdef.
285
                    // - For reused sessions (browser refresh): the primary is the map the user was
286
                    //   viewing, identified via initialActiveMap (from the ?map= URL param). If the
287
                    //   URL param is absent or does not match any map, fall back to first-by-position.
288
                    const isPrimaryMap = (sessionWasReused && activeMapExistsInAppDef)
×
289
                        ? m.name === initialActiveMapName
×
290
                        : isFirstMapDef;
×
UNCOV
291
                    if (canLazyLoad && !isPrimaryMap) {
×
292
                        // Defer non-primary maps in a multi-map layout to avoid loading them upfront.
293
                        // This applies regardless of whether the session is being reused.
294
                        info(`Deferring lazy creation of runtime map (${m.name}) for: ${m.mapDef}`);
×
295
                        pendingMapDefs[m.name] = m;
×
296
                    } else if (sessionWasReused) {
×
297
                        //FIXME: If the map state we're recovering has a selection, we need to re-init the selection client-side
298
                        info(`Session ID re-used. Attempting recovery of map state of: ${m.name}`);
×
299
                        mapPromises.push(this.tryDescribeRuntimeMapAsync(m.name, session, m.mapDef, siteVersion));
×
300
                    } else {
×
301
                        info(`Creating runtime map state (${m.name}) for: ${m.mapDef}`);
×
302
                        assertIsDefined(this.client);
×
303
                        mapPromises.push(this.createRuntimeMap({
×
304
                            mapDefinition: m.mapDef,
×
305
                            requestedFeatures: RuntimeMapFeatureFlags.LayerFeatureSources | RuntimeMapFeatureFlags.LayerIcons | RuntimeMapFeatureFlags.LayersAndGroups,
×
306
                            session: await session.getValueAsync(),
×
307
                            targetMapName: m.name
×
308
                        }, siteVersion));
×
UNCOV
309
                    }
×
310
                    isFirstMapDef = false;
×
311
                }
×
312
            }
×
313
        }
×
314
        const maps = await Promise.all(mapPromises);
×
315
        //All must be non-zero
316
        for (const m of maps) {
×
317
            const epsg = m.CoordinateSystem.EpsgCode;
×
UNCOV
318
            const mapDef = m.MapDefinition;
×
319
            const arbCs = tryParseArbitraryCs(m.CoordinateSystem.MentorCode);
×
320
            if (!arbCs) {
×
321
                if (epsg == "0") {
×
322
                    throw new MgError(tr("INIT_ERROR_UNSUPPORTED_COORD_SYS", locale || DEFAULT_LOCALE, { mapDefinition: mapDef }));
×
323
                }
×
324
                //Must be registered to proj4js if not 4326 or 3857
325
                if (!proj4.defs[`EPSG:${epsg}`]) {
×
326
                    fetchEpsgs.push({ epsg: epsg, mapDef: mapDef });
×
327
                }
×
328
            }
×
329
        }
×
330
        const extraEpsgs = projectionSelector(res);
×
UNCOV
331
        for (const e of extraEpsgs) {
×
UNCOV
332
            if (!proj4.defs[`EPSG:${e}`]) {
×
UNCOV
333
                fetchEpsgs.push({ epsg: e, mapDef: "" });
×
UNCOV
334
            }
×
UNCOV
335
        }
×
UNCOV
336
        const epsgs = await Promise.all(fetchEpsgs.filter(fe => !strIsNullOrEmpty(fe.epsg)).map(f => resolveProjectionFromEpsgCodeAsync(f.epsg, locale, f.mapDef)));
×
337

338
        //Previously, we register proj4 with OpenLayers on the bootstrap phase way before this init
339
        //process is started. This no longer works for OL6 where it doesn't seem to pick up the extra
340
        //projections we've registered with proj4 after linking proj4 to OpenLayers. So that registration
341
        //step has been relocated here, after all the custom projections have been fetched and registered
342
        //with proj4
343
        debug(`Register proj4 with OpenLayers`);
×
344
        register(proj4);
×
345

346
        //Build the Dictionary<MgSubjectLayerType> from loaded maps
347
        const mapsByName: Dictionary<SubjectLayerType> = {};
×
348
        for (const map of maps) {
×
349
            mapsByName[map.Name] = map;
×
350
        }
×
351
        for (const gs of mapDefs) {
×
UNCOV
352
            if (!isMapDefinition(gs)) {
×
UNCOV
353
                mapsByName[gs.name] = gs;
×
UNCOV
354
            }
×
UNCOV
355
        }
×
UNCOV
356
        return [mapsByName, pendingMapDefs, warnings];
×
UNCOV
357
    }
×
358
    private async describeRuntimeMapStateless(client: Client, siteVersion: string, m: MapToLoad): Promise<RuntimeMap> {
1✔
359
        const { name, mapDef, metadata } = m;
4✔
360
        const mdf = await this.client?.getResource<MapDefinition>(mapDef, { username: "Anonymous" });
4✔
361
        if (!mdf)
4✔
362
            throw new Error("Failed to fetch map def");
4✔
363

364
        const rt: RuntimeMap = {
3✔
365
            SessionId: "",
3✔
366
            Extents: {
3✔
367
                LowerLeftCoordinate: {
3✔
368
                    X: mdf.Extents.MinX,
3✔
369
                    Y: mdf.Extents.MinY
3✔
370
                },
3✔
371
                UpperRightCoordinate: {
3✔
372
                    X: mdf.Extents.MaxX,
3✔
373
                    Y: mdf.Extents.MaxY
3✔
374
                }
3✔
375
            },
3✔
376
            SiteVersion: siteVersion,
3✔
377
            Name: name,
3✔
378
            DisplayDpi: 96,
3✔
379
            BackgroundColor: mdf.BackgroundColor,
3✔
380
            MapDefinition: mapDef,
3✔
381
            CoordinateSystem: {
3✔
382
                // We are assuming the app def specifies this data in each <Map> entry as extension properties
383
                // beginning with "Meta_" (eg. Meta_MentorCode, Meta_EpsgCode, etc)
384
                MentorCode: metadata.MentorCode,
3✔
385
                EpsgCode: metadata.EpsgCode,
3✔
386
                MetersPerUnit: metadata.MetersPerUnit,
3✔
387
                Wkt: mdf.CoordinateSystem
3✔
388
            },
3✔
389
            IconMimeType: "image/png",
3✔
390
        };
3✔
391

392
        const groups = [] as MapGroup[];
3✔
393
        const layers = [] as MapLayer[];
3✔
394

395
        if (mdf.TileSetSource) {
4✔
396
            rt.TileSetDefinition = mdf.TileSetSource.ResourceId;
2✔
397
            const tsd = await client.getResource<TileSetDefinition>(mdf.TileSetSource.ResourceId);
2✔
398
            if (tsd.TileStoreParameters.TileProvider == "Default") {
2✔
399
                const sTileWidth = tsd.TileStoreParameters.Parameter.find(p => p.Name == "TileWidth")?.Value;
1✔
400
                const sTileHeight = tsd.TileStoreParameters.Parameter.find(p => p.Name == "TileHeight")?.Value;
1✔
401
                if (!strIsNullOrEmpty(sTileWidth) && !strIsNullOrEmpty(sTileHeight)) {
1✔
402
                    rt.TileWidth = parseInt(sTileWidth, 10);
1✔
403
                    rt.TileHeight = parseInt(sTileHeight, 10);
1✔
404
                }
1✔
405
            } else if (tsd.TileStoreParameters.TileProvider == "XYZ") {
1✔
406
                rt.TileHeight = 256;
1✔
407
                rt.TileHeight = 256;
1✔
408
            }
1✔
409

410
            for (const bg of tsd.BaseMapLayerGroup) {
2✔
411
                groups.push({
2✔
412
                    Name: bg.Name,
2✔
413
                    DisplayInLegend: bg.ShowInLegend,
2✔
414
                    LegendLabel: bg.LegendLabel,
2✔
415
                    ObjectId: bg.Name,
2✔
416
                    ExpandInLegend: bg.ExpandInLegend,
2✔
417
                    Visible: bg.Visible,
2✔
418
                    ActuallyVisible: bg.Visible,
2✔
419
                    Type: 3 /* BaseMapFromTileSet */
2✔
420
                });
2✔
421

422
                for (const lyr of bg.BaseMapLayer) {
2✔
423
                    layers.push({
2✔
424
                        Name: lyr.Name,
2✔
425
                        DisplayInLegend: lyr.ShowInLegend,
2✔
426
                        // We don't have stateless QUERYMAPFEATURES (yet), so there is no point actually respecting this flag
427
                        Selectable: false, //lyr.Selectable,
2✔
428
                        LegendLabel: lyr.LegendLabel,
2✔
429
                        ExpandInLegend: lyr.ExpandInLegend,
2✔
430
                        Visible: true,
2✔
431
                        ParentId: bg.Name,
2✔
432
                        ActuallyVisible: true,
2✔
433
                        LayerDefinition: lyr.ResourceId,
2✔
434
                        ObjectId: lyr.Name,
2✔
435
                        Type: 2 /* BaseMap */
2✔
436
                    });
2✔
437
                }
2✔
438
            }
2✔
439
        }
2✔
440

441
        for (const grp of mdf.MapLayerGroup) {
3✔
442
            groups.push({
3✔
443
                Name: grp.Name,
3✔
444
                DisplayInLegend: grp.ShowInLegend,
3✔
445
                LegendLabel: grp.LegendLabel,
3✔
446
                ObjectId: grp.Name,
3✔
447
                ExpandInLegend: grp.ExpandInLegend,
3✔
448
                Visible: grp.Visible,
3✔
449
                ActuallyVisible: grp.Visible,
3✔
450
                Type: 1 /* Normal */
3✔
451
            });
3✔
452
        }
3✔
453

454
        for (const lyr of mdf.MapLayer) {
3✔
455
            layers.push({
3✔
456
                Name: lyr.Name,
3✔
457
                DisplayInLegend: lyr.ShowInLegend,
3✔
458
                // We don't have stateless QUERYMAPFEATURES (yet), so there is no point actually respecting this flag
459
                Selectable: false, // lyr.Selectable,
3✔
460
                LegendLabel: lyr.LegendLabel,
3✔
461
                ExpandInLegend: lyr.ExpandInLegend,
3✔
462
                Visible: true,
3✔
463
                ParentId: lyr.Group,
3✔
464
                ActuallyVisible: true,
3✔
465
                LayerDefinition: lyr.ResourceId,
3✔
466
                ObjectId: lyr.Name,
3✔
467
                Type: 1 /* Dynamic */
3✔
468
            })
3✔
469
        }
3✔
470

471
        rt.Group = groups;
3✔
472
        rt.Layer = layers;
3✔
473

474
        return rt;
3✔
475
    }
4✔
476
    /**
477
     * @override
478
     * @protected
479
     * @param {ApplicationDefinition} appDef
480
     * @param {Dictionary<SubjectLayerType>} mapsByName
481
     * @param {*} config
482
     * @param {string[]} warnings
483
     * @param {string} locale
484
     * @param {Dictionary<MapToLoad>} [pendingMapDefs]
485
     * @returns {Dictionary<MapInfo>}
486
     *
487
     */
488
    protected setupMaps(appDef: ApplicationDefinition, mapsByName: Dictionary<SubjectLayerType>, config: any, warnings: string[], locale: string, pendingMapDefs?: Dictionary<MapToLoad>): Dictionary<MapInfo> {
1✔
489
        const dict: Dictionary<MapInfo> = {};
7✔
490
        if (appDef.MapSet) {
7✔
491
            for (const mGroup of appDef.MapSet.MapGroup) {
6✔
492
                let mapName: string | undefined;
6✔
493
                //Setup external layers
494
                const initExternalLayers = [] as IGenericSubjectMapLayer[];
6✔
495
                const externalBaseLayers = [] as IExternalBaseLayer[];
6✔
496
                let subject: SubjectLayerType | undefined;
6✔
497
                //Need to do this in 2 passes. 1st pass to try and get the MG map
498
                for (const map of mGroup.Map) {
6✔
499
                    if (map.Type === "MapGuide") {
7✔
500
                        //TODO: Based on the schema, different MG map groups could have different
501
                        //settings here and our redux tree should reflect that. Currently the first one "wins"
502
                        if (!config.selectionColor && map.Extension.SelectionColor != null) {
6✔
503
                            config.selectionColor = map.Extension.SelectionColor;
1✔
504
                        }
1✔
505
                        if (!config.imageFormat && map.Extension.ImageFormat != null) {
6✔
506
                            config.imageFormat = map.Extension.ImageFormat;
1✔
507
                        }
1✔
508
                        if (!config.selectionImageFormat && map.Extension.SelectionFormat != null) {
6✔
509
                            config.selectionImageFormat = map.Extension.SelectionFormat;
1✔
510
                        }
1✔
511

512
                        //NOTE: Although non-sensical, if the same map definition exists across multiple
513
                        //MapGroups, we might be matching the wrong one. We just assume such non-sensical
514
                        //AppDefs won't exist
515
                        for (const name in mapsByName) {
6✔
516
                            const mapDef = mapsByName[name];
5✔
517
                            if (isRuntimeMap(mapDef) && mapDef.MapDefinition == map.Extension.ResourceId) {
5✔
518
                                mapName = name;
4✔
519
                                subject = mapDef;
4✔
520
                                break;
4✔
521
                            }
4✔
522
                        }
5✔
523
                        // If not found in the eagerly-loaded maps, check if it is a pending lazy map
524
                        if (!mapName && pendingMapDefs) {
6✔
525
                            const groupId = mGroup["@id"];
1✔
526
                            if (pendingMapDefs[groupId]) {
1✔
527
                                mapName = groupId;
1✔
528
                                // subject remains undefined for pending maps
529
                            }
1✔
530
                        }
1✔
531
                    }
6✔
532
                }
7✔
533
                const isArbitrary = this.isArbitraryCoordSys(subject);
6✔
534
                //2nd pass to process non-MG maps
535
                for (const map of mGroup.Map) {
6✔
536
                    if (map.Type == "MapGuide") {
7✔
537
                        continue;
6✔
538
                    }
6✔
539
                    if (map.Type == TYPE_SUBJECT) {
7!
540
                        mapName = mGroup["@id"];
×
541
                    } else {
7✔
542
                        if (isArbitrary) {
1!
543
                            warnings.push(tr("INIT_WARNING_ARBITRARY_COORDSYS_INCOMPATIBLE_LAYER", locale, { mapId: mGroup["@id"], type: map.Type }));
×
544
                        } else {
1✔
545
                            if (map.Type == TYPE_EXTERNAL) {
1!
UNCOV
546
                                const layer = buildSubjectLayerDefn(map.Extension.layer_name, map);
×
UNCOV
547
                                if (layer.type == GenericSubjectLayerType.GeoTIFF && !supportsWebGL()) {
×
UNCOV
548
                                    warnings.push(tr("INIT_WARNING_WEBGL_UNSUPPORTED", locale));
×
UNCOV
549
                                }
×
UNCOV
550
                                initExternalLayers.push(layer);
×
551
                            } else {
1✔
552
                                processLayerInMapGroup(map, warnings, config, appDef, externalBaseLayers);
1✔
553
                            }
1✔
554
                        }
1✔
555
                    }
1✔
556
                }
7✔
557

558
                if (isArbitrary) {
6!
559
                    //Check for incompatible widgets
560
                    for (const wset of appDef.WidgetSet) {
×
561
                        for (const widget of wset.Widget) {
×
562
                            switch (widget.Type) {
×
563
                                case "CoordinateTracker":
×
UNCOV
564
                                    warnings.push(tr("INIT_WARNING_ARBITRARY_COORDSYS_UNSUPPORTED_WIDGET", locale, { mapId: mGroup["@id"], widget: widget.Type }));
×
UNCOV
565
                                    break;
×
UNCOV
566
                            }
×
UNCOV
567
                        }
×
UNCOV
568
                    }
×
UNCOV
569
                }
×
570

571
                applyInitialBaseLayerVisibility(externalBaseLayers);
6✔
572

573
                //Setup initial view
574
                let initialView: IMapView | undefined;
6✔
575
                if (mGroup.InitialView) {
6✔
576
                    initialView = {
1✔
577
                        x: mGroup.InitialView.CenterX,
1✔
578
                        y: mGroup.InitialView.CenterY,
1✔
579
                        scale: mGroup.InitialView.Scale
1✔
580
                    };
1✔
581
                }
1✔
582

583
                if (mapName) {
6✔
584
                    const coordinateFormat = parseMapGroupCoordinateFormat(mGroup);
5✔
585
                    const pendingEntry = pendingMapDefs?.[mapName];
5✔
586
                    dict[mapName] = {
5✔
587
                        mapGroupId: mGroup["@id"],
5✔
588
                        map: mapsByName[mapName],
5✔
589
                        initialView: initialView,
5✔
590
                        externalBaseLayers: externalBaseLayers,
5✔
591
                        initialExternalLayers: initExternalLayers,
5✔
592
                        coordinateFormat: coordinateFormat,
5✔
593
                        // If this map is pending lazy creation, store the mapDef for later use
594
                        ...(pendingEntry ? { mapDef: pendingEntry.mapDef, metadata: pendingEntry.metadata } : {})
5✔
595
                    };
5✔
596
                }
5✔
597
            }
6✔
598
        }
6✔
599
        return dict;
7✔
600
    }
7✔
601
    private async initFromAppDefAsync(appDef: ApplicationDefinition, session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload> {
1✔
602
        if (Array.isArray(appDef.Extension?.CustomProjections?.Projection)) {
×
603
            for (const pd of appDef.Extension.CustomProjections.Projection) {
×
604
                let k, v;
×
605
                if (typeof (pd.epsg) === 'string' && typeof (pd.text) === 'string') { // appdef json form
×
606
                    k = pd.epsg;
×
607
                    v = pd.text;
×
608
                } else { // appdef xml translated form
×
609
                    const [epsg] = pd["@epsg"];
×
610
                    const [projStr] = pd["#text"];
×
611
                    k = epsg;
×
612
                    v = projStr;
×
613
                }
×
614
                if (!strIsNullOrEmpty(k) && !strIsNullOrEmpty(v)) {
×
615
                    proj4.defs(`EPSG:${k}`, v);
×
616
                    debug(`Registered proj4 defn from appdef for EPSG:${k}`, v);
×
617
                }
×
618
            }
×
619
            register(proj4);
×
620
        }
×
621
        const [mapsByName, pendingMapDefs, warnings] = await this.createRuntimeMapsAsync(session, appDef, isStateless(appDef), fl => getMapDefinitionsFromFlexLayout(fl), fl => this.getExtraProjectionsFromFlexLayout(fl), sessionWasReused);
×
622
        return await this.initFromAppDefCoreAsync(appDef, this.options, mapsByName, warnings, pendingMapDefs);
×
623
    }
×
624
    /**
625
     * When a viewer is available via {@link setViewer}, routes a fetched appdef through
626
     * {@link initAppFromAppDef} (which handles locale init, session management, and INIT_APP
627
     * dispatch) and returns undefined to signal that the payload was already dispatched.
628
     * Falls back to the legacy in-command path when no viewer is set (e.g. custom implementations).
629
     */
630
    private dispatchOrProcess(
1✔
NEW
631
        fl: ApplicationDefinition,
×
NEW
632
        session: AsyncLazy<string>,
×
NEW
633
        sessionWasReused: boolean
×
NEW
634
    ): Promise<IInitAppActionPayload | undefined> {
×
NEW
635
        if (this._viewer) {
×
NEW
636
            this.dispatch(initAppFromAppDef(fl, this._viewer, this.options));
×
NEW
637
            return Promise.resolve(undefined);
×
NEW
638
        }
×
NEW
639
        return this.initFromAppDefAsync(fl, session, sessionWasReused);
×
NEW
640
    }
×
641
    private async sessionAcquiredAsync(session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload | undefined> {
1✔
642
        const { resourceId, locale } = this.options;
×
643
        if (!resourceId) {
×
644
            //Try assumed default location of appdef.json that we are assuming sits in the same place as the viewer html files
645
            const cl = new Client("", "mapagent");
×
646
            try {
×
647
                const fl = await cl.get<ApplicationDefinition>("appdef.json");
×
NEW
648
                return this.dispatchOrProcess(fl, session, sessionWasReused);
×
649
            } catch (e) { //The appdef.json doesn't exist at the assumed default location?
×
650
                throw new MgError(tr("INIT_ERROR_MISSING_RESOURCE_PARAM", locale));
×
651
            }
×
652
        } else {
×
653
            if (typeof (resourceId) == 'string') {
×
654
                if (strEndsWith(resourceId, "WebLayout")) {
×
UNCOV
655
                    assertIsDefined(this.client);
×
656
                    const wl = await this.client.getResource<WebLayout>(resourceId, { SESSION: await session.getValueAsync() });
×
657
                    return await this.initFromWebLayoutAsync(wl, session, sessionWasReused);
×
658
                } else if (strEndsWith(resourceId, "ApplicationDefinition")) {
×
659
                    assertIsDefined(this.client);
×
660
                    const fl = await this.client.getResource<ApplicationDefinition>(resourceId, { SESSION: await session.getValueAsync() });
×
661
                    return await this.initFromAppDefAsync(fl, session, sessionWasReused);
×
662
                } else {
×
663
                    if (isResourceId(resourceId)) {
×
664
                        throw new MgError(tr("INIT_ERROR_UNKNOWN_RESOURCE_TYPE", locale, { resourceId: resourceId }));
×
665
                    } else {
×
666
                        //Assume URL to a appdef json document
667
                        let fl: ApplicationDefinition;
×
668
                        if (!this.client) {
×
669
                            // This wasn't set up with a mapagent URI (probably a non-MG viewer template), so make a new client on-the-fly
670
                            const cl = new Client("", "mapagent");
×
671
                            fl = await cl.get<ApplicationDefinition>(resourceId);
×
672
                        } else {
×
673
                            fl = await this.client.get<ApplicationDefinition>(resourceId);
×
674
                        }
×
NEW
675
                        return this.dispatchOrProcess(fl, session, sessionWasReused);
×
676
                    }
×
677
                }
×
678
            } else {
×
679
                const fl = await resourceId();
×
NEW
680
                return this.dispatchOrProcess(fl, session, sessionWasReused);
×
681
            }
×
682
        }
×
683
    }
×
684
    public async runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload | undefined> {
1✔
685
        this.options = options;
×
686
        await this.initLocaleAsync(this.options);
×
NEW
687
        return this.initWithSessionAsync(options, (session, sessionWasReused) =>
×
NEW
688
            this.sessionAcquiredAsync(session, sessionWasReused)
×
NEW
689
        );
×
NEW
690
    }
×
691
    /**
692
     * Runs the viewer initialization from the given pre-loaded application definition.
693
     * This method is an implementation detail of {@link initAppFromAppDef} and should not
694
     * be called directly. It handles locale initialization, session setup and runtime map
695
     * creation for the given appdef.
696
     *
697
     * @param appDef The pre-loaded application definition
698
     * @param options The initialization options
699
     * @returns A promise that resolves to the initialization payload
700
     * @since 0.15
701
     */
702
    public async runFromAppDefAsync(appDef: ApplicationDefinition, options: IInitAsyncOptions): Promise<IInitAppActionPayload> {
1✔
703
        this.options = options;
2✔
704
        await this.initLocaleAsync(options);
2✔
705
        return this.initWithSessionAsync(options, (session, sessionWasReused) =>
2✔
NEW
706
            this.initFromAppDefAsync(appDef, session, sessionWasReused)
×
707
        ) as Promise<IInitAppActionPayload>;
2✔
708
        // Safe cast: when called from runFromAppDefAsync, _viewer is never set on this
709
        // temporary internal command, so the resolver never returns undefined.
710
    }
2✔
711
    private async initWithSessionAsync(
1✔
NEW
712
        options: IInitAsyncOptions,
×
NEW
713
        resolver: (session: AsyncLazy<string>, sessionWasReused: boolean) => Promise<IInitAppActionPayload | undefined>
×
NEW
714
    ): Promise<IInitAppActionPayload | undefined> {
×
715
        let sessionWasReused = false;
×
716
        let session: AsyncLazy<string>;
×
NEW
717
        if (!options.session) {
×
718
            session = new AsyncLazy<string>(async () => {
×
719
                assertIsDefined(this.client);
×
720
                const sid = await this.client.createSession("Anonymous", "");
×
721
                return sid;
×
722
            });
×
723
        } else {
×
NEW
724
            info(`Re-using session: ${options.session}`);
×
725
            sessionWasReused = true;
×
NEW
726
            session = new AsyncLazy<string>(() => Promise.resolve(options.session!));
×
727
        }
×
NEW
728
        const payload = await resolver(session, sessionWasReused);
×
NEW
729
        if (!payload) return undefined; // setViewer was used: INIT_APP was dispatched via initAppFromAppDef
×
730
        payload.sessionWasReused = sessionWasReused;
×
731
        if (sessionWasReused) {
×
732
            let initSelections: IRestoredSelectionSets = {};
×
733
            for (const mapName in payload.maps) {
×
734
                const sset = await retrieveSelectionSetFromLocalStorage(session, mapName);
×
735
                if (sset) {
×
736
                    initSelections[mapName] = sset;
×
737
                }
×
738
            }
×
739
            payload.initialSelections = initSelections;
×
740
            try {
×
741
                //In the interest of being a responsible citizen, clean up all selection-related stuff from
742
                //session store
743
                await clearSessionStore();
×
744
            } catch (e) {
×
745

746
            }
×
747
        }
×
748

749
        return payload;
×
750
    }
×
751
}
1✔
752

753
/**
754
 * Initializes the viewer from a pre-loaded application definition.
755
 *
756
 * This action builds the {@link ActionType.INIT_APP} payload directly from an already-loaded
757
 * {@link ApplicationDefinition}. Use this when you have the appdef in-hand and want to bypass
758
 * the resource-URL loading step that {@link initLayout} performs.
759
 *
760
 * The action creates and manages its own {@link DefaultViewerInitCommand} internally — callers
761
 * do not need to supply a command object.
762
 *
763
 * @param appDef The pre-loaded application definition
764
 * @param viewer The map provider context, forwarded to the optional {@link IInitAppLayout.onInit} callback
765
 * @param options The initialization options
766
 * @returns {ReduxThunkedAction}
767
 *
768
 * @since 0.15
769
 */
770
export function initAppFromAppDef(appDef: ApplicationDefinition, viewer: IMapProviderContext, options: IInitAppLayout): ReduxThunkedAction {
1✔
771
    const opts: IInitAsyncOptions = { ...options };
10✔
772
    return (dispatch, getState) => {
10✔
773
        const args = getState().config;
10✔
774
        const cmd = new DefaultViewerInitCommand(dispatch);
10✔
775
        if (args.agentUri && args.agentKind) {
10✔
776
            cmd.attachClient(new Client(args.agentUri, args.agentKind));
1✔
777
        }
1✔
778
        return cmd.runFromAppDefAsync(appDef, opts).then(payload => {
10✔
779
            applyInitPayloadOverrides(payload, opts);
8✔
780
            dispatch({
8✔
781
                type: ActionType.INIT_APP,
8✔
782
                payload
8✔
783
            });
8✔
784
            if (options.onInit) {
8✔
785
                if (viewer) {
2✔
786
                    options.onInit(viewer);
2✔
787
                }
2✔
788
            }
2✔
789
        }).catch(err => {
10✔
790
            processAndDispatchInitError(err, false, dispatch, opts);
2✔
791
        });
10✔
792
    };
10✔
793
}
10✔
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