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,
|
onAddContextObjects: addContextObjectsAndFit,
|
||||||
onRemoveContextObject: removeContextObject,
|
onRemoveContextObject: removeContextObject,
|
||||||
onGenerateTerrain: generateTerrain,
|
onGenerateTerrain: generateTerrain,
|
||||||
|
onSetGeoAnchor: (anchor) => {
|
||||||
|
setProject((p) => ({ ...p, geoAnchor: anchor }));
|
||||||
|
},
|
||||||
onImportDxf: () => dxfInputRef.current?.click(),
|
onImportDxf: () => dxfInputRef.current?.click(),
|
||||||
// ── Element-Baum (Elemente-Panel) ────────────────────────────────────
|
// ── Element-Baum (Elemente-Panel) ────────────────────────────────────
|
||||||
onSelectScheduleRow: (
|
onSelectScheduleRow: (
|
||||||
|
|||||||
@@ -1275,6 +1275,9 @@ export const de = {
|
|||||||
"site.importLocation": "Standort importieren",
|
"site.importLocation": "Standort importieren",
|
||||||
"ctxImport.title": "Standort importieren",
|
"ctxImport.title": "Standort importieren",
|
||||||
"ctxImport.close": "Schließen",
|
"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.location": "Ort / Adresse",
|
||||||
"ctxImport.searchPlaceholder": "z. B. Bundesplatz Bern",
|
"ctxImport.searchPlaceholder": "z. B. Bundesplatz Bern",
|
||||||
"ctxImport.search": "Suchen",
|
"ctxImport.search": "Suchen",
|
||||||
|
|||||||
@@ -1263,6 +1263,9 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"site.importLocation": "Import location",
|
"site.importLocation": "Import location",
|
||||||
"ctxImport.title": "Import location",
|
"ctxImport.title": "Import location",
|
||||||
"ctxImport.close": "Close",
|
"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.location": "Place / address",
|
||||||
"ctxImport.searchPlaceholder": "e.g. Bundesplatz Bern",
|
"ctxImport.searchPlaceholder": "e.g. Bundesplatz Bern",
|
||||||
"ctxImport.search": "Search",
|
"ctxImport.search": "Search",
|
||||||
|
|||||||
@@ -2170,6 +2170,25 @@ export interface Project {
|
|||||||
* Feld noch nicht.
|
* Feld noch nicht.
|
||||||
*/
|
*/
|
||||||
referenceElevationMasl?: number;
|
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 ───────────────────────────────────────────────────────────────
|
// ── Helfer ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export function SitePanel() {
|
|||||||
<ContextImportDialog
|
<ContextImportDialog
|
||||||
onImport={(objs) => host.onAddContextObjects(objs)}
|
onImport={(objs) => host.onAddContextObjects(objs)}
|
||||||
onClose={() => setCtxImportOpen(false)}
|
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).
|
* TerrainMesh-ID oder null (unbekannte ID / kein sinnvolles TIN).
|
||||||
*/
|
*/
|
||||||
onGenerateTerrain: (contourSetId: string) => string | null;
|
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. */
|
/** Öffnet den (in App montierten) versteckten DXF-Datei-Dialog. */
|
||||||
onImportDxf: () => void;
|
onImportDxf: () => void;
|
||||||
|
|
||||||
|
|||||||
@@ -4630,6 +4630,31 @@ body {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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 {
|
.imp-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -29,10 +29,22 @@ import type { ContextObject } from "../model/types";
|
|||||||
* swissBUILDINGS3D-Geometrie (Wände+Dach) in einer der zwei Generationen. */
|
* swissBUILDINGS3D-Geometrie (Wände+Dach) in einer der zwei Generationen. */
|
||||||
export type SwissBuildingsMode = "off" | "flach" | "v2" | "v3";
|
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 {
|
export interface ContextImportDialogProps {
|
||||||
/** Übernimmt die fertigen Kontext-Objekte (nach erfolgreichem Import). */
|
/** Übernimmt die fertigen Kontext-Objekte (nach erfolgreichem Import). */
|
||||||
onImport: (objs: ContextObject[]) => void;
|
onImport: (objs: ContextObject[]) => void;
|
||||||
onClose: () => 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 = {
|
type Sources = {
|
||||||
@@ -64,6 +76,8 @@ const DEFAULT_SOURCES: Sources = {
|
|||||||
export function ContextImportDialog({
|
export function ContextImportDialog({
|
||||||
onImport,
|
onImport,
|
||||||
onClose,
|
onClose,
|
||||||
|
geoAnchor,
|
||||||
|
onSetGeoAnchor,
|
||||||
}: ContextImportDialogProps) {
|
}: ContextImportDialogProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
@@ -130,7 +144,13 @@ export function ContextImportDialog({
|
|||||||
setStatus(t("ctxImport.working"));
|
setStatus(t("ctxImport.working"));
|
||||||
try {
|
try {
|
||||||
const center = selected.lv95;
|
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[] = [];
|
const all: GeoFeature[] = [];
|
||||||
// Echte swissBUILDINGS3D-Meshes (v2/v3) landen direkt als ContextObject
|
// Echte swissBUILDINGS3D-Meshes (v2/v3) landen direkt als ContextObject
|
||||||
// (kein Footprint+Box-Umweg über GeoFeature/featuresToContextObjects).
|
// (kein Footprint+Box-Umweg über GeoFeature/featuresToContextObjects).
|
||||||
@@ -200,6 +220,13 @@ export function ContextImportDialog({
|
|||||||
return;
|
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);
|
const objs = featuresToContextObjects(all, selected.label);
|
||||||
objs.push(...extraObjs);
|
objs.push(...extraObjs);
|
||||||
if (terrain) objs.push(terrain);
|
if (terrain) objs.push(terrain);
|
||||||
@@ -246,6 +273,21 @@ export function ContextImportDialog({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="imp-body">
|
<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 */}
|
{/* Ortssuche */}
|
||||||
<section className="imp-section">
|
<section className="imp-section">
|
||||||
<div className="imp-section-title">{t("ctxImport.location")}</div>
|
<div className="imp-section-title">{t("ctxImport.location")}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user