feat(extract): Chunk-Zuordnung in Python und Beschriftungsliste lesen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-13 18:27:33 +02:00
1 parent a8817e2849
commit c9e0615a88
4 files changed
+299 -1

No files matched your search

@@ -0,0 +1,88 @@
import pytest
from leonida.annotations import KINDS, AnnotationError, load_annotations, parse_annotations
def test_missing_file_and_comment_only_file_mean_no_entries_and_no_grid(tmp_path):
assert load_annotations(tmp_path / "fehlt.toml").annotations == []
(tmp_path / "leer.toml").write_text("# nur Kommentare\n")
parsed = load_annotations(tmp_path / "leer.toml")
assert parsed.annotations == [] and parsed.keep == [] and parsed.grid is None
def test_all_spec_kinds_are_known():
assert set(KINDS) == {"text_white", "text_red", "marker", "symbol", "street_name", "region"}
def test_entries_parse_with_normalised_boxes_and_chunk_names():
parsed = parse_annotations("""
[grid]
spacing = 500
width = 6
[[annotation]]
name = "VICE BEACH (MIAMI BEACH)"
kind = "text_white"
box = [[1900, 705], [1350, 870]]
[[annotation]]
name = "Rest am Marker S2/32"
kind = "region"
polygon = [[1905, 640], [1925, 640], [1925, 615], [1905, 615]]
reason = "vom Nutzer markiert"
[[keep]]
box = [[-856, 194], [-794, -1]]
reason = "knallrotes Gebäude, echt"
""")
first, second = parsed.annotations
assert (first.index, first.kind, first.box) == (1, "text_white", (1350.0, 705.0, 1900.0, 870.0))
assert first.chunk == "K7"
assert second.polygon[0] == (1905.0, 640.0) and second.box is None and second.reason == "vom Nutzer markiert"
assert second.chunk == "L8"
assert parsed.keep[0].box == (-856.0, -1.0, -794.0, 194.0)
assert (parsed.grid.spacing, parsed.grid.width) == (500.0, 6.0)
@pytest.mark.parametrize("body, message", [
('name = "X"\nkind = "banner"\nbox = [[0, 0], [10, 10]]', "unbekanntes kind 'banner'"),
('kind = "marker"\nbox = [[0, 0], [10, 10]]', "Feld 'name' fehlt oder ist leer"),
('name = "X"\nkind = "marker"\nbox = [[0, 0], [10, 10]]\nhight = 3', "Feld 'hight' gibt es nicht"),
('name = "X"\nkind = "marker"', "Feld 'box' fehlt"),
('name = "X"\nkind = "marker"\nbox = [1, 2]', "Feld 'box' muss zwei Ecken"),
('name = "X"\nkind = "marker"\nbox = [[0, 0], [1, 5]]', "mindestens 2 m"),
('name = "X"\nkind = "marker"\nbox = [[1900, 0], [2100, 50]]', "außerhalb des Ausschnitts"),
('name = "X"\nkind = "marker"\npolygon = [[0, 0], [10, 0], [10, 10]]', "Feld 'polygon' gibt es nur bei region"),
('name = "X"\nkind = "region"\nbox = [[0, 0], [10, 10]]\npolygon = [[0, 0], [10, 0], [10, 10]]', "genau eines der Felder"),
('name = "X"\nkind = "region"\npolygon = [[0, 0], [10, 10], [10, 0], [0, 10]]', "ohne sich schneidende Kanten"),
('name = "X"\nkind = "region"\npolygon = [[0, 0], [10, 10]]', "mindestens 3 Punkte"),
('name = "X"\nkind = "marker"\nbox = [[0, 0], [10, 10]]\nreason = 7', "reason muss Text sein"),
])
def test_form_errors_stop_with_entry_number_and_field(body, message):
text = ('[[annotation]]\nname = "OK"\nkind = "marker"\nbox = [[0, 0], [10, 10]]\n\n'
f'[[annotation]]\n{body}\n')
with pytest.raises(AnnotationError, match="Beschriftung 2") as err:
parse_annotations(text)
assert message in str(err.value)
def test_keep_needs_a_reason():
with pytest.raises(AnnotationError, match="Ausnahme 1: Feld 'reason' fehlt oder ist leer"):
parse_annotations("[[keep]]\nbox = [[0, 0], [10, 10]]\n")
@pytest.mark.parametrize("body, message", [
("spacing = 0\nwidth = 6", "Gitternetz: Feld 'spacing' muss eine Zahl größer als 0 sein"),
("spacing = 500", "Gitternetz: Feld 'width' fehlt"),
("spacing = 500\nwidth = 6\ncolour = 1", "Gitternetz: Feld 'colour' gibt es nicht"),
])
def test_grid_form_errors(body, message):
with pytest.raises(AnnotationError, match=message):
parse_annotations(f"[grid]\n{body}\n")
def test_broken_toml_and_unknown_sections_are_form_errors():
with pytest.raises(AnnotationError, match="kein gültiges TOML"):
parse_annotations("[[annotation]\n")
with pytest.raises(AnnotationError, match="unbekannter Abschnitt 'label'"):
parse_annotations('[[label]]\nname = "X"\n')
+11 -1
View File
@@ -1,7 +1,7 @@
import json
from pathlib import Path
from leonida.coords import SLICE, chunk_name, parse_chunk_name, pixel_to_world, slice_tiles, tile_for_world
from leonida.coords import SLICE, chunk_for_world, chunk_name, parse_chunk_name, pixel_to_world, slice_tiles, tile_for_world
# Derselbe Beispielsatz wie tests/coords.test.js — JavaScript und Python dürfen nicht auseinanderlaufen.
NAMES = json.loads((Path(__file__).resolve().parents[3] / "tests/fixtures/chunk-names.json").read_text())
@@ -48,3 +48,13 @@ def test_chunk_names_round_trip_for_all_144_chunks():
def test_chunk_names_reject_invalid_index_types():
for i, j in NAMES["invalidIndices"]:
assert chunk_name(i, j) is None
def test_chunk_for_world_matches_javascript():
# Dieselben Beispiele wie tests/coords.test.js: Ecken des Ausschnitts, der Downtown-Bezugspunkt aus
# data/districts.json in B9 und der höchste Turm, das Southeast Financial Center, in C12.
assert chunk_for_world(-1000, 2500) == (0, 0)
assert chunk_for_world(1999, -499) == (11, 11)
assert chunk_for_world(-511, 254) == (1, 8)
assert chunk_name(*chunk_for_world(-511, 254)) == "B9"
assert chunk_name(*chunk_for_world(-461.31, -460.1)) == "C12"