commit 59fb2af11208c12eb4b0d3a93427fa564ec1eab5
parent 8d8ee742750950607771532de7378b381bd2972e
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date: Fri, 5 Jun 2026 21:33:44 +0300
feat: derive feature id from target layer, drop session counter (A2+T1)
Diffstat:
2 files changed, 63 insertions(+), 12 deletions(-)
diff --git a/profile_interpreter/profile_interpreter.py b/profile_interpreter/profile_interpreter.py
@@ -85,6 +85,23 @@ def _project_xy(xy, src_crs, dst_crs):
return p.x(), p.y()
+def _next_feature_id(layer):
+ """Next integer id for layer: max existing 'id' + 1, or 1.
+ Returns None if the layer has no 'id' field."""
+ names = {f.name() for f in layer.fields()}
+ if 'id' not in names:
+ return None
+ max_id = 0
+ for feat in layer.getFeatures():
+ try:
+ val = feat['id']
+ except (KeyError, IndexError):
+ continue
+ if val is not None and int(val) > max_id:
+ max_id = int(val)
+ return max_id + 1
+
+
class _ProfilePickTool(QgsPlotTool):
"""A plot tool that forwards left-button releases to a callback."""
@@ -107,7 +124,6 @@ class ProfileInterpreterPlugin:
self._pick_tool = None
self._pan_tool = None
self._layer = None
- self._next_id = 1
def initGui(self):
self._action = QAction('Interpret on Profile', self.iface.mainWindow())
@@ -191,10 +207,10 @@ class ProfileInterpreterPlugin:
feature = QgsFeature(layer.fields())
feature.setGeometry(QgsGeometry(QgsPoint(x, y, elevation)))
- current_id = self._next_id
+ feature_id = _next_feature_id(layer)
attrs = _point_attributes(
{f.name() for f in layer.fields()},
- current_id, distance, elevation,
+ feature_id, distance, elevation,
)
for k, v in attrs.items():
feature[k] = v
@@ -206,10 +222,9 @@ class ProfileInterpreterPlugin:
if hasattr(canvas, 'refresh'):
canvas.refresh()
- self._next_id += 1
-
+ id_label = f' #{feature_id}' if feature_id is not None else ''
print(
- f'{MENU}: placed point #{current_id} '
+ f'{MENU}: placed point{id_label} '
f'at x={x:.3f} y={y:.3f} z={elevation:.3f} '
f'(distance={distance:.3f})'
)
diff --git a/test/test_profile_interpreter.py b/test/test_profile_interpreter.py
@@ -12,6 +12,7 @@ fakeqgis.install()
from profile_interpreter.profile_interpreter import ( # noqa: E402
_is_suitable_target,
+ _next_feature_id,
_point_attributes,
_project_xy,
_LEFT_BUTTON,
@@ -164,6 +165,38 @@ class TestProjectXY(unittest.TestCase):
self.assertEqual(len(fq.QgsCoordinateTransform._calls), 1)
+# ── _next_feature_id ─────────────────────────────────────────────────────
+
+class TestNextFeatureId(unittest.TestCase):
+ def _make_layer_with_ids(self, id_values):
+ layer = fq.QgsVectorLayer('PointZ', 'test', 'memory')
+ layer._fields_list.append(fq.QgsField('id'))
+ for v in id_values:
+ feat = fq.QgsFeature(fq._FakeFields(layer._fields_list))
+ feat['id'] = v
+ layer._features.append(feat)
+ return layer
+
+ def test_empty_layer_returns_1(self):
+ layer = self._make_layer_with_ids([])
+ self.assertEqual(_next_feature_id(layer), 1)
+
+ def test_max_id_plus_one(self):
+ layer = self._make_layer_with_ids([1, 2, 5])
+ self.assertEqual(_next_feature_id(layer), 6)
+
+ def test_no_id_field_returns_none(self):
+ layer = fq.QgsVectorLayer('PointZ', 'test', 'memory')
+ self.assertIsNone(_next_feature_id(layer))
+
+ def test_absent_key_on_feature_is_skipped(self):
+ layer = fq.QgsVectorLayer('PointZ', 'test', 'memory')
+ layer._fields_list.append(fq.QgsField('id'))
+ # feature with no 'id' set in _attrs → KeyError → skipped, falls back to 1
+ layer._features.append(fq.QgsFeature(fq._FakeFields(layer._fields_list)))
+ self.assertEqual(_next_feature_id(layer), 1)
+
+
# ── _is_suitable_target ───────────────────────────────────────────────────
class TestIsSuitableTarget(unittest.TestCase):
@@ -206,19 +239,19 @@ class TestOnPickEarlyExits(unittest.TestCase):
canvas = _FakeCanvas(distance=math.nan, elevation=5.0)
plugin, _ = _make_plugin(canvas=canvas)
self._pick(plugin)
- self.assertEqual(plugin._next_id, 1) # not incremented
+ self.assertIsNone(plugin._layer)
def test_nan_elevation_skipped(self):
canvas = _FakeCanvas(distance=10.0, elevation=math.nan)
plugin, _ = _make_plugin(canvas=canvas)
self._pick(plugin)
- self.assertEqual(plugin._next_id, 1)
+ self.assertIsNone(plugin._layer)
def test_none_curve_skipped(self):
canvas = _FakeCanvas(curve=None)
plugin, _ = _make_plugin(canvas=canvas)
self._pick(plugin)
- self.assertEqual(plugin._next_id, 1)
+ self.assertIsNone(plugin._layer)
def test_empty_interpolated_geom_skipped(self):
canvas = _FakeCanvas()
@@ -226,7 +259,7 @@ class TestOnPickEarlyExits(unittest.TestCase):
fq.QgsGeometry._next_interpolate_empty = True
self._pick(plugin)
fq.QgsGeometry._next_interpolate_empty = False
- self.assertEqual(plugin._next_id, 1)
+ self.assertIsNone(plugin._layer)
# ── _on_pick successful pick ──────────────────────────────────────────────
@@ -245,10 +278,13 @@ class TestOnPickSuccess(unittest.TestCase):
self.assertIsNotNone(layer)
self.assertEqual(len(layer._features), 1)
- def test_next_id_increments(self):
+ def test_feature_ids_do_not_collide(self):
plugin, _ = _make_plugin()
plugin._on_pick(_FakeEvent())
- self.assertEqual(plugin._next_id, 2)
+ plugin._on_pick(_FakeEvent())
+ layer = plugin._layer
+ ids = [f._attrs.get('id') for f in layer._features]
+ self.assertEqual(ids, [1, 2])
def test_memory_layer_attrs_set(self):
plugin, _ = _make_plugin(active_layer=None)