Companion factory — interactive geometry selection
This notebook presents five preconfigured companions available in wolfhece.plugins.factory. Each one addresses a common need for interactive input on the map.
Companion |
Result |
Typical use case |
|---|---|---|
|
|
Control point input, mesh nodes |
|
|
Longitudinal profile, axis tracing, flow path |
|
|
Drain network, section outlines |
|
|
Same idea, but stored in a reusable vector layer |
|
|
Flood zones, catchments |
Common interaction for all companions
Key / action |
Effect |
|---|---|
Right click |
Adds a vertex |
Enter |
Validates the geometry in progress (>= 2 points for lines, >= 3 for polygons) |
Esc |
Cancels / stops |
|
Deactivates the action (data preserved) |
|
Deactivates and unregisters all handlers |
Minimal usage (one-liner)
from wolfhece.plugins.factory import point_picker
comp = point_picker(viewer) # creates, registers, AND activates in one line
# ... right click on the map ...
print(comp.points)
comp.destroy()
1 — wx startup
[1]:
import sys
%gui wx
2 — Create the viewer
[2]:
from wolfhece.PyDraw import WolfMapViewer
viewer = WolfMapViewer(None, title="Companion factory", w=1200, h=800)
viewer.Show()
INFO:root:Importing wolfhece modules
INFO:root:wolfhece modules imported
INFO:root:Plugin manager init: skipping auto-discovery because no global configuration is available yet.
[2]:
False
3 — Import the factory
[3]:
from wolfhece.plugins.factory import (
PointPickerCompanion,
PolylineCompanion,
MultiPolylineCompanion,
MultiPolylineZonesCompanion,
PolygonCompanion,
point_picker,
polyline,
multi_polyline,
multi_polyline_zones,
polygon,
)
print('Factory importée.')
Factory importée.
A — PointPickerCompanion: selecting individual points
Available data after interaction: comp.points (list of (x, y)).
[4]:
# One-liner: create + register + activate
comp_pts = point_picker(viewer)
print(comp_pts)
INFO:root:ACTION : Right-click: add | Left-click: select nearest | Ctrl+Z: undo | Esc: stop
<PointPickerCompanion ns='pointpickercompanion' menu=not built actions=[pointpickercompanion.pick]>
[5]:
# Inspect collected points (run this cell at any time)
print(f"{len(comp_pts.points)} point(s) | selected: {comp_pts.selected}")
for i, (x, y) in enumerate(comp_pts.points, 1):
flag = ' ←' if i - 1 == comp_pts.selected else ''
print(f" {i:3d}. X={x:.3f} Y={y:.3f}{flag}")
4 point(s) | selected: 3
1. X=11.052 Y=26.525
2. X=28.788 Y=26.578
3. X=16.645 Y=18.642
4. X=26.871 Y=13.635 ←
[6]:
# Convert to a NumPy array for processing
import numpy as np
if comp_pts.points:
pts_array = np.array(comp_pts.points) # shape (N, 2)
print(f"Shape: {pts_array.shape}")
print(pts_array)
Shape: (4, 2)
[[11.05209177 26.52451642]
[28.78812416 26.57777778]
[16.64453441 18.64183536]
[26.87071525 13.63526766]]
[7]:
comp_pts.stop()
comp_pts.destroy()
print('PointPickerCompanion destroyed.')
INFO:root:ACTION : End of action
PointPickerCompanion destroyed.
B — PolylineCompanion: polyline input
Data available after Enter: comp.vertices (ordered list of vertices), comp.finished (True).
[5]:
comp_line = polyline(viewer)
print(comp_line)
INFO:root:ACTION : Right-click: add vertex | Enter: finalise (≥2 pts) | Esc: cancel
<PolylineCompanion ns='polylinecompanion' menu=not built actions=[polylinecompanion.line]>
[6]:
print(f"finished={comp_line.finished} | {len(comp_line.vertices)} vertex(s)")
for i, (x, y) in enumerate(comp_line.vertices, 1):
print(f" {i:3d}. X={x:.3f} Y={y:.3f}")
finished=True | 6 vertex(s)
1. X=3.276 Y=29.507
2. X=20.000 Y=24.341
3. X=4.607 Y=15.819
4. X=21.971 Y=12.357
5. X=29.374 Y=22.317
6. X=22.716 Y=32.117
[7]:
# Compute the total polyline length
if comp_line.finished and len(comp_line.vertices) >= 2:
import numpy as np
v = np.array(comp_line.vertices)
segments = np.hypot(np.diff(v[:, 0]), np.diff(v[:, 1]))
print(f"Total length: {segments.sum():.3f} m")
print(f"Number of segments: {len(segments)}")
Total length: 77.061 m
Number of segments: 5
[8]:
comp_line.destroy()
print('PolylineCompanion destroyed.')
PolylineCompanion destroyed.
C — MultiPolylineCompanion: multiple polylines in one session
Colors: finalized lines in blue, current line in orange.
Data: comp.polylines (list of lists of vertices).
[9]:
comp_lines = multi_polyline(viewer)
print(comp_lines)
INFO:root:ACTION : Right-click: vertex | Enter: next line | Esc: finish session
<MultiPolylineCompanion ns='multipolylinecompanion' menu=not built actions=[multipolylinecompanion.lines]>
[10]:
print(f"{len(comp_lines.polylines)} finalized polyline(s)")
for j, line in enumerate(comp_lines.polylines, 1):
print(f" Line {j}: {len(line)} vertices")
for i, (x, y) in enumerate(line, 1):
print(f" {i:3d}. X={x:.3f} Y={y:.3f}")
0 finalized polyline(s)
[11]:
comp_lines.destroy()
print('MultiPolylineCompanion destroyed.')
MultiPolylineCompanion destroyed.
D — MultiPolylineZonesCompanion: polylines in a Zones object
Same interaction as MultiPolylineCompanion, but each accepted polyline is stored immediately in a `Zones <../../api/wolfhece.pyvertexvectors.rst>`__ object
Zones→zone→vectorhierarchy - and, by default, automatically added to the viewer as a vector layer.
comp.zones.Data: comp.zones (Zones object, None before the first accepted line).
Key advantage: the
Zonesobject can be exported, manipulated, or passed to other wolfhece tools directly without conversion.
[4]:
# One-liner: create, register, and activate
# By default auto_attach=True, so the Zones is added to the viewer when the first line is accepted
comp_zones = multi_polyline_zones(viewer, zones_id='demo_zones')
print(comp_zones)
INFO:root:ACTION : Right-click: vertex | Enter: accept line (≥2 pts) | Esc: stop
<MultiPolylineZonesCompanion ns='multipolylinezonescompanion' menu=not built actions=[multipolylinezonescompanion.mplz]>
[5]:
# Inspect the lines in the Zones object (run at any time)
if comp_zones.zones is not None:
z_obj = comp_zones.zones
print(f"{z_obj.nbzones} registered zone(s)")
for j, z in enumerate(z_obj.myzones, 1):
for v in z.myvectors:
pts = [(vert.x, vert.y) for vert in v.myvertices]
print(f" Zone {j} '{z.myname}' — {len(pts)} vertex(s)")
for i, (x, y) in enumerate(pts, 1):
print(f" {i:3d}. X={x:.3f} Y={y:.3f}")
else:
print("No line has been accepted yet (press Enter after >= 2 clicks).")
No line has been accepted yet (press Enter after >= 2 clicks).
[6]:
# Convert the Zones object to a NumPy array (all lines concatenated)
import numpy as np
if comp_zones.zones is not None:
all_pts = [
(vert.x, vert.y)
for z in comp_zones.zones.myzones
for v in z.myvectors
for vert in v.myvertices
]
if all_pts:
arr = np.array(all_pts)
print(f"Shape: {arr.shape}")
print(arr)
[7]:
comp_zones.destroy()
print('MultiPolylineZonesCompanion destroyed.')
MultiPolylineZonesCompanion destroyed.
E — PolygonCompanion: closed polygon input
Colors: validated polygons in green, current polygon in orange.
Data: comp.polygons (list of lists; the closing segment last→first is implicit).
[4]:
comp_poly = polygon(viewer)
print(comp_poly)
INFO:root:ACTION : Right-click: vertex | Enter: close polygon (≥3 pts) | Esc: cancel current
<PolygonCompanion ns='polygoncompanion' menu=not built actions=[polygoncompanion.poly]>
[ ]:
print(f"{len(comp_poly.polygons)} finalized polygon(s)")
for j, poly in enumerate(comp_poly.polygons, 1):
print(f" Polygon {j}: {len(poly)} vertices")
for i, (x, y) in enumerate(poly, 1):
print(f" {i:3d}. X={x:.3f} Y={y:.3f}")
[ ]:
# Compute the area of each polygon (Shoelace formula)
import numpy as np
for j, poly in enumerate(comp_poly.polygons, 1):
v = np.array(poly)
x, y = v[:, 0], v[:, 1]
area = 0.5 * abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))
print(f"Polygon {j}: area = {area:.3f} m²")
[5]:
comp_poly.destroy()
print('PolygonCompanion destroyed.')
PolygonCompanion destroyed.
F — Customization: override class attributes
Colors and sizes are class attributes, so you can change them without rewriting any method.
[8]:
class BluePicker(PointPickerCompanion):
"""PointPickerCompanion with blue crosses and larger markers."""
COLOR_NORMAL = (0.1, 0.4, 1.0, 1.0) # blue
COLOR_SELECTED = (1.0, 1.0, 0.0, 1.0) # yellow
CROSS_FRACTION = 0.018 # larger crosses
class ThickPolyline(PolylineCompanion):
"""Thick red PolylineCompanion."""
COLOR_LINE = (1.0, 0.1, 0.1, 1.0) # red
LINE_WIDTH = 3.5
comp_blue = BluePicker()
viewer.attach_companion(comp_blue)
comp_blue.start()
print(comp_blue)
INFO:root:ACTION : Right-click: add | Left-click: select nearest | Ctrl+Z: undo | Esc: stop
<BluePicker ns='bluepicker' menu=not built actions=[bluepicker.pick]>
[9]:
# Cleanup:
comp_blue.destroy()
G — Extension: add click behavior
To add logic without rewriting the whole class, override only the corresponding private handler.
[10]:
class AnnotatedPicker(PointPickerCompanion):
"""Displays a numbered label in the status bar after each click."""
def _rdown(self, ctx):
super()._rdown(ctx) # add the point
n = len(self.points)
self.proxy.set_status(f'Point #{n} X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}')
comp_ann = AnnotatedPicker()
viewer.attach_companion(comp_ann)
comp_ann.start()
print("Click on the map — the status bar updates after each point.")
# comp_ann.destroy()
INFO:root:ACTION : Right-click: add | Left-click: select nearest | Ctrl+Z: undo | Esc: stop
Click on the map — the status bar updates after each point.
[ ]:
comp_ann.destroy()
print('Cleanup complete.')
Cleanup complete.
The Kernel crashed while executing code in the current cell or a previous cell.
Please review the code in the cell(s) to identify a possible cause of the failure.
Click <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info.
View Jupyter <a href='command:jupyter.viewOutput'>log</a> for further details.
Quick reference summary
from wolfhece.plugins.factory import (
point_picker, # → comp.points : list[(x, y)]
polyline, # → comp.vertices : list[(x, y)], comp.finished : bool
multi_polyline, # → comp.polylines : list[list[(x, y)]]
multi_polyline_zones, # → comp.zones : Zones (Zones→zone→vector hierarchy)
polygon, # → comp.polygons : list[list[(x, y)]]
)
# Startup
comp = multi_polyline_zones(viewer, zones_id='my_trace', auto_attach=True)
# Inspection
print(comp.zones) # Zones object (None before the first accepted line)
print(comp.zones.nbzones) # number of registered zones
# Stop
comp.stop() # deactivates, data preserved
comp.destroy() # deactivates + unregisters
# Manual attachment (when auto_attach=False)
comp2 = multi_polyline_zones(viewer, auto_attach=False)
# ... interaction ...
comp2.attach_zones() # adds the Zones to the viewer
# Reset
comp.clear_zones() # clears the Zones and sets zones=None
Customize without rewriting
Class attribute |
Type |
Role |
|---|---|---|
|
|
Color of unselected elements |
|
|
Color of the selected element |