Ensure that the Geometry Viewer refreshes when re-running queries or switching geometry columns, preventing stale data from being displayed. #9392
parent
00a44a5d76
commit
395ff36322
|
|
@ -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('<br />');
|
||||
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 && (
|
||||
<EmptyPanelMessage text={data.infoList.join(' ')} style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 1000,
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
)}
|
||||
{data.selectedSRID === 4326 &&
|
||||
<LayersControl position="topright">
|
||||
<LayersControl.BaseLayer checked name={gettext('Empty')}>
|
||||
|
|
@ -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 (
|
||||
<StyledBox ref={contentRef} width="100%" height="100%" key={data.selectedSRID}>
|
||||
<StyledBox ref={contentRef} width="100%" height="100%" key={`${data.selectedSRID}-${currentColumnKey || 'none'}`}>
|
||||
<MapContainer
|
||||
crs={data.selectedSRID === 4326 ? CRS.EPSG3857 : CRS.Simple}
|
||||
zoom={2} center={[20, 100]}
|
||||
|
|
|
|||
|
|
@ -876,6 +876,16 @@ export function ResultSet() {
|
|||
rsu.current.setLoaderText = setLoaderText;
|
||||
|
||||
const isDataChangedRef = useRef(false);
|
||||
const lastGvSelectionRef = useRef({
|
||||
type: 'all', // 'all' | 'rows' | 'columns' | 'range' | 'cell'
|
||||
geometryColumnKey: null,
|
||||
rowIndices: [],
|
||||
columnIndices: new Set(),
|
||||
rangeStartIdx: null,
|
||||
rangeEndIdx: null,
|
||||
cellIdx: null,
|
||||
});
|
||||
|
||||
useEffect(()=>{
|
||||
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: <GeometryViewer rows={rowsData} columns={columns} column={column}/>,
|
||||
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: <GeometryViewer rows={selRowsData} columns={columns} column={column} />,
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue