When a scenario/session XML is loaded, any node whose has a coordinate of exactly 0 has its entire position discarded and falls back to the Position default of (0.0, 0.0). The node therefore jumps to the top-left corner of the canvas instead of where the file says.
The trigger is a single coordinate being 0:
x="0" or y="0" drops the Cartesian position.
lat="0" (equator), lon="0" (prime meridian), or alt="0" (sea level) drops the geo position.
Because exporters very commonly write alt="0.0", the geo branch is effectively always skipped, and a node sitting on the canvas top/left edge (x=0 or y=0) is the common real-world trigger for the Cartesian branch.
Version: CORE 9.2.0
ROOT CAUSE
daemon/core/xml/corexml.py guards every position assignment with all([...]), which uses Python truthiness — and 0.0 is falsy. So a legitimate zero coordinate is indistinguishable from a missing one. get_float() already returns None for a genuinely-missing attribute, so the intent ("only set if present") should be an explicit "is not None" check, not all([...]).
read_device (around line 660):
x = get_float(position_element, "x")
y = get_float(position_element, "y")
if all([x, y]): # all([1488.0, 0.0]) -> False, so set() is skipped
position.set(x, y)
lat = get_float(position_element, "lat")
lon = get_float(position_element, "lon")
alt = get_float(position_element, "alt")
if all([lat, lon, alt]): # alt is typically 0.0 -> always False
position.set_geo(lon, lat, alt)
Position defaults to the origin (daemon/core/nodes/base.py):
class Position:
x: float = 0.0
y: float = 0.0
z: float = 0.0
So when the guard is False, position.set(...) is never called and the node keeps (0.0, 0.0).
The same pattern appears 6 times across 3 methods:
read_session_origin: x/y guard ~line 584, lat/lon/alt guard ~line 574
read_device: x/y guard ~line 701, lat/lon/alt guard ~line 706
read_network: x/y guard ~line 728, lat/lon/alt guard ~line 733
MINIMAL DEMONSTRATION
The whole bug in two lines:
x, y = 1488.0, 0.0 # a node on the canvas top edge
all([x, y])
False # -> position.set(x, y) is skipped -> node stays at Position() == (0, 0)
STEPS TO REPRODUCE (GUI)
Open the GUI and place a router.
Set its position to something with a zero coordinate, e.g. x=500, y=0 (drag it to the very top edge, or edit the saved XML so ).
Save the session to XML.
Open that XML (core-gui -s session.xml, or File -> Open).
Expected: the node appears at (500, 0).
Actual: the node appears at (0, 0) (top-left corner). Any other node in the file with a zero x or y collapses to the same corner.
SUGGESTED FIX
Replace the truthiness checks with explicit None checks in all three methods:
-
if x is not None and y is not None:
position.set(x, y)
-
if lat is not None and lon is not None and alt is not None:
position.set_geo(lon, lat, alt)
get_float() returns None for a missing attribute, so this preserves the "only set when present" intent while accepting legitimate zero coordinates. The same change applies at the read_session_origin, read_device, and read_network sites.
When a scenario/session XML is loaded, any node whose has a coordinate of exactly 0 has its entire position discarded and falls back to the Position default of (0.0, 0.0). The node therefore jumps to the top-left corner of the canvas instead of where the file says.
The trigger is a single coordinate being 0:
x="0" or y="0" drops the Cartesian position.
lat="0" (equator), lon="0" (prime meridian), or alt="0" (sea level) drops the geo position.
Because exporters very commonly write alt="0.0", the geo branch is effectively always skipped, and a node sitting on the canvas top/left edge (x=0 or y=0) is the common real-world trigger for the Cartesian branch.
Version: CORE 9.2.0
ROOT CAUSE
daemon/core/xml/corexml.py guards every position assignment with all([...]), which uses Python truthiness — and 0.0 is falsy. So a legitimate zero coordinate is indistinguishable from a missing one. get_float() already returns None for a genuinely-missing attribute, so the intent ("only set if present") should be an explicit "is not None" check, not all([...]).
read_device (around line 660):
x = get_float(position_element, "x")
y = get_float(position_element, "y")
if all([x, y]): # all([1488.0, 0.0]) -> False, so set() is skipped
position.set(x, y)
lat = get_float(position_element, "lat")
lon = get_float(position_element, "lon")
alt = get_float(position_element, "alt")
if all([lat, lon, alt]): # alt is typically 0.0 -> always False
position.set_geo(lon, lat, alt)
Position defaults to the origin (daemon/core/nodes/base.py):
class Position:
x: float = 0.0
y: float = 0.0
z: float = 0.0
So when the guard is False, position.set(...) is never called and the node keeps (0.0, 0.0).
The same pattern appears 6 times across 3 methods:
read_session_origin: x/y guard ~line 584, lat/lon/alt guard ~line 574
read_device: x/y guard ~line 701, lat/lon/alt guard ~line 706
read_network: x/y guard ~line 728, lat/lon/alt guard ~line 733
MINIMAL DEMONSTRATION
The whole bug in two lines:
Open the GUI and place a router.
Set its position to something with a zero coordinate, e.g. x=500, y=0 (drag it to the very top edge, or edit the saved XML so ).
Save the session to XML.
Open that XML (core-gui -s session.xml, or File -> Open).
Expected: the node appears at (500, 0).
Actual: the node appears at (0, 0) (top-left corner). Any other node in the file with a zero x or y collapses to the same corner.
SUGGESTED FIX
Replace the truthiness checks with explicit None checks in all three methods:
get_float() returns None for a missing attribute, so this preserves the "only set when present" intent while accepting legitimate zero coordinates. The same change applies at the read_session_origin, read_device, and read_network sites.