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

jumpinjackie / mapguide-react-layout / 24007383380

05 Apr 2026 06:08PM UTC coverage: 41.183% (+0.004%) from 41.179%
24007383380

Pull #1622

github

web-flow
Merge 8924f574f into cf0de7cb2
Pull Request #1622: Chore: Migrate to Redux Toolkit

1929 of 2468 branches covered (78.16%)

91 of 115 new or added lines in 9 files covered. (79.13%)

1 existing line in 1 file now uncovered.

10287 of 24979 relevant lines covered (41.18%)

10.08 hits per line

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

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

33
const TYPE_SUBJECT = "SubjectLayer";
×
34
const TYPE_EXTERNAL = "External";
×
35

36
const scopedId = new ScopedId();
×
37

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

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

131
        if (webLayout.Title != "") {
×
132
            document.title = webLayout.Title || document.title;
×
133
        }
×
134

135
        const maps: any = {};
×
136
        const [firstMapName, firstSessionId] = this.establishInitialMapNameAndSession(mapsByName);
×
137

138
        for (const mapName in mapsByName) {
×
139
            const map = mapsByName[mapName];
×
140
            maps[mapName] = {
×
141
                mapGroupId: mapName,
×
142
                map: map,
×
143
                externalBaseLayers: this.options.externalBaseLayers ?? [],
×
144
                initialView: initialView
×
145
            };
×
146
        }
×
147

148
        const menus: Dictionary<ToolbarConf> = {};
×
149
        menus[WEBLAYOUT_TOOLBAR] = {
×
150
            items: mainToolbar
×
151
        };
×
152
        menus[WEBLAYOUT_TASKMENU] = {
×
153
            items: taskBar
×
154
        };
×
155
        menus[WEBLAYOUT_CONTEXTMENU] = {
×
156
            items: contextMenu
×
157
        };
×
158

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

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

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

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

386
        const groups = [] as MapGroup[];
×
387
        const layers = [] as MapLayer[];
×
388

389
        if (mdf.TileSetSource) {
×
390
            rt.TileSetDefinition = mdf.TileSetSource.ResourceId;
×
391
            const tsd = await client.getResource<TileSetDefinition>(mdf.TileSetSource.ResourceId);
×
392
            if (tsd.TileStoreParameters.TileProvider == "Default") {
×
393
                const sTileWidth = tsd.TileStoreParameters.Parameter.find(p => p.Name == "TileWidth")?.Value;
×
394
                const sTileHeight = tsd.TileStoreParameters.Parameter.find(p => p.Name == "TileHeight")?.Value;
×
395
                if (!strIsNullOrEmpty(sTileWidth) && !strIsNullOrEmpty(sTileHeight)) {
×
396
                    rt.TileWidth = parseInt(sTileWidth, 10);
×
397
                    rt.TileHeight = parseInt(sTileHeight, 10);
×
398
                }
×
399
            } else if (tsd.TileStoreParameters.TileProvider == "XYZ") {
×
400
                rt.TileHeight = 256;
×
401
                rt.TileHeight = 256;
×
402
            }
×
403

404
            for (const bg of tsd.BaseMapLayerGroup) {
×
405
                groups.push({
×
406
                    Name: bg.Name,
×
407
                    DisplayInLegend: bg.ShowInLegend,
×
408
                    LegendLabel: bg.LegendLabel,
×
409
                    ObjectId: bg.Name,
×
410
                    ExpandInLegend: bg.ExpandInLegend,
×
411
                    Visible: bg.Visible,
×
412
                    ActuallyVisible: bg.Visible,
×
413
                    Type: 3 /* BaseMapFromTileSet */
×
414
                });
×
415

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

435
        for (const grp of mdf.MapLayerGroup) {
×
436
            groups.push({
×
437
                Name: grp.Name,
×
438
                DisplayInLegend: grp.ShowInLegend,
×
439
                LegendLabel: grp.LegendLabel,
×
440
                ObjectId: grp.Name,
×
441
                ExpandInLegend: grp.ExpandInLegend,
×
442
                Visible: grp.Visible,
×
443
                ActuallyVisible: grp.Visible,
×
444
                Type: 1 /* Normal */
×
445
            });
×
446
        }
×
447

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

465
        rt.Group = groups;
×
466
        rt.Layer = layers;
×
467

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

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

552
                if (isArbitrary) {
×
553
                    //Check for incompatible widgets
554
                    for (const wset of appDef.WidgetSet) {
×
555
                        for (const widget of wset.Widget) {
×
556
                            switch (widget.Type) {
×
557
                                case "CoordinateTracker":
×
558
                                    warnings.push(tr("INIT_WARNING_ARBITRARY_COORDSYS_UNSUPPORTED_WIDGET", locale, { mapId: mGroup["@id"], widget: widget.Type }));
×
559
                                    break;
×
560
                            }
×
561
                        }
×
562
                    }
×
563
                }
×
564

565
                applyInitialBaseLayerVisibility(externalBaseLayers);
×
566

567
                //Setup initial view
568
                let initialView: IMapView | undefined;
×
569
                if (mGroup.InitialView) {
×
570
                    initialView = {
×
571
                        x: mGroup.InitialView.CenterX,
×
572
                        y: mGroup.InitialView.CenterY,
×
573
                        scale: mGroup.InitialView.Scale
×
574
                    };
×
575
                }
×
576

577
                if (mapName) {
×
578
                    const coordinateFormat = parseMapGroupCoordinateFormat(mGroup);
×
579
                    const pendingEntry = pendingMapDefs?.[mapName];
×
580
                    dict[mapName] = {
×
581
                        mapGroupId: mGroup["@id"],
×
582
                        map: mapsByName[mapName],
×
583
                        initialView: initialView,
×
584
                        externalBaseLayers: externalBaseLayers,
×
585
                        initialExternalLayers: initExternalLayers,
×
586
                        coordinateFormat: coordinateFormat,
×
587
                        // If this map is pending lazy creation, store the mapDef for later use
588
                        ...(pendingEntry ? { mapDef: pendingEntry.mapDef, metadata: pendingEntry.metadata } : {})
×
589
                    };
×
590
                }
×
591
            }
×
592
        }
×
593
        return dict;
×
594
    }
×
595
    private async initFromAppDefAsync(appDef: ApplicationDefinition, session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload> {
×
596
        if (Array.isArray(appDef.Extension?.CustomProjections?.Projection)) {
×
597
            for (const pd of appDef.Extension.CustomProjections.Projection) {
×
598
                let k, v;
×
599
                if (typeof (pd.epsg) === 'string' && typeof (pd.text) === 'string') { // appdef json form
×
600
                    k = pd.epsg;
×
601
                    v = pd.text;
×
602
                } else { // appdef xml translated form
×
603
                    const [epsg] = pd["@epsg"];
×
604
                    const [projStr] = pd["#text"];
×
605
                    k = epsg;
×
606
                    v = projStr;
×
607
                }
×
608
                if (!strIsNullOrEmpty(k) && !strIsNullOrEmpty(v)) {
×
609
                    proj4.defs(`EPSG:${k}`, v);
×
610
                    debug(`Registered proj4 defn from appdef for EPSG:${k}`, v);
×
611
                }
×
612
            }
×
613
            register(proj4);
×
614
        }
×
615
        const [mapsByName, pendingMapDefs, warnings] = await this.createRuntimeMapsAsync(session, appDef, isStateless(appDef), fl => getMapDefinitionsFromFlexLayout(fl), fl => this.getExtraProjectionsFromFlexLayout(fl), sessionWasReused);
×
616
        return await this.initFromAppDefCoreAsync(appDef, this.options, mapsByName, warnings, pendingMapDefs);
×
617
    }
×
618
    private async sessionAcquiredAsync(session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload> {
×
619
        const { resourceId, locale } = this.options;
×
620
        if (!resourceId) {
×
621
            //Try assumed default location of appdef.json that we are assuming sits in the same place as the viewer html files
622
            const cl = new Client("", "mapagent");
×
623
            try {
×
624
                const fl = await cl.get<ApplicationDefinition>("appdef.json");
×
625
                return await this.initFromAppDefAsync(fl, session, sessionWasReused);
×
626
            } catch (e) { //The appdef.json doesn't exist at the assumed default location?
×
627
                throw new MgError(tr("INIT_ERROR_MISSING_RESOURCE_PARAM", locale));
×
628
            }
×
629
        } else {
×
630
            if (typeof (resourceId) == 'string') {
×
631
                if (strEndsWith(resourceId, "WebLayout")) {
×
632
                    assertIsDefined(this.client);
×
633
                    const wl = await this.client.getResource<WebLayout>(resourceId, { SESSION: await session.getValueAsync() });
×
634
                    return await this.initFromWebLayoutAsync(wl, session, sessionWasReused);
×
635
                } else if (strEndsWith(resourceId, "ApplicationDefinition")) {
×
636
                    assertIsDefined(this.client);
×
637
                    const fl = await this.client.getResource<ApplicationDefinition>(resourceId, { SESSION: await session.getValueAsync() });
×
638
                    return await this.initFromAppDefAsync(fl, session, sessionWasReused);
×
639
                } else {
×
640
                    if (isResourceId(resourceId)) {
×
641
                        throw new MgError(tr("INIT_ERROR_UNKNOWN_RESOURCE_TYPE", locale, { resourceId: resourceId }));
×
642
                    } else {
×
643
                        //Assume URL to a appdef json document
644
                        let fl: ApplicationDefinition;
×
645
                        if (!this.client) {
×
646
                            // This wasn't set up with a mapagent URI (probably a non-MG viewer template), so make a new client on-the-fly
647
                            const cl = new Client("", "mapagent");
×
648
                            fl = await cl.get<ApplicationDefinition>(resourceId);
×
649
                        } else {
×
650
                            fl = await this.client.get<ApplicationDefinition>(resourceId);
×
651
                        }
×
652
                        return await this.initFromAppDefAsync(fl, session, sessionWasReused);
×
653
                    }
×
654
                }
×
655
            } else {
×
656
                const fl = await resourceId();
×
657
                return await this.initFromAppDefAsync(fl, session, sessionWasReused);
×
658
            }
×
659
        }
×
660
    }
×
661
    public async runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload> {
×
662
        this.options = options;
×
663
        await this.initLocaleAsync(this.options);
×
664
        let sessionWasReused = false;
×
665
        let session: AsyncLazy<string>;
×
666
        if (!this.options.session) {
×
667
            session = new AsyncLazy<string>(async () => {
×
668
                assertIsDefined(this.client);
×
669
                const sid = await this.client.createSession("Anonymous", "");
×
670
                return sid;
×
671
            });
×
672
        } else {
×
673
            info(`Re-using session: ${this.options.session}`);
×
674
            sessionWasReused = true;
×
675
            session = new AsyncLazy<string>(() => Promise.resolve(this.options.session!));
×
676
        }
×
677
        const payload = await this.sessionAcquiredAsync(session, sessionWasReused);
×
NEW
678
        if (sessionWasReused) {
×
679
            let initSelections: IRestoredSelectionSets = {};
×
680
            for (const mapName in payload.maps) {
×
681
                const sset = await retrieveSelectionSetFromLocalStorage(session, mapName);
×
682
                if (sset) {
×
683
                    initSelections[mapName] = sset;
×
684
                }
×
685
            }
×
686
            payload.initialSelections = initSelections;
×
687
            try {
×
688
                //In the interest of being a responsible citizen, clean up all selection-related stuff from
689
                //session store
690
                await clearSessionStore();
×
691
            } catch (e) {
×
692

693
            }
×
694
        }
×
695

696
        return payload;
×
697
    }
×
698
}
×
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