Skip to content

Commit c7b5597

Browse files
Add draw control (#29)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 60e62c5 commit c7b5597

File tree

2 files changed

+309
-3
lines changed

2 files changed

+309
-3
lines changed

mapwidget/js/maplibre.js

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ function render({ model, el }) {
88
document.head.appendChild(link);
99
}
1010

11+
// Inject mapbox-gl-draw CSS if not already loaded
12+
if (!document.getElementById("mapbox-gl-draw-css")) {
13+
const link = document.createElement("link");
14+
link.id = "mapbox-gl-draw-css";
15+
link.rel = "stylesheet";
16+
link.href = "https://www.unpkg.com/@mapbox/mapbox-gl-draw@1.5.0/dist/mapbox-gl-draw.css";
17+
document.head.appendChild(link);
18+
}
19+
1120
function updateModel(model, map) {
1221
const viewState = {
1322
center: map.getCenter(),
@@ -51,6 +60,13 @@ function render({ model, el }) {
5160
const controlRegistry = new Map();
5261
let processedCallsCount = 0;
5362

63+
// Initialize draw features in model
64+
model.set("draw_features_selected", []);
65+
model.set("draw_feature_collection_all", { type: "FeatureCollection", features: [] });
66+
model.set("draw_features_created", []);
67+
model.set("draw_features_updated", []);
68+
model.set("draw_features_deleted", []);
69+
5470
map.on("click", function (e) {
5571
model.set("clicked_latlng", [e.lngLat.lng, e.lngLat.lat]);
5672
model.save_changes();
@@ -101,6 +117,7 @@ function render({ model, el }) {
101117
console.log("Map loaded");
102118
model.set("loaded", true);
103119
model.save_changes();
120+
map.getCanvas().style.cursor = 'pointer';
104121
});
105122

106123
// Support JS calls from Python
@@ -123,6 +140,16 @@ function render({ model, el }) {
123140
// Handle removeControl specially
124141
const [controlType] = args;
125142
removeControlFromMap(map, controlType);
143+
} else if (method === "addDrawControl") {
144+
// Handle addDrawControl specially
145+
const [options, controls, position, geojson] = args;
146+
addDrawControlToMap(map, options, controls, position, geojson);
147+
} else if (method === "removeDrawControl") {
148+
// Handle removeDrawControl specially
149+
removeDrawControlFromMap(map);
150+
} else if (method === "drawFeaturesDeleteAll") {
151+
// Handle delete all draw features
152+
deleteAllDrawFeatures(map);
126153
} else if (typeof map[method] === "function") {
127154
try {
128155
map[method](...(args || []));
@@ -196,16 +223,211 @@ function render({ model, el }) {
196223
}
197224
}
198225

226+
// Function to add draw control to the map
227+
function addDrawControlToMap(map, options = {}, controls = {}, position = "top-right", geojson = null) {
228+
// Check if MapboxDraw is available
229+
if (typeof MapboxDraw === "undefined") {
230+
console.warn("MapboxDraw is not loaded. Loading now...");
231+
loadMapboxDraw(() => addDrawControlToMap(map, options, controls, position, geojson));
232+
return;
233+
}
234+
235+
// Patch MapboxDraw constants to work with MapLibre GL
236+
if (MapboxDraw.constants && MapboxDraw.constants.classes) {
237+
MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas';
238+
MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl';
239+
MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-';
240+
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';
241+
MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib';
242+
}
243+
244+
// Default controls configuration
245+
const defaultControls = {
246+
polygon: true,
247+
line_string: true,
248+
point: true,
249+
trash: true,
250+
combine_features: false,
251+
uncombine_features: false
252+
};
253+
254+
// Merge provided controls with defaults
255+
const drawControls = { ...defaultControls, ...controls };
256+
257+
// Default options
258+
const defaultOptions = {
259+
displayControlsDefault: false,
260+
controls: {
261+
polygon: drawControls.polygon,
262+
line_string: drawControls.line_string,
263+
point: drawControls.point,
264+
trash: drawControls.trash,
265+
combine_features: drawControls.combine_features,
266+
uncombine_features: drawControls.uncombine_features
267+
}
268+
};
269+
270+
// Merge provided options with defaults
271+
const drawOptions = { ...defaultOptions, ...options };
272+
273+
// Create draw control
274+
const draw = new MapboxDraw(drawOptions);
275+
276+
try {
277+
// For better control positioning, don't specify position if it's default
278+
if (position === "top-right") {
279+
map.addControl(draw);
280+
} else {
281+
map.addControl(draw, position);
282+
}
283+
console.log(`Added draw control at ${position}`);
284+
controlRegistry.set("draw", draw);
285+
286+
// Add initial geojson if provided
287+
if (geojson) {
288+
if (geojson.type === "FeatureCollection") {
289+
geojson.features.forEach(feature => {
290+
draw.add(feature);
291+
});
292+
} else if (geojson.type === "Feature") {
293+
draw.add(geojson);
294+
}
295+
updateDrawFeatures(map, draw);
296+
}
297+
298+
// Set up draw event handlers
299+
setupDrawEventHandlers(map, draw);
300+
301+
} catch (err) {
302+
console.warn("Failed to add draw control:", err);
303+
}
304+
}
305+
306+
// Function to set up draw event handlers
307+
function setupDrawEventHandlers(map, draw) {
308+
map.on('draw.create', function (e) {
309+
console.log('Features created:', e.features);
310+
model.set("draw_features_created", e.features);
311+
updateDrawFeatures(map, draw);
312+
model.save_changes();
313+
});
314+
315+
map.on('draw.update', function (e) {
316+
console.log('Features updated:', e.features);
317+
model.set("draw_features_updated", e.features);
318+
updateDrawFeatures(map, draw);
319+
model.save_changes();
320+
});
321+
322+
map.on('draw.delete', function (e) {
323+
console.log('Features deleted:', e.features);
324+
model.set("draw_features_deleted", e.features);
325+
updateDrawFeatures(map, draw);
326+
model.save_changes();
327+
});
328+
329+
map.on('draw.selectionchange', function (e) {
330+
console.log('Selection changed:', e.features);
331+
model.set("draw_features_selected", e.features);
332+
model.save_changes();
333+
});
334+
}
335+
336+
// Function to update all draw features in model
337+
function updateDrawFeatures(map, draw) {
338+
const allFeatures = draw.getAll();
339+
model.set("draw_feature_collection_all", allFeatures);
340+
}
341+
342+
// Function to remove draw control from the map
343+
function removeDrawControlFromMap(map) {
344+
const draw = controlRegistry.get("draw");
345+
if (draw) {
346+
map.removeControl(draw);
347+
console.log("Removed draw control");
348+
controlRegistry.delete("draw");
349+
350+
// Clear draw features from model
351+
model.set("draw_features_selected", []);
352+
model.set("draw_feature_collection_all", { type: "FeatureCollection", features: [] });
353+
model.set("draw_features_created", []);
354+
model.set("draw_features_updated", []);
355+
model.set("draw_features_deleted", []);
356+
model.save_changes();
357+
} else {
358+
console.warn("Draw control not found");
359+
}
360+
}
361+
362+
// Function to delete all draw features
363+
function deleteAllDrawFeatures(map) {
364+
const draw = controlRegistry.get("draw");
365+
if (draw) {
366+
const allFeatures = draw.getAll();
367+
if (allFeatures.features.length > 0) {
368+
const featureIds = allFeatures.features.map(f => f.id);
369+
draw.delete(featureIds);
370+
console.log("Deleted all draw features");
371+
372+
// Update model
373+
model.set("draw_features_deleted", allFeatures.features);
374+
updateDrawFeatures(map, draw);
375+
model.save_changes();
376+
}
377+
} else {
378+
console.warn("Draw control not found");
379+
}
380+
}
381+
382+
// Function to load MapboxDraw if not available
383+
function loadMapboxDraw(callback) {
384+
if (typeof MapboxDraw !== "undefined") {
385+
callback();
386+
return;
387+
}
388+
389+
const script = document.createElement("script");
390+
script.src = "https://www.unpkg.com/@mapbox/mapbox-gl-draw@1.5.0/dist/mapbox-gl-draw.js";
391+
script.onload = () => {
392+
// Patch MapboxDraw constants immediately after loading
393+
if (MapboxDraw.constants && MapboxDraw.constants.classes) {
394+
MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas';
395+
MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl';
396+
MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-';
397+
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';
398+
MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib';
399+
}
400+
callback();
401+
};
402+
script.onerror = () => {
403+
console.error("Failed to load MapboxDraw library");
404+
};
405+
document.body.appendChild(script);
406+
}
407+
199408
// Resize after layout stabilizes
200409
setTimeout(() => map.resize(), 100);
201410
}
202411

412+
// Preload MapboxDraw before map initialization
413+
function preloadMapboxDraw() {
414+
if (typeof MapboxDraw === "undefined") {
415+
loadMapboxDraw(() => {
416+
console.log("MapboxDraw preloaded and ready");
417+
});
418+
}
419+
}
420+
203421
if (typeof maplibregl === "undefined") {
204422
const script = document.createElement("script");
205423
script.src = "https://unpkg.com/maplibre-gl@5.5.0/dist/maplibre-gl.js";
206-
script.onload = initMap;
424+
script.onload = () => {
425+
preloadMapboxDraw();
426+
initMap();
427+
};
207428
document.body.appendChild(script);
208429
} else {
430+
preloadMapboxDraw();
209431
initMap();
210432
}
211433
}

mapwidget/maplibre.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pathlib
44
import anywidget
55
import traitlets
6+
from typing import Optional, Dict, Any
67

78

89
class Map(anywidget.AnyWidget):
@@ -27,6 +28,23 @@ class Map(anywidget.AnyWidget):
2728
controls = traitlets.List(traitlets.Dict(), default_value=[]).tag(sync=True, o=True)
2829
style = traitlets.Any().tag(sync=True)
2930

31+
# Draw-related traitlets
32+
draw_features_selected = traitlets.List(traitlets.Dict(), default_value=[]).tag(
33+
sync=True
34+
)
35+
draw_feature_collection_all = traitlets.Dict(
36+
default_value={"type": "FeatureCollection", "features": []}
37+
).tag(sync=True)
38+
draw_features_created = traitlets.List(traitlets.Dict(), default_value=[]).tag(
39+
sync=True
40+
)
41+
draw_features_updated = traitlets.List(traitlets.Dict(), default_value=[]).tag(
42+
sync=True
43+
)
44+
draw_features_deleted = traitlets.List(traitlets.Dict(), default_value=[]).tag(
45+
sync=True
46+
)
47+
3048
def __init__(
3149
self,
3250
center=[0, 20],
@@ -35,7 +53,7 @@ def __init__(
3553
pitch=0,
3654
style="https://tiles.openfreemap.org/styles/liberty",
3755
controls=None,
38-
**kwargs
56+
**kwargs,
3957
):
4058
"""Initialize the Map widget.
4159
@@ -51,7 +69,7 @@ def __init__(
5169
bearing=bearing,
5270
pitch=pitch,
5371
style=style,
54-
**kwargs
72+
**kwargs,
5573
)
5674

5775
# Store default controls to add after initialization
@@ -205,3 +223,69 @@ def remove_control(self, control_type: str):
205223
self.controls = [
206224
control for control in self.controls if control["type"] != control_type
207225
]
226+
227+
def add_draw_control(
228+
self,
229+
options: Optional[Dict[str, Any]] = None,
230+
controls: Optional[Dict[str, Any]] = None,
231+
position: str = "top-right",
232+
geojson: Optional[Dict[str, Any]] = None,
233+
**kwargs: Any,
234+
) -> None:
235+
"""
236+
Adds a drawing control to the map.
237+
238+
This method enables users to add interactive drawing controls to the map,
239+
allowing for the creation, editing, and deletion of geometric shapes on
240+
the map. The options, position, and initial GeoJSON can be customized.
241+
242+
Args:
243+
options (Optional[Dict[str, Any]]): Configuration options for the
244+
drawing control. Defaults to None.
245+
controls (Optional[Dict[str, Any]]): The drawing controls to enable.
246+
Can be one or more of the following: 'polygon', 'line_string',
247+
'point', 'trash', 'combine_features', 'uncombine_features'.
248+
Defaults to None.
249+
position (str): The position of the control on the map. Defaults
250+
to "top-right".
251+
geojson (Optional[Dict[str, Any]]): Initial GeoJSON data to load
252+
into the drawing control. Defaults to None.
253+
**kwargs (Any): Additional keyword arguments to be passed to the
254+
drawing control.
255+
256+
Returns:
257+
None
258+
"""
259+
if options is None:
260+
options = {}
261+
if controls is None:
262+
controls = {}
263+
264+
# Merge kwargs into options
265+
options.update(kwargs)
266+
267+
self.add_call("addDrawControl", [options, controls, position, geojson])
268+
269+
def remove_draw_control(self) -> None:
270+
"""
271+
Removes the drawing control from the map.
272+
273+
This method removes the drawing control and clears all associated
274+
draw features from the map and model.
275+
276+
Returns:
277+
None
278+
"""
279+
self.add_call("removeDrawControl")
280+
281+
def draw_features_delete_all(self) -> None:
282+
"""
283+
Deletes all features from the drawing control.
284+
285+
This method removes all drawn features from the map and updates
286+
the model accordingly.
287+
288+
Returns:
289+
None
290+
"""
291+
self.add_call("drawFeaturesDeleteAll")

0 commit comments

Comments
 (0)