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

Interactive vector tile layers #1213

Merged
merged 13 commits into from
Jul 22, 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
62 changes: 58 additions & 4 deletions python/ipyleaflet/ipyleaflet/leaflet.py
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,7 @@ class VectorTileLayer(Layer):
Url to the vector tile service.
attribution: string, default ""
Vector tile service attribution.
vector_tile_layer_styles: dict or string, default {}. If string, it will be parsed as a javascript object (useful for defining styles that depend on properties and/or zoom).
layer_styles: dict or string, default {}. If string, it will be parsed as a javascript object (useful for defining styles that depend on properties and/or zoom).
CSS Styles to apply to the vector data.
min_zoom: int, default 0
The minimum zoom level down to which this layer will be displayed (inclusive).
Expand All @@ -1110,6 +1110,12 @@ class VectorTileLayer(Layer):
Opacity of the layer between 0. (fully transparent) and 1. (fully opaque).
visible: boolean, default True
Whether the layer is visible or not.
renderer: string, default 'svg'
Engine for rendering VectorTileLayers; either 'canvas' or 'svg'. Use 'svg' for interactive layers.
interactive: boolean, default False
Whether the layer is interactive or not.
feature_id: string, default None
Optional attribute name of a unique feature identifier.
"""

_view_name = Unicode("LeafletVectorTileLayerView").tag(sync=True)
Expand All @@ -1118,13 +1124,34 @@ class VectorTileLayer(Layer):
url = Unicode().tag(sync=True, o=True)
attribution = Unicode().tag(sync=True, o=True)

vector_tile_layer_styles = Union([Dict(), Unicode()]).tag(sync=True, o=True)
opacity = Float(1.0, min=0.0, max=1.0).tag(sync=True)
visible = Bool(True).tag(sync=True)
layer_styles = Union([Dict(), Unicode()]).tag(sync=True, o=True)
opacity = Float(1.0, min=0.0, max=1.0).tag(sync=True,o=True)
visible = Bool(True).tag(sync=True, o=True)
interactive = Bool(False).tag(sync=True, o=True)
min_zoom = Int(0).tag(sync=True, o=True)
max_zoom = Int(18).tag(sync=True, o=True)
min_native_zoom = Int(default_value=None, allow_none=True).tag(sync=True, o=True)
max_native_zoom = Int(default_value=None, allow_none=True).tag(sync=True, o=True)
renderer = Unicode('svg').tag(sync=True, o=True)
feature_id = Unicode(allow_none=True, default_value=None).tag(sync=True, o=True)
feature_style = Dict().tag(sync=True)

# Backwards compatibility: allow vector_tile_layer_styles as input:
@property
def vector_tile_layer_styles(self):
return self.layer_styles

@vector_tile_layer_styles.setter
def vector_tile_layer_styles(self, value):
self.layer_styles = value

def __init__(self, **kwargs):
super(VectorTileLayer, self).__init__(**kwargs)
# Backwards compatibility: allow vector_tile_layer_styles as input:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be backward compatible only at the layer creation.

I see three options here:

  • should we really rename this property? If not we can keep vector_tile_layer_styles?
    If yes:
  • we remain backward compatible and we'd need a Python setter too @vector_tile_layer_styles.setter def set_vector_tile_layer_styles
  • we remove backward compatibility and go with 0.20.0

I'd prefer one of the two first options

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinRenou

I went ahead with option 2: vector_tile_layer_styles is backwards compatible, and it uses a setter that updates layer_styles.

Regarding the ability to update styles after initialization -- this wasn't contemplated in #1206, so I also fixed this by adding a listener to layer_styles.

Here's a demo of the new capabilities:

if "vector_tile_layer_styles" in kwargs:
vtl_style = kwargs["vector_tile_layer_styles"]
if(vtl_style):
self.layer_styles = vtl_style

def redraw(self):
"""Force redrawing the tiles.
Expand All @@ -1134,6 +1161,33 @@ def redraw(self):
"""
self.send({"msg": "redraw"})

def set_feature_style(self, id:Int, layer_style:Dict):
"""Re-symbolize one feature.

Given the unique ID for a vector features, re-symbolizes that feature across all tiles it appears in.
Reverts the effects of a previous set_feature_style call. get_feature_id must be defined for
set_feature_style to work.

Attributes
----------
id: int
The unique identifier for the feature to re-symbolize
layer_styles: dict
Style to apply to the feature
"""
self.feature_style = {"id": id, "layerStyle": layer_style, "reset": False}

def reset_feature_style(self, id:Int):
"""Reset feature style

Reverts the style to the layer's deafult.

Attributes
----------
id: int
The unique identifier for the feature to re-symbolize
"""
self.feature_style = {"id": id, "reset": True}

class PMTilesLayer(Layer):
"""PMTilesLayer class, with Layer as parent class.
Expand Down
97 changes: 82 additions & 15 deletions python/jupyter_leaflet/src/layers/VectorTileLayer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { VectorGrid } from 'leaflet';
import { LeafletMouseEvent, VectorGrid } from 'leaflet';
import L from '../leaflet';
import { LeafletLayerModel, LeafletLayerView } from './Layer';

Expand All @@ -17,40 +17,107 @@ export class LeafletVectorTileLayerModel extends LeafletLayerModel {
max_zoom: 18,
min_native_zoom: null,
max_native_zoom: null,
interactive: true,
interactive: false,
visible: true,
opacity: 1.0,
rendererFactory: L.svg.tile,
getFeatureId: null,
};
}
}

export class LeafletVectorTileLayerView extends LeafletLayerView {
obj: VectorGrid.Protobuf;

async set_vector_tile_layer_styles(options: any) {
if ('layerStyles' in options) {
let x: any = options['layerStyles'];
options['vectorTileLayerStyles'] = x;
if (typeof x === 'string') {
try {
let blobCode = `const jsStyle=${x}; export { jsStyle };`;

const blob = new Blob([blobCode], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
const module = await import(/* webpackIgnore: true*/ url);
const jsStyle = module.jsStyle;

options['vectorTileLayerStyles'] = jsStyle;
} catch (error) {
options['vectorTileLayerStyles'] = {};
}
}
}
return options;
}

async create_obj() {
let options = {
...this.get_options(),
};
options['rendererFactory'] = L.canvas.tile;

let x: any = options['vectorTileLayerStyles'];
if (typeof x === 'string') {
try {
let blobCode = `const jsStyle=${x}; export { jsStyle };`;

const blob = new Blob([blobCode], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
const module = await import(/* webpackIgnore: true*/ url);
const jsStyle = module.jsStyle;
if ('featureId' in options) {
let idVar = options['featureId'];
options['getFeatureId'] = function (feat: any) {
return feat.properties[idVar];
};
}

options['vectorTileLayerStyles'] = jsStyle;
} catch (error) {
options['vectorTileLayerStyles'] = {};
if ('renderer' in options) {
let r: any = options['renderer'];
if (r === 'canvas') {
options['rendererFactory'] = L.canvas.tile;
} else {
options['rendererFactory'] = L.svg.tile;
}
}

options = await this.set_vector_tile_layer_styles(options);

this.obj = L.vectorGrid.protobuf(this.model.get('url'), options);
this.model.on('msg:custom', this.handle_message.bind(this));

if (this.model.get('visible') == false) {
this.obj.setOpacity(0);
}

this.model.on('change:layer_styles', async () => {
let options = {
...this.get_options(),
};
options = await this.set_vector_tile_layer_styles(options);
this.obj.options.vectorTileLayerStyles = options['vectorTileLayerStyles'];
if (this.model.get('visible') == false) {
this.obj.setOpacity(0);
}
this.obj.redraw();
});

this.model.on('change:feature_style', () => {
const feature_style = this.model.get('feature_style');
const reset = feature_style['reset'];
if (reset) {
this.obj.resetFeatureStyle(feature_style['id']);
} else {
this.obj.setFeatureStyle(
feature_style['id'],
feature_style['layerStyle']
);
}
});

this.obj.on(
'click mouseover mouseout' as any,
(event: LeafletMouseEvent) => {
this.send({
event: 'interaction',
type: event.type,
coordinates: [event.latlng.lat, event.latlng.lng],
properties: event.propagatedFrom.properties,
options: event.propagatedFrom.options,
});
}
);
}

model_events() {
Expand Down
Loading