diff --git a/package.json b/package.json index 2ab2f3e7..75fb51cc 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "dompurify": "^3.4.0", "geojson-vt": "^4.0.2", "immutability-helper": "^3.1.1", + "jspdf": "^4.2.1", "lodash.debounce": "^4.0.8", "lodash.xor": "^4.5.0", "lodash.xorby": "^4.7.0", diff --git a/src/api/mapguide-commands.ts b/src/api/mapguide-commands.ts index 70dd92cb..e5b52042 100644 --- a/src/api/mapguide-commands.ts +++ b/src/api/mapguide-commands.ts @@ -30,7 +30,8 @@ export function initMapGuideCommands() { enabled: state => !state.stateless, invoke: (dispatch, getState, _viewer, parameters) => { const config = getState().config; - const url = "component://QuickPlot"; + const isClientSide = parameters?.ClientSide === "true"; + const url = isClientSide ? "component://QuickPlot?clientSide=true" : "component://QuickPlot"; const cmdDef = buildTargetedCommand(config, parameters); openUrlInTarget(DefaultCommands.QuickPlot, cmdDef, config.capabilities.hasTaskPane, dispatch, url); } diff --git a/src/containers/quick-plot.tsx b/src/containers/quick-plot.tsx index 74b2f2fe..e2dc63a9 100644 --- a/src/containers/quick-plot.tsx +++ b/src/containers/quick-plot.tsx @@ -156,7 +156,14 @@ function toggleMapCapturerLayer(locale: string, } export interface IQuickPlotContainerOwnProps { - + /** + * When set to "true", the QuickPlot component operates in fully client-side mode + * and generates the PDF locally without requiring a MapGuide Server connection. + * This value is read from the widget's Extension.ClientSide property in the appdef. + * + * @since 0.15 + */ + clientSide?: string; } export interface IQuickPlotContainerConnectedState { @@ -195,9 +202,10 @@ export interface IQuickPlotContainerState { normalizedBox: string; } -export const QuickPlotContainer = () => { +export const QuickPlotContainer = (props: IQuickPlotContainerOwnProps) => { + const isClientSide = props.clientSide === "true"; const { Slider, Callout, Button, Select, FormGroup, InputGroup, Checkbox } = useElementContext(); - const [title, setTitle] = React.useState(""); `` + const [title, setTitle] = React.useState(""); const [subTitle, setSubTitle] = React.useState(""); const [showLegend, setShowLegend] = React.useState(false); const [showNorthBar, setShowNorthBar] = React.useState(false); @@ -212,6 +220,7 @@ export const QuickPlotContainer = () => { const [rotation, setRotation] = React.useState(0); const [box, setBox] = React.useState(""); const [normalizedBox, setNormalizedBox] = React.useState(""); + const [isGenerating, setIsGenerating] = React.useState(false); const viewer = useMapProviderContext(); const locale = useViewerLocale(); @@ -251,7 +260,50 @@ export const QuickPlotContainer = () => { const onRotationChanged = (value: number) => { setRotation(value); }; - const onGeneratePlot = () => { }; + const onGeneratePlot = (e: React.MouseEvent) => { + if (!isClientSide) return; + e.preventDefault(); + const tokens = paperSize.split(","); + const baseW = parseFloat(tokens[0]); + const baseH = parseFloat(tokens[1]); + setIsGenerating(true); + viewer.exportImage({ + callback: async (imageData) => { + try { + const { jsPDF } = await import("jspdf"); + const doc = new jsPDF({ + orientation: orientation === "P" ? "p" : "l", + unit: "mm", + format: [baseW, baseH] + }); + const pageW = doc.internal.pageSize.getWidth(); + const pageH = doc.internal.pageSize.getHeight(); + const margins = getMargin(); + let yPos = margins.top / 2; + if (title) { + doc.setFontSize(16); + doc.text(title, pageW / 2, yPos + 8, { align: "center" }); + yPos += 14; + } + if (subTitle) { + doc.setFontSize(12); + doc.text(subTitle, pageW / 2, yPos + 4, { align: "center" }); + yPos += 10; + } + const mapLeft = margins.left; + const mapTop = yPos; + const mapWidth = pageW - margins.left - margins.right; + const mapHeight = pageH - mapTop - margins.buttom; + doc.addImage(imageData, "PNG", mapLeft, mapTop, mapWidth, mapHeight); + doc.save("quickplot.pdf"); + } catch (err) { + console.error("QuickPlot client-side PDF generation failed:", err); + } finally { + setIsGenerating(false); + } + } + }); + }; const updateBoxCoords = (box: string, normalizedBox: string): void => { setBox(box); setNormalizedBox(normalizedBox); @@ -305,7 +357,10 @@ export const QuickPlotContainer = () => { } } }, [mapNames, activeMapName, showAdvanced, scale, paperSize, orientation, rotation, locale]); - if (!viewer.isReady() || !map || !view) { + if (!viewer.isReady() || !view) { + return