diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 8842d6d287c..6476aaa47a4 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -6,7 +6,7 @@ // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useMemo } from 'react'; import { styled } from '@mui/material/styles'; import ReactDOMServer from 'react-dom/server'; import _ from 'lodash'; @@ -18,10 +18,12 @@ import gettext from 'sources/gettext'; import Theme from 'sources/Theme'; import PropTypes from 'prop-types'; import { Box } from '@mui/material'; +import EmptyPanelMessage from '../../../../../../static/js/components/EmptyPanelMessage'; import { PANELS } from '../QueryToolConstants'; import { QueryToolContext } from '../QueryToolComponent'; const StyledBox = styled(Box)(({theme}) => ({ + position: 'relative', '& .GeometryViewer-mapContainer': { backgroundColor: theme.palette.background.default, height: '100%', @@ -191,6 +193,7 @@ function parseData(rows, columns, column) { }; } + function PopupTable({data}) { return ( @@ -285,20 +288,10 @@ GeoJsonLayer.propTypes = { function TheMap({data}) { const mapObj = useMap(); - const infoControl = useRef(null); const resetLayersKey = useRef(0); const zoomControlWithHome = useRef(null); const homeCoordinates = useRef(null); useEffect(()=>{ - infoControl.current = Leaflet.control({position: 'topright'}); - infoControl.current.onAdd = function () { - let ele = Leaflet.DomUtil.create('div', 'geometry-viewer-info-control'); - ele.innerHTML = data.infoList.join('
'); - return ele; - }; - if(data.infoList.length > 0) { - infoControl.current.addTo(mapObj); - } resetLayersKey.current++; zoomControlWithHome.current = Leaflet.control.zoom({ @@ -348,7 +341,6 @@ function TheMap({data}) { zoomControlWithHome.current.addTo(mapObj); return ()=>{ - infoControl.current?.remove(); zoomControlWithHome.current?.remove(); }; }, [data]); @@ -359,6 +351,17 @@ function TheMap({data}) { return ( <> + {data.infoList.length > 0 && ( + + )} {data.selectedSRID === 4326 && @@ -436,25 +439,47 @@ export function GeometryViewer({rows, columns, column}) { const mapRef = React.useRef(); const contentRef = React.useRef(); - const data = parseData(rows, columns, column); const queryToolCtx = React.useContext(QueryToolContext); + const currentColumnKey = useMemo(() => column?.key, [column]); + + const data = React.useMemo(() => { + if (!currentColumnKey) { + const hasGeometryColumn = columns.some(c => c.cell === 'geometry' || c.cell === 'geography'); + return { + 'geoJSONs': [], + 'selectedSRID': 0, + 'getPopupContent': undefined, + 'infoList': hasGeometryColumn + ? [gettext('Query complete. Use the Geometry Viewer button in the Data Output tab to visualize results.')] + : [gettext('No spatial data found. At least one geometry or geography column is required for visualization.')], + }; + } + return parseData(rows, columns, column); + }, [rows, columns, column, currentColumnKey]); + useEffect(()=>{ let timeoutId; const contentResizeObserver = new ResizeObserver(()=>{ clearTimeout(timeoutId); - if(queryToolCtx.docker.isTabVisible(PANELS.GEOMETRY)) { + if(queryToolCtx?.docker?.isTabVisible(PANELS.GEOMETRY)) { timeoutId = setTimeout(function () { mapRef.current?.invalidateSize(); }, 100); } }); - contentResizeObserver.observe(contentRef.current); - }, []); + if(contentRef.current) { + contentResizeObserver.observe(contentRef.current); + } + return () => { + clearTimeout(timeoutId); + contentResizeObserver.disconnect(); + }; + }, [queryToolCtx]); - // Dyanmic CRS is not supported. Use srid as key and recreate the map on change + // Dynamic CRS is not supported. Use srid and column key as key and recreate the map on change return ( - + { isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted)); }, [dataChangeStore]); @@ -1460,30 +1470,103 @@ export function ResultSet() { return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, triggerAddRows); }, [columns, selectedRows.size]); + const openGeometryViewerTab = React.useCallback((column, rowsData) => { + layoutDocker.openTab({ + id: PANELS.GEOMETRY, + title: gettext('Geometry Viewer'), + content: , + closable: true, + }, PANELS.MESSAGES, 'after-tab', true); + }, [layoutDocker, columns]); + + // Handle manual Geometry Viewer opening. + // Determines which rows to plot based on the current grid selection (rows, columns, + // range, or cell) and stores the selection indices in lastGvSelectionRef so the + // auto-update effect can re-apply the same selection on subsequent query re-runs. useEffect(()=>{ const renderGeometries = (column)=>{ + const defaultSel = { geometryColumnKey: column?.key, rowIndices: [], columnIndices: new Set(), rangeStartIdx: null, rangeEndIdx: null, cellIdx: null }; let selRowsData = rows; - if(selectedRows.size != 0) { - selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r))); + + if(selectedRows.size > 0) { + // Specific rows selected in the grid — plot only those rows + const rowIndices = []; + rows.forEach((r, i) => { + if(selectedRows.has(rowKeyGetter(r))) { + rowIndices.push(i); + } + }); + selRowsData = rowIndices.map(i => rows[i]); + lastGvSelectionRef.current = { ...defaultSel, type: 'rows', rowIndices }; } else if(selectedColumns.size > 0) { - let selectedCols = _.filter(columns, (_c, i)=>selectedColumns.has(i+1)); - selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key))); + // Specific columns selected — plot all rows but only with selected column data + let selectedCols = _.filter(columns, (_c, i) => selectedColumns.has(i + 1)); + selRowsData = _.map(rows, (r) => _.pick(r, _.map(selectedCols, (c) => c.key))); + lastGvSelectionRef.current = { ...defaultSel, type: 'columns', columnIndices: new Set(selectedColumns) }; } else if(selectedRange.current) { + // Cell range selected — plot the rows within the range let [,, startRowIdx, endRowIdx] = getRangeIndexes(); - selRowsData = rows.slice(startRowIdx, endRowIdx+1); + selRowsData = rows.slice(startRowIdx, endRowIdx + 1); + lastGvSelectionRef.current = { ...defaultSel, type: 'range', rangeStartIdx: startRowIdx, rangeEndIdx: endRowIdx }; } else if(selectedCell.current?.[0]) { + // Single cell selected — plot only that row + const cellIdx = rows.indexOf(selectedCell.current[0]); selRowsData = [selectedCell.current[0]]; + lastGvSelectionRef.current = { ...defaultSel, type: 'cell', cellIdx: cellIdx >= 0 ? cellIdx : null }; + } else { + // No selection — plot all rows + lastGvSelectionRef.current = { ...defaultSel, type: 'all' }; } - layoutDocker.openTab({ - id: PANELS.GEOMETRY, - title:gettext('Geometry Viewer'), - content: , - closable: true, - }, PANELS.MESSAGES, 'after-tab', true); + + openGeometryViewerTab(column, selRowsData); }; eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); - }, [rows, columns, selectedRows.size, selectedColumns.size]); + }, [openGeometryViewerTab, eventBus, rows, columns, selectedRows, selectedColumns]); + + // Auto-update Geometry Viewer when rows/columns change + useEffect(()=>{ + if(layoutDocker.isTabOpen(PANELS.GEOMETRY)) { + const lastGeomKey = lastGvSelectionRef.current.geometryColumnKey; + const matchedGeomCol = lastGeomKey + ? columns.find(c => c.key === lastGeomKey && (c.cell === 'geometry' || c.cell === 'geography')) + : null; + + if(matchedGeomCol) { + // Previously plotted geometry column still exists → re-apply selection and re-render + const lastSel = lastGvSelectionRef.current; + let selRowsData = rows; + + // Re-apply row selection — plot only previously selected rows if indices are still valid + if(lastSel.type === 'rows' && lastSel.rowIndices.length > 0) { + if(lastSel.rowIndices.every(idx => idx < rows.length)) { + selRowsData = lastSel.rowIndices.map(idx => rows[idx]); + } + // Re-apply column selection — filter each row to only the previously selected columns + } else if(lastSel.type === 'columns' && lastSel.columnIndices.size > 0) { + let selectedCols = _.filter(columns, (_c, i) => lastSel.columnIndices.has(i + 1)); + if(selectedCols.length > 0) { + selRowsData = _.map(rows, (r) => _.pick(r, _.map(selectedCols, (c) => c.key))); + } + // Re-apply range selection — plot the previously selected row range if bounds are still valid + } else if(lastSel.type === 'range' && lastSel.rangeStartIdx != null) { + if(lastSel.rangeStartIdx < rows.length && lastSel.rangeEndIdx < rows.length) { + selRowsData = rows.slice(lastSel.rangeStartIdx, lastSel.rangeEndIdx + 1); + } + // Re-apply single cell selection — plot the row of the previously selected cell if still valid + } else if(lastSel.type === 'cell' && lastSel.cellIdx != null) { + if(lastSel.cellIdx < rows.length) { + selRowsData = [rows[lastSel.cellIdx]]; + } + } + // If any validation fails above, selRowsData remains as all rows (default) + openGeometryViewerTab(matchedGeomCol, selRowsData); + } else { + // Previously plotted geometry column not found - clear GV + openGeometryViewerTab(null, []); + } + } + }, [rows, columns, layoutDocker]); const triggerResetScroll = () => { // Reset the scroll position to previously saved location.