Add Object Pipeline
This document describes the complete architecture of the add_object
pipeline in WolfMapViewer (wolfhece/PyDraw.py) and provides a
step-by-step guide for registering a new object type.
Overview
add_object is the single entry point for loading any object into the
viewer. It handles file selection, ID assignment, tree registration, and
delegates object construction to a per-type handler.
add_object(which, ...)
│
├─ Phase 1 — file/dir dialog (_prompt_file_for_type)
├─ Phase 2 — path existence check
├─ Phase 3 — snapshot of existing IDs
├─ Phase 4 — type dispatch (_ADD_HANDLERS → _add_xxx method)
│ │
│ ├─ returns (curtree, newobj, id) → continue
│ ├─ returns None → self-managed, return 0
│ └─ returns -1 → error/cancel, return -1
├─ Phase 5 — ID resolution (_resolve_object_id)
└─ Phase 6 — tree registration (_register_object_in_tree)
Files involved
File |
Role |
|---|---|
|
|
Class-level constants
Three dictionaries / string constants drive the pipeline. All are defined at
class level in WolfMapViewer.
_ADD_FILTER_xxx — file dialog wildcards
Named constants for the wildcard argument of wx.FileDialog:
Constant |
Used for |
|---|---|
|
Raster arrays ( |
|
JSON files |
|
Wildcard |
|
Vector / zone files ( |
|
Point-cloud files ( |
|
LAZ/LAS/NPZ point clouds |
|
Triangulation files ( |
|
Cross-section files ( |
|
GeoTIFF images |
_ADD_DIALOG_SPECS — dialog configuration
Maps a which string to a (dialog_kind, title, filter_attr) tuple used
by _prompt_file_for_type:
dialog_kindis'FileDialog'or'DirDialog'.filter_attris the name of one of the_ADD_FILTER_xxxclass attributes, orNonefor directory dialogs (which have no wildcard).
Types that always require a pre-built newobj (e.g. 'injector') are
intentionally absent from this dict — _prompt_file_for_type returns
('', 0) in that case and the pipeline skips the dialog.
_ADD_HANDLERS — type dispatch
Maps each which string to the name of the private handler method that
constructs and wires up the object:
|
Handler method |
Collection attribute |
Tree item |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
(recursive |
— |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(via |
|
|
|
(via |
|
|
|
|
|
Public method: add_object
def add_object(self,
which: Literal['array', 'vector', 'cloud', ...] = 'array',
filename: str = '',
newobj=None,
ToCheck: bool = True,
id: str = '') -> int:
Parameters
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Object category. Must be a key of |
|
|
|
Path to the file or directory to load. When empty and |
|
object |
|
Pre-constructed object. Bypasses both the dialog and object construction inside the handler. |
|
|
|
Whether to check (make visible) the item in the treelist after insertion. |
|
|
|
Unique identifier for the object. When empty, a prompt is shown; uniqueness is enforced automatically. |
Return value
Value |
Meaning |
|---|---|
|
Success |
|
Error or user cancellation |
Typical call sites
# From a menu handler — open a file dialog, then load:
self.add_object('array')
# From code — supply an already-constructed object:
self.add_object('cloud', newobj=my_cloud, id='survey_2024', ToCheck=False)
# From code — supply a file path and an explicit ID:
self.add_object('vector', filename='/path/to/zones.vec', id='flood_zones')
Pipeline phases
Phase 1 — File/directory dialog
_prompt_file_for_type(which) looks up _ADD_DIALOG_SPECS[which] and
opens the appropriate dialog. Returns (filename, curfilter) on success or
None if the user cancels (causing add_object to return -1).
Types absent from _ADD_DIALOG_SPECS (e.g. 'injector') silently return
('', 0) — the dialog is skipped.
Phase 1 is skipped entirely when filename != '' or newobj is not None.
Phase 2 — Path existence check
If filename is non-empty, os.path.exists(filename) is checked. A
warning is logged and -1 is returned when the path does not exist.
Phase 3 — ID snapshot
get_list_keys(None, checked_state=None) collects all IDs currently loaded
in the viewer before the new object is added. This snapshot is used later
by _resolve_object_id to guarantee uniqueness.
Phase 4 — Type dispatch
handler_result = getattr(self, self._ADD_HANDLERS[which])(
which, filename, newobj, curfilter, ToCheck, id)
The handler receives the raw arguments and returns one of three values:
Return value |
Meaning |
|---|---|
|
Construction succeeded; continue with phases 5–6 |
|
Handler managed everything itself (e.g. recursive |
|
Error or user cancellation; return |
Phase 5 — ID resolution
_resolve_object_id(id, filename, all_ids):
If
idis empty, opens awx.TextEntryDialogpre-filled with the filename stem. Loops until a non-empty, unique value is entered.If the resolved
idalready exists (e.g. supplied programmatically but colliding), appends a zero-padded counter:name→name001.
Phase 6 — Tree registration
_register_object_in_tree(newobj, curtree, id, ToCheck, filename):
Sets
newobj.idx = id.lower().Appends a
treelistitem undercurtree.Checks the item (and its parent) when
ToCheckisTrue.Sets
newobj.checked = ToCheck.For
crosssectionsobjects, automatically callsadd_object('cloud', ...)twice to register the intersection cloud and the full-survey cloud.
Handler contract
Every _add_xxx method has the signature:
def _add_xxx(self, which: str, filename: str, newobj, curfilter: int,
ToCheck: bool, id: str):
Rules:
Construct
newobjfromfilenamewhennewobj is None; otherwise reuse the supplied object.Append the object to its collection (
self.myarrays,self.myvectors, …).Set the relevant
active_xxxattribute on the viewer.Call any required menu-creation helper (
self.menu_bridges(), etc.).Return
(curtree, newobj, id)to hand off to phases 5–6.Return
Nonewhen the handler has already inserted everything itself (e.g. recursiveadd_objectfor.npzmulti-array files or tile maps).Return
-1on error or user cancellation.
Important
Handlers must not call _resolve_object_id or
_register_object_in_tree themselves (unless they return None).
Adding a new object type: step-by-step guide
Step 1 — Add a file-dialog wildcard (if needed)
Add a class-level constant in WolfMapViewer:
_ADD_FILTER_MYTYPE = "MyType files (*.ext)|*.ext|all (*.*)|*.*"
Step 2 — Register the dialog specification
Add an entry to _ADD_DIALOG_SPECS:
'mytype': ('FileDialog', "Choose a MyType file", '_ADD_FILTER_MYTYPE'),
# or for a directory:
'mytype': ('DirDialog', "Choose a directory for MyType", None),
Omit this entry only if add_object will always be called with an explicit
filename or newobj (e.g. purely programmatic use).
Step 3 — Register the handler name
Add an entry to _ADD_HANDLERS:
'mytype': '_add_mytype',
Step 4 — Write the handler
def _add_mytype(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
with wx.BusyInfo(_('Importing MyType')):
wait = wx.BusyCursor()
newobj = MyType(filename, mapviewer=self)
del wait
self.mymytypes.append(newobj)
self.active_mytype = newobj
# call self.menu_mytype() if a dedicated context menu exists
return self.myitemsmytype, newobj, id
Add the corresponding treelist root item (e.g. myitemsmytype) to
WolfMapViewer.__init__ alongside the other mytreelist.AppendItem
calls.
Step 6 — Update the which type hint
Extend the Literal[...] annotation on add_object’s which
parameter:
which: Literal[..., 'mytype'] = 'array',
Step 7 — Unit tests
class TestAddMyType(unittest.TestCase):
def _make_viewer(self):
v = MagicMock(spec=WolfMapViewer)
v.mymytypes = []
v.active_mytype = None
v.myitemsmytype = MagicMock()
return v
def test_nominal_with_newobj(self):
v = self._make_viewer()
obj = MagicMock()
result = v._add_mytype('mytype', '', obj, 0, True, 'test_id')
self.assertEqual(result[1], obj)
self.assertIn(obj, v.mymytypes)
def test_returns_minus1_on_cancel(self):
v = self._make_viewer()
# simulate user cancel inside the handler
with patch('wolfhece.PyDraw.wx.BusyInfo', side_effect=Exception):
result = v._add_mytype('mytype', 'missing.ext', None, 0, True, '')
self.assertEqual(result, -1)
Reference: helper methods
_prompt_file_for_type(which)
def _prompt_file_for_type(self, which: str) -> tuple[str, int] | None:
Opens a wx.FileDialog or wx.DirDialog according to
_ADD_DIALOG_SPECS. Returns (filename, curfilter) on success, None
on cancel. curfilter is the zero-based index of the selected filter
(always 0 for directory dialogs).
_resolve_object_id(id, filename, all_ids)
def _resolve_object_id(self, id: str, filename: str, all_ids: list) -> str:
Ensures the returned id is non-empty and unique:
If
idis empty, prompts the user (pre-filled withPath(filename).stem).If the
idcollides, appends001,002, … until unique.
_register_object_in_tree(newobj, curtree, id, ToCheck, filename)
def _register_object_in_tree(self, newobj, curtree, id: str,
ToCheck: bool, filename: str) -> None:
Final wiring step. Sets newobj.idx, inserts the treelist item, checks it,
and handles the crosssections special case (auto-registers two cloud
objects).
Checklist
☐ _ADD_FILTER_MYTYPE constant added (if a new file format)
☐ Entry added to _ADD_DIALOG_SPECS (skip only for always-programmatic types)
☐ Entry added to _ADD_HANDLERS
☐ _add_mytype() handler written
☐ └─ appends to collection, sets active_xxx, calls menu helper
☐ └─ returns (curtree, newobj, id) / None / -1 correctly
☐ myitemsmytype treelist root added in __init__
☐ Triggered from menu/toolbar via add_object('mytype')
☐ 'mytype' added to Literal[...] annotation on add_object
☐ Unit tests added