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

jumpinjackie / mapguide-react-layout / 29921515611

22 Jul 2026 12:53PM UTC coverage: 62.625% (+1.5%) from 61.081%
29921515611

push

github

web-flow
Update to React 18 + other package updates (#1683)

* Update to React 18 and other assorted package updates

* More package updates

* Update react-redux

* Fix TS error

3301 of 6236 branches covered (52.93%)

1 of 2 new or added lines in 2 files covered. (50.0%)

145 existing lines in 29 files now uncovered.

6910 of 11034 relevant lines covered (62.62%)

15.05 hits per line

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

50.45
/src/containers/quick-plot.tsx
1
import * as React from "react";
2
import { getFusionRoot } from "../api/runtime";
3
import { tr as xlate, tr } from "../api/i18n";
4
import { RuntimeMap } from "../api/contracts/runtime-map";
5
import {
6
    GenericEvent,
7
    IMapView,
8
    IConfigurationReducerState,
9
    IExternalBaseLayer,
10
    IMapViewer,
11
    Size,
12
    isVisualBaseLayer,
13
    IMapPrintCaptureOptions
14
} from "../api/common";
15
import { MapCapturerContext, IMapCapturerContextCallback } from "./map-capturer-context";
16
import { useActiveMapName, useActiveMapView, useActiveMapExternalBaseLayers, useViewerLocale, useAvailableMaps, usePrevious, useConfiguredAgentUri, useConfiguredAgentKind } from './hooks';
17
import { setViewRotation, setViewRotationEnabled } from '../actions/map';
18
import { debug } from '../utils/logger';
19
import { useActiveMapState } from './hooks-mapguide';
20
import { useMapProviderContext, useReduxDispatch } from "../components/map-providers/context";
21
import { TypedSelect, useElementContext } from "../components/elements/element-context";
22
import { IMapProviderContext } from "../components/map-providers/base";
23
import { MapAgentRequestBuilder } from "../api/builders/mapagent";
24

25
const PAPER_SIZES = [
2✔
26
    { value: "210.0,297.0,A4", label: "A4 (210x297 mm; 8.27x11.69 In) " },
27
    { value: "297.0,420.0,A3", label: "A3 (297x420 mm; 11.69x16.54 In) " },
28
    { value: "148.0,210.0,A5", label: "A5 (148x210 mm; 5.83x8.27 in) " },
29
    { value: "216.0,279.0,Letter", label: "Letter (216x279 mm; 8.50x11.00 In) " },
30
    { value: "216.0,356.0,Legal", label: "Legal (216x356 mm; 8.50x14.00 In) " }
31
];
32

33
const DPIS = [
2✔
34
    { value: "96", label: "96" },
35
    { value: "150", label: "150" },
36
    { value: "300", label: "300" },
37
    { value: "600", label: "600" }
38
];
39

40
const SCALES = [
2✔
41
    { value: "500", label: "1: 500" },
42
    { value: "1000", label: "1: 1000" },
43
    { value: "2500", label: "1: 2500" },
44
    { value: "5000", label: "1: 5000" }
45
]
46

47
function getMargin() {
48
    /*
49
    var widget = getParent().Fusion.getWidgetsByType("QuickPlot")[0];
50
    var margin;
51
    
52
    if(!!widget.margin){
53
         margin = widget.margin;
54
    }else{
55
        //the default margin
56
        margin = {top: 25.4, buttom: 12.7, left: 12.7, right: 12.7};
57
    }
58
    return margin;
59
    */
60
    return { top: 18, buttom: 6.4, left: 12.7, right: 12.7 };
9✔
61
}
62

63
function getPrintSize(viewer: IMapViewer, showAdvanced: boolean, paperSize: string, orientation: Orientation): Size {
64
    const value = paperSize.split(",");
8✔
65
    let size: Size;
66
    if (orientation === "P") {
8!
67
        size = { w: parseFloat(value[0]), h: parseFloat(value[1]) };
8✔
68
    } else {
69
        size = { w: parseFloat(value[1]), h: parseFloat(value[0]) };
×
70
    }
71

72
    if (!showAdvanced) {
8!
73
        // Calculate the paper size to make sure it has a same ratio with the viweport
74
        const paperRatio = size.w / size.h;
×
75
        var viewSize = viewer.getSize();
×
76
        let vs: Size | undefined;
77
        if (orientation === "P") {
×
78
            vs = {
×
79
                w: viewSize[1],
80
                h: viewSize[0]
81
            };
82
        } else {
83
            vs = {
×
84
                w: viewSize[0],
85
                h: viewSize[1]
86
            };
87
        }
88
        if (vs) {
×
89
            const viewRatio = vs.w / vs.h;
×
90
            if (paperRatio > viewRatio) {
×
91
                size.w = size.h * viewRatio;
×
92
            } else {
93
                size.h = size.w / viewRatio;
×
94
            }
95
        }
96
    }
97

98
    const margins = getMargin();
8✔
99
    size.h = size.h - margins.top - margins.buttom;
8✔
100
    size.w = size.w - margins.left - margins.right;
8✔
101

102
    return size;
8✔
103
}
104

105
const _mapCapturers: MapCapturerContext[] = [];
2✔
106

107
function getActiveCapturer(viewer: IMapViewer, mapNames: string[], activeMapName: string): MapCapturerContext | undefined {
108
    let activeCapturer: MapCapturerContext | undefined;
109
    if (_mapCapturers.length == 0) {
12✔
110
        if (mapNames.length) {
1!
111
            for (const mapName of mapNames) {
1✔
112
                const context = new MapCapturerContext(viewer, mapName);
1✔
113
                _mapCapturers.push(context);
1✔
114
                if (activeMapName == mapName) {
1!
115
                    activeCapturer = context;
1✔
116
                }
117
            }
118
        }
119
    } else {
120
        activeCapturer = _mapCapturers.filter(m => m.getMapName() === activeMapName)[0];
11✔
121
    }
122
    return activeCapturer;
12✔
123
}
124

125
function toggleMapCapturerLayer(locale: string,
126
    viewer: IMapProviderContext,
127
    mapNames: string[] | undefined,
128
    activeMapName: string,
129
    showAdvanced: boolean,
130
    paperSize: string,
131
    orientation: Orientation,
132
    scale: string,
133
    rotation: number,
134
    updateBoxCoords: (box: string, normalizedBox: string) => void,
135
    setViewRotationEnabled: (flag: boolean) => void,
136
    setViewRotation: (rot: number) => void) {
137
    const bVisible: boolean = showAdvanced;
4✔
138
    if (viewer.isReady() && mapNames) {
4!
139
        const activeCapturer = getActiveCapturer(viewer, mapNames, activeMapName);
4✔
140
        if (activeCapturer) {
4!
141
            if (bVisible) {
4!
142
                const ppSize = getPrintSize(viewer, showAdvanced, paperSize, orientation);
4✔
143
                const cb: IMapCapturerContextCallback = {
4✔
144
                    updateBoxCoords
145
                }
146
                activeCapturer.activate(cb, ppSize, parseFloat(scale), rotation);
4✔
147
                //For simplicity, reset rotation to 0 and prevent the ability to rotate while the map capture box
148
                //is active
149
                setViewRotationEnabled(false);
4✔
150
                setViewRotation(0);
4✔
151
                viewer.toastPrimary("info-sign", tr("QUICKPLOT_BOX_INFO", locale));
4✔
152
            } else {
153
                activeCapturer.deactivate();
×
154
                setViewRotationEnabled(true);
×
155
            }
156
        }
157
    }
158
}
159

160
export interface IQuickPlotContainerOwnProps {
161
    /**
162
     * When true, PDF is generated client-side via jsPDF instead of posting to PlotAsPDF.php.
163
     *
164
     * @since 0.15
165
     */
166
    clientSide?: boolean;
167
    /**
168
     * Custom disclaimer text for client-side PDF plots. When provided, this overrides
169
     * the default localized disclaimer string.
170
     *
171
     * @since 0.15
172
     */
173
    disclaimer?: string;
174
}
175

176
export interface IQuickPlotContainerConnectedState {
177
    config: IConfigurationReducerState;
178
    map: RuntimeMap;
179
    view: IMapView;
180
    externalBaseLayers: IExternalBaseLayer[];
181
    mapNames: string[];
182
}
183

184
export interface IQuickPlotContainerDispatch {
185
    setViewRotation: (rotation: number) => void;
186
    setViewRotationEnabled: (enabled: boolean) => void;
187
}
188

189
/**
190
 * @since 0.15
191
 */
192
export type Orientation = "L" | "P";
193

194
export interface IQuickPlotContainerState {
195
    title: string;
196
    subTitle: string;
197
    showLegend: boolean;
198
    showNorthBar: boolean;
199
    showCoordinates: boolean;
200
    showScaleBar: boolean;
201
    showDisclaimer: boolean;
202
    showAdvanced: boolean;
203
    orientation: Orientation;
204
    paperSize: string;
205
    scale: string;
206
    dpi: string;
207
    rotation: number;
208
    box: string;
209
    normalizedBox: string;
210
}
211

212
/**
213
 * Parses a comma-separated coordinate string (polygon ring) into a bounding box extent.
214
 *
215
 * @since 0.15
216
 * @hidden
217
 */
218
function parseBoxToExtent(box: string): [number, number, number, number] | undefined {
219
    const parts = box.split(",").map(Number);
×
220
    if (parts.length < 8 || parts.some(isNaN)) {
×
221
        return undefined;
×
222
    }
223
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
×
224
    for (let i = 0; i < parts.length; i += 2) {
×
225
        if (i + 1 >= parts.length) break;
×
226
        const x = parts[i];
×
227
        const y = parts[i + 1];
×
228
        if (x < minX) minX = x;
×
229
        if (y < minY) minY = y;
×
230
        if (x > maxX) maxX = x;
×
231
        if (y > maxY) maxY = y;
×
232
    }
233
    return [minX, minY, maxX, maxY];
×
234
}
235

236
/**
237
 * Generates a client-side PDF using jsPDF and the OpenLayers export-pdf technique.
238
 *
239
 * @since 0.15
240
 * @hidden
241
 */
242
async function generateClientSidePdf(
243
    viewer: IMapProviderContext,
244
    paperSize: string,
245
    orientation: Orientation,
246
    dpi: string,
247
    title: string,
248
    subtitle: string,
249
    showNorthBar: boolean,
250
    showScaleBar: boolean,
251
    showDisclaimer: boolean,
252
    showCoordinates: boolean,
253
    showLegend: boolean,
254
    disclaimerText: string | undefined,
255
    legendImageDataUrl: string | undefined,
256
    locale: string,
257
    backgroundColor?: string,
258
    fitExtent?: [number, number, number, number],
259
    rotationDeg?: number,
260
    scaleDenom?: number
261
): Promise<void> {
262
    const { jsPDF } = await import('jspdf');
1✔
263
    const tokens = paperSize.split(",");
1✔
264
    let paperW = parseFloat(tokens[0]);
1✔
265
    let paperH = parseFloat(tokens[1]);
1✔
266
    // Swap for landscape
267
    if (orientation === "L") {
1!
268
        paperW = parseFloat(tokens[1]);
×
269
        paperH = parseFloat(tokens[0]);
×
270
    }
271
    const margins = getMargin();
1✔
272
    const fullPrintW = paperW - margins.left - margins.right;
1✔
273
    const fullPrintH = paperH - margins.top - margins.buttom;
1✔
274
    // Footer space for elements below the map (scale bar, disclaimer)
275
    let footerHeight = 0;
1✔
276
    if (showScaleBar) footerHeight += 12;
1!
277
    if (showDisclaimer) footerHeight += 6;
1!
278
    // Legend column: 300px at capture DPI converted to mm
279
    const dpiVal = parseInt(dpi, 10);
1✔
280
    const LEGEND_COL_MM = showLegend ? (300 / dpiVal) * 25.4 : 0;
1!
281
    const mapPrintW = fullPrintW - LEGEND_COL_MM;
1✔
282
    // Map fills from the top margin down to just above the footer
283
    const mapHeight = fullPrintH - footerHeight;
1✔
284
    // Capture the map at print resolution (map area only, excluding legend column)
285
    const mapImagePromise = new Promise<string>((resolve) => {
1✔
286
        const captureOpts: IMapPrintCaptureOptions = {
1✔
287
            paperWidthMm: mapPrintW,
288
            paperHeightMm: mapHeight,
289
            dpi: dpiVal,
290
            backgroundColor,
291
            fitExtent,
292
            rotation: rotationDeg,
293
            scale: scaleDenom,
UNCOV
294
            callback: (imageDataUrl) => resolve(imageDataUrl)
×
295
        };
296
        viewer.captureMapPrintImage(captureOpts);
1✔
297
    });
298
    const mapImage = await mapImagePromise;
1✔
299
    // Create PDF
UNCOV
300
    const pdfOrientation = orientation === "L" ? "landscape" : "portrait";
×
301
    const format = tokens[2]?.toLowerCase() as string || "a4";
1!
302
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
303
    const pdf = new (jsPDF as any)(pdfOrientation, "mm", format);
1✔
304
    // Map image starts at the top margin, offset by legend column
305
    const mapTop = margins.top;
1✔
306
    const mapLeft = margins.left + LEGEND_COL_MM;
1✔
307
    pdf.addImage(mapImage, "JPEG", mapLeft, mapTop, mapPrintW, mapHeight);
1✔
308
    // Black outline around the map image
309
    pdf.setDrawColor(0);
1✔
310
    pdf.setLineWidth(0.2);
1✔
311
    pdf.rect(mapLeft, mapTop, mapPrintW, mapHeight, "S");
1✔
312
    // Legend image column (left side)
313
    if (showLegend) {
1!
314
        if (legendImageDataUrl) {
×
315
            pdf.addImage(legendImageDataUrl, "PNG", margins.left, mapTop, LEGEND_COL_MM, mapHeight);
×
316
        } else {
317
            // Placeholder for failed legend fetch
318
            pdf.setFillColor(220, 220, 220);
×
319
            pdf.rect(margins.left, mapTop, LEGEND_COL_MM, mapHeight, "F");
×
320
            pdf.setFontSize(8);
×
321
            pdf.setTextColor(128, 128, 128);
×
322
            pdf.text("Legend\nunavailable", margins.left + LEGEND_COL_MM / 2, mapTop + mapHeight / 2, { align: "center" });
×
323
        }
324
        // Outline around legend column
325
        pdf.setDrawColor(0);
×
326
        pdf.setLineWidth(0.2);
×
327
        pdf.rect(margins.left, mapTop, LEGEND_COL_MM, mapHeight, "S");
×
328
    }
329
    // Title and subtitle overlay within the top margin area, above the map
330
    const TITLE_Y = 8;
×
331
    const SUBTITLE_Y = 14;
×
332
    if (title) {
×
333
        pdf.setFontSize(14);
×
334
        pdf.text(title, margins.left, TITLE_Y, { align: "left" });
×
335
    }
336
    if (subtitle) {
×
337
        pdf.setFontSize(10);
×
338
        pdf.text(subtitle, margins.left, SUBTITLE_Y, { align: "left" });
×
339
    }
340
    // Determine map extent for coordinate labels
341
    const extent = fitExtent ?? viewer.getCurrentExtent();
×
342
    const mapBottom = mapTop + mapHeight;
1✔
343
    const MAP_RIGHT = mapLeft + mapPrintW;
1✔
344
    const COORD_BOX_H = 5;   // height of coordinate label box at font size 8
1✔
345
    const CORNER_MARGIN = 8; // mm spacing from map edges (room for watermark on bottom-right)
1✔
346
    const COORD_PADDING = 2; // mm padding inside coordinate box
1✔
347
    // Draw coordinates (white box with black outline)
348
    if (showCoordinates) {
1!
349
        const labelFontSize = 8;
×
350
        pdf.setFontSize(labelFontSize);
×
351
        // Top-left (NW corner): (minX, maxY)
352
        const tlLabel = `x:${extent[0].toFixed(6)}, y:${extent[3].toFixed(6)}`;
×
353
        const tlW = pdf.getTextWidth(tlLabel) + COORD_PADDING * 2;
×
354
        const tlX = mapLeft + CORNER_MARGIN;
×
355
        const tlY = mapTop + CORNER_MARGIN;
×
356
        pdf.setFillColor(255, 255, 255);
×
357
        pdf.setDrawColor(0);
×
358
        pdf.setLineWidth(0.3);
×
359
        pdf.rect(tlX, tlY, tlW, COORD_BOX_H, "FD");
×
360
        pdf.setTextColor(0);
×
361
        pdf.text(tlLabel, tlX + COORD_PADDING, tlY + COORD_BOX_H - 1.5, { align: "left" });
×
362
        // Bottom-right (SE corner): (maxX, minY)
363
        const brLabel = `x:${extent[2].toFixed(6)}, y:${extent[1].toFixed(6)}`;
×
364
        const brW = pdf.getTextWidth(brLabel) + COORD_PADDING * 2;
×
365
        const brX = MAP_RIGHT - brW - CORNER_MARGIN;
×
366
        const brY = mapBottom - COORD_BOX_H - CORNER_MARGIN;
×
367
        pdf.setFillColor(255, 255, 255);
×
368
        pdf.setDrawColor(0);
×
369
        pdf.setLineWidth(0.3);
×
370
        pdf.rect(brX, brY, brW, COORD_BOX_H, "FD");
×
371
        pdf.text(brLabel, brX + COORD_PADDING, brY + COORD_BOX_H - 1.5, { align: "left" });
×
372
    }
373
    // Draw north arrow at bottom-right, pointing up
374
    if (showNorthBar) {
×
375
        // Arrow sits above the bottom-right coordinate label (if visible) or near the map edge
376
        const arrowBaseY = showCoordinates
×
377
            ? mapBottom - COORD_BOX_H - CORNER_MARGIN - 3   // gap above coordinate box
378
            : mapBottom - CORNER_MARGIN;                      // gap from map edge
379
        const ARROW_CENTER_X = MAP_RIGHT - CORNER_MARGIN - 6;
×
380
        const ARROW_HEIGHT = 10;
×
381
        // Triangle pointing up: tip at top (smaller Y), base at bottom (larger Y)
382
        pdf.setFillColor(0);
×
383
        pdf.triangle(
×
384
            ARROW_CENTER_X, arrowBaseY - ARROW_HEIGHT,   // tip (top)
385
            ARROW_CENTER_X - 5, arrowBaseY,               // base left
386
            ARROW_CENTER_X + 5, arrowBaseY,               // base right
387
            "F"
388
        );
389
        pdf.setFontSize(10);
×
390
        pdf.setTextColor(0);
×
391
        pdf.text("N", ARROW_CENTER_X, arrowBaseY - ARROW_HEIGHT - 1, { align: "center" });
×
392
    }
393
    // Draw scale bar in footer area (below map image)
394
    if (showScaleBar) {
×
395
        const scaleBarY = mapBottom + 8; // vertically centered in footer
×
396
        // OpenLayers ScaleLine-inspired calculation
397
        // Scale 1:N means 1mm on paper = N mm on ground = N/1000 meters on ground
398
        const N = scaleDenom!;
×
399
        // pointResolution = meters per mm on paper
400
        let pointResolution = N / 1000;
×
401
        // OGC standard pixel size (0.28mm), OL's default minWidth of 64px
402
        const OGC_PIXEL_MM = 0.28;
×
403
        const MIN_WIDTH_MM = 64 * OGC_PIXEL_MM; // ~17.92mm
×
404
        const MAX_WIDTH_MM = fullPrintW * 0.55; // prevent overflow, leave room for text
×
405
        // Unit scaling following OL logic
406
        let suffix: string;
407
        let nominalCount = MIN_WIDTH_MM * pointResolution;
×
408
        if (nominalCount < 1e-6) {
×
409
            suffix = 'nm';
×
410
            pointResolution *= 1e9;
×
411
        } else if (nominalCount < 0.001) {
×
412
            suffix = 'μm';
×
413
            pointResolution *= 1e6;
×
414
        } else if (nominalCount < 1) {
×
415
            suffix = 'mm';
×
416
            pointResolution *= 1000;
×
417
        } else if (nominalCount < 1000) {
×
418
            suffix = 'm';
×
419
        } else {
420
            suffix = 'km';
×
421
            pointResolution /= 1000;
×
422
        }
423
        // Find nice round count using OL's LEADING_DIGITS approach
424
        const LEADING_DIGITS = [1, 2, 5];
×
425
        let i = 3 * Math.floor(Math.log(MIN_WIDTH_MM * pointResolution) / Math.log(10));
×
426
        let count = 0, widthMm = 0, prevCount = 0, prevWidth = 0;
×
427
        while (true) {
×
428
            const decimalCount = Math.floor(i / 3);
×
429
            const decimal = Math.pow(10, decimalCount);
×
430
            count = LEADING_DIGITS[((i % 3) + 3) % 3] * decimal;
×
431
            widthMm = count / pointResolution;
×
432
            if (isNaN(widthMm)) break;
×
433
            if (prevCount > 0 && widthMm > MAX_WIDTH_MM) {
×
434
                count = prevCount;
×
435
                widthMm = prevWidth;
×
436
                break;
×
437
            }
438
            if (widthMm >= MIN_WIDTH_MM) break;
×
439
            prevCount = count;
×
440
            prevWidth = widthMm;
×
441
            i++;
×
442
        }
443
        // Convert count to meters (count is in the display unit: km, m, mm, etc.)
444
        const UNIT_TO_M: Record<string, number> = { 'nm': 1e-9, 'μm': 1e-6, 'mm': 0.001, 'm': 1, 'km': 1000 };
×
445
        const groundM = count * (UNIT_TO_M[suffix] ?? 1);
×
446
        const groundFt = Math.round(groundM * 3.28084);
×
447
        // Actual printed scale
448
        const actualScale = Math.round(N);
×
449
        // Draw the scale bar
450
        pdf.setDrawColor(0);
×
451
        pdf.setLineWidth(0.3);
×
452
        pdf.line(margins.left, scaleBarY, margins.left + widthMm, scaleBarY);
×
453
        // Tick marks
454
        pdf.line(margins.left, scaleBarY - 2, margins.left, scaleBarY + 2);
×
455
        pdf.line(margins.left + widthMm, scaleBarY - 2, margins.left + widthMm, scaleBarY + 2);
×
456
        // Unit labels: metric above the bar, imperial below the bar
457
        pdf.setFontSize(7);
×
458
        pdf.setTextColor(0);
×
459
        const unitLabel = `${count} ${suffix}`;
×
460
        pdf.text(unitLabel, margins.left, scaleBarY - 3, { align: "left" });
×
461
        pdf.text(`${groundFt} ft`, margins.left, scaleBarY + 4, { align: "left" });
×
462
        // Scale ratio and date beside the scale bar (to the right)
463
        const now = new Date();
×
464
        const dateStr = now.toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric" });
×
465
        const infoX = margins.left + widthMm + 6; // gap after scale bar
×
466
        pdf.setFontSize(7);
×
467
        pdf.text(`${dateStr}`, infoX, scaleBarY - 3, { align: "left" });
×
468
        pdf.text(`Scale 1:${actualScale}`, infoX, scaleBarY + 4, { align: "left" });
×
469
    }
470
    // Draw disclaimer (bottom-right of page)
471
    if (showDisclaimer) {
×
472
        const discText = disclaimerText || tr("QUICKPLOT_DISCLAIMER", locale);
×
473
        const discY = showScaleBar ? mapBottom + 16 : mapBottom + 5;
×
474
        pdf.setFontSize(8);
×
475
        pdf.setTextColor(0);
×
476
        pdf.text(discText, MAP_RIGHT, discY, { align: "right", maxWidth: fullPrintW * 0.6 });
×
477
    }
478
    // Trigger download
479
    const filename = title
×
480
        ? `${title.replace(/[^a-zA-Z0-9]/g, "_")}.pdf`
481
        : "quickplot.pdf";
482
    pdf.save(filename);
1✔
483
}
484

485
export const QuickPlotContainer = (props: IQuickPlotContainerOwnProps) => {
2✔
486
    const { clientSide, disclaimer } = props;
16✔
487
    const { Slider, Callout, Button, Select, FormGroup, InputGroup, Checkbox } = useElementContext();
16✔
488
    const [title, setTitle] = React.useState(""); ``
16✔
489
    const [subTitle, setSubTitle] = React.useState("");
16✔
490
    const [showLegend, setShowLegend] = React.useState(false);
16✔
491
    const [showNorthBar, setShowNorthBar] = React.useState(false);
16✔
492
    const [showCoordinates, setShowCoordinates] = React.useState(false);
16✔
493
    const [showScaleBar, setShowScaleBar] = React.useState(false);
16✔
494
    const [showDisclaimer, setShowDisclaimer] = React.useState(false);
16✔
495
    const [showAdvanced, setShowAdvanced] = React.useState(false);
16✔
496
    const [orientation, setOrientation] = React.useState<Orientation>("P");
16✔
497
    const [paperSize, setPaperSize] = React.useState("210.0,297.0,A4");
16✔
498
    const [scale, setScale] = React.useState("5000");
16✔
499
    const [dpi, setDpi] = React.useState("96");
16✔
500
    const [rotation, setRotation] = React.useState(0);
16✔
501
    const [box, setBox] = React.useState("");
16✔
502
    const [normalizedBox, setNormalizedBox] = React.useState("");
16✔
503

504
    const viewer = useMapProviderContext();
16✔
505
    const locale = useViewerLocale();
16✔
506
    const agentUri = useConfiguredAgentUri();
16✔
507
    const agentKind = useConfiguredAgentKind();
16✔
508
    const activeMapName = useActiveMapName();
16✔
509
    const mapNames = useAvailableMaps()?.map(m => m.value);
16✔
510
    const map = useActiveMapState();
16✔
511
    const view = useActiveMapView();
16✔
512
    const externalBaseLayers = useActiveMapExternalBaseLayers()?.filter(ebl => isVisualBaseLayer(ebl));
16✔
513
    const dispatch = useReduxDispatch();
16✔
514
    const setViewRotationAction = (rotation: number) => dispatch(setViewRotation(rotation));
16✔
515
    const setViewRotationEnabledAction = (enabled: boolean) => dispatch(setViewRotationEnabled(enabled));
16✔
516

517
    const onTitleChanged = (e: GenericEvent) => {
16✔
518
        setTitle(e.target.value);
1✔
519
    };
520
    const onSubTitleChanged = (e: GenericEvent) => {
16✔
521
        setSubTitle(e.target.value);
1✔
522
    };
523
    const onShowLegendChanged = () => {
16✔
524
        setShowLegend(!showLegend);
1✔
525
    };
526
    const onShowNorthArrowChanged = () => {
16✔
527
        setShowNorthBar(!showNorthBar);
1✔
528
    };
529
    const onShowCoordinatesChanged = () => {
16✔
530
        setShowCoordinates(!showCoordinates);
1✔
531
    };
532
    const onShowScaleBarChanged = () => {
16✔
533
        setShowScaleBar(!showScaleBar);
1✔
534
    };
535
    const onShowDisclaimerChanged = () => {
16✔
536
        setShowDisclaimer(!showDisclaimer);
1✔
537
    };
538
    const onAdvancedOptionsChanged = () => {
16✔
539
        setShowAdvanced(!showAdvanced);
2✔
540
    };
541
    const onRotationChanged = (value: number) => {
16✔
542
        setRotation(value);
1✔
543
    };
544
    const onGeneratePlot = async () => {
16✔
545
        if (clientSide) {
1!
546
            const fitExtent = showAdvanced && normalizedBox
1!
547
                ? parseBoxToExtent(normalizedBox)
548
                : undefined;
549
            const rotDeg = showAdvanced ? rotation : undefined;
1!
550
            const scaleVal = showAdvanced ? parseFloat(scale) : (view?.scale ?? 5000);
1!
551
            // Hide the capture box layer during PDF generation so it
552
            // doesn't appear in the captured map image
553
            let activeCapturer: MapCapturerContext | undefined;
554
            if (showAdvanced && mapNames && activeMapName) {
1!
555
                activeCapturer = getActiveCapturer(viewer, mapNames, activeMapName);
×
556
                activeCapturer?.setVisible(false);
×
557
            }
558
            // Fetch legend image if Show Legend is checked and we have a MapGuide OL layer
559
            let legendDataUrl: string | undefined;
560
            if (showLegend && map && agentUri && agentKind === "mapagent") {
1!
561
                try {
×
562
                    const builder = new MapAgentRequestBuilder(agentUri, locale);
×
563
                    const dpiVal = parseInt(dpi, 10);
×
564
                    // Compute map image height in mm to match legend strip height
565
                    const mg = getMargin();
×
566
                    const tokens = paperSize.split(",");
×
567
                    const pH = orientation === "L" ? parseFloat(tokens[0]) : parseFloat(tokens[1]);
×
568
                    let fh = 0;
×
569
                    if (showScaleBar) fh += 12;
×
570
                    if (showDisclaimer) fh += 6;
×
571
                    const mapH = pH - mg.top - mg.buttom - fh;
×
572
                    const legendPxH = Math.round((mapH / 25.4) * dpiVal);
×
573
                    legendDataUrl = await builder.getMapLegendImage(
×
574
                        map.SessionId,
575
                        map.Name,
576
                        300,
577
                        legendPxH
578
                    );
579
                } catch (e) {
580
                    debug(`Failed to fetch legend image: ${e}`);
×
581
                    // legendDataUrl stays undefined → placeholder shown
582
                }
583
            }
584
            generateClientSidePdf(
1✔
585
                viewer,
586
                paperSize,
587
                orientation,
588
                dpi,
589
                title,
590
                subTitle,
591
                showNorthBar,
592
                showScaleBar,
593
                showDisclaimer,
594
                showCoordinates,
595
                showLegend,
596
                disclaimer,
597
                legendDataUrl,
598
                locale,
599
                map?.BackgroundColor,
600
                fitExtent,
601
                rotDeg,
602
                scaleVal
603
            ).finally(() => {
604
                // Restore capture box visibility after PDF generation
605
                activeCapturer?.setVisible(true);
×
606
            });
607
        }
608
    };
609
    const updateBoxCoords = (box: string, normalizedBox: string): void => {
16✔
610
        setBox(box);
×
611
        setNormalizedBox(normalizedBox);
×
612
    };
613
    //Side-effect that emulates the old componentWillUnmount lifecyle method to tear down
614
    //the active map capturer
615
    React.useEffect(() => {
16✔
616
        return () => {
5✔
617
            //Tear down all active capture box layers
618
            if (viewer.isReady() && mapNames) {
5✔
619
                for (const activeMapName of mapNames) {
4✔
620
                    const activeCapturer = getActiveCapturer(viewer, mapNames, activeMapName);
4✔
621
                    if (activeCapturer) {
4!
622
                        debug(`De-activating map capturer for: ${activeMapName}`);
4✔
623
                        activeCapturer.deactivate();
4✔
624
                    }
625
                }
626
            }
627
        };
628
    }, []);
629
    //Although the dep array arg of React.useEffect() has effectively rendered the need to
630
    //track previous values obsolete, we still need to track the previous advanced flag to
631
    //verify that the flag is amongst the actual values in the dep array that has changed
632
    const prevShowAdvanced = usePrevious(showAdvanced);
16✔
633
    //Side-effect that toggles/updates associated map capturers
634
    React.useEffect(() => {
16✔
635
        if (activeMapName && mapNames) {
16!
636
            if (showAdvanced != prevShowAdvanced) {
16✔
637
                toggleMapCapturerLayer(locale,
4✔
638
                    viewer,
639
                    mapNames,
640
                    activeMapName,
641
                    showAdvanced,
642
                    paperSize,
643
                    orientation,
644
                    scale,
645
                    rotation,
646
                    updateBoxCoords,
647
                    setViewRotationEnabledAction,
648
                    setViewRotationAction);
649
            }
650
            if (showAdvanced) {
16✔
651
                if (viewer.isReady()) {
4!
652
                    const capturer = getActiveCapturer(viewer, mapNames, activeMapName);
4✔
653
                    if (capturer) {
4!
654
                        const ppSize = getPrintSize(viewer, showAdvanced, paperSize, orientation);
4✔
655
                        debug(`Updating map capturer for: ${activeMapName}`);
4✔
656
                        capturer.updateBox(ppSize, parseFloat(scale), rotation);
4✔
657
                    }
658
                }
659
            }
660
        }
661
    }, [mapNames, activeMapName, showAdvanced, scale, paperSize, orientation, rotation, locale]);
662
    if (!viewer.isReady() || !map || !view) {
16✔
663
        return <noscript />;
1✔
664
    }
665
    let hasExternalBaseLayers = false;
15✔
666
    if (externalBaseLayers) {
15!
667
        hasExternalBaseLayers = externalBaseLayers.length > 0;
15✔
668
    }
669
    let normBox = normalizedBox;
15✔
670
    let theBox = box;
15✔
671
    if (!showAdvanced) {
15✔
672
        const extent = viewer.getCurrentExtent();
11✔
673
        theBox = `${extent[0]}, ${extent[1]}, ${extent[2]}, ${extent[1]}, ${extent[2]}, ${extent[3]}, ${extent[0]}, ${extent[3]}, ${extent[0]}, ${extent[1]}`;
11✔
674
        normBox = theBox;
11✔
675
    }
676
    let ppSize: string;
677
    let prSize: string;
678
    const tokens = paperSize.split(",");
15✔
679
    if (orientation === "L") {
15!
680
        prSize = `${tokens[1]},${tokens[0]}`;
×
681
        ppSize = `${prSize},${tokens[2]}`;
×
682
    } else { // P
683
        prSize = `${tokens[0]},${tokens[1]}`;
15✔
684
        ppSize = `${prSize},${tokens[2]}`;
15✔
685
    }
686
    const url = `${getFusionRoot()}/widgets/QuickPlot/PlotAsPDF.php`;
15✔
687
    const ORIENTATIONS = [
15✔
688
        { value: "P" as Orientation, label: xlate("QUICKPLOT_ORIENTATION_P", locale) },
689
        { value: "L" as Orientation, label: xlate("QUICKPLOT_ORIENTATION_L", locale) }
690
    ];
691
    return <div className="component-quick-plot">
15✔
692
        <form id="Form1" name="Form1" target={clientSide ? undefined : "_blank"} method={clientSide ? undefined : "post"} action={clientSide ? undefined : url} onSubmit={clientSide ? (e) => { e.preventDefault(); onGeneratePlot(); } : undefined}>
1✔
693
            {!clientSide && <input type="hidden" id="printId" name="printId" value={`${Math.random() * 1000}`} />}
29✔
694
            <div className="Title FixWidth">{xlate("QUICKPLOT_HEADER", locale)}</div>
695
            <FormGroup label={xlate("QUICKPLOT_TITLE", locale)}>
696
                <InputGroup name="{field:title}" id="title" value={title} onChange={onTitleChanged} />
697
            </FormGroup>
698
            <FormGroup label={xlate("QUICKPLOT_SUBTITLE", locale)}>
699
                <InputGroup name="{field:sub_title}" id="subtitle" value={subTitle} onChange={onSubTitleChanged} />
700
            </FormGroup>
701
            <FormGroup label={xlate("QUICKPLOT_PAPER_SIZE", locale)}>
702
                {/*
703
                    The pre-defined paper size list. The value for each "option" item is in this format: [width,height]. The unit is in millimeter.
704
                    We can change the html code to add more paper size or remove some ones.
705
                */}
706
                <TypedSelect<string, false> fill
707
                    id="paperSizeSelect"
708
                    name="paperSizeSelect"
709
                    value={paperSize}
710
                    onChange={e => setPaperSize(e)}
1✔
711
                    items={PAPER_SIZES} />
712
            </FormGroup>
713
            <FormGroup label={xlate("QUICKPLOT_ORIENTATION", locale)}>
714
                {/*
715
                    The pre-defined paper orientations
716
                */}
717
                <TypedSelect<Orientation, false>
718
                    fill
719
                    id="orientation"
720
                    name="orientation"
721
                    value={orientation}
UNCOV
722
                    onChange={e => setOrientation(e)}
×
723
                    items={ORIENTATIONS} />
724
            </FormGroup>
725
            {!clientSide && <>
29✔
726
                <input type="hidden" id="paperSize" name="paperSize" value={ppSize} />
727
                <input type="hidden" id="printSize" name="printSize" value={prSize} />
728
            </>}
729
            <fieldset>
730
                <legend>{xlate("QUICKPLOT_SHOWELEMENTS", locale)}</legend>
33✔
731
                {(!clientSide || !!map) && <div><Checkbox id="ShowLegendCheckBox" name="ShowLegend" checked={showLegend} onChange={onShowLegendChanged} label={xlate("QUICKPLOT_SHOWLEGEND", locale)} /></div>}
732
                <div><Checkbox id="ShowNorthArrowCheckBox" name="ShowNorthArrow" checked={showNorthBar} onChange={onShowNorthArrowChanged} label={xlate("QUICKPLOT_SHOWNORTHARROW", locale)} /></div>
733
                <div><Checkbox id="ShowCoordinatesCheckBox" name="ShowCoordinates" checked={showCoordinates} onChange={onShowCoordinatesChanged} label={xlate("QUICKPLOT_SHOWCOORDINTES", locale)} /></div>
734
                <div><Checkbox id="ShowScaleBarCheckBox" name="ShowScaleBar" checked={showScaleBar} onChange={onShowScaleBarChanged} label={xlate("QUICKPLOT_SHOWSCALEBAR", locale)} /></div>
735
                <div><Checkbox id="ShowDisclaimerCheckBox" name="ShowDisclaimer" checked={showDisclaimer} onChange={onShowDisclaimerChanged} label={xlate("QUICKPLOT_SHOWDISCLAIMER", locale)} /></div>
736
            </fieldset>
737
            <div className="HPlaceholder5px"></div>
738
            <div>
739
                <Checkbox id="AdvancedOptionsCheckBox" onChange={onAdvancedOptionsChanged} label={xlate("QUICKPLOT_ADVANCED_OPTIONS", locale)} />
740
            </div>
741
            {(() => {
742
                if (showAdvanced) {
15✔
743
                    return <div>
4✔
744
                        <FormGroup label={xlate("QUICKPLOT_SCALING", locale)}>
745
                            {/*
746
                                The pre-defined scales. The value for each "option" item is the scale denominator.
747
                                We can change the html code to extend the pre-defined scales
748
                            */}
749
                            <TypedSelect<string, false> fill
750
                                id="scaleDenominator"
751
                                name="scaleDenominator"
752
                                value={scale}
UNCOV
753
                                onChange={e => setScale(e)}
×
754
                                items={SCALES} />
755
                        </FormGroup>
756
                        <FormGroup label={xlate("QUICKPLOT_DPI", locale)}>
757
                            {/*
758
                                The pre-defined print DPI.
759
                                We can change the html code to extend the pre-defined values
760
                            */}
761
                            <TypedSelect<string, false> fill
762
                                id="dpi"
763
                                name="dpi"
764
                                value={dpi}
UNCOV
765
                                onChange={e => setDpi(e)}
×
766
                                items={DPIS} />
767
                        </FormGroup>
768
                        <FormGroup label={xlate("QUICKPLOT_BOX_ROTATION", locale)}>
769
                            <div style={{ paddingLeft: 16, paddingRight: 16 }}>
770
                                <Slider min={0} max={360} labelStepSize={90} stepSize={1} value={rotation} onChange={onRotationChanged} />
771
                            </div>
772
                        </FormGroup>
773
                    </div>;
774
                } else {
775
                    return <div>
11✔
776
                        <input type="hidden" id="scaleDenominator" name="scaleDenominator" value={`${view.scale}`} />
777
                        <input type="hidden" id="dpi" name="dpi" value={dpi} />
778
                    </div>;
779
                }
780
            })()}
781
            <div className="HPlaceholder5px"></div>
782
            {(() => {
783
                if (hasExternalBaseLayers) {
15✔
784
                    return <Callout variant="primary" icon="info-sign">
2✔
785
                        {xlate("QUICKPLOT_COMMERCIAL_LAYER_WARNING", locale)}
786
                    </Callout>;
787
                }
788
            })()}
789
            <div className="ButtonContainer FixWidth">
790
                <Button type={clientSide ? "button" : "submit"} variant="primary" icon="print" onClick={onGeneratePlot}>{xlate("QUICKPLOT_GENERATE", locale)}</Button>
15✔
791
            </div>
792
            {!clientSide && <>
29✔
793
                <input type="hidden" id="margin" name="margin" />
794
                <input type="hidden" id="normalizedBox" name="normalizedBox" value={normBox} />
795
                <input type="hidden" id="rotation" name="rotation" value={-(rotation || 0)} />
25✔
796
                <input type="hidden" id="sessionId" name="sessionId" value={map.SessionId} />
797
                <input type="hidden" id="mapName" name="mapName" value={map.Name} />
798
                <input type="hidden" id="box" name="box" value={theBox} />
799
                <input type="hidden" id="legalNotice" name="legalNotice" />
800
            </>}
801
        </form>
802
    </div>;
803
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc