Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"proj4": "2.19.3",
"proj4-list": "1.0.4",
"react": "^18.0.1",
"react-data-grid": "^7.0.0-beta.57",
"react-day-picker": "^9.7.0",
"shpjs": "^6.1.0",
"styled-components": "^5.3.6",
Expand Down
1 change: 1 addition & 0 deletions packages/base/src/commands/BaseCommandIDs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ export const selectCompleter = 'jupytergis:selectConsoleCompleter';
export const addAnnotation = 'jupytergis:addAnnotation';
export const zoomToLayer = 'jupytergis:zoomToLayer';
export const downloadGeoJSON = 'jupytergis:downloadGeoJSON';
export const openAttributeTable = 'jupytergis:openAttributeTable';
47 changes: 47 additions & 0 deletions packages/base/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { fromLonLat } from 'ol/proj';

import { CommandIDs, icons } from '../constants';
import { ProcessingFormDialog } from '../dialogs/ProcessingFormDialog';
import { AttributeTableWidget } from '../dialogs/attributeTable';
import { LayerBrowserWidget } from '../dialogs/layerBrowserDialog';
import { LayerCreationFormDialog } from '../dialogs/layerCreationFormDialog';
import { SymbologyWidget } from '../dialogs/symbology/symbologyDialog';
Expand Down Expand Up @@ -843,6 +844,37 @@ export function addCommands(
icon: targetWithCenterIcon,
});

commands.addCommand(CommandIDs.openAttributeTable, {
label: trans.__('Open Attribute Table'),
isEnabled: () => {
const selectedLayer = getSingleSelectedLayer(tracker);
return selectedLayer
? ['VectorLayer', 'VectorTileLayer', 'ShapefileLayer'].includes(
selectedLayer.type,
)
: false;
},
execute: async () => {
const currentWidget = tracker.currentWidget;
if (!currentWidget) {
return;
}

const model = currentWidget.model;
const selectedLayers = model.localState?.selected?.value;

if (!selectedLayers) {
console.warn('No layer selected');
return;
}

const layerId = Object.keys(selectedLayers)[0];

console.log('Open attribute table for layer:', layerId);
Private.createAttributeTableDialog(tracker, layerId)();
},
});

loadKeybindings(commands, keybindings);
}

Expand Down Expand Up @@ -887,6 +919,21 @@ namespace Private {
};
}

export function createAttributeTableDialog(
tracker: JupyterGISTracker,
layerId: string,
) {
return async () => {
const current = tracker.currentWidget;
if (!current) {
return;
}

const dialog = new AttributeTableWidget(current.model, layerId);
await dialog.launch();
};
}

export function createEntry({
tracker,
formSchemaRegistry,
Expand Down
82 changes: 82 additions & 0 deletions packages/base/src/dialogs/attributeTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { IJupyterGISModel } from '@jupytergis/schema';
import { Dialog } from '@jupyterlab/apputils';
import React from 'react';
import DataGrid from 'react-data-grid';

import { useGetFeatures } from './symbology/hooks/useGetFeatures';

export interface IAttributeTableProps {
model: IJupyterGISModel;
layerId: string;
}

const AttributeTable: React.FC<IAttributeTableProps> = ({ model, layerId }) => {
const [columns, setColumns] = React.useState<any[]>([]);
const [rows, setRows] = React.useState<any[]>([]);

const { features, isLoading, error } = useGetFeatures({ layerId, model });

React.useEffect(() => {
if (isLoading) {
return;
}
if (error) {
console.error('[AttributeTable] Error loading features:', error);
return;
}
if (!features.length) {
console.warn('[AttributeTable] No features found.');
setColumns([]);
setRows([]);
return;
}

const sampleProps = features[0]?.properties ?? {};
const cols = [
{ key: 'sno', name: 'S. No.', resizable: true, sortable: true },
...Object.keys(sampleProps).map(key => ({
key,
name: key,
resizable: true,
sortable: true
}))
];

const rowData = features.map((f, i) => ({
sno: i + 1,
...f.properties
}));

setColumns(cols);
setRows(rowData);
}, [features, isLoading, error]);

return (
<div style={{ height: '100%', width: '100%', overflow: 'auto' }}>
<DataGrid
columns={columns}
rows={rows}
className="rdg-light"
style={{ minHeight: '100%' }}
/>
</div>
);
};

export class AttributeTableWidget extends Dialog<void> {
constructor(model: IJupyterGISModel, layerId: string) {
const body = (
<div style={{ minWidth: '70vw', maxHeight: '80vh' }}>
<AttributeTable model={model} layerId={layerId} />
</div>
);

super({
title: 'Attribute Table',
body
});

this.id = 'jupytergis::attributeTable';
this.addClass('jp-gis-attribute-table-dialog');
}
}
61 changes: 61 additions & 0 deletions packages/base/src/dialogs/symbology/hooks/useGetFeatures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { GeoJSONFeature1, IJupyterGISModel } from '@jupytergis/schema';
import { useEffect, useState } from 'react';

import { loadFile } from '@/src/tools';

interface IUseGetFeaturesProps {
layerId?: string;
model: IJupyterGISModel;
}

interface IUseGetFeaturesResult {
features: GeoJSONFeature1[];
isLoading: boolean;
error?: Error;
}

export const useGetFeatures = ({
layerId,
model,
}: IUseGetFeaturesProps): IUseGetFeaturesResult => {
const [features, setFeatures] = useState<GeoJSONFeature1[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | undefined>();

const fetchFeatures = async () => {
if (!layerId) {
return;
}

try {
const layer = model.getLayer(layerId);
const source = model.getSource(layer?.parameters?.source);

if (!source) {
throw new Error('Source not found');
}

const data = await loadFile({
filepath: source.parameters?.path,
type: 'GeoJSONSource',
model: model,
});

if (!data) {
throw new Error('Failed to read GeoJSON data');
}

setFeatures(data.features || []);
} catch (err) {
setError(err as Error);
} finally {
setIsLoading(false);
}
};

useEffect(() => {
fetchFeatures();
}, [model, layerId]);

return { features, isLoading, error };
};
6 changes: 6 additions & 0 deletions python/jupytergis_lab/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ const plugin: JupyterFrontEndPlugin<void> = {
rank: 2,
});

app.contextMenu.addItem({
command: CommandIDs.openAttributeTable,
selector: '.jp-gis-layerItem',
rank: 2,
});

// Create the Download submenu
const downloadSubmenu = new Menu({ commands: app.commands });
downloadSubmenu.title.label = translator.load('jupyterlab').__('Download');
Expand Down
20 changes: 20 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,7 @@ __metadata:
proj4: 2.19.3
proj4-list: 1.0.4
react: ^18.0.1
react-data-grid: ^7.0.0-beta.57
react-day-picker: ^9.7.0
rimraf: ^3.0.2
shpjs: ^6.1.0
Expand Down Expand Up @@ -5015,6 +5016,13 @@ __metadata:
languageName: node
linkType: hard

"clsx@npm:^1.1.1":
version: 1.2.1
resolution: "clsx@npm:1.2.1"
checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12
languageName: node
linkType: hard

"clsx@npm:^2.1.1":
version: 2.1.1
resolution: "clsx@npm:2.1.1"
Expand Down Expand Up @@ -10672,6 +10680,18 @@ __metadata:
languageName: node
linkType: hard

"react-data-grid@npm:^7.0.0-beta.57":
version: 7.0.0-canary.49
resolution: "react-data-grid@npm:7.0.0-canary.49"
dependencies:
clsx: ^1.1.1
peerDependencies:
react: ^16.14 || ^17.0
react-dom: ^16.14 || ^17.0
checksum: fe57d441a5e56dac39a0f7e06979a27d5606515653ead31fc06f9c2fe08ebaf88ff4ab70f1565dd356b343cf4612137dea2d188a40a7e246b85dd5aa2589da04
languageName: node
linkType: hard

"react-day-picker@npm:^9.7.0":
version: 9.8.1
resolution: "react-day-picker@npm:9.8.1"
Expand Down
Loading