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.