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..9f67d72455b 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';
@@ -49,6 +49,8 @@ const StyledBox = styled(Box)(({theme}) => ({
},
}));
+const PK_COLUMN_NAMES = ['id', 'oid'];
+
function parseEwkbData(rows, column) {
let key = column.key;
const maxRenderByteLength = 20 * 1024 * 1024; //render geometry data up to 20MB
@@ -191,6 +193,33 @@ function parseData(rows, columns, column) {
};
}
+// Find primary key column from columns array
+function findPkColumn(columns) {
+ return columns.find(c => PK_COLUMN_NAMES.includes(c.name));
+}
+
+// Get unique row identifier using PK column or first column
+function getRowIdentifier(row, pkColumn, columns) {
+ if (pkColumn?.key && row[pkColumn.key] !== undefined) {
+ return row[pkColumn.key];
+ }
+ const firstKey = columns[0]?.key;
+ return firstKey && row[firstKey] !== undefined ? row[firstKey] : JSON.stringify(row);
+}
+
+// Create Set of row identifiers
+function createIdentifierSet(rowData, pkColumn, columns) {
+ return new Set(rowData.map(row => getRowIdentifier(row, pkColumn, columns)));
+}
+
+// Match rows from previous selection to current rows
+function matchRowSelection(prevRowData, currentRows, pkColumn, columns) {
+ if (prevRowData.length === 0) return [];
+
+ const prevIdSet = createIdentifierSet(prevRowData, pkColumn, columns);
+ return currentRows.filter(row => prevIdSet.has(getRowIdentifier(row, pkColumn, columns)));
+}
+
function PopupTable({data}) {
return (
@@ -294,6 +323,27 @@ function TheMap({data}) {
infoControl.current.onAdd = function () {
let ele = Leaflet.DomUtil.create('div', 'geometry-viewer-info-control');
ele.innerHTML = data.infoList.join('
');
+ // Style the parent control container after it's added to the map
+ setTimeout(() => {
+ let controlContainer = ele.closest('.leaflet-control');
+ if(controlContainer) {
+ controlContainer.style.cssText = `
+ position: fixed;
+ top: 70%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ margin: 0;
+ max-width: 80%;
+ text-align: center;
+ white-space: normal;
+ word-wrap: break-word;
+ background: none;
+ box-shadow: none;
+ border: none;
+ font-size: 16px;
+ `;
+ }
+ }, 0);
return ele;
};
if(data.infoList.length > 0) {
@@ -436,25 +486,111 @@ 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);
+ // Track previous column state AND selected row data
+ const prevStateRef = React.useRef({
+ columnKey: null,
+ columnNames: null,
+ selectedRowData: [],
+ });
+
+ const [mapKey, setMapKey] = React.useState(0);
+ const currentColumnKey = useMemo(() => column?.key, [column]);
+ const currentColumnNames = React.useMemo(
+ () => columns.map(c => c.key).sort().join(','),
+ [columns]
+ );
+
+ const pkColumn = useMemo(() => findPkColumn(columns), [columns]);
+
+ // Detect when to clear, filter, or re-render the map based on changes in geometry column, columns list, or rows
+ useEffect(() => {
+ const prevState = prevStateRef.current;
+
+ if (!currentColumnKey) {
+ setMapKey(prev => prev + 1);
+ prevStateRef.current = {
+ columnKey: null,
+ columnNames: null,
+ selectedRowData: [],
+ };
+ return;
+ }
+
+ if (currentColumnKey !== prevState.columnKey ||
+ currentColumnNames !== prevState.columnNames) {
+ setMapKey(prev => prev + 1);
+ prevStateRef.current = {
+ columnKey: currentColumnKey,
+ columnNames: currentColumnNames,
+ selectedRowData: rows,
+ };
+ return;
+ }
+
+ if (currentColumnKey === prevState.columnKey &&
+ currentColumnNames === prevState.columnNames &&
+ rows.length > 0) {
+ prevStateRef.current.selectedRowData = displayRows;
+ }
+ }, [currentColumnKey, currentColumnNames, rows, pkColumn, columns]);
+
+ // Get rows to display based on selection
+ const displayRows = React.useMemo(() => {
+ if (!currentColumnKey || rows.length === 0) return [];
+ const prevState = prevStateRef.current;
+ if (currentColumnKey !== prevState.columnKey || currentColumnNames !== prevState.columnNames) {
+ return rows;
+ }
+
+ const prevSelected = prevState.selectedRowData;
+ if (prevSelected.length === 0) return rows;
+ if (prevSelected.length < rows.length) {
+ const matched = matchRowSelection(prevSelected, rows, pkColumn, columns);
+ return matched.length > 0 ? matched : rows;
+ }
+ return rows;
+ }, [rows, currentColumnKey, currentColumnNames, pkColumn, columns]);
+
+ // Parse geometry data only when needed
+ 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(displayRows, columns, column);
+ }, [displayRows, 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 mapKey as key and recreate the map on change
return (
-
+
{
isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted));
}, [dataChangeStore]);
@@ -1460,30 +1468,86 @@ export function ResultSet() {
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, triggerAddRows);
}, [columns, selectedRows.size]);
+ const getFilteredRowsForGeometryViewer = React.useCallback((useLastGvSelection = false) => {
+ let selRowsData = rows;
+ if(selectedRows.size != 0) {
+ selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r)));
+ } 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)));
+ } else if(useLastGvSelection && lastGvSelectionRef.current.type === 'columns'
+ && lastGvSelectionRef.current.selectedColumns.size > 0) {
+ let selectedCols = _.filter(columns, (_c, i)=>lastGvSelectionRef.current.selectedColumns.has(i+1));
+ if(selectedCols.length > 0) {
+ selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key)));
+ }
+ } else if(selectedRange.current) {
+ let [,, startRowIdx, endRowIdx] = getRangeIndexes();
+ selRowsData = rows.slice(startRowIdx, endRowIdx+1);
+ } else if(selectedCell.current?.[0]) {
+ selRowsData = [selectedCell.current[0]];
+ }
+ return selRowsData;
+ }, [rows, columns, selectedRows, selectedColumns]);
+
+ 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
useEffect(()=>{
const renderGeometries = (column)=>{
- let selRowsData = rows;
- if(selectedRows.size != 0) {
- selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r)));
+ gvClearedForColumnsRef.current = null;
+ if(selectedRows.size > 0) {
+ lastGvSelectionRef.current = { type: 'rows', selectedColumns: new Set() };
} 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)));
- } else if(selectedRange.current) {
- let [,, startRowIdx, endRowIdx] = getRangeIndexes();
- selRowsData = rows.slice(startRowIdx, endRowIdx+1);
- } else if(selectedCell.current?.[0]) {
- selRowsData = [selectedCell.current[0]];
+ lastGvSelectionRef.current = { type: 'columns', selectedColumns: new Set(selectedColumns) };
+ } else {
+ lastGvSelectionRef.current = { type: 'all', selectedColumns: new Set() };
}
- layoutDocker.openTab({
- id: PANELS.GEOMETRY,
- title:gettext('Geometry Viewer'),
- content: ,
- closable: true,
- }, PANELS.MESSAGES, 'after-tab', true);
+ const selRowsData = getFilteredRowsForGeometryViewer();
+ 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]);
+ }, [getFilteredRowsForGeometryViewer, openGeometryViewerTab, eventBus, selectedRows, selectedColumns]);
+
+ // Auto-update Geometry Viewer when rows/columns change
+ useEffect(()=>{
+ const rowsChanged = prevRowsRef.current !== rows;
+ const columnsChanged = prevColumnsRef.current !== columns;
+ const currentGeometryColumn = columns.find(col => col.cell === 'geometry' || col.cell === 'geography');
+
+ if((rowsChanged || columnsChanged) && layoutDocker.isTabOpen(PANELS.GEOMETRY)) {
+
+ const prevColumnNames = prevColumnsRef.current?.map(c => c.key).sort().join(',') ?? '';
+ const currColumnNames = columns.map(c => c.key).sort().join(',');
+ const columnsChanged = prevColumnNames !== currColumnNames;
+
+ if(columnsChanged && currentGeometryColumn) {
+ gvClearedForColumnsRef.current = currColumnNames;
+ lastGvSelectionRef.current = { type: 'all', selectedColumns: new Set() };
+ openGeometryViewerTab(null, []);
+ } else if(gvClearedForColumnsRef.current === currColumnNames) {
+ openGeometryViewerTab(null, []);
+ } else if(currentGeometryColumn && rowsChanged) {
+ const useColSelection = lastGvSelectionRef.current.type === 'columns';
+ const selRowsData = getFilteredRowsForGeometryViewer(useColSelection);
+ openGeometryViewerTab(currentGeometryColumn, selRowsData);
+ } else {
+ // No geometry column
+ openGeometryViewerTab(null, []);
+ }
+ }
+
+ prevRowsRef.current = rows;
+ prevColumnsRef.current = columns;
+ }, [rows, columns, getFilteredRowsForGeometryViewer, layoutDocker]);
const triggerResetScroll = () => {
// Reset the scroll position to previously saved location.