Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for creating tile layers #6

Merged
merged 3 commits into from
Jun 20, 2024
Merged
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
22 changes: 21 additions & 1 deletion examples/test.jGIS
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
{
"layers": [],
"sources": {
"699facc9-e7c4-4f38-acf1-1fd7f02d9f36": {
"type": "RasterSource",
"name": "RasterSource",
"parameters": {
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"minZoom": 0,
"maxZoom": 24
}
}
},
"layers": {
"2467576f-b527-4cb7-998d-fa1d056fb8a1": {
"type": "RasterLayer",
"parameters": {
"source": "699facc9-e7c4-4f38-acf1-1fd7f02d9f36"
},
"visible": true,
"name": "RasterSource Layer"
}
},
"options": {}
}
2 changes: 1 addition & 1 deletion packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"@types/d3-color": "^3.1.0",
"@types/three": "^0.134.0",
"d3-color": "^3.1.0",
"ol": "^9.2.4",
"maplibre-gl": "^4.4.1",
"react": "^18.0.1",
"styled-components": "^5.3.6",
"three": "^0.135.0",
Expand Down
134 changes: 133 additions & 1 deletion packages/base/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import { ITranslator } from '@jupyterlab/translation';
import { redoIcon, undoIcon } from '@jupyterlab/ui-components';

import { JupyterGISWidget } from './widget';
import { IDict, IJGISFormSchemaRegistry, IJGISLayer, IJGISSource, IJupyterGISModel } from '@jupytergis/schema';
import { FormDialog } from './formdialog';
import { UUID } from '@lumino/coreutils';


/**
* Add the commands to the application's command registry.
*/
export function addCommands(
app: JupyterFrontEnd,
tracker: WidgetTracker<JupyterGISWidget>,
translator: ITranslator
translator: ITranslator,
formSchemaRegistry: IJGISFormSchemaRegistry
): void {
Private.updateFormSchema(formSchemaRegistry);
const trans = translator.load('jupyterlab');
const { commands } = app;
commands.addCommand(CommandIDs.redo, {
Expand Down Expand Up @@ -48,6 +54,17 @@ export function addCommands(
},
icon: undoIcon
});

commands.addCommand(CommandIDs.newRasterLayer, {
label: trans.__('New Tile Layer'),
isEnabled: () => {
return tracker.currentWidget
? tracker.currentWidget.context.model.sharedModel.editable
: false;
},
iconClass: 'fa fa-map',
execute: Private.createRasterSourceAndLayer(tracker)
});
}

/**
Expand All @@ -56,4 +73,119 @@ export function addCommands(
export namespace CommandIDs {
export const redo = 'jupytergis:redo';
export const undo = 'jupytergis:undo';

export const newRasterLayer = 'jupytergis:newRasterLayer';
}


namespace Private {
export const FORM_SCHEMA = {};

export function updateFormSchema(
formSchemaRegistry: IJGISFormSchemaRegistry
) {
if (Object.keys(FORM_SCHEMA).length > 0) {
return;
}
const formSchema = formSchemaRegistry.getSchemas();
formSchema.forEach((val, key) => {
const value = (FORM_SCHEMA[key] = JSON.parse(JSON.stringify(val)));
value['required'] = ['name', ...value['required']];
value['properties'] = {
name: { type: 'string', description: 'The name of the layer/source' },
...value['properties']
};
});
}

// TODO Allow for creating only a source (e.g. loading a CSV file)
// TODO Allow for creating only a layer (e.g. creating a vector layer given a source selected from a dropdown)

export function createRasterSourceAndLayer(
tracker: WidgetTracker<JupyterGISWidget>
) {
return async (args: any) => {
const current = tracker.currentWidget;

if (!current) {
return;
}

const form = {
title: 'Raster Layer parameters',
default: (model: IJupyterGISModel) => {
return {
name: 'RasterSource',
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
maxZoom: 24,
minZoom: 0
};
}
};

current.context.model.syncFormData(form);

const syncSelectedField = (
id: string | null,
value: any,
parentType: 'panel' | 'dialog'
): void => {
let property: string | null = null;
if (id) {
const prefix = id.split('_')[0];
property = id.substring(prefix.length);
}
current.context.model.syncSelectedPropField({
id: property,
value,
parentType
});
};

const dialog = new FormDialog({
context: current.context,
title: form.title,
sourceData: form.default(current.context.model),
schema: FORM_SCHEMA["RasterSource"],
syncData: (props: IDict) => {
const sharedModel = current.context.model.sharedModel;
if (!sharedModel) {
return;
}

const { name, ...parameters } = props;

const sourceId = UUID.uuid4();

const sourceModel: IJGISSource = {
type: "RasterSource",
name,
parameters: {
url: parameters.url,
minZoom: parameters.minZoom,
maxZoom: parameters.maxZoom
}
};

const layerModel: IJGISLayer = {
type: "RasterLayer",
parameters: {
source: sourceId
},
visible: true,
name: name + " Layer"
};

sharedModel.addSource(sourceId, sourceModel)
sharedModel.addLayer(UUID.uuid4(), layerModel);
},
cancelButton: () => {
current.context.model.syncFormData(undefined);
},
syncSelectedPropField: syncSelectedField
});
await dialog.launch();
};
}

}
94 changes: 74 additions & 20 deletions packages/base/src/mainview/mainview.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { MapChange } from '@jupyter/ydoc';
import {
IJGISLayerDocChange,
IJupyterGISClientState,
IJupyterGISDoc,
IJupyterGISModel,
IRasterSource,
} from '@jupytergis/schema';
import { IObservableMap, ObservableMap } from '@jupyterlab/observables';
import { User } from '@jupyterlab/services';
import { JSONValue } from '@lumino/coreutils';
import * as React from 'react';

import * as OpenLayer from 'ol';
import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';
import * as MapLibre from 'maplibre-gl';

import 'ol/ol.css';
// import 'maplibre-gl.css';

import { isLightTheme } from '../tools';
import { MainViewModel } from './mainviewmodel';
Expand Down Expand Up @@ -51,6 +51,11 @@ export class MainView extends React.Component<IProps, IStates> {
this
);

this._model.sharedLayersChanged.connect(
this._onLayersChanged,
this
);

this.state = {
id: this._mainViewModel.id,
lightTheme: isLightTheme(),
Expand Down Expand Up @@ -88,23 +93,10 @@ export class MainView extends React.Component<IProps, IStates> {

generateScene = (): void => {
if (this.divRef.current) {
this._openLayersMap = new OpenLayer.Map({
target: this.divRef.current,
layers: [
new TileLayer({
source: new XYZ({
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
})
})
],
view: new OpenLayer.View({
center: [0, 0],
zoom: 2
})
this._Map = new MapLibre.Map({
container: this.divRef.current
});

console.log('created map', this._openLayersMap);

this.setState(old => ({ ...old, loading: false }));
}
};
Expand All @@ -130,6 +122,68 @@ export class MainView extends React.Component<IProps, IStates> {
// TODO SOMETHING
}

private _onLayersChanged(
sender: IJupyterGISDoc,
change: IJGISLayerDocChange
): void {
// TODO Why is this empty?? We need this for granular updates
// change.layerChange?.forEach((change) => {
// console.log('new change', change);
// })

for (const layerId of Object.keys(this._model.sharedModel.layers)) {
const layer = this._model.sharedModel.getLayer(layerId);

if (!layer) {
console.log(`Layer id ${layerId} does not exist`);
continue;
}

switch(layer.type) {
case 'RasterLayer':
const sourceId = layer.parameters?.source;
const source = this.getSource<IRasterSource>(sourceId);

if (!source) {
continue;
}

// Workaround stupid maplibre issue
this._Map._lazyInitEmptyStyle();

// If the source does not exist, create it
if (!this._Map.getSource(sourceId)) {
this._Map.addSource(sourceId, {
type: 'raster',
tiles: [source.url],
tileSize: 256,
});
} else {
// TODO If the source already existed, update it
}

this._Map.addLayer({
id: layerId,
type: 'raster',
source: sourceId,
minzoom: source.minZoom || 0,
maxzoom: source.maxZoom || 24,
});
}
}
}

private getSource<T>(id: string): T | undefined {
const source = this._model.sharedModel.getSource(id);

if (!source || !source.parameters) {
console.log(`Source id ${id} does not exist`);
return;
}

return source.parameters as T;
}

private _handleThemeChange = (): void => {
const lightTheme = isLightTheme();

Expand Down Expand Up @@ -167,7 +221,7 @@ export class MainView extends React.Component<IProps, IStates> {

private divRef = React.createRef<HTMLDivElement>(); // Reference of render div

private _openLayersMap: OpenLayer.Map;
private _Map: MapLibre.Map;

private _model: IJupyterGISModel;
private _mainViewModel: MainViewModel;
Expand Down
Loading
Loading