{layersTree.map(layer =>
typeof layer === 'string' ? (
-
+
) : (
@@ -155,7 +150,8 @@ function LayersBodyComponent(props: IBodyProps): JSX.Element {
/**
* Properties of the layers group component.
*/
-interface ILayersGroupProps extends LayersPanel.IOptions {
+interface ILayersGroupProps {
+ gisModel: IJupyterGISModel | undefined;
group: IJGISLayersGroup | undefined;
onClick: (item?: string) => void;
}
@@ -164,7 +160,7 @@ interface ILayersGroupProps extends LayersPanel.IOptions {
* The component to handle group of layers.
*/
function LayersGroupComponent(props: ILayersGroupProps): JSX.Element {
- const { group, model } = props;
+ const { group, gisModel } = props;
if (group === undefined) {
return <>>;
}
@@ -189,13 +185,13 @@ function LayersGroupComponent(props: ILayersGroupProps): JSX.Element {
{layers.map(layer =>
typeof layer === 'string' ? (
) : (
@@ -210,22 +206,32 @@ function LayersGroupComponent(props: ILayersGroupProps): JSX.Element {
/**
* Properties of the layer component.
*/
-interface ILayerProps extends LayersPanel.IOptions {
+interface ILayerProps {
+ gisModel: IJupyterGISModel | undefined;
layerId: string;
onClick: (item?: string) => void;
}
+function isSelected(layerId: string, model: IJupyterGISModel | undefined) {
+ return (
+ (model?.localState?.selected?.value &&
+ Object.keys(model?.localState?.selected?.value).includes(layerId)) ||
+ false
+ );
+}
+
/**
* The component to display a single layer.
*/
function LayerComponent(props: ILayerProps): JSX.Element {
- const { layerId, model } = props;
- const layer = model?.getLayer(layerId);
+ const { layerId, gisModel } = props;
+ const layer = gisModel?.getLayer(layerId);
if (layer === undefined) {
return <>>;
}
const [selected, setSelected] = useState
(
- model?.currentLayer === layerId
+ // TODO Support multi-selection as `model?.jGISModel?.localState?.selected.value` does
+ isSelected(layerId, gisModel)
);
const name = layer.name;
@@ -233,22 +239,26 @@ function LayerComponent(props: ILayerProps): JSX.Element {
* Listen to the changes on the current layer.
*/
useEffect(() => {
- const isSelected = () => {
- setSelected(model?.currentLayer === layerId);
+ const onClientSharedStateChanged = (
+ sender: IJupyterGISModel,
+ clients: Map
+ ) => {
+ // TODO Support follow mode and remoteUser state
+ setSelected(isSelected(layerId, gisModel));
};
- model?.currentLayerChanged.connect(isSelected);
+ gisModel?.clientStateChanged.connect(onClientSharedStateChanged);
return () => {
- model?.currentLayerChanged.disconnect(isSelected);
+ gisModel?.clientStateChanged.disconnect(onClientSharedStateChanged);
};
- }, [model]);
+ }, [gisModel]);
/**
* Toggle layer visibility.
*/
const toggleVisibility = () => {
layer.visible = !layer.visible;
- model?.sharedModel.updateLayer(layerId, layer);
+ gisModel?.sharedModel?.updateLayer(layerId, layer);
};
return (
diff --git a/packages/base/src/panelview/leftpanel.tsx b/packages/base/src/panelview/leftpanel.tsx
index 1958af714..891e8c923 100644
--- a/packages/base/src/panelview/leftpanel.tsx
+++ b/packages/base/src/panelview/leftpanel.tsx
@@ -16,13 +16,12 @@ export class LeftPanelWidget extends SidePanel {
// const datasources = new DataSourceList({ controlPanelModel: this._model });
// this.addWidget(datasources);
- const layersTree = new LayersPanel({ model: this._model.jGISModel });
+ const layersTree = new LayersPanel({ model: this._model });
layersTree.title.caption = 'Layer tree';
layersTree.title.label = 'Layers';
this.addWidget(layersTree);
options.tracker.currentChanged.connect((_, changed) => {
- layersTree.model = this._model.jGISModel;
if (changed) {
header.title.label = changed.context.localPath;
} else {
diff --git a/packages/base/src/panelview/objectproperties.tsx b/packages/base/src/panelview/objectproperties.tsx
index 18d52512b..74a6cbfdb 100644
--- a/packages/base/src/panelview/objectproperties.tsx
+++ b/packages/base/src/panelview/objectproperties.tsx
@@ -1,7 +1,9 @@
import {
IDict,
IJGISFormSchemaRegistry,
+ IJGISLayer,
IJGISLayerDocChange,
+ IJGISSource,
IJupyterGISClientState,
IJupyterGISDoc,
IJupyterGISModel,
@@ -38,8 +40,12 @@ interface IStates {
jGISOption?: IDict;
filePath?: string;
selectedObjectData?: IDict;
+ selectedObjectSourceData?: IDict;
selectedObject?: string;
+ selectedObjectType?: 'layer' | 'source';
+ selectedObjectSource?: string;
schema?: IDict;
+ sourceSchema?: IDict;
clientId: number | null; // ID of the yjs client
id: string; // ID of the component, it is used to identify which component
//is the source of awareness updates.
@@ -59,6 +65,7 @@ class ObjectPropertiesReact extends React.Component {
clientId: null,
id: uuid()
};
+ this._formSchema = props.formSchemaRegistry.getSchemas();
this.props.cpModel.jGISModel?.sharedLayersChanged.connect(
this._sharedJGISModelChanged
@@ -84,8 +91,11 @@ class ObjectPropertiesReact extends React.Component {
jGISOption: undefined,
filePath: undefined,
selectedObjectData: undefined,
+ selectedObjectSourceData: undefined,
selectedObject: undefined,
- schema: undefined
+ selectedObjectSource: undefined,
+ schema: undefined,
+ sourceSchema: undefined
});
}
});
@@ -136,19 +146,7 @@ class ObjectPropertiesReact extends React.Component {
): void => {
this.setState(old => {
if (old.selectedObject) {
- const selectedObject =
- this.props.cpModel.jGISModel?.sharedModel.getObject(
- old.selectedObject
- );
- if (selectedObject) {
- const selectedObjectData = selectedObject.parameters;
- return {
- ...old,
- selectedObjectData
- };
- } else {
- return old;
- }
+ return this.getStateForSelection(old, old.selectedObject);
} else {
return old;
}
@@ -161,6 +159,7 @@ class ObjectPropertiesReact extends React.Component {
): void => {
const remoteUser = this.props.cpModel.jGISModel?.localState?.remoteUser;
let newState: IJupyterGISClientState | undefined;
+ const clientId = this.state.clientId;
if (remoteUser) {
newState = clients.get(remoteUser);
@@ -177,6 +176,7 @@ class ObjectPropertiesReact extends React.Component {
);
}
} else {
+ const localState = clientId ? clients.get(clientId) : null;
if (this._lastSelectedPropFieldId) {
removeStyleFromProperty(
`${this.state.filePath}::panel`,
@@ -186,27 +186,173 @@ class ObjectPropertiesReact extends React.Component {
this._lastSelectedPropFieldId = undefined;
}
+ if (
+ localState &&
+ localState.selected?.emitter &&
+ localState.selected.emitter !== this.state.id &&
+ localState.selected?.value
+ ) {
+ newState = localState;
+ }
+ }
+ if (newState) {
+ const selection = newState.selected.value;
+ const selectedObjectIds = Object.keys(selection || {});
+ // Only show object properties if ONE object is selected
+ if (selection === undefined || selectedObjectIds.length !== 1) {
+ this.setState(old => ({
+ ...old,
+ selectedObject: undefined,
+ selectedObjectSource: undefined,
+ selectedObjectData: undefined,
+ selectedObjectSourceData: undefined,
+ schema: undefined,
+ sourceSchema: undefined
+ }));
+ return;
+ }
+
+ const selectedObject = selectedObjectIds[0];
+ if (selectedObject !== this.state.selectedObject) {
+ this.setState(old => {
+ return this.getStateForSelection(old, selectedObject);
+ });
+ }
}
};
+ private getStateForSelection(old: IStates, selectedObject: string): IStates {
+ let selectedObj: IJGISLayer | IJGISSource | undefined;
+ // This will be the layer source in case where the selected object is a layer
+ let selectedObjSource: IJGISSource | undefined;
+
+ selectedObj = this.props.cpModel.jGISModel?.getLayer(selectedObject);
+
+ if (!selectedObj) {
+ selectedObj = this.props.cpModel.jGISModel?.getSource(selectedObject);
+ }
+
+ if (selectedObj && selectedObj.parameters?.source) {
+ selectedObjSource = this.props.cpModel.jGISModel?.getSource(
+ selectedObj!.parameters?.source
+ );
+ }
+
+ if (!selectedObj) {
+ return {
+ ...old,
+ selectedObject: undefined,
+ selectedObjectSource: undefined,
+ selectedObjectData: undefined,
+ selectedObjectSourceData: undefined,
+ schema: undefined,
+ sourceSchema: undefined
+ };
+ }
+
+ let schema: IDict | undefined;
+ let selectedObjectSourceId: string | undefined;
+ const selectedObjectData = selectedObj.parameters;
+ if (selectedObj.type) {
+ schema = this._formSchema.get(selectedObj.type);
+
+ // Generate dropdown for layer source entry
+ if (
+ schema &&
+ schema.properties.source &&
+ selectedObjectData &&
+ selectedObjectData.source
+ ) {
+ const sourceNames: string[] = [];
+ for (const sourceId of Object.keys(
+ this.props.cpModel.jGISModel?.getSources() || {}
+ )) {
+ const source = this.props.cpModel.jGISModel?.getSource(sourceId);
+ if (source) {
+ sourceNames.push(source.name);
+ }
+ }
+ selectedObjectSourceId = selectedObjectData.source;
+ selectedObjectData.source = this.props.cpModel.jGISModel?.getSource(
+ selectedObjectData.source
+ )?.name;
+ schema.properties.source.enum = sourceNames;
+ }
+ }
+
+ let sourceSchema: IDict | undefined;
+ let selectedObjectSourceData: IDict | undefined;
+ if (selectedObjSource) {
+ sourceSchema = this._formSchema.get(selectedObjSource.type);
+ selectedObjectSourceData = selectedObjSource.parameters;
+ }
+
+ return {
+ ...old,
+ selectedObjectData,
+ selectedObject,
+ selectedObjectType: selectedObjSource === undefined ? 'source' : 'layer',
+ selectedObjectSource: selectedObjectSourceId,
+ schema,
+ selectedObjectSourceData,
+ sourceSchema
+ };
+ }
+
render(): React.ReactNode {
return this.state.schema && this.state.selectedObjectData ? (
- {
- this.syncObjectProperties(this.state.selectedObject, properties);
- }}
- syncSelectedField={this.syncSelectedField}
- />
+
+
Layer Properties
+ {
+ if (properties.source) {
+ const sources = this.props.cpModel.jGISModel?.getSources();
+ if (!sources) {
+ throw Error('Unreachable');
+ }
+
+ for (const source of Object.keys(sources)) {
+ if (sources[source].name === properties.source) {
+ properties.source = source;
+ break;
+ }
+ }
+ }
+
+ this.syncObjectProperties(this.state.selectedObject, properties);
+ }}
+ syncSelectedField={this.syncSelectedField}
+ />
+ {this.state.selectedObjectSourceData && this.state.sourceSchema && (
+ <>
+ Source Properties
+ {
+ this.syncObjectProperties(
+ this.state.selectedObjectSource,
+ properties
+ );
+ }}
+ syncSelectedField={this.syncSelectedField}
+ />
+ >
+ )}
+
) : (
);
}
private _lastSelectedPropFieldId?: string;
+ private _formSchema: Map;
}
export namespace ObjectProperties {
diff --git a/packages/schema/src/doc.ts b/packages/schema/src/doc.ts
index 8a48d919e..e078e2c25 100644
--- a/packages/schema/src/doc.ts
+++ b/packages/schema/src/doc.ts
@@ -202,11 +202,7 @@ export class JupyterGISDoc
}
updateSource(id: string, value: any): void {
- const obj = this._getSourceAsYMap(id);
- if (!obj) {
- return;
- }
- this.transact(() => obj.set(id, value));
+ this.transact(() => this._sources.set(id, value));
}
getOption(key: keyof IJGISOptions): IDict | undefined {
diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts
index 382e06088..529dcc3f3 100644
--- a/packages/schema/src/interfaces.ts
+++ b/packages/schema/src/interfaces.ts
@@ -45,7 +45,13 @@ export interface IJGISSourceDocChange {
}>;
}
+export interface ISelection {
+ type: 'layer' | 'source';
+ parent?: string;
+}
+
export interface IJupyterGISClientState {
+ selected: { value?: { [key: string]: ISelection }; emitter?: string | null };
selectedPropField?: {
id: string | null;
value: any;
@@ -109,7 +115,6 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel {
isDisposed: boolean;
sharedModel: IJupyterGISDoc;
localState: IJupyterGISClientState | null;
- currentLayer: string | null;
themeChanged: Signal<
IJupyterGISModel,
@@ -119,7 +124,6 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel {
IJupyterGISModel,
Map
>;
- currentLayerChanged: ISignal;
sharedOptionsChanged: ISignal;
sharedLayersChanged: ISignal;
sharedLayersTreeChanged: ISignal;
@@ -132,11 +136,12 @@ export interface IJupyterGISModel extends DocumentRegistry.IModel {
getSource(id: string): IJGISSource | undefined;
getLayersTree(): IJGISLayersTree;
+ syncSelected(value: { [key: string]: ISelection }, emitter?: string): void;
syncSelectedPropField(data: {
id: string | null;
value: any;
parentType: 'panel' | 'dialog';
- });
+ }): void;
setUserToFollow(userId?: number): void;
syncFormData(form: any): void;
diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts
index 998bf3d25..6ef443cf5 100644
--- a/packages/schema/src/model.ts
+++ b/packages/schema/src/model.ts
@@ -22,6 +22,7 @@ import {
IJupyterGISClientState,
IJupyterGISDoc,
IJupyterGISModel,
+ ISelection,
IUserData
} from './interfaces';
import jgisSchema from './schema/jgis.json';
@@ -47,17 +48,6 @@ export class JupyterGISModel implements IJupyterGISModel {
readonly collaborative = true;
- /**
- * Getter and setter for the current selected layer.
- */
- get currentLayer(): string | null {
- return this._currentLayer;
- }
- set currentLayer(layer: string | null) {
- this._currentLayer = layer;
- this._currentLayerChanged.emit(layer);
- }
-
get sharedModel(): IJupyterGISDoc {
return this._sharedModel;
}
@@ -141,10 +131,6 @@ export class JupyterGISModel implements IJupyterGISModel {
return this.sharedModel.sourcesChanged;
}
- get currentLayerChanged(): ISignal {
- return this._currentLayerChanged;
- }
-
get disposed(): ISignal {
return this._disposed;
}
@@ -227,6 +213,13 @@ export class JupyterGISModel implements IJupyterGISModel {
return this.sharedModel.getSource(id);
}
+ syncSelected(value: { [key: string]: ISelection }, emitter?: string): void {
+ this.sharedModel.awareness.setLocalStateField('selected', {
+ value,
+ emitter: emitter
+ });
+ }
+
syncSelectedPropField(data: {
id: string | null;
value: any;
@@ -287,9 +280,6 @@ export class JupyterGISModel implements IJupyterGISModel {
Map
>(this);
- private _currentLayer: string | null = null;
- private _currentLayerChanged = new Signal(this);
-
static worker: Worker;
}