diff --git a/docs/layers/tooltip.rst b/docs/layers/tooltip.rst new file mode 100644 index 00000000..c0b9450e --- /dev/null +++ b/docs/layers/tooltip.rst @@ -0,0 +1,70 @@ +Tooltip +===== + +Example +------- + +.. jupyter-execute:: + + from ipyleaflet import Map, Tooltip, Marker, Polygon, Circle + + m = Map(center=(51.505,-0.09), zoom=13) + + standalone_tooltip = Tooltip( + location=[51.5, -0.09], + content="Hello world!
This is a nice tooltip.", + offset=[-30,50], # Offset in pixels + permanent=False, # The default is False, in which case you can remove the tooltip by clicking anywhere on the map. + direction='bottom', # Default is 'auto' + ) + + marker_tooltip = Tooltip( + content="I'm a marker tooltip! πŸ‘‹
Appears on hover.", + ) + + marker = Marker( + location=[51.5, -0.09], + draggable=False, + tooltip=marker_tooltip, + ) + + polygon = Polygon( + locations= [ + [51.509, -0.08], + [51.503, -0.06], + [51.51, -0.047] + ]) + + polygon_tooltip = Tooltip( + content = "Polygon's Permanent Tooltip πŸ—ΊοΈ", + permanent = True, + direction = 'center', # Centers the tooltip on the polygon + ) + + polygon.tooltip = polygon_tooltip + + circle = Circle( + location = [51.515, -0.1], + radius = 500, + color = 'green', + fillColor = '#0f3', + fillOpacity = 0.5, + tooltip = Tooltip( + content = "Sticky Tooltip here! πŸ“
Stays with the mouse.", + sticky = True, + ) + ) + + m.add(standalone_tooltip) + m.add(marker) + m.add(polygon) + m.add(circle) + + m + + +Attributes and methods +---------------------- + +.. autoclass:: ipyleaflet.leaflet.Tooltip + :members: diff --git a/python/ipyleaflet/ipyleaflet/leaflet.py b/python/ipyleaflet/ipyleaflet/leaflet.py index dc1fee5a..1c733503 100644 --- a/python/ipyleaflet/ipyleaflet/leaflet.py +++ b/python/ipyleaflet/ipyleaflet/leaflet.py @@ -175,6 +175,8 @@ class Layer(Widget, InteractMixin): Make Leaflet-Geoman ignore the layer, so it cannot modify it. snap_ignore: boolean Make Leaflet-Geoman snapping ignore the layer, so it is not used as a snap target when editing. + tooltip: Tooltip widget + Tooltip widget to bind to the layer. """ _view_name = Unicode("LeafletLayerView").tag(sync=True) @@ -196,6 +198,10 @@ class Layer(Widget, InteractMixin): popup_max_height = Int(default_value=None, allow_none=True).tag(sync=True) pane = Unicode("").tag(sync=True) + tooltip = Instance(Widget, allow_none=True, default_value=None).tag( + sync=True, **widget_serialization + ) + options = List(trait=Unicode()).tag(sync=True) subitems = Tuple().tag(trait=Instance(Widget), sync=True, **widget_serialization) @@ -640,6 +646,43 @@ def close_popup(self): self.send({"msg": "close"}) +class Tooltip(UILayer): + """Tooltip class. + + Used to display small texts on top of map layers. + + Attributes + ---------- + location: tuple, default None + Optional tuple containing the latitude/longitude of the stand-alone tooltip. + content: str, default "" + The text to show inside the tooltip + offset: tuple, default (0, 0) + Optional offset of the tooltip position (in pixels). + direction: str, default 'auto' + Direction where to open the tooltip. + Possible values are: right, left, top, bottom, center, auto. + auto will dynamically switch between right and left according + to the tooltip position on the map. + permanent: bool, default False + Whether to open the tooltip permanently or only on mouseover. + sticky: bool, default False + If true, the tooltip will follow the mouse instead of being fixed at the feature center. + This option only applies when binding the tooltip to a Layer, not as stand-alone. + """ + _view_name = Unicode("LeafletTooltipView").tag(sync=True) + _model_name = Unicode("LeafletTooltipModel").tag(sync=True) + + location = List(allow_none=True, default_value=None).tag(sync=True) + + # Options + content = Unicode('').tag(sync=True, o=True) + offset = List(def_loc).tag(sync=True, o=True) + direction=Unicode('auto').tag(sync=True, o=True) + permanent = Bool(False).tag(sync=True, o=True) + sticky = Bool(False).tag(sync=True, o=True) + + class RasterLayer(Layer): """Abstract RasterLayer class. diff --git a/python/jupyter_leaflet/src/jupyter-leaflet.ts b/python/jupyter_leaflet/src/jupyter-leaflet.ts index acf10313..b35d939f 100644 --- a/python/jupyter_leaflet/src/jupyter-leaflet.ts +++ b/python/jupyter_leaflet/src/jupyter-leaflet.ts @@ -27,6 +27,7 @@ export * from './layers/Popup'; export * from './layers/RasterLayer'; export * from './layers/Rectangle'; export * from './layers/TileLayer'; +export * from './layers/Tooltip'; export * from './layers/VectorLayer'; export * from './layers/VectorTileLayer'; export * from './layers/Velocity'; diff --git a/python/jupyter_leaflet/src/layers/Layer.ts b/python/jupyter_leaflet/src/layers/Layer.ts index be9b5861..fd3e1b13 100644 --- a/python/jupyter_leaflet/src/layers/Layer.ts +++ b/python/jupyter_leaflet/src/layers/Layer.ts @@ -9,7 +9,7 @@ import { } from '@jupyter-widgets/base'; import { IMessageHandler, MessageLoop } from '@lumino/messaging'; import { Widget } from '@lumino/widgets'; -import { Control, Layer, LeafletMouseEvent, Popup } from 'leaflet'; +import { Control, Layer, LeafletMouseEvent, Popup, Tooltip } from 'leaflet'; import { LeafletControlView, LeafletMapView } from '../jupyter-leaflet'; import L from '../leaflet'; import { LeafletWidgetView } from '../utils'; @@ -29,6 +29,7 @@ export interface ILeafletLayerModel { popup_max_width: number; popup_max_height: number | null; pane: string; + tooltip: WidgetModel | null; subitems: Layer[]; pm_ignore: boolean; snap_ignore: boolean; @@ -55,6 +56,7 @@ export class LeafletLayerModel extends WidgetModel { subitems: [], pm_ignore: true, snap_ignore: false, + tooltip: null, }; } } @@ -62,6 +64,7 @@ export class LeafletLayerModel extends WidgetModel { LeafletLayerModel.serializers = { ...WidgetModel.serializers, popup: { deserialize: unpack_models }, + tooltip: { deserialize: unpack_models }, subitems: { deserialize: unpack_models }, }; @@ -83,6 +86,8 @@ export class LeafletLayerView extends LeafletWidgetView { obj: Layer; subitems: WidgetModel[]; pWidget: IMessageHandler; + tooltip_content: LeafletLayerView; + tooltip_content_promise: Promise; create_obj(): void {} @@ -90,6 +95,7 @@ export class LeafletLayerView extends LeafletWidgetView { super.initialize(parameters); this.map_view = this.options.map_view; this.popup_content_promise = Promise.resolve(); + this.tooltip_content_promise = Promise.resolve(); } remove_subitem_view(child_view: LeafletLayerView) { @@ -128,6 +134,7 @@ export class LeafletLayerView extends LeafletWidgetView { this.listenTo(this.model, 'change:popup', (model, value_2) => { this.bind_popup(value_2); }); + this.bind_tooltip(this.model.get('tooltip')); this.update_pane(); this.subitem_views = new ViewList( this.add_subitem_model, @@ -200,6 +207,9 @@ export class LeafletLayerView extends LeafletWidgetView { this.listenTo(this.model, 'change:subitems', () => { this.subitem_views.update(this.subitems); }); + this.listenTo(this.model, 'change:tooltip', () => { + this.bind_tooltip(this.model.get('tooltip')); + }); } remove() { @@ -210,6 +220,11 @@ export class LeafletLayerView extends LeafletWidgetView { this.popup_content.remove(); } }); + this.tooltip_content_promise.then(() => { + if (this.tooltip_content) { + this.tooltip_content.remove(); + } + }); } bind_popup(value: WidgetModel) { @@ -252,6 +267,27 @@ export class LeafletLayerView extends LeafletWidgetView { this.obj.togglePopup(); this.obj.togglePopup(); } + + bind_tooltip(value: WidgetModel) { + if (this.tooltip_content) { + this.obj.unbindTooltip(); + this.tooltip_content.remove(); + } + if (value) { + this.tooltip_content_promise = this.tooltip_content_promise.then( + async () => { + const view = await this.create_child_view(value, { + map_view: this.map_view, + }); + if (view.obj instanceof Tooltip) { + this.obj.bindTooltip(view.obj); + } + this.tooltip_content = view; + } + ); + } + return this.tooltip_content_promise; + } } export class LeafletUILayerView extends LeafletLayerView {} diff --git a/python/jupyter_leaflet/src/layers/Tooltip.ts b/python/jupyter_leaflet/src/layers/Tooltip.ts new file mode 100644 index 00000000..25eb16fa --- /dev/null +++ b/python/jupyter_leaflet/src/layers/Tooltip.ts @@ -0,0 +1,66 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { WidgetView } from '@jupyter-widgets/base'; +import { Tooltip, TooltipOptions } from 'leaflet'; +import L from '../leaflet'; +import { + ILeafletLayerModel, + LeafletUILayerModel, + LeafletUILayerView, +} from './Layer'; + +interface ILeafletTooltipModel extends ILeafletLayerModel { + _view_name: string; + _model_name: string; + location: number[] | null; +} + +export class LeafletTooltipModel extends LeafletUILayerModel { + defaults(): ILeafletTooltipModel { + return { + ...super.defaults(), + _view_name: 'LeafletMarkerView', + _model_name: 'LeafletMarkerModel', + location: null, + }; + } +} + +export class LeafletTooltipView extends LeafletUILayerView { + obj: Tooltip; + + initialize( + parameters: WidgetView.IInitializeParameters + ) { + super.initialize(parameters); + } + + create_obj() { + if (this.model.get('location')) { + // Stand-alone tooltip + this.obj = (L.tooltip as any)( + this.model.get('location'), + this.get_options() as TooltipOptions + ); + } else { + this.obj = L.tooltip(this.get_options() as TooltipOptions); + } + } + + model_events() { + super.model_events(); + this.listenTo(this.model, 'change:location', () => { + if (this.model.get('location')) { + this.obj.setLatLng(this.model.get('location')); + this.send({ + event: 'move', + location: this.model.get('location'), + }); + } + }); + this.listenTo(this.model, 'change:content', () => { + this.obj.setContent(this.model.get('content')); + }); + } +}