Georeferenzierung: persistenter Standort-Bezug statt Neuberechnung je Import
Neues Project.geoAnchor: verbindet EINEN Modell-Punkt mit seiner realen LV95-Koordinate. Bisher berechnete jeder Standort-Import (Gebäude/Terrain/ OSM) unabhängig einen neuen Bezug aus der jeweils gesuchten Adresse — bei mehreren Importen mit leicht unterschiedlichen Suchbegriffen landete importierter Kontext lagefalsch zueinander. Der erste Import in einem Projekt setzt den Bezug automatisch (Modell-(0,0) = gesuchter Ort) und speichert ihn; alle weiteren Importe verwenden densel- ben Bezug, unabhängig vom neu gesuchten Ort (der bestimmt nur noch WOVON Daten geladen werden, nicht mehr WOHIN sie im Modell platziert werden). UI: Anzeige des aktiven Bezugs im Import-Dialog mit Zurücksetzen-Option. Löst das strukturelle Problem noch nicht vollständig (der Bezug sitzt immer bei Modell-(0,0) — passt nur, wenn das eigene Gebäude dort gezeichnet ist), aber behebt die akute Inkonsistenz zwischen mehreren Importen.
This commit is contained in:
@@ -3534,6 +3534,9 @@ export default function App() {
|
||||
onAddContextObjects: addContextObjectsAndFit,
|
||||
onRemoveContextObject: removeContextObject,
|
||||
onGenerateTerrain: generateTerrain,
|
||||
onSetGeoAnchor: (anchor) => {
|
||||
setProject((p) => ({ ...p, geoAnchor: anchor }));
|
||||
},
|
||||
onImportDxf: () => dxfInputRef.current?.click(),
|
||||
// ── Element-Baum (Elemente-Panel) ────────────────────────────────────
|
||||
onSelectScheduleRow: (
|
||||
|
||||
@@ -1275,6 +1275,9 @@ export const de = {
|
||||
"site.importLocation": "Standort importieren",
|
||||
"ctxImport.title": "Standort importieren",
|
||||
"ctxImport.close": "Schließen",
|
||||
"ctxImport.geoAnchor": "Georeferenziert ab: {label}",
|
||||
"ctxImport.geoAnchor.reset": "Zurücksetzen",
|
||||
"ctxImport.geoAnchor.reset.hint": "Löscht den Standort-Bezug — der nächste Import setzt ihn neu.",
|
||||
"ctxImport.location": "Ort / Adresse",
|
||||
"ctxImport.searchPlaceholder": "z. B. Bundesplatz Bern",
|
||||
"ctxImport.search": "Suchen",
|
||||
|
||||
@@ -1263,6 +1263,9 @@ export const en: Record<TranslationKey, string> = {
|
||||
"site.importLocation": "Import location",
|
||||
"ctxImport.title": "Import location",
|
||||
"ctxImport.close": "Close",
|
||||
"ctxImport.geoAnchor": "Georeferenced from: {label}",
|
||||
"ctxImport.geoAnchor.reset": "Reset",
|
||||
"ctxImport.geoAnchor.reset.hint": "Clears the location reference — the next import sets it again.",
|
||||
"ctxImport.location": "Place / address",
|
||||
"ctxImport.searchPlaceholder": "e.g. Bundesplatz Bern",
|
||||
"ctxImport.search": "Search",
|
||||
|
||||
@@ -2170,6 +2170,25 @@ export interface Project {
|
||||
* Feld noch nicht.
|
||||
*/
|
||||
referenceElevationMasl?: number;
|
||||
/**
|
||||
* Georeferenzierung: verbindet EINEN Modell-Punkt (lokale Meter) mit seiner
|
||||
* realen LV95-Koordinate. Wird beim ERSTEN Standort-Import automatisch
|
||||
* gesetzt (Modell-Position per Default (0,0), editierbar) und danach für
|
||||
* ALLE weiteren Importe (Gebäude/Terrain/OSM) wiederverwendet — vorher
|
||||
* berechnete jeder Import unabhängig einen neuen Bezug aus der jeweils
|
||||
* gesuchten Adresse, wodurch mehrere Importe lagefalsch zueinander landen
|
||||
* konnten, sobald nicht exakt derselbe Suchbegriff verwendet wurde.
|
||||
* `label` ist die Anzeige-Adresse (rein informativ). Optional, damit
|
||||
* bestehende Projekte ohne Bezug gültig bleiben.
|
||||
*/
|
||||
geoAnchor?: {
|
||||
/** Reale Landeskoordinate (LV95/EPSG:2056, Meter). */
|
||||
lv95: { e: number; n: number };
|
||||
/** Zugehöriger Punkt im Modell (lokale Meter), Default (0,0). */
|
||||
model: Vec2;
|
||||
/** Anzeige-Adresse/Bezeichnung des Bezugspunkts. */
|
||||
label?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Helfer ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -82,6 +82,8 @@ export function SitePanel() {
|
||||
<ContextImportDialog
|
||||
onImport={(objs) => host.onAddContextObjects(objs)}
|
||||
onClose={() => setCtxImportOpen(false)}
|
||||
geoAnchor={host.project.geoAnchor}
|
||||
onSetGeoAnchor={host.onSetGeoAnchor}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -411,6 +411,14 @@ export interface PanelHostValue {
|
||||
* TerrainMesh-ID oder null (unbekannte ID / kein sinnvolles TIN).
|
||||
*/
|
||||
onGenerateTerrain: (contourSetId: string) => string | null;
|
||||
/**
|
||||
* Setzt/ersetzt den Georeferenzierungs-Bezug (`Project.geoAnchor`) — welcher
|
||||
* Modell-Punkt welcher realen LV95-Koordinate entspricht. `undefined` löscht
|
||||
* den Bezug (nächster Import setzt ihn automatisch neu).
|
||||
*/
|
||||
onSetGeoAnchor: (
|
||||
anchor: { lv95: { e: number; n: number }; model: { x: number; y: number }; label?: string } | undefined,
|
||||
) => void;
|
||||
/** Öffnet den (in App montierten) versteckten DXF-Datei-Dialog. */
|
||||
onImportDxf: () => void;
|
||||
|
||||
|
||||
@@ -4630,6 +4630,31 @@ body {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cim-geo-anchor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--line);
|
||||
font-size: 11px;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.imp-linklike {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
text-decoration: underline;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.imp-linklike:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.imp-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -29,10 +29,22 @@ import type { ContextObject } from "../model/types";
|
||||
* swissBUILDINGS3D-Geometrie (Wände+Dach) in einer der zwei Generationen. */
|
||||
export type SwissBuildingsMode = "off" | "flach" | "v2" | "v3";
|
||||
|
||||
/** Georeferenzierungs-Bezug: welcher Modell-Punkt entspricht welcher realen
|
||||
* LV95-Koordinate (s. `Project.geoAnchor`). */
|
||||
export interface GeoAnchor {
|
||||
lv95: { e: number; n: number };
|
||||
model: { x: number; y: number };
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface ContextImportDialogProps {
|
||||
/** Übernimmt die fertigen Kontext-Objekte (nach erfolgreichem Import). */
|
||||
onImport: (objs: ContextObject[]) => void;
|
||||
onClose: () => void;
|
||||
/** Aktueller Georeferenzierungs-Bezug des Projekts (falls schon gesetzt). */
|
||||
geoAnchor?: GeoAnchor;
|
||||
/** Setzt/ersetzt (`undefined` löscht) den Georeferenzierungs-Bezug. */
|
||||
onSetGeoAnchor: (anchor: GeoAnchor | undefined) => void;
|
||||
}
|
||||
|
||||
type Sources = {
|
||||
@@ -64,6 +76,8 @@ const DEFAULT_SOURCES: Sources = {
|
||||
export function ContextImportDialog({
|
||||
onImport,
|
||||
onClose,
|
||||
geoAnchor,
|
||||
onSetGeoAnchor,
|
||||
}: ContextImportDialogProps) {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -130,7 +144,13 @@ export function ContextImportDialog({
|
||||
setStatus(t("ctxImport.working"));
|
||||
try {
|
||||
const center = selected.lv95;
|
||||
let origin: GeoOrigin | undefined;
|
||||
// Bestehenden Georeferenzierungs-Bezug wiederverwenden, statt bei jedem
|
||||
// Import einen neuen (inkonsistenten) zu berechnen — der Bezugspunkt
|
||||
// bleibt fix im Modell, nur die GESUCHTE Stelle (center/radius) steuert,
|
||||
// WOVON Daten geladen werden (s. Project.geoAnchor-Doc).
|
||||
let origin: GeoOrigin | undefined = geoAnchor
|
||||
? { e: geoAnchor.lv95.e - geoAnchor.model.x, n: geoAnchor.lv95.n - geoAnchor.model.y }
|
||||
: undefined;
|
||||
const all: GeoFeature[] = [];
|
||||
// Echte swissBUILDINGS3D-Meshes (v2/v3) landen direkt als ContextObject
|
||||
// (kein Footprint+Box-Umweg über GeoFeature/featuresToContextObjects).
|
||||
@@ -200,6 +220,13 @@ export function ContextImportDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
// Erster Import in diesem Projekt (noch kein Bezug) -> DIESEN Lauf als
|
||||
// Georeferenzierungs-Bezug speichern (Modell-(0,0) = der gesuchte Ort),
|
||||
// damit künftige Importe lagerichtig zueinander bleiben.
|
||||
if (!geoAnchor && origin) {
|
||||
onSetGeoAnchor({ lv95: { e: origin.e, n: origin.n }, model: { x: 0, y: 0 }, label: selected.label });
|
||||
}
|
||||
|
||||
const objs = featuresToContextObjects(all, selected.label);
|
||||
objs.push(...extraObjs);
|
||||
if (terrain) objs.push(terrain);
|
||||
@@ -246,6 +273,21 @@ export function ContextImportDialog({
|
||||
</header>
|
||||
|
||||
<div className="imp-body">
|
||||
{/* Georeferenzierungs-Bezug: sobald gesetzt, teilen sich ALLE Importe
|
||||
dieses Projekts denselben Modell↔LV95-Bezug (s. Project.geoAnchor). */}
|
||||
{geoAnchor && (
|
||||
<div className="cim-geo-anchor">
|
||||
<span>{t("ctxImport.geoAnchor", { label: geoAnchor.label ?? "?" })}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="imp-linklike"
|
||||
onClick={() => onSetGeoAnchor(undefined)}
|
||||
title={t("ctxImport.geoAnchor.reset.hint")}
|
||||
>
|
||||
{t("ctxImport.geoAnchor.reset")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Ortssuche */}
|
||||
<section className="imp-section">
|
||||
<div className="imp-section-title">{t("ctxImport.location")}</div>
|
||||
|
||||
Reference in New Issue
Block a user