From 68662574c584c8c95eb9c070fcdc4b8a3ec611d1 Mon Sep 17 00:00:00 2001 From: Kaushik Srinivasan Date: Thu, 29 May 2025 09:39:37 -0400 Subject: [PATCH 1/2] init adding geojson plugin --- .../jupytergis_core/geojson_ydoc.py | 39 +++++ .../src/geojsonplugin/model.ts | 89 +++++++++++ .../src/geojsonplugin/modelfactory.ts | 123 ++++++++++++++++ .../src/geojsonplugin/plugins.ts | 139 ++++++++++++++++++ python/jupytergis_core/src/index.ts | 4 +- 5 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 python/jupytergis_core/jupytergis_core/geojson_ydoc.py create mode 100644 python/jupytergis_core/src/geojsonplugin/model.ts create mode 100644 python/jupytergis_core/src/geojsonplugin/modelfactory.ts create mode 100644 python/jupytergis_core/src/geojsonplugin/plugins.ts diff --git a/python/jupytergis_core/jupytergis_core/geojson_ydoc.py b/python/jupytergis_core/jupytergis_core/geojson_ydoc.py new file mode 100644 index 000000000..c4de7298d --- /dev/null +++ b/python/jupytergis_core/jupytergis_core/geojson_ydoc.py @@ -0,0 +1,39 @@ +import json +from typing import Any, Callable +from functools import partial + +from pycrdt import Text +from jupyter_ydoc.ybasedoc import YBaseDoc + +from .schema import SCHEMA_VERSION + + +class YGEOJSON(YBaseDoc): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._ydoc["source"] = self._ysource = Text() + + def version(self) -> str: + return SCHEMA_VERSION + + def get(self) -> str: + """ + Returns the content of the document. + :return: Document's content. + :rtype: Any + """ + return json.dumps(self._ysource.to_py()) + + def set(self, value: str) -> None: + """ + Sets the content of the document. + :param value: The content of the document. + :type value: Any + """ + self._ysource[:] = value + + def observe(self, callback: Callable[[str, Any], None]): + self.unobserve() + self._subscriptions[self._ysource] = self._ysource.observe( + partial(callback, "source") + ) diff --git a/python/jupytergis_core/src/geojsonplugin/model.ts b/python/jupytergis_core/src/geojsonplugin/model.ts new file mode 100644 index 000000000..194d8d102 --- /dev/null +++ b/python/jupytergis_core/src/geojsonplugin/model.ts @@ -0,0 +1,89 @@ +import { + SCHEMA_VERSION, + IJupyterGISDoc, + JupyterGISDoc +} from '@jupytergis/schema'; +import { JSONExt } from '@lumino/coreutils'; +import { ISignal, Signal } from '@lumino/signaling'; +import * as Y from 'yjs'; + +export class JupyterGISGeoJSONDoc extends JupyterGISDoc { + constructor() { + super(); + + this._source = this.ydoc.getText('source'); + this._source.observeDeep(this._sourceObserver); + } + + set source(value: string) { + this._source.insert(0, value); + } + + get version(): string { + return SCHEMA_VERSION; + } + + get objectsChanged(): ISignal { + return this._objectChanged; + } + + get objects(): Array { + const source = this._source.toJSON(); + console.log("calling source"); + console.log(source); + + if (!source) { + return []; + } + + return [ + { + name: 'GeoJSON File', + visible: true, + type: 'VectorTileLayer', + parameters: { + content: this._source.toJSON(), + style: { + color: '#3388ff', + weight: 2, + opacity: 0.8, + fillOpacity: 0.2 + } + } + } + ]; + } + + setSource(value: string): void { + this._source.insert(0, value); + } + + static create(): JupyterGISGeoJSONDoc { + return new JupyterGISGeoJSONDoc(); + } + + editable = false; + toJgisEndpoint = 'jupytergis/export'; + + private _sourceObserver = (events: Y.YEvent[]): void => { + const changes: Array<{ + name: string; + key: string; + newValue: any; + }> = []; + events.forEach(event => { + event.keys.forEach((change, key) => { + changes.push({ + name: 'GeoJSON File', + key: key as string, + newValue: JSONExt.deepCopy(event.target.toJSON()) + }); + }); + }); + this._objectChanged.emit({ objectChange: changes }); + this._changed.emit({ layerChange: changes }); + }; + + private _source: Y.Text; + private _objectChanged = new Signal(this); +} diff --git a/python/jupytergis_core/src/geojsonplugin/modelfactory.ts b/python/jupytergis_core/src/geojsonplugin/modelfactory.ts new file mode 100644 index 000000000..6b031bce0 --- /dev/null +++ b/python/jupytergis_core/src/geojsonplugin/modelfactory.ts @@ -0,0 +1,123 @@ +import { IJupyterGISDoc, JupyterGISModel } from '@jupytergis/schema'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { Contents } from '@jupyterlab/services'; +import { JupyterGISGeoJSONDoc } from './model'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { IAnnotationModel } from '@jupytergis/schema'; + +class JupyterGISGeoJSONModel extends JupyterGISModel { + constructor(options: { + sharedModel?: IJupyterGISDoc; + languagePreference?: string; + settingRegistry?: ISettingRegistry; + annotationModel?: IAnnotationModel; + }) { + super({ + sharedModel: options.sharedModel, + languagePreference: options.languagePreference, + settingRegistry: options.settingRegistry, + annotationModel: options.annotationModel + }); + } + + fromString(data: string): void { + (this.sharedModel as JupyterGISGeoJSONDoc).source = data; + this.dirty = true; + } + + protected createSharedModel(): IJupyterGISDoc { + return JupyterGISGeoJSONDoc.create(); + } +} + +/** + * A Model factory to create new instances of JupyterGISGeoJSONModel. + */ +export class JupyterGISGeoJSONModelFactory + implements DocumentRegistry.IModelFactory +{ + constructor(options: { + settingRegistry?: ISettingRegistry; + annotationModel?: IAnnotationModel; + }) { + this._settingRegistry = options.settingRegistry; + this._annotationModel = options.annotationModel; + } + + readonly collaborative = true; + + /** + * The name of the model. + * + * @returns The name + */ + get name(): string { + return 'jupytergis-geojsonmodel'; + } + + /** + * The content type of the file. + * + * @returns The content type + */ + get contentType(): Contents.ContentType { + return 'geojson'; + } + + /** + * The format of the file. + * + * @returns the file format + */ + get fileFormat(): Contents.FileFormat { + return 'text'; + } + + /** + * Get whether the model factory has been disposed. + * + * @returns disposed status + */ + get isDisposed(): boolean { + return this._disposed; + } + + /** + * Dispose the model factory. + */ + dispose(): void { + this._disposed = true; + } + + /** + * Get the preferred language given the path on the file. + * + * @param path path of the file represented by this document model + * @returns The preferred language + */ + preferredLanguage(path: string): string { + return ''; + } + + /** + * Create a new instance of JupyterGISGeoJSONModel. + * + * @returns The model + */ + createNew( + options: DocumentRegistry.IModelOptions + ): JupyterGISGeoJSONModel { + const model = new JupyterGISGeoJSONModel({ + sharedModel: options.sharedModel, + languagePreference: options.languagePreference, + settingRegistry: this._settingRegistry, + annotationModel: this._annotationModel + }); + model.initSettings(); + return model; + } + + private _disposed = false; + private _settingRegistry: ISettingRegistry | undefined; + private _annotationModel: IAnnotationModel | undefined; +} diff --git a/python/jupytergis_core/src/geojsonplugin/plugins.ts b/python/jupytergis_core/src/geojsonplugin/plugins.ts new file mode 100644 index 000000000..fb0e95e33 --- /dev/null +++ b/python/jupytergis_core/src/geojsonplugin/plugins.ts @@ -0,0 +1,139 @@ +import { + ICollaborativeDrive, + SharedDocumentFactory +} from '@jupyter/collaborative-drive'; +import { + IJupyterGISDocTracker, + IJupyterGISWidget, + IJGISExternalCommandRegistry, + IJGISExternalCommandRegistryToken, + IAnnotationModel, + IAnnotationToken +} from '@jupytergis/schema'; +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; +import { IThemeManager, WidgetTracker } from '@jupyterlab/apputils'; +import { ConsolePanel, IConsoleTracker } from '@jupyterlab/console'; +import { IEditorServices } from '@jupyterlab/codeeditor'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { ICommandPalette } from '@jupyterlab/apputils'; + +import { JupyterGISGeoJSONModelFactory } from './modelfactory'; +import { JupyterGISDocumentWidgetFactory } from '../factory'; +import { JupyterGISGeoJSONDoc } from './model'; +import { logoMiniIcon, JupyterGISDocumentWidget } from '@jupytergis/base'; + +const FACTORY = 'JupyterGIS GeoJSON Viewer'; +const SETTINGS_ID = '@jupytergis/jupytergis-core:jupytergis-settings'; + +const activate = async ( + app: JupyterFrontEnd, + tracker: WidgetTracker, + themeManager: IThemeManager, + drive: ICollaborativeDrive, + externalCommandRegistry: IJGISExternalCommandRegistry, + contentFactory: ConsolePanel.IContentFactory, + editorServices: IEditorServices, + rendermime: IRenderMimeRegistry, + consoleTracker: IConsoleTracker, + annotationModel: IAnnotationModel, + settingRegistry: ISettingRegistry, + commandPalette: ICommandPalette | null +): Promise => { + let settings: ISettingRegistry.ISettings | null = null; + + if (settingRegistry) { + try { + settings = await settingRegistry.load(SETTINGS_ID); + console.log(`Loaded settings for ${SETTINGS_ID}`, settings); + } catch (error) { + console.warn(`Failed to load settings for ${SETTINGS_ID}`, error); + } + } else { + console.warn('No settingRegistry available; using default settings.'); + } + + const widgetFactory = new JupyterGISDocumentWidgetFactory({ + name: FACTORY, + modelName: 'jupytergis-geojsonmodel', + fileTypes: ['geojson'], + defaultFor: ['geojson'], + tracker, + commands: app.commands, + externalCommandRegistry, + manager: app.serviceManager, + contentFactory, + rendermime, + mimeTypeService: editorServices.mimeTypeService, + consoleTracker + }); + + console.log("geojson widget factory created", widgetFactory); + + app.docRegistry.addWidgetFactory(widgetFactory); + + const modelFactory = new JupyterGISGeoJSONModelFactory({ + annotationModel, + settingRegistry + }); + app.docRegistry.addModelFactory(modelFactory); + + app.docRegistry.addFileType({ + name: 'geojson', + displayName: 'GeoJSON', + mimeTypes: ['application/json'], + extensions: ['.geojson', '.GEOJSON'], + fileFormat: 'text', + contentType: 'geojson', + icon: logoMiniIcon + }); + + const geojsonSharedModelFactory: SharedDocumentFactory = () => { + return new JupyterGISGeoJSONDoc(); + }; + drive.sharedModelFactory.registerDocumentFactory( + 'geojson', + geojsonSharedModelFactory + ); + + const widgetCreatedCallback = ( + sender: any, + widget: JupyterGISDocumentWidget + ) => { + console.log("calling geojson widget callback"); + widget.title.icon = logoMiniIcon; + widget.context.pathChanged.connect(() => { + tracker.save(widget); + }); + themeManager.themeChanged.connect((_, changes) => + widget.model.themeChanged.emit(changes) + ); + tracker.add(widget); + }; + + widgetFactory.widgetCreated.connect(widgetCreatedCallback); +}; + +const geojsonPlugin: JupyterFrontEndPlugin = { + id: '@jupytergis/jupytergis-core:geojsonplugin', + requires: [ + IJupyterGISDocTracker, + IThemeManager, + ICollaborativeDrive, + IJGISExternalCommandRegistryToken, + ConsolePanel.IContentFactory, + IEditorServices, + IRenderMimeRegistry, + IConsoleTracker, + IAnnotationToken, + ISettingRegistry + ], + optional: [ICommandPalette], + autoStart: true, + activate +}; + +export default geojsonPlugin; diff --git a/python/jupytergis_core/src/index.ts b/python/jupytergis_core/src/index.ts index 7b1c54436..e72ac1f69 100644 --- a/python/jupytergis_core/src/index.ts +++ b/python/jupytergis_core/src/index.ts @@ -6,6 +6,7 @@ import { trackerPlugin, annotationPlugin } from './plugin'; +import geojsonPlugin from './geojsonplugin/plugins'; export * from './factory'; export default [ @@ -14,5 +15,6 @@ export default [ formSchemaRegistryPlugin, externalCommandRegistryPlugin, layerBrowserRegistryPlugin, - annotationPlugin + annotationPlugin, + geojsonPlugin ]; From c12e28b03bd5b9e43f954d860185d074eeb877fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 13:44:06 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- python/jupytergis_core/src/geojsonplugin/modelfactory.ts | 2 +- python/jupytergis_core/src/geojsonplugin/plugins.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/jupytergis_core/src/geojsonplugin/modelfactory.ts b/python/jupytergis_core/src/geojsonplugin/modelfactory.ts index 6b031bce0..cfd15fc6a 100644 --- a/python/jupytergis_core/src/geojsonplugin/modelfactory.ts +++ b/python/jupytergis_core/src/geojsonplugin/modelfactory.ts @@ -36,7 +36,7 @@ class JupyterGISGeoJSONModel extends JupyterGISModel { export class JupyterGISGeoJSONModelFactory implements DocumentRegistry.IModelFactory { - constructor(options: { + constructor(options: { settingRegistry?: ISettingRegistry; annotationModel?: IAnnotationModel; }) { diff --git a/python/jupytergis_core/src/geojsonplugin/plugins.ts b/python/jupytergis_core/src/geojsonplugin/plugins.ts index fb0e95e33..e42593e67 100644 --- a/python/jupytergis_core/src/geojsonplugin/plugins.ts +++ b/python/jupytergis_core/src/geojsonplugin/plugins.ts @@ -107,7 +107,7 @@ const activate = async ( widget.title.icon = logoMiniIcon; widget.context.pathChanged.connect(() => { tracker.save(widget); - }); + }); themeManager.themeChanged.connect((_, changes) => widget.model.themeChanged.emit(changes) );