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

wolfhece/PyDraw.py

WolfMapViewer: add_object, all _add_xxx handlers, helper methods, class-level constants

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

_ADD_FILTER_ARRAY

Raster arrays (.bin, .tif, .top, .flt, .npy, .npz, .vrt)

_ADD_FILTER_JSON

JSON files

_ADD_FILTER_ALL

Wildcard *.*

_ADD_FILTER_VECTOR

Vector / zone files (.vec, .vecz, .dxf, .shp)

_ADD_FILTER_CLOUD

Point-cloud files (.xyz, .laz, .las, .json, .shp)

_ADD_FILTER_LAZ

LAZ/LAS/NPZ point clouds

_ADD_FILTER_TRI

Triangulation files (.tri, .dxf, .gltf, .glb)

_ADD_FILTER_CS

Cross-section files (.vecz, .txt, .sxy, .xlsx)

_ADD_FILTER_IMAGE

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_kind is 'FileDialog' or 'DirDialog'.

  • filter_attr is the name of one of the _ADD_FILTER_xxx class attributes, or None for 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:

which value

Handler method

Collection attribute

Tree item

'array', 'array_crop'

_add_array

myarrays

myitemsarray

'array_xyz'

_add_array_xyz

myarrays

myitemsarray

'array_tiles'

_add_array_tiles

(recursive add_object )

'array_lidar_first', 'array_lidar_second'

_add_array_lidar

myarrays

myitemsarray

'picture_collection'

_add_picture_collection

mypicturecollections

myitemspictcollection

'imagestiles'

_add_imagestiles

myimagestiles

myitemsvector

'bridges'

_add_bridges

myvectors

myitemsvector

'weirs'

_add_weirs

myvectors

myitemsvector

'tiles', 'tilescomp'

_add_tiles

mytiles

myitemsvector

'res2d'

_add_res2d

myres2D

myitemsres2d

'res2d_gpu'

_add_res2d_gpu

myres2D

myitemsres2d

'vector'

_add_vector

myvectors

myitemsvector

'cross_sections'

_add_cross_sections

myvectors

myitemsvector

'laz'

_add_laz

mylazdata

myitemslaz

'cloud'

_add_cloud

myclouds

myitemscloud

'clouds'

_add_clouds

myclouds

myitemscloud

'triangulation'

_add_triangulation

mytri

myitemstri

'other'

_add_other

myothers

myitemsothers

'views'

_add_views

myviews

myitemsviews

'wmsback'

_add_wmsback

mywmsback

myitemswmsback

'wmsfore'

_add_wmsfore

mywmsfore

myitemswmsfore

'particlesystem'

_add_particlesystem

mypartsystems

myitemsps

'drowning'

_add_drowning

(via _drowning.register )

myitemsdrowning

'dike'

_add_dike

(via _dike.register )

myitemsdike

'injector'

_add_injector

myinjectors

myitemsinjector

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

which

str

'array'

Object category. Must be a key of _ADD_HANDLERS. Case-insensitive (lowercased internally).

filename

str

''

Path to the file or directory to load. When empty and newobj is also None, a dialog is shown.

newobj

object

None

Pre-constructed object. Bypasses both the dialog and object construction inside the handler.

ToCheck

bool

True

Whether to check (make visible) the item in the treelist after insertion.

id

str

''

Unique identifier for the object. When empty, a prompt is shown; uniqueness is enforced automatically.

Return value

Value

Meaning

0

Success

-1

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

(curtree, newobj, id)

Construction succeeded; continue with phases 5–6

None

Handler managed everything itself (e.g. recursive add_object calls); return 0 immediately

-1

Error or user cancellation; return -1

Phase 5 — ID resolution

_resolve_object_id(id, filename, all_ids):

  1. If id is empty, opens a wx.TextEntryDialog pre-filled with the filename stem. Loops until a non-empty, unique value is entered.

  2. If the resolved id already exists (e.g. supplied programmatically but colliding), appends a zero-padded counter: namename001.

Phase 6 — Tree registration

_register_object_in_tree(newobj, curtree, id, ToCheck, filename):

  1. Sets newobj.idx = id.lower().

  2. Appends a treelist item under curtree.

  3. Checks the item (and its parent) when ToCheck is True.

  4. Sets newobj.checked = ToCheck.

  5. For crosssections objects, automatically calls add_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 newobj from filename when newobj is None; otherwise reuse the supplied object.

  • Append the object to its collection (self.myarrays, self.myvectors, …).

  • Set the relevant active_xxx attribute 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 None when the handler has already inserted everything itself (e.g. recursive add_object for .npz multi-array files or tile maps).

  • Return -1 on 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 5 — Expose via menu or toolbar

# Inside OnPopupItemSelected, OnMenu, or a dedicated handler:
elif itemlabel == _('Add MyType'):
    self.add_object('mytype')

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 id is empty, prompts the user (pre-filled with Path(filename).stem).

  • If the id collides, appends 001, 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