diff --git a/css/widget.css b/css/widget.css index 85bb861..2641c91 100644 --- a/css/widget.css +++ b/css/widget.css @@ -19,3 +19,18 @@ overflow: hidden; flex: 1 1 auto; } + +.swiper-container { + position: absolute; + top: 10px; + left: 10px; + z-index: 1000; + background: rgba(255, 255, 255, 0.8); + padding: 5px; + border-radius: 4px; +} + +.swipe { + width: 100%; + margin: 0; +} diff --git a/examples/RasterLayer.ipynb b/examples/RasterLayer.ipynb index d054f2b..f5958bd 100644 --- a/examples/RasterLayer.ipynb +++ b/examples/RasterLayer.ipynb @@ -13,24 +13,12 @@ "metadata": {}, "outputs": [], "source": [ - "from ipyopenlayers import Map, RasterTileLayer" + "from ipyopenlayers import Map, RasterTileLayer,SplitMapControl, ZoomSlider" ] }, { "cell_type": "code", "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import configparser\n", - "config = configparser.ConfigParser()\n", - "config.read('.config.ini')\n", - "key = config['DEFAULT']['key']" - ] - }, - { - "cell_type": "code", - "execution_count": 3, "metadata": { "scrolled": true }, @@ -38,21 +26,21 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "bf54f7b270eb4c12ba2fbcaeea2583da", + "model_id": "eb8f7885d1b94654942794f633a79e87", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Map(center=[4.299875503991089, 46.85012303279379], zoom=0.0)" + "Map(center=[0.0, 0.0])" ] }, - "execution_count": 3, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "m = Map(center=[4.299875503991089, 46.85012303279379], zoom=0)\n", + "m = Map()\n", "m" ] }, @@ -74,64 +62,210 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "m.add_layer(layere) " + "layer_b=RasterTileLayer(url='https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png')" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - "attributions = '© MapTiler ' +'© OpenStreetMap contributors';\n", - "\n", - "raster = RasterTileLayer(attributions=attributions,url='https://api.maptiler.com/maps/dataviz-dark/{z}/{x}/{y}.png?key=' + key,\n", - " tileSize= 512)\n", - "m.add_layer(raster) " + "m.add_layer(layere) " ] }, { "cell_type": "code", - "execution_count": 8, - "metadata": { - "scrolled": true - }, + "execution_count": 4, + "metadata": {}, "outputs": [], "source": [ - "m.remove_layer(raster)" + "zoom=ZoomSlider()\n", + "m.add_control(zoom)" ] }, { "cell_type": "code", - "execution_count": 9, - "metadata": { - "jupyter": { - "source_hidden": true + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "eb8f7885d1b94654942794f633a79e87", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[0.0, 0.0])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" } - }, + ], + "source": [ + "m.remove(zoom)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, "outputs": [], "source": [ - "attributions = [\n", - " '© MapTiler',\n", - " '© OpenStreetMap contributors'\n", - "]\n", - "\n", - "rasterlay = RasterTileLayer(url=f'https://api.maptiler.com/maps/satellite/{{z}}/{{x}}/{{y}}.jpg?key={key}',attributions=attributions,tileSize=512,maxZoom=20)\n", - "m.add_layer(rasterlay)" + "split = SplitMapControl(left_layer=layer_b)" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ - "m.remove_layer(rasterlay)" + "m.add_control(split)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "118567532fa24b9bbfe23392434a10ff", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[0.0, 0.0], layers=[RasterTileLayer()], zoom=3.1446582428318823)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.remove(split)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.controls" ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9ca94f4d55c341d88bec99d839f1d744", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[47.167155938883134, 33.72586333359965], layers=[RasterTileLayer()])" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[0.00033276346057838606, 0.011182868644951327], controls=[SplitMapControl(left_layer=RasterTileLaye…" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "m.remove_control(split)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "249f37982ae74162851c0f1b99a36b1a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[0.00033276346057838606, 0.011182868644951327], layers=[RasterTileLayer()], zoom=1.9997517395318722…" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/ipyopenlayers/__init__.py b/ipyopenlayers/__init__.py index 4cf0919..0671fce 100644 --- a/ipyopenlayers/__init__.py +++ b/ipyopenlayers/__init__.py @@ -4,7 +4,7 @@ # Copyright (c) QuantStack. # Distributed under the terms of the Modified BSD License. -from .Map import * +from .openlayers import * from ._version import __version__, version_info def _jupyter_labextension_paths(): diff --git a/ipyopenlayers/Map.py b/ipyopenlayers/openlayers.py similarity index 83% rename from ipyopenlayers/Map.py rename to ipyopenlayers/openlayers.py index d17b0ec..b3f48f7 100644 --- a/ipyopenlayers/Map.py +++ b/ipyopenlayers/openlayers.py @@ -67,9 +67,6 @@ class HeatmapLayer(Layer): blur =Int(15).tag(sync=True) radius = Int(8).tag(sync=True) - - - class BaseOverlay(DOMWidget): _model_module = Unicode(module_name).tag(sync=True) @@ -87,7 +84,6 @@ class ImageOverlay (BaseOverlay): class VideoOverlay (BaseOverlay): _view_name = Unicode('VideoOverlayView').tag(sync=True) _model_name = Unicode('VideoOverlayModel').tag(sync=True) - video_url = Unicode('').tag(sync=True) class PopupOverlay (BaseOverlay): @@ -121,6 +117,14 @@ class MousePosition(BaseControl): _view_name = Unicode('MousePositionView').tag(sync=True) _model_name = Unicode('MousePositionModel').tag(sync=True) +class SplitMapControl(BaseControl): + _model_name = Unicode('SplitMapControlModel').tag(sync=True) + _view_name = Unicode('SplitMapControlView').tag(sync=True) + left_layer = Instance(Layer).tag(sync=True, **widget_serialization) + #right_layer = Instance(Layer).tag(sync=True, **widget_serialization) + swipe_position = Int(50).tag(sync=True) + + class Map(DOMWidget): _model_name = Unicode('MapModel').tag(sync=True) @@ -137,7 +141,6 @@ class Map(DOMWidget): controls=List(Instance(BaseControl)).tag(sync=True, **widget_serialization) - def __init__(self, center=None, zoom=None, **kwargs): super().__init__(**kwargs) @@ -163,7 +166,34 @@ def add_control(self, control): def remove_control(self, control): self.controls = [x for x in self.controls if x != control] - + + def remove(self, item): + """Remove an item from the map : either a layer or a control. + + Parameters + ---------- + item: Layer or Control instance + The layer or control to remove. + """ + if isinstance(item, Layer): + self.layers = tuple( + [layer for layer in self.layers if layer.model_id != item.model_id] + ) + + elif isinstance(item, BaseControl): + self.controls = tuple( + [ + control + for control in self.controls + if control.model_id != item.model_id + ] + ) + return self + def clear_layers(self): - self.layers = [] \ No newline at end of file + self.layers = [] + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 70e3887..0a58d87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,9 @@ classifiers = [ ] dependencies = [ "ipywidgets>=8.0.0", + "traitlets", ] -version = "0.1.0.dev0" +version = "0.1.0" [project.optional-dependencies] docs = [ diff --git a/src/rastertilelayer.ts b/src/rastertilelayer.ts index 68c934f..3726a31 100644 --- a/src/rastertilelayer.ts +++ b/src/rastertilelayer.ts @@ -1,12 +1,15 @@ -// Copyright (c) QuantStack -// Distributed under the terms of the Modified BSD License. import { DOMWidgetModel, ISerializers } from '@jupyter-widgets/base'; -import TileLayer from 'ol/layer/WebGLTile.js'; +import WebGLTileLayer from 'ol/layer/WebGLTile.js'; import XYZ from 'ol/source/XYZ.js'; import { MODULE_NAME, MODULE_VERSION } from './version'; import { MapView } from './widget'; import { LayerModel, LayerView } from './layer'; +/* +type WebGLEvent = { + context: WebGLRenderingContext; +}; +*/ export class RasterTileLayerModel extends LayerModel { defaults() { return { @@ -28,7 +31,7 @@ export class RasterTileLayerModel extends LayerModel { static serializers: ISerializers = { ...DOMWidgetModel.serializers, - // Add any extra serializers here + // Add any extra serializers ici }; static model_name = 'RasterTileLayerModel'; @@ -41,15 +44,36 @@ export class RasterTileLayerModel extends LayerModel { export class RasterTileLayerView extends LayerView { map_view: MapView; + tileLayer: WebGLTileLayer; + /* + private prerenderListener: (event: WebGLEvent) => void; + private postrenderListener: (event: WebGLEvent) => void; + private previousSwipePosition: number | undefined; + + constructor(options: any) { + super(options); + this.map_view = options.options.map_view; + this.prerenderListener = this.map_view.handlePrerender.bind(this.map_view); + this.postrenderListener = this.map_view.handlePostrender.bind( + this.map_view, + ); + this.previousSwipePosition = undefined; + }*/ render() { super.render(); this.urlChanged(); this.model.on('change:url', this.urlChanged, this); + /*this.model.on( + 'change:swipe_position', + this.handleSwipePositionChanged, + this, + );*/ + //this.updateEventListeners(); } create_obj() { - this.obj = this.tileLayer = new TileLayer({ + this.obj = this.tileLayer = new WebGLTileLayer({ source: new XYZ({ url: this.model.get('url'), attributions: this.model.get('attributions'), @@ -74,5 +98,27 @@ export class RasterTileLayerView extends LayerView { } } - tileLayer: TileLayer; + /*handleSwipePositionChanged() { + const swipePosition = this.model.get('swipe_position'); + console.log('Swipe Position Changed:', swipePosition); + + if (this.previousSwipePosition !== swipePosition) { + this.previousSwipePosition = swipePosition; + this.updateEventListeners(); + this.map_view.map.render(); + } + } + + updateEventListeners() { + console.log('Updating event listeners'); + const swipePosition = this.model.get('swipe_position'); + (this.tileLayer as any).un('precompose', this.prerenderListener); + (this.tileLayer as any).un('postcompose', this.postrenderListener); + + if (swipePosition >= 0) { + (this.tileLayer as any).on('precompose', this.prerenderListener); + (this.tileLayer as any).on('postcompose', this.postrenderListener); + } + console.log('Event listeners updated'); + }*/ } diff --git a/src/splitcontrol.ts b/src/splitcontrol.ts new file mode 100644 index 0000000..9868ac1 --- /dev/null +++ b/src/splitcontrol.ts @@ -0,0 +1,91 @@ +import Control from 'ol/control/Control'; +import { getRenderPixel } from 'ol/render'; +import 'ol/ol.css'; +import '../css/widget.css'; +import { MapView } from './widget'; + +// Interface for SplitMapControl options +interface SplitMapControlOptions { + target?: string; + map_view?: MapView; + swipe_position?: number; +} + +export default class SplitMapControl extends Control { + swipe: HTMLInputElement; + leftLayer: any; + map_view: MapView; + private _swipe_position: number; + + constructor(leftLayer: any, options: SplitMapControlOptions = {}) { + const element = document.createElement('div'); + element.className = 'ol-unselectable ol-control split-map-control'; + + super({ + element: element, + target: options.target, + }); + + this.leftLayer = leftLayer; + + if (options.map_view) { + this.map_view = options.map_view; + } else { + throw new Error('MapView is required for SplitMapControl.'); + } + + this._swipe_position = options.swipe_position ?? 0; + + const swiperContainer = document.createElement('div'); + swiperContainer.className = 'swiper-container'; + swiperContainer.style.width = '100%'; + + this.swipe = document.createElement('input'); + this.swipe.type = 'range'; + this.swipe.className = 'swipe'; + this.swipe.style.width = '100%'; + this.updateSwipeValue(); + swiperContainer.appendChild(this.swipe); + + this.map_view.map_container.style.position = 'relative'; + this.map_view.map_container.appendChild(swiperContainer); + + const map_view = this.map_view; + + this.leftLayer.on('prerender', (event: any) => { + const gl = event.context; + gl.enable(gl.SCISSOR_TEST); + + const mapSize = map_view.getSize(); + + if (mapSize) { + const bottomLeft = getRenderPixel(event, [0, mapSize[1]]); + const topRight = getRenderPixel(event, [mapSize[0], 0]); + + const width = Math.round( + (topRight[0] - bottomLeft[0]) * (this._swipe_position / 100), + ); + const height = topRight[1] - bottomLeft[1]; + + gl.scissor(bottomLeft[0], bottomLeft[1], width, height); + } + }); + + this.leftLayer.on('postrender', (event: any) => { + const gl = event.context; + gl.disable(gl.SCISSOR_TEST); + }); + + this.swipe.addEventListener('input', () => { + this._swipe_position = parseInt(this.swipe.value, 10); + this.updateSwipeValue(); + map_view.map.render(); + }); + } + + private updateSwipeValue() { + if (this.swipe) { + this.swipe.value = this._swipe_position.toString(); + } + } +} diff --git a/src/splitmapcontrol.ts b/src/splitmapcontrol.ts new file mode 100644 index 0000000..511bbaf --- /dev/null +++ b/src/splitmapcontrol.ts @@ -0,0 +1,79 @@ +// Copyright (c) QuantStack +// Distributed under the terms of the Modified BSD License. +import { unpack_models, ISerializers } from '@jupyter-widgets/base'; +import { BaseControlModel, BaseControlView } from './basecontrol'; +import 'ol/ol.css'; +import '../css/widget.css'; +import SplitMapControl from './splitcontrol'; +import { MODULE_NAME, MODULE_VERSION } from './version'; + +export class SplitMapControlModel extends BaseControlModel { + defaults() { + return { + ...super.defaults(), + _model_name: SplitMapControlModel.model_name, + _model_module: SplitMapControlModel.model_module, + _model_module_version: SplitMapControlModel.model_module_version, + _view_name: SplitMapControlModel.view_name, + _view_module: SplitMapControlModel.view_module, + _view_module_version: SplitMapControlModel.view_module_version, + left_layer: undefined, + right_layer: undefined, + swipe_position: 50, + }; + } + + static serializers: ISerializers = { + ...BaseControlModel.serializers, + left_layer: { deserialize: unpack_models }, + right_layer: { deserialize: unpack_models }, + }; + + static model_name = 'SplitMapControlModel'; + static model_module = MODULE_NAME; + static model_module_version = MODULE_VERSION; + static view_name = 'SplitMapControlView'; + static view_module = MODULE_NAME; + static view_module_version = MODULE_VERSION; +} + +function asArray(arg: any) { + return Array.isArray(arg) ? arg : [arg]; +} + +export class SplitMapControlView extends BaseControlView { + swipe_position: number; + + initialize(parameters: any) { + super.initialize(parameters); + this.map_view = this.options.map_view; + this.map_view.layer_views = this.options.map_view.layerViews; + if (this.map_view && !this.map_view.layerViews) { + console.warn( + 'Layer views is not initialized. Ensure it is properly set.', + ); + } + } + + createObj() { + const left_models = asArray(this.model.get('left_layer')); + let layersModel = this.map_view.model.get('layers'); + layersModel = layersModel.concat(left_models); + + return this.map_view.layer_views.update(layersModel).then((views: any) => { + const left_views: any[] = []; + views.forEach((view: any) => { + if (left_models.indexOf(view.model) !== -1) { + left_views.push(view.obj); + } + }); + + this.swipe_position = this.model.get('swipe_position'); + + this.obj = new SplitMapControl(left_views[0], { + map_view: this.map_view, + swipe_position: this.swipe_position, + }); + }); + } +} diff --git a/src/widget.ts b/src/widget.ts index 20f3298..9eaa924 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -11,7 +11,8 @@ import { LayerModel, LayerView } from './layer'; import { BaseOverlayModel, BaseOverlayView } from './baseoverlay'; import { BaseControlModel, BaseControlView } from './basecontrol'; import { ViewObjectEventTypes } from 'ol/View'; - +import { SplitMapControlModel, SplitMapControlView } from './splitmapcontrol'; +export { SplitMapControlModel, SplitMapControlView }; import { Map } from 'ol'; import View from 'ol/View'; import 'ol/ol.css'; @@ -31,6 +32,8 @@ export * from './heatmap'; export * from './rastertilelayer'; export * from './geotifflayer'; export * from './vectortilelayer'; +export * from './splitmapcontrol'; +export * from './splitcontrol'; const DEFAULT_LOCATION = [0.0, 0.0]; @@ -49,6 +52,7 @@ export class MapModel extends DOMWidgetModel { overlays: [], zoom: 2, center: DEFAULT_LOCATION, + swipe_position: 0, }; } @@ -96,7 +100,6 @@ export class MapView extends DOMWidgetView { this.removeLayerView, this, ); - this.overlayViews = new ViewList( this.addOverlayModel, this.removeOverlayView, @@ -128,21 +131,22 @@ export class MapView extends DOMWidgetView { this.model.set('zoom', this.map.getView().getZoom()); this.model.save_changes(); }); - this.layersChanged(); - this.overlayChanged(); this.controlChanged(); + this.overlayChanged(); + this.zoomChanged(); + this.centerChanged(); this.model.on('change:layers', this.layersChanged, this); this.model.on('change:overlays', this.overlayChanged, this); this.model.on('change:controls', this.controlChanged, this); this.model.on('change:zoom', this.zoomChanged, this); this.model.on('change:center', this.centerChanged, this); } + layersChanged() { const layers = this.model.get('layers') as LayerModel[]; this.layerViews.update(layers); } - overlayChanged() { const overlay = this.model.get('overlays') as BaseOverlayModel[]; this.overlayViews.update(overlay); @@ -159,6 +163,10 @@ export class MapView extends DOMWidgetView { this.map.getView().setZoom(newZoom); } } + getSize() { + const size = this.map.getSize(); + return size; + } centerChanged() { const newCenter = this.model.get('center'); @@ -192,9 +200,9 @@ export class MapView extends DOMWidgetView { this.displayed.then(() => { view.trigger('displayed', this); }); + return view; } - async addOverlayModel(child_model: BaseOverlayModel) { const view = await this.create_child_view(child_model, { map_view: this, @@ -224,6 +232,7 @@ export class MapView extends DOMWidgetView { imageElement: HTMLImageElement; map_container: HTMLDivElement; map: Map; + map_view: MapView; layerViews: ViewList; overlayViews: ViewList; controlViews: ViewList;