diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb index e7b23e4..a1b8738 100644 --- a/examples/introduction.ipynb +++ b/examples/introduction.ipynb @@ -9,22 +9,22 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ - "from ipyopenlayers import Map, TileLayer" + "from ipyopenlayers import Map, TileLayer, ImageOverlay" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "757d5de3cec24c4e83d186b710dcfa59", + "model_id": "b5ca1d6e4568477b9b74c923a0be1262", "version_major": 2, "version_minor": 0 }, @@ -32,206 +32,125 @@ "Map(center=[0.0, 0.0])" ] }, - "execution_count": 2, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "m = Map()\n", + "m = Map(center=[0.0, 0.0], zoom=2)\n", "m" ] }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "m.zoom=4\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "m.center=[10.0,5.0]" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "layere= TileLayer()\n", - "layere.url=\"https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\"\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.add_layer(layere)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, + "execution_count": 28, "metadata": {}, "outputs": [ { "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2bf7058855764a519a4ff61ea74324a7", + "version_major": 2, + "version_minor": 0 + }, "text/plain": [ - "[TileLayer(url='https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png')]" + "ImageOverLayer(image_bounds=[0, 0])" ] }, - "execution_count": 8, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "m.layers" + "image=ImageOverlay()\n", + "image" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 29, "metadata": {}, "outputs": [], "source": [ - "m.remove_layer(layere)" + "image.image_url=\"https://i.imgur.com/06Q1fSz.png\"" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 30, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "m.layers" + "image.image_url=\"\"" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 31, "metadata": {}, "outputs": [], "source": [ - "m.clear_layers()" + "m.add_overlay(image)" ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 32, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "m.layers" + "image.image_bounds=[-70,70]" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "59aea50b07f14f028e2fb5e01c43a461", - "version_major": 2, - "version_minor": 0 - }, "text/plain": [ - "Map(center=[0.0, 0.0])" + "[ImageOverLayer(image_bounds=[-70, 70])]" ] }, - "execution_count": 12, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "m1 = Map(center=[0.0, 0.0], zoom=2)\n", - "m1" + "m.overlays" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 34, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "2.0" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "m1.zoom\n" + "imaget=ImageOverlay()\n", + "imaget.image_url=\"https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg\"\n" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 35, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[0.0, 0.0]" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "m1.center" + "m.add_overlay(imaget)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "m.remove_overlay(image)" + ] } ], "metadata": { diff --git a/ipyopenlayers/example.py b/ipyopenlayers/example.py index 8ead0e1..aca5db5 100644 --- a/ipyopenlayers/example.py +++ b/ipyopenlayers/example.py @@ -8,9 +8,10 @@ TODO: Add module docstring """ -from ipywidgets import DOMWidget, Widget, widget_serialization -from traitlets import Unicode, List, Instance, CFloat +from ipywidgets import DOMWidget, Widget, widget_serialization, HTML +from traitlets import Unicode, List, Instance, CFloat, Bool,Int, Float from ._frontend import module_name, module_version + def_loc = [0.0, 0.0] class TileLayer(Widget): @@ -22,7 +23,19 @@ class TileLayer(Widget): _view_module = Unicode(module_name).tag(sync=True) _view_module_version = Unicode(module_version).tag(sync=True) url = Unicode('https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png').tag(sync=True) - + +class ImageOverlay (DOMWidget): + + _model_name = Unicode('ImageOverlayModel').tag(sync=True) + _model_module = Unicode(module_name).tag(sync=True) + _model_module_version = Unicode(module_version).tag(sync=True) + _view_name = Unicode('ImageOverlayView').tag(sync=True) + _view_module = Unicode(module_name).tag(sync=True) + _view_module_version = Unicode(module_version).tag(sync=True) + + image_url = Unicode('').tag(sync=True) + image_bounds = List([0, 0]).tag(sync=True) + class Map(DOMWidget): _model_name = Unicode('MapModel').tag(sync=True) @@ -36,6 +49,7 @@ class Map(DOMWidget): center = List(def_loc).tag(sync=True, o=True) zoom = CFloat(2).tag(sync=True, o=True) layers = List(Instance(TileLayer)).tag(sync=True, **widget_serialization) + overlays=List(Instance(ImageOverlay)).tag(sync=True, **widget_serialization) def __init__(self, center=None, zoom=None, **kwargs): super().__init__(**kwargs) @@ -49,10 +63,14 @@ def __init__(self, center=None, zoom=None, **kwargs): def add_layer(self, layer): self.layers = self.layers + [layer] + def add_overlay(self, overlay): + self.overlays = self.overlays + [overlay] + def remove_layer(self, layer): self.layers = [x for x in self.layers if x != layer] + + def remove_overlay(self, overlay): + self.overlays = [x for x in self.overlays if x != overlay] def clear_layers(self): - self.layers = [] - - + self.layers = [] \ No newline at end of file diff --git a/src/__tests__/utils.ts b/src/__tests__/utils.ts index 0a61d63..06f6387 100644 --- a/src/__tests__/utils.ts +++ b/src/__tests__/utils.ts @@ -54,7 +54,7 @@ export class DummyManager extends baseManager.ManagerBase { display_view( msg: services.KernelMessage.IMessage, view: widgets.DOMWidgetView, - options: any + options: any, ) { // TODO: make this a spy // TODO: return an html element @@ -68,7 +68,7 @@ export class DummyManager extends baseManager.ManagerBase { protected loadClass( className: string, moduleName: string, - moduleVersion: string + moduleVersion: string, ): Promise { if (moduleName === '@jupyter-widgets/base') { if ((widgets as any)[className]) { @@ -106,7 +106,7 @@ export interface Constructor { export function createTestModel( constructor: Constructor, - attributes?: any + attributes?: any, ): T { const id = widgets.uuid(); const widget_manager = new DummyManager(); diff --git a/src/plugin.ts b/src/plugin.ts index dbad27f..b469593 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -32,7 +32,7 @@ export default examplePlugin; */ function activateWidgetExtension( app: Application, - registry: IJupyterWidgetRegistry + registry: IJupyterWidgetRegistry, ): void { registry.registerWidget({ name: MODULE_NAME, diff --git a/src/widget.ts b/src/widget.ts index 0f845d7..e56a3a4 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -1,6 +1,5 @@ // Copyright (c) QuantStack // Distributed under the terms of the Modified BSD License. - import { DOMWidgetModel, DOMWidgetView, @@ -17,12 +16,16 @@ import TileLayer from 'ol/layer/Tile'; import View from 'ol/View'; import XYZ from 'ol/source/XYZ'; import 'ol/ol.css'; - import { MODULE_NAME, MODULE_VERSION } from './version'; import '../css/widget.css'; import { useGeographic } from 'ol/proj'; +import Overlay from 'ol/Overlay'; +import ImageLayer from 'ol/layer/Image'; +import ImageSource from 'ol/source/Image'; +import ImageStatic from 'ol/source/ImageStatic'; const DEFAULT_LOCATION = [0.0, 0.0]; + export class MapModel extends DOMWidgetModel { defaults() { return { @@ -37,21 +40,21 @@ export class MapModel extends DOMWidgetModel { layers: [], zoom: 2, center: DEFAULT_LOCATION, - }; } static serializers: ISerializers = { ...DOMWidgetModel.serializers, layers: { deserialize: unpack_models }, + overlays: { deserialize: unpack_models }, // Add any extra serializers here }; static model_name = 'MapModel'; static model_module = MODULE_NAME; static model_module_version = MODULE_VERSION; - static view_name = 'MapView'; - static view_module = MODULE_NAME; + static view_name = 'MapView'; + static view_module = MODULE_NAME; static view_module_version = MODULE_VERSION; } @@ -62,62 +65,80 @@ export class MapView extends DOMWidgetView { this.mapContainer = document.createElement('div'); this.mapContainer.style.height = '500px'; - this.el.appendChild(this.mapContainer); - this.layer_views = new ViewList( - this.add_layer_model, - this.remove_layer_view, - this + this.layerViews = new ViewList( + this.addLayerModel, + this.removeLayerView, + this, ); - - this.layers_changed(); - this.model.on('change:layers', this.layers_changed, this); - this.model.on('change:zoom', this.zoom_changed, this) - this.model.on('change:center', this.center_changed, this); + this.overlayViews = new ViewList( + this.addOverlayModel, + this.removeOverlayView, + this, + ); this.map = new Map({ target: this.mapContainer, view: new View({ - center: this.model.get('center'), - zoom : this.model.get('zoom'), + center: this.model.get('center'), + zoom: this.model.get('zoom'), }), layers: [ new TileLayer({ - source: new OSM() - }) - ] + source: new OSM(), + }), + ], }); + + this.layersChanged(); + this.model.on('change:layers', this.layersChanged, this); + this.model.on('change:overlays', this.overlayChanged, this); + this.model.on('change:zoom', this.zoomChanged, this); + this.model.on('change:center', this.centerChanged, this); } - layers_changed(){ + layersChanged() { const layers = this.model.get('layers') as TileLayerModel[]; - this.layer_views.update(layers); + this.layerViews.update(layers); + } + + overlayChanged() { + const overlay = this.model.get('overlays') as ImageOverlayModel[]; + this.overlayViews.update(overlay); } - zoom_changed() { + + zoomChanged() { const newZoom = this.model.get('zoom'); if (newZoom !== undefined && newZoom !== null) { - this.map.getView().setZoom(newZoom); + this.map.getView().setZoom(newZoom); } -} - center_changed() { + } + + centerChanged() { const newCenter = this.model.get('center'); if (newCenter !== undefined && newCenter !== null) { - this.map.getView().setCenter(newCenter); + this.map.getView().setCenter(newCenter); } -} + } + + removeLayerView(child_view: TileLayerView) { + this.map.removeLayer(child_view.tileLayer); + child_view.remove(); + } - remove_layer_view(child_view: TileLayerView) { - this.map.removeLayer(child_view.tileLayer); - child_view.remove(); + removeOverlayView(child_view: ImageOverlayView) { + if (child_view.overlay) { + this.map.removeOverlay(child_view.overlay); + } + child_view.remove(); } - async add_layer_model(child_model: TileLayerModel) { + async addLayerModel(child_model: TileLayerModel) { const view = await this.create_child_view(child_model, { map_view: this, }); - this.map.addLayer(view.tileLayer); this.displayed.then(() => { @@ -126,14 +147,25 @@ export class MapView extends DOMWidgetView { return view; } - mapContainer: HTMLDivElement; + async addOverlayModel(child_model: ImageOverlayModel) { + const view = await this.create_child_view(child_model, { + map_view: this, + }); + this.map.addOverlay(view.overlay); + this.displayed.then(() => { + view.trigger('displayed', this); + }); + console.log('supposee added'); + return view; + } + imageElement: HTMLImageElement; + mapContainer: HTMLDivElement; map: Map; - - layer_views: ViewList; + layerViews: ViewList; + overlayViews: ViewList; } - export class TileLayerModel extends WidgetModel { defaults() { return { @@ -156,37 +188,136 @@ export class TileLayerModel extends WidgetModel { static model_name = 'TileLayerModel'; static model_module = MODULE_NAME; static model_module_version = MODULE_VERSION; - static view_name = 'TileLayerView'; - static view_module = MODULE_NAME; + static view_name = 'TileLayerView'; + static view_module = MODULE_NAME; static view_module_version = MODULE_VERSION; } export class TileLayerView extends WidgetView { render() { super.render(); - const url= this.model.get('url') + const url = this.model.get('url'); this.tileLayer = new TileLayer({ source: new XYZ({ - url: url - }) + url: url, + }), }); - this.url_changed(); - this.model.on('change:url', this.url_changed, this); + this.urlChanged(); + this.model.on('change:url', this.urlChanged, this); } - url_changed() { - const newUrl = this.model.get('url'); - if (newUrl) { - const newSource = new XYZ({ - url: newUrl - }); - this.tileLayer.setSource(newSource); - - }} + urlChanged() { + const newUrl = this.model.get('url'); + if (newUrl) { + const newSource = new XYZ({ + url: newUrl, + }); + this.tileLayer.setSource(newSource); + } + } - tileLayer: TileLayer; +} -} \ No newline at end of file +export class ImageOverlayModel extends DOMWidgetModel { + defaults() { + return { + ...super.defaults(), + _model_name: ImageOverlayModel.model_name, + _model_module: ImageOverlayModel.model_module, + _model_module_version: ImageOverlayModel.model_module_version, + _view_name: ImageOverlayModel.view_name, + _view_module: ImageOverlayModel.view_module, + _view_module_version: ImageOverlayModel.view_module_version, + value: 'Hello World', + }; + } + + static serializers: ISerializers = { + ...DOMWidgetModel.serializers, + // Ajoutez ici tous les sérialiseurs supplémentaires + }; + + static model_name = 'ImageOverlayModel'; + static model_module = MODULE_NAME; + static model_module_version = MODULE_VERSION; + static view_name = 'ImageOverlayView'; + static view_module = MODULE_NAME; + static view_module_version = MODULE_VERSION; +} + +export class ImageOverlayView extends DOMWidgetView { + imageLayer: ImageLayer; + overlay: Overlay; + imageElement: HTMLImageElement; + render() { + super.render(); + console.log('Render called'); + this.updateImageElement(); + } + + initialize(parameters: WidgetView.IInitializeParameters) { + super.initialize(parameters); + this.imageElement = document.createElement('img'); + this.createImageLayer(); + this.createOverlay(); + this.model_events(); + console.log('Overlay created in initialize:', this.overlay); + } + + createImageLayer() { + this.imageLayer = new ImageLayer({ + source: new ImageStatic({ + url: this.model.get('image_url'), + imageExtent: this.model.get('image_bounds'), + }), + }); + } + createOverlay() { + const imageExtent = this.model.get('image_bounds'); + this.overlay = new Overlay({ + position: imageExtent, + element: this.imageElement, + }); + return this.overlay; + } + model_events() { + this.listenTo(this.model, 'change:image_url', () => { + const url = this.model.get('image_url'); + if (url) { + const newSource = new ImageStatic({ + url: this.model.get('image_url'), + imageExtent: this.model.get('image_bounds'), + }); + this.imageLayer.setSource(newSource); + this.imageElement.src = url; + this.updateImageElement(); + } + }); + this.updateImageElement(); + this.listenTo(this.model, 'change:image_bounds', () => { + const nv_image_bounds = this.model.get('image_bounds'); + this.imageLayer.setExtent(nv_image_bounds); + this.trigger('image_bounds_changed', nv_image_bounds); + }); + this.on('image_bounds_changed', (nv_image_bounds: number[]) => { + this.updatePositionOverlay(nv_image_bounds); + }); + } + updateImageElement() { + const imageSource = this.imageLayer.getSource() as ImageStatic; + if (imageSource) { + const imageUrl = imageSource.getUrl(); + if (imageUrl) { + this.imageElement.src = imageUrl; + } + } + } + updatePositionOverlay(nv_image_bounds: number[]) { + if (nv_image_bounds && this.overlay) { + this.overlay.setPosition(nv_image_bounds); + } + } +}