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

jumpinjackie / mapguide-react-layout / 26217425474

21 May 2026 09:24AM UTC coverage: 60.54% (-0.3%) from 60.795%
26217425474

Pull #1651

github

web-flow
Merge 1c5de0250 into 44e26a5b7
Pull Request #1651: Implement spy mode

3289 of 4035 branches covered (81.51%)

178 of 582 new or added lines in 16 files covered. (30.58%)

7 existing lines in 4 files now uncovered.

16218 of 26789 relevant lines covered (60.54%)

14.3 hits per line

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

92.53
/src/api/default-commands.ts
1
import {
1✔
2
    registerCommand,
3
    DefaultCommands,
4
    CommandConditions,
5
    openUrlInTarget
6
} from "./registry/command";
7
import {
1✔
8
    IMapViewer,
9
    ActiveMapTool,
10
    RefreshMode,
11
    ReduxDispatch,
12
    getInitialView,
13
    getRuntimeMap,
14
    getCurrentView,
15
    ITargetedCommand,
16
    IConfigurationReducerState,
17
    CommandTarget
18
} from "./common";
19
import { tr } from "../api/i18n";
1✔
20
import { DefaultComponentNames } from "../api/registry/component";
1✔
21
import {
1✔
22
    SPRITE_SELECT,
23
    SPRITE_PAN,
24
    SPRITE_ZOOM_IN,
25
    SPRITE_ZOOM_IN_FIXED,
26
    SPRITE_ZOOM_OUT_FIXED,
27
    SPRITE_PAN_WEST,
28
    SPRITE_PAN_EAST,
29
    SPRITE_PAN_NORTH,
30
    SPRITE_PAN_SOUTH,
31
    SPRITE_ABOUT,
32
    SPRITE_HELP,
33
    SPRITE_MEASURE,
34
    SPRITE_INITIAL_CENTER,
35
    SPRITE_ZOOM_FULL,
36
    SPRITE_VIEW_BACK,
37
    SPRITE_ICON_REFRESHMAP,
38
    SPRITE_VIEW_FORWARD,
39
    SPRITE_GEOLOCATION,
40
    SPRITE_INVOKE_SCRIPT,
41
    SPRITE_COORDINATE_TRACKER,
42
    SPRITE_LAYER_ADD,
43
    SPRITE_PRINT,
44
    SPRITE_MAP_SWIPE
45
} from "../constants/assets";
46
import { setCurrentView, setActiveTool, previousView, nextView, setComparisonMode } from '../actions/map';
1✔
47
import { showModalComponent, showModalUrl } from '../actions/modal';
1✔
48
import { refresh } from '../actions/legend';
1✔
49
import { setTaskPaneVisibility, setLegendVisibility, setSelectionPanelVisibility } from '../actions/template';
1✔
50
import React from "react";
1✔
51
import ReactDOM from "react-dom";
1✔
52

53
function panMap(dispatch: ReduxDispatch, viewer: IMapViewer, value: "right" | "left" | "up" | "down") {
2✔
54
    const settings: any = {
2✔
55
        "right": [2, 1],
2✔
56
        "left": [0, 1],
2✔
57
        "down": [0, 1],
2✔
58
        "up": [0, 3]
2✔
59
    };
2✔
60

61
    const view = viewer.getCurrentView();
2✔
62
    const current_center = [view.x, view.y];
2✔
63
    const currentExtent = viewer.getCurrentExtent();
2✔
64
    let newPos: number[];
2✔
65

66
    const direction = settings[value];
2✔
67

68
    if (value == "right" || value == "left") {
2✔
69
        newPos = [
1✔
70
            currentExtent[direction[0]],
1✔
71
            current_center[direction[1]]
1✔
72
        ];
1✔
73

74
    } else {
1✔
75
        newPos = [
1✔
76
            current_center[direction[0]],
1✔
77
            currentExtent[direction[1]]
1✔
78
        ];
1✔
79
    }
1✔
80

81
    dispatch(setCurrentView({ x: newPos[0], y: newPos[1], scale: view.scale }));
2✔
82
}
2✔
83

84
/**
85
 * @hidden
86
 */
87
export function buildTargetedCommand(config: Readonly<IConfigurationReducerState>, parameters?: Record<string, unknown>): ITargetedCommand {
1✔
88
    const cmdTarget = (parameters || {})["Target"] as CommandTarget;
7✔
89
    const cmdDef: ITargetedCommand = {
7✔
90
        target: cmdTarget || "NewWindow"
7✔
91
    };
7✔
92
    if (config.capabilities.hasTaskPane && cmdTarget == "TaskPane") {
7✔
93
        cmdDef.target = "TaskPane";
4✔
94
    }
4✔
95
    if (cmdTarget == "SpecifiedFrame") {
7✔
96
        cmdDef.target = cmdTarget;
1✔
97
        cmdDef.targetFrame = (parameters || {})["TargetFrame"] as string | undefined;
1!
98
    }
1✔
99
    return cmdDef;
7✔
100
}
7✔
101

102
/**
103
 * Registers the default set of commands into the command registry. This is automatically called by the default viewer
104
 * bundle. If creating your own viewer bundle, be sure to call this function in your entry point, or individually register
105
 * the commands you want to make available in your custom viewer bundle
106
 */
107
export function initDefaultCommands() {
1✔
108
    //Select Tool
109
    registerCommand(DefaultCommands.Select, {
1✔
110
        iconClass: SPRITE_SELECT,
1✔
111
        selected: (state) => {
1✔
112
            return state.activeTool === ActiveMapTool.Select;
1✔
113
        },
1✔
114
        enabled: () => true,
1✔
115
        invoke: (dispatch) => {
1✔
116
            return dispatch(setActiveTool(ActiveMapTool.Select));
1✔
117
        }
1✔
118
    });
1✔
119
    //Pan Tool
120
    registerCommand(DefaultCommands.Pan, {
1✔
121
        iconClass: SPRITE_PAN,
1✔
122
        selected: (state) => {
1✔
123
            return state.activeTool === ActiveMapTool.Pan;
1✔
124
        },
1✔
125
        enabled: () => true,
1✔
126
        invoke: (dispatch) => {
1✔
127
            return dispatch(setActiveTool(ActiveMapTool.Pan));
1✔
128
        }
1✔
129
    });
1✔
130
    //Zoom Tool
131
    registerCommand(DefaultCommands.Zoom, {
1✔
132
        iconClass: SPRITE_ZOOM_IN,
1✔
133
        selected: (state) => {
1✔
134
            return state.activeTool === ActiveMapTool.Zoom;
1✔
135
        },
1✔
136
        enabled: () => true,
1✔
137
        invoke: (dispatch) => {
1✔
138
            return dispatch(setActiveTool(ActiveMapTool.Zoom));
1✔
139
        }
1✔
140
    });
1✔
141
    //Zoom in
142
    registerCommand(DefaultCommands.ZoomIn, {
1✔
143
        iconClass: SPRITE_ZOOM_IN_FIXED,
1✔
144
        selected: () => false,
1✔
145
        enabled: () => true,
1✔
146
        invoke: (_dispatch, _getState, viewer) => {
1✔
147
            if (viewer) {
1✔
148
                viewer.zoomDelta(1);
1✔
149
            }
1✔
150
        }
1✔
151
    });
1✔
152
    //Zoom Out
153
    registerCommand(DefaultCommands.ZoomOut, {
1✔
154
        iconClass: SPRITE_ZOOM_OUT_FIXED,
1✔
155
        selected: () => false,
1✔
156
        enabled: () => true,
1✔
157
        invoke: (_dispatch, _getState, viewer) => {
1✔
158
            if (viewer) {
1✔
159
                viewer.zoomDelta(-1);
1✔
160
            }
1✔
161
        }
1✔
162
    });
1✔
163
    //Pan Left
164
    registerCommand(DefaultCommands.PanLeft, {
1✔
165
        iconClass: SPRITE_PAN_WEST,
1✔
166
        selected: () => false,
1✔
167
        enabled: () => true,
1✔
168
        invoke: (dispatch, _getState, viewer) => {
1✔
169
            if (viewer) {
1✔
170
                panMap(dispatch, viewer, "left");
1✔
171
            }
1✔
172
        }
1✔
173
    });
1✔
174
    //Pan Right
175
    registerCommand(DefaultCommands.PanRight, {
1✔
176
        iconClass: SPRITE_PAN_EAST,
1✔
177
        selected: () => false,
1✔
178
        enabled: () => true,
1✔
179
        invoke: (dispatch, _getState, viewer) => {
1✔
180
            if (viewer) {
×
181
                panMap(dispatch, viewer, "right");
×
182
            }
×
183
        }
×
184
    });
1✔
185
    //Pan Up
186
    registerCommand(DefaultCommands.PanUp, {
1✔
187
        iconClass: SPRITE_PAN_NORTH,
1✔
188
        selected: () => false,
1✔
189
        enabled: () => true,
1✔
190
        invoke: (dispatch, _getState, viewer) => {
1✔
191
            if (viewer) {
×
192
                panMap(dispatch, viewer, "up");
×
193
            }
×
194
        }
×
195
    });
1✔
196
    //Pan Down
197
    registerCommand(DefaultCommands.PanDown, {
1✔
198
        iconClass: SPRITE_PAN_SOUTH,
1✔
199
        selected: () => false,
1✔
200
        enabled: () => true,
1✔
201
        invoke: (dispatch, _getState, viewer) => {
1✔
202
            if (viewer) {
1✔
203
                panMap(dispatch, viewer, "down");
1✔
204
            }
1✔
205
        }
1✔
206
    });
1✔
207
    //About
208
    registerCommand(DefaultCommands.About, {
1✔
209
        iconClass: SPRITE_ABOUT,
1✔
210
        selected: () => false,
1✔
211
        enabled: () => true,
1✔
212
        invoke: (dispatch, getState) => {
1✔
213
            dispatch(showModalComponent({
1✔
214
                modal: {
1✔
215
                    title: tr("ABOUT", getState().config.locale),
1✔
216
                    backdrop: true
1✔
217
                },
1✔
218
                name: DefaultComponentNames.About,
1✔
219
                component: DefaultComponentNames.About
1✔
220
            }));
1✔
221
        }
1✔
222
    });
1✔
223
    //Help
224
    registerCommand(DefaultCommands.Help, {
1✔
225
        iconClass: SPRITE_HELP,
1✔
226
        selected: () => false,
1✔
227
        enabled: () => true,
1✔
228
        invoke: (dispatch, getState) => {
1✔
229
            dispatch(showModalUrl({
1✔
230
                modal: {
1✔
231
                    title: tr("HELP", getState().config.locale),
1✔
232
                    backdrop: true
1✔
233
                },
1✔
234
                name: DefaultCommands.Help,
1✔
235
                url: "help/index.html"
1✔
236
            }));
1✔
237
        }
1✔
238
    });
1✔
239
    //Measure
240
    registerCommand(DefaultCommands.Measure, {
1✔
241
        iconClass: SPRITE_MEASURE,
1✔
242
        selected: () => false,
1✔
243
        enabled: () => true,
1✔
244
        invoke: (dispatch, getState, _viewer, parameters) => {
1✔
245
            const config = getState().config;
1✔
246
            const url = "component://Measure";
1✔
247
            const cmdDef = buildTargetedCommand(config, parameters);
1✔
248
            openUrlInTarget(DefaultCommands.Measure, cmdDef, config.capabilities.hasTaskPane, dispatch, url, tr("MEASURE", config.locale));
1✔
249
        }
1✔
250
    });
1✔
251
    
252
    //Initial Center and scale
253
    registerCommand(DefaultCommands.RestoreView, {
1✔
254
        iconClass: SPRITE_INITIAL_CENTER,
1✔
255
        selected: () => false,
1✔
256
        enabled: () => true,
1✔
257
        invoke: (_dispatch, getState, viewer) => {
1✔
258
            if (viewer) {
2✔
259
                const view = getInitialView(getState());
2✔
260
                if (view != null) {
2✔
261
                    viewer.zoomToView(view.x, view.y, view.scale);
1✔
262
                } else {
1✔
263
                    viewer.initialView();
1✔
264
                }
1✔
265
            }
2✔
266
        }
2✔
267
    })
1✔
268
    //Zoom Extents
269
    registerCommand(DefaultCommands.ZoomExtents, {
1✔
270
        iconClass: SPRITE_ZOOM_FULL,
1✔
271
        selected: () => false,
1✔
272
        enabled: () => true,
1✔
273
        invoke: (_dispatch, _getState, viewer) => {
1✔
274
            if (viewer) {
1✔
275
                viewer.initialView();
1✔
276
            }
1✔
277
        }
1✔
278
    });
1✔
279
    //Refresh Map
280
    registerCommand(DefaultCommands.RefreshMap, {
1✔
281
        iconClass: SPRITE_ICON_REFRESHMAP,
1✔
282
        selected: () => false,
1✔
283
        enabled: CommandConditions.isNotBusy,
1✔
284
        invoke: (dispatch, _getState, viewer) => {
1✔
285
            if (viewer) {
1✔
286
                viewer.refreshMap(RefreshMode.LayersOnly | RefreshMode.SelectionOnly);
1✔
287
                dispatch(refresh());
1✔
288
            }
1✔
289
        }
1✔
290
    });
1✔
291
    //Previous View
292
    registerCommand(DefaultCommands.PreviousView, {
1✔
293
        iconClass: SPRITE_VIEW_BACK,
1✔
294
        selected: () => false,
1✔
295
        enabled: CommandConditions.hasPreviousView,
1✔
296
        invoke: (dispatch, getState) => {
1✔
297
            const mapName = getState().config.activeMapName;
1✔
298
            if (mapName) {
1✔
299
                dispatch(previousView(mapName));
1✔
300
            }
1✔
301
        }
1✔
302
    });
1✔
303
    //Next View
304
    registerCommand(DefaultCommands.NextView, {
1✔
305
        iconClass: SPRITE_VIEW_FORWARD,
1✔
306
        selected: () => false,
1✔
307
        enabled: CommandConditions.hasNextView,
1✔
308
        invoke: (dispatch, getState) => {
1✔
309
            const mapName = getState().config.activeMapName;
1✔
310
            if (mapName) {
1✔
311
                dispatch(nextView(mapName));
1✔
312
            }
1✔
313
        }
1✔
314
    });
1✔
315
    //Geolocation
316
    registerCommand(DefaultCommands.Geolocation, {
1✔
317
        iconClass: SPRITE_GEOLOCATION,
1✔
318
        selected: () => false,
1✔
319
        enabled: CommandConditions.isNotBusy,
1✔
320
        invoke: (_dispatch, getState, viewer, parameters) => {
1✔
321
            const state = getState();
1✔
322
            const view = getCurrentView(state);
1✔
323
            const rtMap = getRuntimeMap(state);
1✔
324
            const locale = state.config.locale;
1✔
325
            if (viewer && view && rtMap) {
1✔
326
                const fact = viewer.getOLFactory();
1✔
327
                const geoOptions: Partial<PositionOptions> = {};
1✔
328
                let zoomScale = view.scale;
1✔
329
                if (parameters?.["ZoomLevel"]) {
1✔
330
                    zoomScale = parseInt(`${parameters["ZoomLevel"]}`, 10);
1✔
331
                }
1✔
332
                if (parameters?.["EnableHighAccuracy"]) {
1✔
333
                    geoOptions.enableHighAccuracy = (parameters["EnableHighAccuracy"] == "true");
1✔
334
                }
1✔
335
                if (parameters?.["Timeout"]) {
1✔
336
                    geoOptions.timeout = parseInt(`${parameters["Timeout"]}`, 10);
1✔
337
                }
1✔
338
                if (parameters?.["MaximumAge"]) {
1✔
339
                    geoOptions.maximumAge = parseInt(`${parameters["MaximumAge"]}`, 10);
1✔
340
                }
1✔
341
                navigator.geolocation.getCurrentPosition(pos => {
1✔
342
                    const proj = viewer.getProjection();
1✔
343
                    const txCoord = fact.transformCoordinateFromLonLat([pos.coords.longitude, pos.coords.latitude], proj);
1✔
344
                    const testCoord = fact.transformCoordinateFromLonLat([pos.coords.longitude, pos.coords.latitude], `EPSG:${rtMap.CoordinateSystem.EpsgCode}`);
1✔
345
                    viewer.zoomToView(txCoord[0], txCoord[1], zoomScale);
1✔
346
                    const extents: [number, number, number, number] = [
1✔
347
                        rtMap.Extents.LowerLeftCoordinate.X,
1✔
348
                        rtMap.Extents.LowerLeftCoordinate.Y,
1✔
349
                        rtMap.Extents.UpperRightCoordinate.X,
1✔
350
                        rtMap.Extents.UpperRightCoordinate.Y
1✔
351
                    ];
1✔
352
                    if (fact.extentContainsXY(extents, testCoord[0], testCoord[1])) {
1✔
353
                        viewer.toastSuccess("geolocation", tr("GEOLOCATION_SUCCESS", locale));
1✔
354
                        //getTopToaster().show({ icon: "geolocation", message: tr("GEOLOCATION_SUCCESS", locale), intent: Intent.SUCCESS });
355
                    } else {
1!
356
                        viewer.toastWarning("warning-sign", tr("GEOLOCATION_WARN_OUTSIDE_MAP", locale));
×
357
                        //getTopToaster().show({ icon: "warning-sign", message: tr("GEOLOCATION_WARN_OUTSIDE_MAP", locale), intent: Intent.WARNING });
358
                    }
×
359
                }, err => {
1✔
360
                    viewer.toastError("error", tr("GEOLOCATION_ERROR", locale, { message: err.message, code: err.code }));
×
361
                    //getTopToaster().show({ icon: "error", message: tr("GEOLOCATION_ERROR", locale, { message: err.message, code: err.code }), intent: Intent.DANGER });
362
                }, geoOptions);
1✔
363
            }
1✔
364
        }
1✔
365
    });
1✔
366
    //Coordinate Tracker
367
    registerCommand(DefaultCommands.CoordinateTracker, {
1✔
368
        iconClass: SPRITE_COORDINATE_TRACKER,
1✔
369
        selected: () => false,
1✔
370
        enabled: () => true,
1✔
371
        invoke: (dispatch, getState, _viewer, parameters) => {
1✔
372
            const config = getState().config;
1✔
373
            const codes = Array.isArray(parameters?.["Projection"]) ? parameters["Projection"] : [];
1!
374
            const url = `component://CoordinateTracker?${codes.map((p: string) => "projections=" + p).join("&")}`;
1✔
375
            const cmdDef = buildTargetedCommand(config, parameters);
1✔
376
            openUrlInTarget(DefaultCommands.CoordinateTracker, cmdDef, config.capabilities.hasTaskPane, dispatch, url, tr("COORDTRACKER", config.locale));
1✔
377
        }
1✔
378
    });
1✔
379
    //External Layer Manager
380
    registerCommand(DefaultCommands.AddManageLayers, {
1✔
381
        iconClass: SPRITE_LAYER_ADD,
1✔
382
        selected: () => false,
1✔
383
        enabled: () => true,
1✔
384
        invoke: (dispatch, getState, _viewer, parameters) => {
1✔
385
            const config = getState().config;
1✔
386
            const url = `component://${DefaultComponentNames.AddManageLayers}`;
1✔
387
            const cmdDef = buildTargetedCommand(config, parameters);
1✔
388
            openUrlInTarget(DefaultCommands.AddManageLayers, cmdDef, config.capabilities.hasTaskPane, dispatch, url, tr("ADD_MANAGE_LAYERS", config.locale));
1✔
389
        }
1✔
390
    });
1✔
391
    //Print
392
    registerCommand(DefaultCommands.Print, {
1✔
393
        iconClass: SPRITE_PRINT,
1✔
394
        selected: () => false,
1✔
395
        enabled: () => true,
1✔
396
        invoke: (_dispatch, _getState, viewer, _parameters) => {
1✔
397
            viewer?.exportImage({
×
398
                callback: (image) => {
×
399
                    const el = React.createElement("img", { src: image });
×
400
                    const printWindow = window.open();
×
401
                    if (printWindow) {
×
402
                        // Open and immediately close the document. This works around a problem in Firefox that is
403
                        // captured here: https://bugzilla.mozilla.org/show_bug.cgi?id=667227.
404
                        // Essentially, when we first create an iframe, it has no document loaded and asynchronously
405
                        // starts a load of "about:blank". If we access the document object and start manipulating it
406
                        // before that async load completes, a new document will be automatically created. But then
407
                        // when the async load completes, the original, automatically-created document gets unloaded
408
                        // and the new "about:blank" gets swapped in. End result: everything we add to the DOM before
409
                        // the async load complete gets lost and Firefox ends up printing a blank page.
410
                        // Explicitly opening and then closing a new document _seems_ to avoid this.
411
                        printWindow.document.open();
×
412
                        printWindow.document.close();
×
413
    
414
                        printWindow.document.head.innerHTML = `
×
415
                                <meta charset="UTF-8">
416
                                <title>Print View</title>
417
                                `;
418
                        printWindow.document.body.innerHTML = '<div id="print"></div>';
×
419
                        ReactDOM.render(el, printWindow.document.getElementById("print"));
×
420
                    }
×
421
                }
×
422
            });
×
423
        }
×
424
    });
1✔
425
    //MapSwipe
426
    registerCommand(DefaultCommands.MapSwipe, {
1✔
427
        iconClass: SPRITE_MAP_SWIPE,
1✔
428
        selected: (state) => (state.comparisonMode ?? (state.swipeActive ? "swipe" : "none")) !== "none",
1!
429
        enabled: (state) => {
1✔
430
            const pairs = state.comparisonPairs ?? state.mapSwipePairs;
1✔
431
            if (!pairs || pairs.length === 0) {
1!
432
                return false;
×
433
            }
×
434
            const activeMapName = state.activeMapName;
1✔
435
            // Only enable when the PRIMARY map is active; activating from the secondary
436
            // map would cause the same layer set to be used for both sides of the split.
437
            return pairs.some(p => p.primaryMapName === activeMapName);
1✔
438
        },
1✔
439
        invoke: (dispatch, getState) => {
1✔
440
            const config = getState().config;
1✔
441
            const currentMode = config.comparisonMode ?? (config.swipeActive ? "swipe" : "none");
1!
442
            const nextMode = currentMode === "none"
1✔
443
                ? (config.lastComparisonMode ?? "swipe")
1!
NEW
444
                : "none";
×
445
            dispatch(setComparisonMode(nextMode));
1✔
446
        }
1✔
447
    });
1✔
448
    //Fusion template helper commands
449
    /*
450
    registerCommand("showOverview", {
451
        icon: "images/icons/invoke-script",
452
        selected: () => false,
453
        enabled: CommandConditions.isNotBusy,
454
        invoke: (dispatch, getState, viewer, parameters) => {
455

456
        }
457
    });
458
    */
459
    registerCommand("showTaskPane", {
1✔
460
        iconClass: SPRITE_INVOKE_SCRIPT,
1✔
461
        selected: () => false,
1✔
462
        enabled: CommandConditions.isNotBusy,
1✔
463
        invoke: (dispatch, _getState) => {
1✔
464
            dispatch(setTaskPaneVisibility(true));
1✔
465
        }
1✔
466
    });
1✔
467
    registerCommand("showLegend", {
1✔
468
        iconClass: SPRITE_INVOKE_SCRIPT,
1✔
469
        selected: () => false,
1✔
470
        enabled: CommandConditions.isNotBusy,
1✔
471
        invoke: (dispatch, _getState) => {
1✔
472
            dispatch(setLegendVisibility(true));
1✔
473
        }
1✔
474
    });
1✔
475
    registerCommand("showSelectionPanel", {
1✔
476
        iconClass: SPRITE_INVOKE_SCRIPT,
1✔
477
        selected: () => false,
1✔
478
        enabled: CommandConditions.isNotBusy,
1✔
479
        invoke: (dispatch, _getState) => {
1✔
480
            dispatch(setSelectionPanelVisibility(true));
1✔
481
        }
1✔
482
    });
1✔
483
}
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