Skip to content

Commit

Permalink
Allow for creating tile layers (#6)
Browse files Browse the repository at this point in the history
* Implement Raster source and layer

* Iterate

* Update snapshots
  • Loading branch information
martinRenou authored Jun 20, 2024
1 parent c476d17 commit b5fed91
Show file tree
Hide file tree
Showing 22 changed files with 965 additions and 315 deletions.
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

0 comments on commit b5fed91

Please sign in to comment.