CSV-Export: Nachbarschaftserkennung neu aufgebaut (ohne shapely), Fehlerspalte ergaenzt
Nachbarschaft wird jetzt in drei getrennten Gruppen geprueft statt global
ueber alle Elemente: Kreisel/Eckrad gegeneinander, Kreisel/Eckrad gegen
Gefaellestrecke/Foerderer/Strecke-Modul (diese drei nicht gegeneinander),
und Omniflo-Elemente gegeneinander (rasterbasiert vorgefiltert nach x/y-
Koordinate). Ersetzt die bisherige shapely-STRtree-Loesung durch reines
Python, da die shapely-Abhaengigkeit den regulaeren CSV-Export in der von
BricsCAD genutzten Python-Umgebung brechen konnte.
Neue Spalte "Fehler" markiert Elemente mit zu wenigen Partnern
("unverbunden"/"nur ein Partner") je nach TeileArt-Mindestanzahl.
Ausserdem: SSG_RUN_ALL_TESTS-Absturz nach TEST_KREISEL behoben (ssg-start/
ssg-end nutzten flache globale Variablen statt eines Stacks, wodurch
verschachtelte Aufrufe den *error*-Handler dauerhaft korrumpierten) und
FILEDIA=0 um den DXFOUT-Kommandozeilenaufruf ergaenzt.
This commit is contained in:
+49
-21
@@ -11,7 +11,7 @@ Aufruf:
|
||||
python export_csv.py <export_raw.json> <data_dir> <output.csv>
|
||||
|
||||
CSV-Format:
|
||||
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Merkmale
|
||||
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Fehler;Merkmale
|
||||
|
||||
Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per
|
||||
vla-getboundingbox) ermittelten Bounding-Box je Block:
|
||||
@@ -20,10 +20,21 @@ vla-getboundingbox) ermittelten Bounding-Box je Block:
|
||||
Planquadrat wird aus der x/y-Koordinate des Blocks berechnet, siehe
|
||||
export_planquadrat.py ([Planquadrate] in cfg/export.cfg).
|
||||
Nachbarn = kommaseparierte TeileId-Liste ueberschneidender Elemente (Bounding-
|
||||
Box-Ueberschneidung in der x/y-Ebene per shapely-STRtree, Toleranz konfigurierbar
|
||||
ueber [Nachbarschaft] -> toleranz_mm in cfg/export.cfg), siehe export_neighbors.py.
|
||||
Position, Boundingbox, Planquadrat und Nachbarn sind nur fuer EXPORTCSV
|
||||
vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export.
|
||||
Box-Ueberschneidung in der x/y-Ebene, reines Python ohne externe Abhaengigkeit,
|
||||
siehe export_neighbors.py). Geprueft wird NICHT global ueber alle Elemente,
|
||||
sondern in drei getrennten Gruppen: 1) Kreisel/Eckrad gegen Kreisel/Eckrad,
|
||||
2) Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul (diese drei
|
||||
nicht gegeneinander), 3) Omniflo-Elemente gegen Omniflo-Elemente (rasterbasiert
|
||||
vorgefiltert nach x/y-Koordinate). Toleranz und Omniflo-Rastergroesse
|
||||
konfigurierbar ueber [Nachbarschaft] in cfg/export.cfg.
|
||||
Fehler = "unverbunden" (keine Partner, aber mindestens einer noetig),
|
||||
"nur ein Partner" (ein Partner, aber mindestens zwei noetig) oder leer
|
||||
(Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/
|
||||
Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/
|
||||
Foerderer/Strecke-Modul mindestens zwei (siehe export_neighbors.py
|
||||
MIN_PARTNER).
|
||||
Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer
|
||||
EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -31,12 +42,18 @@ import sys
|
||||
import os
|
||||
|
||||
from export_blockpatterns import load_patterns, matches_any
|
||||
from export_planquadrat import load_planquadrat_config, compute_planquadrat
|
||||
from export_neighbors import load_neighbor_tolerance_mm, compute_neighbor_ids
|
||||
from export_planquadrat import load_planquadrat_config, resolve_origins, compute_planquadrat
|
||||
from export_neighbors import (
|
||||
load_neighbor_tolerance_mm,
|
||||
load_omniflo_cell_size_mm,
|
||||
compute_neighbor_ids,
|
||||
compute_neighbor_errors,
|
||||
)
|
||||
|
||||
BLOCKPATTERNS = load_patterns()
|
||||
PLANQUADRAT_CFG = load_planquadrat_config()
|
||||
NEIGHBOR_TOLERANCE_MM = load_neighbor_tolerance_mm()
|
||||
NEIGHBOR_OMNIFLO_CELL_SIZE_MM = load_omniflo_cell_size_mm()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -258,19 +275,21 @@ def bbox_columns(block):
|
||||
}
|
||||
|
||||
|
||||
def planquadrat_column(block):
|
||||
def planquadrat_column(block, planquadrat_cfg):
|
||||
"""Berechnet das Planquadrat aus x/y des Blocks (siehe export_planquadrat.py).
|
||||
|
||||
planquadrat_cfg = Rueckgabe von resolve_origins() fuer den aktuellen
|
||||
Exportlauf (x_origin_mm/y_origin_mm bereits aufgeloest).
|
||||
Leerer String, wenn keine Planquadrat-Konfiguration geladen werden konnte
|
||||
(fehlende/unvollstaendige [Planquadrate]-Sektion in cfg/export.cfg).
|
||||
"""
|
||||
if PLANQUADRAT_CFG is None:
|
||||
if planquadrat_cfg is None:
|
||||
return ""
|
||||
x = block.get("x")
|
||||
y = block.get("y")
|
||||
if x is None or y is None:
|
||||
return ""
|
||||
return compute_planquadrat(float(x), float(y), PLANQUADRAT_CFG)
|
||||
return compute_planquadrat(float(x), float(y), planquadrat_cfg)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -285,6 +304,11 @@ SKIP_BLOCKS = set(BLOCKPATTERNS.get("pattern_ks_subblocks", ["K1", "K2", "K3", "
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def process_blocks(blocks, lookup):
|
||||
# x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe
|
||||
# export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls
|
||||
# nicht explizit in [Planquadrate] konfiguriert.
|
||||
planquadrat_cfg = resolve_origins(PLANQUADRAT_CFG, blocks)
|
||||
|
||||
items = []
|
||||
elem_nr = 0
|
||||
bogen_count = 0
|
||||
@@ -322,7 +346,7 @@ def process_blocks(blocks, lookup):
|
||||
"teileart": "Omniflo Kurve",
|
||||
"teileid": shape_id,
|
||||
"bezeichnung": f"OFBogen :{bogen_count}",
|
||||
"planquadrat": planquadrat_column(block),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_bogen_merkmale(block, eintrag),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
@@ -335,7 +359,7 @@ def process_blocks(blocks, lookup):
|
||||
"teileart": "Omniflo Weiche",
|
||||
"teileid": shape_id,
|
||||
"bezeichnung": f"OFWeiche :{weiche_count[wt]}",
|
||||
"planquadrat": planquadrat_column(block),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_weiche_merkmale(block, eintrag),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
@@ -362,7 +386,7 @@ def process_blocks(blocks, lookup):
|
||||
"teileart": "Omniflo Gerade",
|
||||
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
||||
"bezeichnung": f"OFGerade :{gerade_count}",
|
||||
"planquadrat": planquadrat_column(block),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_gerade_merkmale(block),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
@@ -378,7 +402,7 @@ def process_blocks(blocks, lookup):
|
||||
"teileart": "ILS 2.0 Strecke",
|
||||
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
||||
"bezeichnung": f"VarioFoerderer :{vf_count}",
|
||||
"planquadrat": planquadrat_column(block),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_variofoerderer_merkmale(block),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
@@ -393,7 +417,7 @@ def process_blocks(blocks, lookup):
|
||||
"teileart": "ILS 2.0 Gefaellestrecke",
|
||||
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
||||
"bezeichnung": f"Gefaellestrecke :{gf_count}",
|
||||
"planquadrat": planquadrat_column(block),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_variofoerderer_merkmale(block),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
@@ -409,7 +433,7 @@ def process_blocks(blocks, lookup):
|
||||
"teileart": "ILS 2.0 Eckrad",
|
||||
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
||||
"bezeichnung": f"Eckrad :{eckrad_count}",
|
||||
"planquadrat": planquadrat_column(block),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_kreisel_merkmale(block),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
@@ -424,7 +448,7 @@ def process_blocks(blocks, lookup):
|
||||
"teileart": "ILS 2.0 Kreisel",
|
||||
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
||||
"bezeichnung": f"Kreisel :{kreisel_count}",
|
||||
"planquadrat": planquadrat_column(block),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_kreisel_merkmale(block),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
@@ -439,7 +463,7 @@ def process_blocks(blocks, lookup):
|
||||
"teileart": "ILS 2.0 Strecke - Modul",
|
||||
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
||||
"bezeichnung": f"Streckenmodul :{strecke_modul_count}",
|
||||
"planquadrat": planquadrat_column(block),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": {"Blockname": bname},
|
||||
**bbox_columns(block),
|
||||
})
|
||||
@@ -460,9 +484,12 @@ def process_blocks(blocks, lookup):
|
||||
"boundingbox": "",
|
||||
})
|
||||
|
||||
neighbor_ids = compute_neighbor_ids(items, NEIGHBOR_TOLERANCE_MM)
|
||||
for item, nachbarn in zip(items, neighbor_ids):
|
||||
neighbor_ids = compute_neighbor_ids(
|
||||
items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM)
|
||||
neighbor_errors = compute_neighbor_errors(items, neighbor_ids)
|
||||
for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors):
|
||||
item["nachbarn"] = nachbarn
|
||||
item["fehler"] = fehler
|
||||
|
||||
return items
|
||||
|
||||
@@ -483,6 +510,7 @@ def format_csv_line(item):
|
||||
f';"{item["position"]}"'
|
||||
f';"{item["boundingbox"]}"'
|
||||
f';"{item["nachbarn"]}"'
|
||||
f';"{item["fehler"]}"'
|
||||
f';{merkmale_json}'
|
||||
)
|
||||
|
||||
@@ -514,7 +542,7 @@ def main():
|
||||
|
||||
items = process_blocks(blocks, lookup)
|
||||
|
||||
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Merkmale"
|
||||
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Fehler;Merkmale"
|
||||
with open(output_csv, "w", encoding="utf-8") as f:
|
||||
f.write(header + "\n")
|
||||
for item in items:
|
||||
|
||||
+195
-41
@@ -1,27 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
export_neighbors.py - Ermittelt benachbarte Elemente ueber die Bounding-Box (STRtree).
|
||||
export_neighbors.py - Ermittelt benachbarte Elemente ueber Bounding-Box-
|
||||
Ueberschneidung in der x/y-Ebene (Grundriss, Z/Hoehe wird ignoriert).
|
||||
|
||||
Zwei Elemente gelten als benachbart, wenn ihre Bounding-Boxes sich in der
|
||||
x/y-Ebene (Grundriss, Z/Hoehe wird ignoriert) ueberschneiden. Um auch nah
|
||||
beieinander liegende, aber nicht direkt beruehrende Elemente (z.B. Kreisel)
|
||||
als benachbart zu erkennen, wird jede Bounding-Box vor der Pruefung um die
|
||||
Haelfte der konfigurierten Toleranz nach allen Seiten erweitert - zwei
|
||||
Elemente mit einem Abstand <= toleranz_mm gelten dann ebenfalls als
|
||||
benachbart. Die Toleranz wird aus cfg/export.cfg ([Nachbarschaft] ->
|
||||
toleranz_mm) gelesen.
|
||||
Reines Python (keine externe Abhaengigkeit wie shapely) - die Bloecke werden
|
||||
dazu in drei Gruppen eingeteilt und NUR innerhalb dieser Kombinationen
|
||||
gegeneinander geprueft (nicht mehr alle Elemente einer Zeichnung gemeinsam):
|
||||
|
||||
1. Kreisel/Eckrad gegen Kreisel/Eckrad
|
||||
2. Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul - diese
|
||||
drei Kategorien werden NICHT gegeneinander getestet (nur relevant, wo
|
||||
sie an einen Kreisel/Eckrad anschliessen)
|
||||
3. Omniflo-Elemente (Bogen/Weiche/Gerade) gegen Omniflo-Elemente
|
||||
|
||||
Omniflo-Anlagen koennen mehrere hundert Elemente enthalten - ein Test jedes
|
||||
gegen jedes (O(n^2)) waere dort zu teuer. Die Omniflo-Gruppe wird daher vorab
|
||||
per Raster (Zellgroesse siehe [Nachbarschaft] -> omniflo_zellgroesse_mm in
|
||||
cfg/export.cfg) nach x/y-Koordinate in Quadranten/Zellen eingeteilt; jedes
|
||||
Element wird nur gegen Elemente in derselben und den 8 angrenzenden Zellen
|
||||
geprueft (3x3-Nachbarschaft, damit auch Elemente knapp jenseits einer
|
||||
Zellgrenze noch erkannt werden).
|
||||
|
||||
Zwei Bounding-Boxes gelten als benachbart, wenn sie sich (nach Erweiterung
|
||||
um die halbe Toleranz je Seite) in x/y ueberschneiden. Toleranz aus
|
||||
cfg/export.cfg ([Nachbarschaft] -> toleranz_mm), Wert in mm.
|
||||
|
||||
compute_neighbor_errors() prueft zusaetzlich, ob jedes Element die fuer
|
||||
seine TeileArt noetige Mindestanzahl an Partnern (Nachbarn) hat (siehe
|
||||
MIN_PARTNER) - Ergebnis ist die CSV-Spalte "Fehler" in export_csv.py.
|
||||
|
||||
Nur von export_csv.py genutzt (EXPORTCSV), nicht von export_sivas.py.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
|
||||
from shapely.geometry import box
|
||||
from shapely.strtree import STRtree
|
||||
|
||||
from export_blockpatterns import cfg_path_from_env
|
||||
|
||||
KREISEL_TEILEARTEN = {"ILS 2.0 Kreisel", "ILS 2.0 Eckrad"}
|
||||
STRECKEN_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"}
|
||||
OMNIFLO_TEILEARTEN = {"Omniflo Kurve", "Omniflo Weiche", "Omniflo Gerade"}
|
||||
|
||||
# Mindestanzahl Partner (Nachbarn) je TeileArt fuer die Fehlerspalte "Fehler"
|
||||
# (siehe compute_neighbor_errors): Kreisel/Eckrad und Omniflo-Elemente
|
||||
# brauchen mindestens einen Partner, Gefaellestrecke/Foerderer/Strecke-Modul
|
||||
# muessen an zwei Seiten anschliessen (mindestens zwei Partner). TeileArten,
|
||||
# die hier nicht auftauchen (z.B. die synthetische "Omniflo Sum"-Zeile),
|
||||
# werden nicht geprueft.
|
||||
MIN_PARTNER = {}
|
||||
MIN_PARTNER.update({t: 1 for t in KREISEL_TEILEARTEN})
|
||||
MIN_PARTNER.update({t: 1 for t in OMNIFLO_TEILEARTEN})
|
||||
MIN_PARTNER.update({t: 2 for t in STRECKEN_TEILEARTEN})
|
||||
|
||||
|
||||
def load_neighbor_tolerance_mm(cfg_path=None):
|
||||
"""Liest die Nachbarschafts-Toleranz (mm) aus export.cfg. Default: 0."""
|
||||
@@ -32,41 +62,165 @@ def load_neighbor_tolerance_mm(cfg_path=None):
|
||||
return parser.getfloat("Nachbarschaft", "toleranz_mm", fallback=0.0)
|
||||
|
||||
|
||||
def compute_neighbor_ids(items, tolerance_mm):
|
||||
"""Ermittelt je Item die IDs (item["teileid"]) ueberschneidender Nachbarn.
|
||||
def load_omniflo_cell_size_mm(cfg_path=None):
|
||||
"""Liest die Rastergroesse (mm) fuer die Omniflo-Nachbarschaftspruefung.
|
||||
|
||||
items = Liste von dict mit "teileid" und optional "_bbox"
|
||||
({"cx","cy","dx","dy",...}, siehe export.lsp csv:get-bbox / export_csv.py
|
||||
bbox_columns). Items ohne _bbox (z.B. die synthetische Omniflo-Sum-Zeile)
|
||||
haben keine Nachbarn.
|
||||
Default 3000mm: groesser als die ueblichen Omniflo-Bauteile (AP110-
|
||||
Geraden ca. 2000mm), damit ein Element nicht durch ein zu kleines
|
||||
Raster in mehr als eine Zelle ueberlappt.
|
||||
"""
|
||||
if cfg_path is None:
|
||||
cfg_path = cfg_path_from_env()
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=("#",))
|
||||
parser.read(cfg_path, encoding="utf-8")
|
||||
return parser.getfloat("Nachbarschaft", "omniflo_zellgroesse_mm", fallback=3000.0)
|
||||
|
||||
|
||||
def _bounds(bbox, half_tol):
|
||||
"""(minx, maxx, miny, maxy) einer Bounding-Box, je Seite um half_tol erweitert."""
|
||||
return (
|
||||
bbox["cx"] - bbox["dx"] / 2.0 - half_tol,
|
||||
bbox["cx"] + bbox["dx"] / 2.0 + half_tol,
|
||||
bbox["cy"] - bbox["dy"] / 2.0 - half_tol,
|
||||
bbox["cy"] + bbox["dy"] / 2.0 + half_tol,
|
||||
)
|
||||
|
||||
|
||||
def _overlaps(bounds_a, bounds_b):
|
||||
"""True, wenn sich zwei (minx, maxx, miny, maxy)-Rechtecke in x/y ueberschneiden."""
|
||||
a_minx, a_maxx, a_miny, a_maxy = bounds_a
|
||||
b_minx, b_maxx, b_miny, b_maxy = bounds_b
|
||||
return a_minx <= b_maxx and b_minx <= a_maxx and a_miny <= b_maxy and b_miny <= a_maxy
|
||||
|
||||
|
||||
def _add_neighbor(result, idx_a, idx_b, id_a, id_b):
|
||||
result[idx_a].append(id_b)
|
||||
result[idx_b].append(id_a)
|
||||
|
||||
|
||||
def _test_group_pairs(group, result):
|
||||
"""Alle Paare EINER Gruppe gegeneinander testen (O(n^2) - fuer Kreisel/
|
||||
Eckrad-Stueckzahlen pro Zeichnung unkritisch)."""
|
||||
n = len(group)
|
||||
for a in range(n):
|
||||
idx_a, bounds_a, id_a = group[a]
|
||||
for b in range(a + 1, n):
|
||||
idx_b, bounds_b, id_b = group[b]
|
||||
if _overlaps(bounds_a, bounds_b):
|
||||
_add_neighbor(result, idx_a, idx_b, id_a, id_b)
|
||||
|
||||
|
||||
def _test_group_against_group(group_a, group_b, result):
|
||||
"""Jedes Element aus group_a gegen jedes Element aus group_b testen.
|
||||
group_b wird NICHT gegen sich selbst getestet (Aufgabe des Aufrufers)."""
|
||||
for idx_a, bounds_a, id_a in group_a:
|
||||
for idx_b, bounds_b, id_b in group_b:
|
||||
if _overlaps(bounds_a, bounds_b):
|
||||
_add_neighbor(result, idx_a, idx_b, id_a, id_b)
|
||||
|
||||
|
||||
def _grid_cell(bbox, cell_size_mm):
|
||||
"""Rasterzelle (Quadrant) einer Bounding-Box nach ihrem Mittelpunkt."""
|
||||
return (int(bbox["cx"] // cell_size_mm), int(bbox["cy"] // cell_size_mm))
|
||||
|
||||
|
||||
def _test_omniflo_grid(group, cell_size_mm, result):
|
||||
"""Omniflo-Elemente nur gegen Elemente in derselben oder einer der 8
|
||||
angrenzenden Rasterzellen testen (3x3-Nachbarschaft) statt gegen alle -
|
||||
haelt den Aufwand bei mehreren hundert Omniflo-Elementen niedrig, ohne
|
||||
Ueberschneidungen an Zellgrenzen zu verpassen."""
|
||||
buckets = {}
|
||||
for entry in group:
|
||||
idx, bounds, teileid, bbox = entry
|
||||
buckets.setdefault(_grid_cell(bbox, cell_size_mm), []).append(entry)
|
||||
|
||||
checked = set()
|
||||
for (cx, cy), cell_items in buckets.items():
|
||||
candidates = []
|
||||
for dx in (-1, 0, 1):
|
||||
for dy in (-1, 0, 1):
|
||||
candidates.extend(buckets.get((cx + dx, cy + dy), []))
|
||||
|
||||
for idx_a, bounds_a, id_a, _ in cell_items:
|
||||
for idx_b, bounds_b, id_b, _ in candidates:
|
||||
if idx_a >= idx_b:
|
||||
continue
|
||||
pair = (idx_a, idx_b)
|
||||
if pair in checked:
|
||||
continue
|
||||
checked.add(pair)
|
||||
if _overlaps(bounds_a, bounds_b):
|
||||
_add_neighbor(result, idx_a, idx_b, id_a, id_b)
|
||||
|
||||
|
||||
def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0):
|
||||
"""Ermittelt je Item die IDs (item["teileid"]) benachbarter Elemente.
|
||||
|
||||
items = Liste von dict mit "teileart", "teileid" und optional "_bbox"
|
||||
({"cx","cy","dx","dy",...}, siehe export_csv.py bbox_columns). Items
|
||||
ohne _bbox (z.B. die synthetische Omniflo-Sum-Zeile) bleiben ohne
|
||||
Nachbarn.
|
||||
|
||||
Rueckgabe: Liste von kommaseparierten Nachbar-ID-Strings, positionsgleich
|
||||
zu items (nicht ueber die ID dedupliziert, da TeileId nicht zwingend
|
||||
eindeutig ist).
|
||||
"""
|
||||
idx_with_bbox = [i for i, it in enumerate(items) if it.get("_bbox")]
|
||||
result = [""] * len(items)
|
||||
if len(idx_with_bbox) < 2:
|
||||
return result
|
||||
|
||||
half_tol = tolerance_mm / 2.0
|
||||
geoms = []
|
||||
for i in idx_with_bbox:
|
||||
bb = items[i]["_bbox"]
|
||||
minx = bb["cx"] - bb["dx"] / 2.0 - half_tol
|
||||
maxx = bb["cx"] + bb["dx"] / 2.0 + half_tol
|
||||
miny = bb["cy"] - bb["dy"] / 2.0 - half_tol
|
||||
maxy = bb["cy"] + bb["dy"] / 2.0 + half_tol
|
||||
geoms.append(box(minx, miny, maxx, maxy))
|
||||
result = [[] for _ in items]
|
||||
|
||||
tree = STRtree(geoms)
|
||||
for local_i, global_i in enumerate(idx_with_bbox):
|
||||
hits = tree.query(geoms[local_i], predicate="intersects")
|
||||
neighbor_ids = [
|
||||
items[idx_with_bbox[int(local_j)]]["teileid"]
|
||||
for local_j in hits
|
||||
if int(local_j) != local_i
|
||||
]
|
||||
result[global_i] = ", ".join(neighbor_ids)
|
||||
kreisel_group = []
|
||||
strecken_group = []
|
||||
omniflo_group = []
|
||||
|
||||
return result
|
||||
for idx, item in enumerate(items):
|
||||
bbox = item.get("_bbox")
|
||||
if not bbox:
|
||||
continue
|
||||
teileart = item.get("teileart", "")
|
||||
teileid = item.get("teileid", "")
|
||||
bounds = _bounds(bbox, half_tol)
|
||||
if teileart in KREISEL_TEILEARTEN:
|
||||
kreisel_group.append((idx, bounds, teileid))
|
||||
elif teileart in STRECKEN_TEILEARTEN:
|
||||
strecken_group.append((idx, bounds, teileid))
|
||||
elif teileart in OMNIFLO_TEILEARTEN:
|
||||
omniflo_group.append((idx, bounds, teileid, bbox))
|
||||
|
||||
# 1. Kreisel/Eckrad gegen Kreisel/Eckrad
|
||||
_test_group_pairs(kreisel_group, result)
|
||||
|
||||
# 2. Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul -
|
||||
# diese drei Kategorien nicht gegeneinander (siehe Modul-Docstring)
|
||||
_test_group_against_group(kreisel_group, strecken_group, result)
|
||||
|
||||
# 3. Omniflo gegen Omniflo, rasterbasiert vorgefiltert
|
||||
_test_omniflo_grid(omniflo_group, omniflo_cell_size_mm, result)
|
||||
|
||||
return [", ".join(neighbor_ids) for neighbor_ids in result]
|
||||
|
||||
|
||||
def compute_neighbor_errors(items, neighbor_ids):
|
||||
"""Prueft je Item die Mindestanzahl an Partnern (siehe MIN_PARTNER).
|
||||
|
||||
items = dieselbe Liste wie bei compute_neighbor_ids (braucht "teileart").
|
||||
neighbor_ids = Rueckgabe von compute_neighbor_ids (positionsgleich zu items).
|
||||
|
||||
Rueckgabe: Liste von Fehlertexten, positionsgleich zu items:
|
||||
"unverbunden" - keine Partner, obwohl mindestens einer noetig waere
|
||||
"nur ein Partner" - genau ein Partner, obwohl mindestens zwei noetig waeren
|
||||
"" - Mindestanzahl erreicht, oder TeileArt wird nicht geprueft
|
||||
"""
|
||||
errors = []
|
||||
for item, nachbarn in zip(items, neighbor_ids):
|
||||
min_partner = MIN_PARTNER.get(item.get("teileart", ""))
|
||||
if min_partner is None:
|
||||
errors.append("")
|
||||
continue
|
||||
anzahl = len([p for p in nachbarn.split(",") if p.strip()])
|
||||
if anzahl == 0:
|
||||
errors.append("unverbunden")
|
||||
elif anzahl == 1 and min_partner >= 2:
|
||||
errors.append("nur ein Partner")
|
||||
else:
|
||||
errors.append("")
|
||||
return errors
|
||||
|
||||
Reference in New Issue
Block a user