pipeworks_mud_mapper.components.map_view ======================================== .. py:module:: pipeworks_mud_mapper.components.map_view .. autoapi-nested-parse:: Plotly-based map visualization component for the MUD Mapper. This module provides the core visualization functions for rendering MUD zone maps using Plotly. It creates interactive 2D map views that display rooms as nodes and exits as connecting lines, with support for: - **Flattened multi-level display**: All Z-levels shown on a single 2D plane - **Visual Z-level differentiation**: Size and color distinguish floor levels - **Room selection highlighting**: Selected room shown in red - **Exit visualization**: Lines for cardinal directions, labels for up/down, and triangle markers for cross-zone exits - **Interactive pan, zoom, and hover tooltips** Design Principles ----------------- 1. **Flattened View**: All rooms rendered on one plane regardless of Z 2. **Z-Ordered Rendering**: Rooms drawn back-to-front so ground level is clickable 3. **Separation of Concerns**: Base figure creation separate from room rendering 4. **Graceful Degradation**: Handles missing data, empty rooms, invalid exits 5. **Deterministic Output**: Same inputs produce identical figures Coordinate System ----------------- The map follows standard cartographic conventions: - **X-axis**: East (+) / West (-), displayed left-to-right - **Y-axis**: North (+) / South (-), displayed bottom-to-top - **Z-axis**: Represented by visual styling (size/color), not position The coordinate range is -20 to +20 on both axes, with gridlines every 5 units. Visual Design ------------- Rooms are styled by Z-level for easy identification: - **z=-1 (Down)**: Black filled, 14px (smallest) - cellars, basements - **z=+1 (Up)**: White with black border, 18px (medium) - towers, attics - **z=0 (Ground)**: Blue filled, 20px (largest) - main floor - **Selected**: Red, regardless of Z-level Exit connections: - **Cardinal exits (N/E/S/W)**: Gray lines between rooms on same Z-level - **Vertical exits (U/D)**: Dashed lines between stacked rooms plus U/D labels - **Zone exits**: Triangle markers pointing in the exit direction - **Crosshair**: Dashed gray lines at origin for orientation Constants --------- Z_LEVEL_STYLES : dict[int, dict] Visual styling for each Z-level (size, color, border) Z_RENDER_ORDER : list[int] Order in which Z-levels are rendered (back to front) SELECTED_ROOM_COLOR : str Color for highlighted/selected rooms Functions --------- create_map_figure(title) -> go.Figure Create empty map canvas with grid and crosshair create_map_figure_with_rooms(rooms, visible_z_levels, selected_room) -> go.Figure Create map with rooms and exits rendered across multiple Z-levels Internal Functions ------------------ _group_rooms_by_z_level(rooms, visible_z_levels) -> dict Group rooms into buckets by their Z coordinate _find_stacked_positions(rooms, visible_z_levels) -> dict Find X,Y positions with rooms at multiple Z-levels _draw_all_exit_lines(fig, rooms, visible_z_levels) -> None Draw connecting lines for cardinal exits _draw_vertical_exit_lines(fig, rooms, visible_z_levels) -> None Draw dashed lines for up/down exits _draw_zone_exit_markers(fig, rooms, visible_z_levels) -> None Draw directional markers for cross-zone exits _draw_rooms_at_z_level(fig, rooms_at_level, z_level, selected_room) -> None Render rooms for a single Z-level as a Scatter trace _add_vertical_exit_labels(fig, stacked_positions, rooms) -> None Add "U/D" labels near stacked rooms with vertical exits Usage ----- Create an empty map canvas:: >>> from pipeworks_mud_mapper.components.map_view import create_map_figure >>> fig = create_map_figure() >>> fig.show() # Opens in browser Render rooms from all Z-levels:: >>> from pipeworks_mud_mapper.components.map_view import ( ... create_map_figure_with_rooms ... ) >>> rooms = { ... "ground": {"name": "Ground", "coords": [0, 0, 0], "exits": {"down": "cellar"}}, ... "cellar": {"name": "Cellar", "coords": [0, 0, -1], "exits": {"up": "ground"}}, ... } >>> fig = create_map_figure_with_rooms(rooms, selected_room="ground") >>> fig.show() Filter to specific Z-levels:: >>> fig = create_map_figure_with_rooms(rooms, visible_z_levels=[0]) # Ground only Integration with Dash:: >>> import dash >>> from dash import dcc >>> fig = create_map_figure_with_rooms(rooms) >>> graph = dcc.Graph(figure=fig, id="map-graph") Architecture ------------ The module uses a layered rendering approach: 1. **Base Figure** (create_map_figure) - Creates the canvas with grid, crosshair, and axis labels - Fixed dimensions (700x650) to prevent layout thrashing 2. **Exit Lines** (_draw_all_exit_lines) - Drawn first so rooms appear on top - Only cardinal directions on same Z-level 3. **Vertical Exit Lines** (_draw_vertical_exit_lines) - Dashed connectors between stacked rooms with U/D exits 4. **Zone Exit Markers** (_draw_zone_exit_markers) - Directional triangles for cross-zone exits 5. **Room Nodes** (_draw_rooms_at_z_level) - Rendered in Z-order: z=-1 first, z=+1 second, z=0 last - Ground level on top ensures it receives clicks 6. **Vertical Labels** (_add_vertical_exit_labels) - Added last as annotations - Show U/D for stacked rooms with vertical exits Performance Considerations -------------------------- - Rooms batched into single Scatter trace per Z-level - Exit lines deduplicated (bidirectional exits draw once) - Zone exit markers render as a single Scatter trace - For zones with 100+ rooms, consider filter controls - Figure creation is synchronous and typically < 50ms Attributes ---------- .. autoapisummary:: pipeworks_mud_mapper.components.map_view.Z_LEVEL_STYLES pipeworks_mud_mapper.components.map_view.Z_RENDER_ORDER pipeworks_mud_mapper.components.map_view.SELECTED_ROOM_COLOR pipeworks_mud_mapper.components.map_view.Z_LEVEL_VISUAL_OFFSET pipeworks_mud_mapper.components.map_view.ZONE_EXIT_MARKER_COLOR pipeworks_mud_mapper.components.map_view.ZONE_EXIT_MARKER_OFFSETS pipeworks_mud_mapper.components.map_view.ZONE_EXIT_MARKER_SYMBOLS Functions --------- .. autoapisummary:: pipeworks_mud_mapper.components.map_view.create_map_figure pipeworks_mud_mapper.components.map_view.create_map_figure_with_rooms Module Contents --------------- .. py:data:: Z_LEVEL_STYLES :type: dict[int, dict] Visual styling configuration for each Z-level. Each Z-level maps to a dictionary containing: - **size**: Marker diameter in pixels - **color**: Fill color (rgba string) - **border_color**: Marker outline color - **border_width**: Marker outline width in pixels - **label**: Human-readable label for legends/tooltips The size hierarchy (14 < 18 < 20) ensures that when rooms overlap at the same X,Y position, the ground-level room (largest) is most visible and receives clicks first due to render order. .. py:data:: Z_RENDER_ORDER :type: list[int] Order in which Z-levels are rendered (back to front). Rooms are drawn in this order, meaning: 1. z=-1 (Down) is drawn first, appearing at the back 2. z=+1 (Up) is drawn second 3. z=0 (Ground) is drawn last, appearing on top This ensures that when rooms overlap at the same X,Y position (stacked vertically), the ground-level room is on top and receives mouse clicks. Users can use the Z-level filter to hide ground level when they need to select a room below or above. .. py:data:: SELECTED_ROOM_COLOR :type: str :value: 'rgba(255, 100, 100, 1)' Color used for the currently selected room. This red color overrides the Z-level-based color when a room is selected, providing clear visual feedback regardless of which floor the selected room is on. The color is intentionally bright and distinct from all Z-level colors. .. py:data:: Z_LEVEL_VISUAL_OFFSET :type: dict[int, tuple[float, float]] Visual X,Y offset applied to rooms based on Z-level. When rooms are stacked at the same logical X,Y position but different Z-levels, this offset separates them visually so they don't overlap. The diagonal offset creates an intuitive "stacking" effect: - z=-1 (Down): Shifted left and down, appearing "behind" - z=0 (Ground): No offset, the reference position - z=+1 (Up): Shifted right and up, appearing "in front" The offset value (0.4) is small enough that stacked rooms are clearly at the "same position" but large enough to be visually distinct and separately clickable. Note: This is purely visual. The logical coordinates used for U/D connections remain unchanged. .. py:data:: ZONE_EXIT_MARKER_COLOR :type: str :value: 'rgba(255, 165, 0, 0.85)' Color used for cross-zone exit markers. Zone exits are visually distinct from local exits so authors can quickly spot which directions lead out of the current zone. The orange hue reads as a "handoff" indicator without overpowering room nodes. .. py:data:: ZONE_EXIT_MARKER_OFFSETS :type: dict[str, tuple[float, float]] Offsets for positioning zone exit markers relative to the room node. Each offset is expressed in map units and is applied after the Z-level visual offset. This keeps markers near their originating room but prevents overlap with the room node itself. .. py:data:: ZONE_EXIT_MARKER_SYMBOLS :type: dict[str, str] Plotly marker symbols for zone exits by direction. .. py:function:: create_map_figure(title = None) Create the base map figure with crosshair and grid. Creates an empty Plotly figure configured as a 2D map canvas with: - Cartesian grid with 5-unit spacing - Crosshair at origin for orientation - Compass-labeled axes (N/S/E/W at extremes) - Fixed dimensions to prevent resize loops in Dash This function creates only the canvas - use create_map_figure_with_rooms to add room visualizations. :param title: Title text to display above the map (default: None). If None, no title is shown. This is the default for flattened views where a Z-level title would be misleading. :type title: :py:class:`str | None`, *optional* :returns: Plotly Figure configured as an interactive map canvas. The figure has: - Range: -20 to +20 on both axes - Grid: 5-unit spacing with light gray lines - Crosshair: Dashed gray lines at x=0 and y=0 - Dimensions: 700x650 pixels (fixed) :rtype: :py:class:`go.Figure` .. admonition:: Examples Create a map canvas with no title (default):: >>> fig = create_map_figure() >>> fig.layout.title is None or fig.layout.title.text is None True Create a map canvas with a custom title:: >>> fig = create_map_figure(title="Crooked Pipe District") >>> fig.layout.title.text 'Crooked Pipe District' .. admonition:: Notes - The figure uses fixed dimensions (autosize=False) to prevent infinite resize loops when embedded in Dash layouts - Axis labels show compass directions at the extremes: "W 20" at x=-20, "E 20" at x=+20, "S 20" at y=-20, "N 20" at y=+20 - The scaleanchor constraint ensures 1:1 aspect ratio (square grid) - Pan and zoom are enabled (fixedrange=False) .. seealso:: :py:obj:`create_map_figure_with_rooms` Add rooms to the map canvas .. py:function:: create_map_figure_with_rooms(rooms = None, visible_z_levels = None, selected_room = None, visual_offset_x = 1.0, visual_offset_y = 1.0) Create map figure with all rooms flattened onto a single 2D plane. Renders a complete zone map showing rooms from multiple Z-levels simultaneously. Rooms are visually differentiated by Z-level using size and color, with the selected room highlighted in red. The rendering order ensures that ground-level rooms (z=0) appear on top and receive clicks first when rooms overlap at the same X,Y position. :param rooms: Dictionary mapping room IDs to room data. Each room should have: - "coords": [x, y, z] list of coordinates - "name": Display name (falls back to room_id if missing) - "exits": Dict mapping direction to target room_id If None or empty, returns base map only. :type rooms: :py:class:`dict[str`, :py:class:`dict] | None`, *optional* :param visible_z_levels: List of Z-levels to display (default: None, which shows all). Pass ``[-1, 0, 1]`` explicitly for all levels, or a subset like ``[0]`` to show only ground level, ``[-1, 0]`` to hide upper level. :type visible_z_levels: :py:class:`list[int] | None`, *optional* :param selected_room: Room ID to highlight as selected (default: None). Selected room appears in red regardless of its Z-level. :type selected_room: :py:class:`str | None`, *optional* :param visual_offset_x: X-axis scale factor for Z-level visual offset (default: 1.0). :type visual_offset_x: :py:class:`float`, *optional* :param visual_offset_y: Y-axis scale factor for Z-level visual offset (default: 1.0). :type visual_offset_y: :py:class:`float`, *optional* :returns: * :py:class:`go.Figure` -- Plotly Figure with rooms rendered in Z-order. * :py:class:`Visual Styling` * :py:class:`--------------` * :py:class:`Rooms are styled based on their Z coordinate` * **- **z=-1 (Down)**** (:py:class:`Black filled`, :py:class:`14px - smallest`, :py:class:`drawn first (back)`) * **- **z=+1 (Up)**** (:py:class:`White with black border`, :py:class:`18px - medium`, :py:class:`drawn second`) * **- **z=0 (Ground)**** (:py:class:`Blue filled`, :py:class:`20px - largest`, :py:class:`drawn last (front)`) * **- **Selected**** (:py:class:`Always red`, :py:class:`regardless` of :py:class:`Z-level`) * :py:class:`Exit connections` * **- **Cardinal (N/E/S/W)**** (:py:class:`Gray lines between rooms on same Z-level`) * **- **Vertical (U/D)**** (``"U"``, ``"D"``, or ``"U/D"`` labels near stacked rooms) .. admonition:: Examples Render all rooms from a zone:: >>> rooms = { ... "ground": { ... "name": "Ground Floor", ... "coords": [0, 0, 0], ... "exits": {"down": "cellar", "north": "hall"} ... }, ... "cellar": { ... "name": "The Cellar", ... "coords": [0, 0, -1], ... "exits": {"up": "ground"} ... }, ... "hall": { ... "name": "Great Hall", ... "coords": [0, 5, 0], ... "exits": {"south": "ground"} ... }, ... } >>> fig = create_map_figure_with_rooms(rooms) Filter to ground level only:: >>> fig = create_map_figure_with_rooms(rooms, visible_z_levels=[0]) Highlight a selected room:: >>> fig = create_map_figure_with_rooms( ... rooms, selected_room="cellar" ... ) Handle empty rooms gracefully:: >>> fig = create_map_figure_with_rooms(None) # Returns base map >>> fig = create_map_figure_with_rooms({}) # Returns base map .. admonition:: Notes - Rooms without "coords" key are treated as being at [0, 0, 0] - Cross-zone exits (containing ':') are skipped during line drawing - Exit lines are drawn before room nodes (proper layering) - Vertical exits show text labels instead of lines - Hover text shows room name, ID, Z-level, and exit directions - When rooms overlap (stacked vertically), ground level is on top Click Selection with Stacked Rooms ---------------------------------- When rooms share the same X,Y but different Z, they overlap visually. Due to render order, ground level (z=0) receives clicks first. To select a lower-level room: 1. Uncheck "Ground (z=0)" in the layer filter 2. Click the now-visible lower-level room 3. Re-check ground level when done .. seealso:: :py:obj:`create_map_figure` Create empty map canvas :py:obj:`Z_LEVEL_STYLES` Visual configuration for each Z-level :py:obj:`Z_RENDER_ORDER` Rendering order for Z-levels