Skip to content

Commit c87e2d2

Browse files
Merge pull request #8 from nextmv-io/merschformann/visual-improvements
Adds advanced layer control, a fullscreen button and zoom to bounds
2 parents 74698e0 + d0e1011 commit c87e2d2

15 files changed

+1037
-157
lines changed

nextplot/cluster.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy as np
66
import plotly.graph_objects as go
77
import scipy.spatial
8+
from folium import plugins
89

910
from . import common, types
1011

@@ -289,18 +290,21 @@ def plot(
289290
if not map_file:
290291
map_file = base_name + ".map.html"
291292
print(f"Plotting map to {map_file}")
292-
m = common.create_map(
293+
m, base_tree = common.create_map(
293294
(bbox.max_x + bbox.min_x) / 2.0,
294295
(bbox.max_y + bbox.min_y) / 2.0,
295296
custom_map_tile,
296297
)
297298
plot_groups = {}
299+
group_names = {}
298300

299301
# Plot the clusters themselves
300302
for i, cluster in enumerate(clusters):
301303
if len(cluster.points) <= 0:
302304
continue
303-
plot_groups[i] = folium.FeatureGroup(f"Cluster {i+1}")
305+
layer_name = f"Cluster {i+1}"
306+
plot_groups[i] = folium.FeatureGroup(name=layer_name)
307+
group_names[plot_groups[i]] = layer_name
304308
text = (
305309
"<p>"
306310
+ f"Cluster: {i} / {len(clusters)}</br>"
@@ -331,8 +335,34 @@ def plot(
331335
for k in plot_groups:
332336
plot_groups[k].add_to(m)
333337

338+
# Add button to expand the map to fullscreen
339+
plugins.Fullscreen(
340+
position="topright",
341+
title="Expand me",
342+
title_cancel="Exit me",
343+
).add_to(m)
344+
345+
# Create overlay tree for advanced control of route/unassigned layers
346+
overlay_tree = {
347+
"label": "Overlays",
348+
"select_all_checkbox": "Un/select all",
349+
"children": [
350+
{
351+
"label": "Clusters",
352+
"select_all_checkbox": True,
353+
"collapsed": True,
354+
"children": [{"label": group_names[v], "layer": v} for v in plot_groups.values()],
355+
}
356+
],
357+
}
358+
334359
# Add control for all layers and write file
335-
folium.LayerControl().add_to(m)
360+
plugins.TreeLayerControl(base_tree=base_tree, overlay_tree=overlay_tree).add_to(m)
361+
362+
# Fit map to bounds
363+
m.fit_bounds([[bbox.min_y, bbox.min_x], [bbox.max_y, bbox.max_x]])
364+
365+
# Save map
336366
m.save(map_file)
337367

338368

nextplot/common.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -340,31 +340,56 @@ def bounding_box(points) -> types.BoundingBox:
340340
return types.BoundingBox(min(pos_x), max(pos_x), min(pos_y), max(pos_y))
341341

342342

343-
def create_map(lon: float, lat: float, custom_layers: list[str] = None) -> folium.Map:
343+
def create_map(lon: float, lat: float, custom_layers: list[str] = None) -> tuple[folium.Map, dict[str, any]]:
344344
"""
345-
Creates a default folium map focused on the given coordinates.
345+
Creates a default folium map focused on the given coordinates. Furthermore, it
346+
returns a tree structure that can be used to select different base layers
347+
using the TreeLayerControl.
346348
"""
347349
m = folium.Map(
348350
location=[lat, lon],
349-
tiles="cartodb positron",
350351
zoomSnap=0.25,
351352
zoomDelta=0.25,
352353
wheelPxPerZoomLevel=180,
353354
)
354-
folium.TileLayer("cartodbdark_matter").add_to(m)
355-
folium.TileLayer("openstreetmap").add_to(m)
355+
356+
tile_layers = [
357+
("openstreetmap", folium.TileLayer("openstreetmap").add_to(m)),
358+
("cartodbdark_matter", folium.TileLayer("cartodbdark_matter").add_to(m)),
359+
("cartodb positron", folium.TileLayer("cartodb positron").add_to(m)),
360+
]
361+
356362
if custom_layers:
357363
for layer in custom_layers:
358364
if layer.startswith("http"):
359365
elements = layer.split(",")
360-
folium.TileLayer(
361-
tiles=elements[0],
362-
name=elements[1],
363-
attr=elements[2],
364-
).add_to(m)
366+
if len(elements) != 3:
367+
raise Exception(f"Invalid custom layer definition: {layer}. Expected <url>,<name>,<attribution>")
368+
tile_layers.append(
369+
(
370+
elements[1],
371+
folium.TileLayer(
372+
tiles=elements[0],
373+
name=elements[1],
374+
attr=elements[2],
375+
).add_to(m),
376+
)
377+
)
365378
else:
366379
raise Exception(f"Invalid custom layer definition: {layer}. Expected <url>,<name>,<attribution>")
367-
return m
380+
381+
base_tree = {
382+
"label": "Base Layers",
383+
"children": [
384+
{
385+
"label": "Tiles",
386+
"radioGroup": "tiles",
387+
"children": [{"label": name, "layer": layer} for name, layer in tile_layers],
388+
},
389+
],
390+
}
391+
392+
return m, base_tree
368393

369394

370395
# ==================== Color handling

nextplot/geojson.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import folium
44
import jsonpath_ng
5+
from folium import plugins
56
from folium.elements import JSCSSMixin
67
from folium.map import Layer
78
from jinja2 import Template
@@ -23,14 +24,14 @@ def arguments(parser):
2324
type=str,
2425
nargs="?",
2526
default="",
26-
help="path to the geojson file to plot",
27+
help="path to the GeoJSON file to plot",
2728
)
2829
parser.add_argument(
2930
"--jpath_geojson",
3031
type=str,
3132
nargs="?",
3233
default="",
33-
help="JSON path to the geojson elements (XPATH like,"
34+
help="JSON path to the GeoJSON elements (XPATH like,"
3435
+ " see https://goessner.net/articles/JsonPath/,"
3536
+ ' example: "state.routes[*].geojson")',
3637
)
@@ -149,27 +150,56 @@ def plot(
149150
bbox_sw[0], bbox_sw[1] = min(bbox_sw[0], sw[0]), min(bbox_sw[1], sw[1])
150151
bbox_ne[0], bbox_ne[1] = max(bbox_ne[0], ne[0]), max(bbox_ne[1], ne[1])
151152

152-
# Make map plot of routes
153+
# Make map plot of geojson data
153154
map_file = output_map
154155
if not map_file:
155156
map_file = base_name + ".map.html"
156157
print(f"Plotting map to {map_file}")
157-
m = common.create_map(
158+
m, base_tree = common.create_map(
158159
(bbox_sw[1] + bbox_ne[1]) / 2.0,
159160
(bbox_sw[0] + bbox_ne[0]) / 2.0,
160161
custom_map_tile,
161162
)
163+
plot_groups = {}
164+
group_names = {}
162165
for i, gj in enumerate(geojsons):
163-
group = folium.FeatureGroup(f"geojson {i}")
166+
group_name = f"GeoJSON {i}"
167+
plot_groups[i] = folium.FeatureGroup(name=group_name)
168+
group_names[plot_groups[i]] = group_name
164169
if style:
165-
StyledGeoJson(gj).add_to(group)
170+
StyledGeoJson(gj).add_to(plot_groups[i])
166171
else:
167-
folium.GeoJson(gj).add_to(group)
168-
group.add_to(m)
172+
folium.GeoJson(gj).add_to(plot_groups[i])
173+
plot_groups[i].add_to(m)
174+
175+
# Add button to expand the map to fullscreen
176+
plugins.Fullscreen(
177+
position="topright",
178+
title="Expand me",
179+
title_cancel="Exit me",
180+
).add_to(m)
181+
182+
# Create overlay tree for advanced control of route/unassigned layers
183+
overlay_tree = {
184+
"label": "Overlays",
185+
"select_all_checkbox": "Un/select all",
186+
"children": [
187+
{
188+
"label": "GeoJSONs",
189+
"select_all_checkbox": True,
190+
"collapsed": True,
191+
"children": [{"label": group_names[v], "layer": v} for v in plot_groups.values()],
192+
}
193+
],
194+
}
169195

170196
# Add control for all layers and write file
171-
folium.LayerControl().add_to(m)
197+
plugins.TreeLayerControl(base_tree=base_tree, overlay_tree=overlay_tree).add_to(m)
198+
199+
# Fit bounds
172200
m.fit_bounds([sw, ne])
201+
202+
# Save map
173203
m.save(map_file)
174204

175205

nextplot/point.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import folium
66
import plotly.graph_objects as go
7+
from folium import plugins
78

89
from . import common, types
910

@@ -219,12 +220,20 @@ def plot(
219220
if not map_file:
220221
map_file = base_name + ".map.html"
221222
print(f"Plotting map to {map_file}")
222-
m = common.create_map(
223+
m, base_tree = common.create_map(
223224
(bbox.max_x + bbox.min_x) / 2.0,
224225
(bbox.max_y + bbox.min_y) / 2.0,
225226
custom_map_tile,
226227
)
227-
for ps in points:
228+
plot_groups = {}
229+
group_names = {}
230+
231+
for i, ps in enumerate(points):
232+
if len(ps.points) <= 0:
233+
continue
234+
layer_name = f"Point group {i+1}"
235+
plot_groups[i] = folium.FeatureGroup(name=layer_name)
236+
group_names[plot_groups[i]] = layer_name
228237
for point in ps.points:
229238
d = point.desc.replace("\n", "<br/>").replace(r"`", r"\`")
230239
popup_text = folium.Html(
@@ -246,10 +255,40 @@ def plot(
246255
fillOpacity=1.0,
247256
)
248257
marker.options["fillOpacity"] = 1.0
249-
marker.add_to(m)
258+
marker.add_to(plot_groups[i])
259+
260+
# Add all grouped parts to the map
261+
for g in plot_groups:
262+
plot_groups[g].add_to(m)
263+
264+
# Add button to expand the map to fullscreen
265+
plugins.Fullscreen(
266+
position="topright",
267+
title="Expand me",
268+
title_cancel="Exit me",
269+
).add_to(m)
270+
271+
# Create overlay tree for advanced control of route/unassigned layers
272+
overlay_tree = {
273+
"label": "Overlays",
274+
"select_all_checkbox": "Un/select all",
275+
"children": [
276+
{
277+
"label": "Point groups",
278+
"select_all_checkbox": True,
279+
"collapsed": True,
280+
"children": [{"label": group_names[v], "layer": v} for v in plot_groups.values()],
281+
}
282+
],
283+
}
250284

251285
# Add control for all layers and write file
252-
folium.LayerControl().add_to(m)
286+
plugins.TreeLayerControl(base_tree=base_tree, overlay_tree=overlay_tree).add_to(m)
287+
288+
# Fit bounds
289+
m.fit_bounds([[bbox.min_y, bbox.min_x], [bbox.max_y, bbox.max_x]])
290+
291+
# Save map
253292
m.save(map_file)
254293

255294

nextplot/route.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -340,22 +340,25 @@ def create_map(
340340
Plots the given routes on a folium map.
341341
"""
342342
# Determine bbox
343-
bbox = common.bounding_box([r.points for r in routes])
343+
bbox = common.bounding_box([r.points for r in routes] + unassigned)
344344

345345
# Make map plot of routes
346-
m = common.create_map(
346+
m, base_tree = common.create_map(
347347
(bbox.max_x + bbox.min_x) / 2.0,
348348
(bbox.max_y + bbox.min_y) / 2.0,
349349
custom_map_tile,
350350
)
351351
route_groups = {}
352+
route_layer_names = {}
352353
unassigned_group = folium.FeatureGroup("Unassigned")
353354

354355
# Plot the routes themselves
355356
for i, route in enumerate(routes):
356357
if len(route.points) <= 0:
357358
continue
358-
route_groups[route] = folium.FeatureGroup(f"Route {i+1}")
359+
layer_name = f"Route {i+1}"
360+
route_groups[route] = folium.FeatureGroup(layer_name)
361+
route_layer_names[route_groups[route]] = layer_name
359362
plot_map_route(
360363
route_groups[route],
361364
route,
@@ -450,8 +453,36 @@ def create_map(
450453
if len(unassigned) > 0:
451454
unassigned_group.add_to(m)
452455

456+
# Add button to expand the map to fullscreen
457+
plugins.Fullscreen(
458+
position="topright",
459+
title="Expand me",
460+
title_cancel="Exit me",
461+
).add_to(m)
462+
463+
# Create overlay tree for advanced control of route/unassigned layers
464+
overlay_tree = {
465+
"label": "Overlays",
466+
"select_all_checkbox": "Un/select all",
467+
"children": [],
468+
}
469+
if len(unassigned) > 0:
470+
overlay_tree["children"].append({"label": "Unassigned", "layer": unassigned_group})
471+
if len(route_groups) > 0:
472+
overlay_tree["children"].append(
473+
{
474+
"label": "Routes",
475+
"select_all_checkbox": True,
476+
"collapsed": True,
477+
"children": [{"label": route_layer_names[v], "layer": v} for v in route_groups.values()],
478+
},
479+
)
480+
453481
# Add control for all layers and write file
454-
folium.LayerControl().add_to(m)
482+
plugins.TreeLayerControl(base_tree=base_tree, overlay_tree=overlay_tree).add_to(m)
483+
484+
# Fit map to bounds
485+
m.fit_bounds([[bbox.min_y, bbox.min_x], [bbox.max_y, bbox.max_x]])
455486

456487
# Return map
457488
return m
@@ -658,7 +689,7 @@ def nextroute_profile() -> RoutePlotProfile:
658689
jpath_route="solutions[-1].vehicles[*].route",
659690
jpath_x="stop.location.lon",
660691
jpath_y="stop.location.lat",
661-
jpath_unassigned="solutions[-1].unplanned",
692+
jpath_unassigned="solutions[-1].unplanned[*]",
662693
jpath_unassigned_x="location.lon",
663694
jpath_unassigned_y="location.lat",
664695
)
@@ -746,7 +777,7 @@ def plot_map_route(
746777
polyline.add_to(map)
747778
plugins.PolyLineTextPath(
748779
polyline,
749-
"\u25BA ",
780+
"\u25ba ",
750781
repeat=True,
751782
center=True,
752783
offset=10.35 * weight,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ classifiers = [
1414
dependencies = [
1515
"argcomplete>=2.0.0",
1616
"colorutils>=0.3.0",
17-
"folium>=0.12.1",
17+
"folium>=0.17.0",
1818
"jsonpath_ng>=1.5.3",
1919
"kaleido>=0.2.1",
2020
"numpy>=1.22.3",

0 commit comments

Comments
 (0)