Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -436,25 +465,108 @@ 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) {
return {
'geoJSONs': [],
'selectedSRID': 0,
'getPopupContent': undefined,
'infoList': [gettext('Select a geometry/geography column to visualize.')],
};
}
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 (
<StyledBox ref={contentRef} width="100%" height="100%" key={data.selectedSRID}>
<StyledBox ref={contentRef} width="100%" height="100%" key={`${data.selectedSRID}-${mapKey}`}>
<MapContainer
crs={data.selectedSRID === 4326 ? CRS.EPSG3857 : CRS.Simple}
zoom={2} center={[20, 100]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,9 @@ export function ResultSet() {
rsu.current.setLoaderText = setLoaderText;

const isDataChangedRef = useRef(false);
const prevRowsRef = React.useRef(null);
const prevColumnsRef = React.useRef(null);

useEffect(()=>{
isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted));
}, [dataChangeStore]);
Expand Down Expand Up @@ -1460,30 +1463,61 @@ export function ResultSet() {
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, triggerAddRows);
}, [columns, selectedRows.size]);

const getFilteredRowsForGeometryViewer = React.useCallback(() => {
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(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: <GeometryViewer rows={rowsData} columns={columns} column={column} />,
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)));
} 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]];
}
layoutDocker.openTab({
id: PANELS.GEOMETRY,
title:gettext('Geometry Viewer'),
content: <GeometryViewer rows={selRowsData} columns={columns} column={column} />,
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]);

// 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)) {

if(currentGeometryColumn) {
const selRowsData = getFilteredRowsForGeometryViewer();
openGeometryViewerTab(currentGeometryColumn, selRowsData);
} else {
// No geometry column
openGeometryViewerTab(null, []);
}
}

prevRowsRef.current = rows;
prevColumnsRef.current = columns;
}, [rows, columns, getFilteredRowsForGeometryViewer, openGeometryViewerTab, layoutDocker]);

const triggerResetScroll = () => {
// Reset the scroll position to previously saved location.
Expand Down
Loading