Aussengelaende in Schraegsicht von Suedwesten

Veranda und Terrasse gibt es zweimal, je Wohnpartei eine. In der Draufsicht
laege das obere Paar genau auf dem unteren: nicht auseinanderzuhalten und
schon gar nicht getrennt anzuklicken. Deshalb wird AG nicht flach von oben
gezeichnet, sondern schraeg von Suedwesten - von dort sieht man den Hof, auf
den die Veranden zeigen, und die Ostseite des Hauses traegt nichts, was
vorkommen muesste.

restricted/aussenplan.php haelt die Projektion und die Geometrie. Gezeichnet
wird in Militaerprojektion, nicht in echter Perspektive:

    X = x·cos φ − y·sin φ
    Y = (x·sin φ + y·cos φ)·STAUCHUNG − z·UEBERHOEHUNG

Der Grundriss bleibt dabei masshaltig - Geraden bleiben gerade, Rechtecke
Rechtecke. Genau deshalb diese und keine perspektivische: die Flaechen lassen
sich am Luftbild von oben abklicken, und dieselben Zahlen ergeben ohne Umweg
die Schraegsicht. Eine echte Perspektive muesste man neu vermessen.

Das Vorzeichen des Winkels bestimmt die Himmelsrichtung. Bei +35° zeigt der
Y-Gradient nach Osten und Sueden, die naechste Ecke waere die suedoestliche;
fuer Suedwesten muss er negativ sein. Was die Paare trennt, ist ohnehin nicht
die Drehung, sondern Stauchung und Ueberhoehung - bei 1,4 stehen Terrasse EG
und Terrasse OG rund 34 Einheiten auseinander, eine Kachel ist 19 hoch.

Hoehen: Traufe 6 m, First 9 m, Carport 2,5 m. Die OG-Ebene ist mit 3 m
angenommen - die halbe Traufhoehe bei zwei Geschossen.

tools/lageplan.php nimmt dafuer jetzt je Flaeche eine Hoehe entgegen und
zeigt die Schraegsicht live neben dem Luftbild: nur dort sieht man beim
Abklicken, ob die Paare wirklich auseinandergehen. Die Vorschau rechnet
dieselben Formeln im Browser nach; massgeblich bleibt der PHP-Code, von dem
spaeter die gezeichnete Etage und die Kachelpositionen kommen.

Dazu die Raeume: aus "Veranda" und "Terrasse" werden je zwei, eine pro
Partei. Welche der vier Geraete zu welcher gehoeren, sagt kein Name - die
beiden Wechselrichter tragen immerhin "OG" ("Veranda OG2-4", "Veranda
UG/OG1"), Licht und Rollladen der Terrasse gar nichts. Sie stehen deshalb
vorlaeufig auf Veranda OG und Terrasse EG.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 20:04:46 +02:00
co-authored by Claude Opus 5
parent 3c67303acf
commit e2b97c049e
3 changed files with 617 additions and 151 deletions
+302
View File
@@ -0,0 +1,302 @@
<?php
/**
* Der Lageplan des Außengeländes und die Projektion, die ihn zeichnet.
*
* OG, EG und UG haben je ein Rendering in assets/img. Für das Außengelände
* gibt es keins - die Planungssoftware kennt nur das einzelne Gebäude, nicht
* das Grundstück -, und eine schlichte Draufsicht taugt hier ohnehin nicht:
* Veranda und Terrasse gibt es zweimal, einmal für jede Wohnpartei, und in
* der Draufsicht liegt das obere Paar genau auf dem unteren. Man könnte die
* beiden nicht auseinanderhalten und schon gar nicht getrennt anklicken.
*
* Deshalb eine Schrägsicht von Südwesten. Von dort sieht man den Hof, auf den
* die Veranden zeigen; die Ostseite des Hauses trägt nichts, was hier
* vorkommen müsste.
*
* Gezeichnet wird in Militärprojektion (Planoblique), nicht in echter
* Perspektive:
*
* X = x·cos φ y·sin φ
* Y = (x·sin φ + y·cos φ)·STAUCHUNG z·UEBERHOEHUNG
*
* Der Grundriss bleibt dabei maßhaltig - Geraden bleiben gerade, Parallelen
* parallel, Rechtecke Rechtecke. Genau deshalb diese Projektion und keine
* perspektivische: die Flächen lassen sich am Luftbild von oben abklicken
* (tools/lageplan.php), und dieselben Zahlen ergeben ohne Umweg die
* Schrägsicht. Eine echte Perspektive müsste man neu vermessen.
*
* Das Vorzeichen des Winkels bestimmt die Himmelsrichtung: bei +35° zeigt der
* Y-Gradient nach Osten und Süden, die nächste Ecke wäre die südöstliche.
* Für Südwesten muss er negativ sein.
*
* Was die Paare trennt, ist nicht die Drehung, sondern Stauchung und
* Überhöhung: auch ohne jede Drehung stünde Terrasse OG über Terrasse EG.
* Die Drehung macht nur die Verwandtschaft zu den anderen Geschossen.
*/
require_once(__DIR__ . "/rooms.php");
/**
* Vorgabefarben je Flaeche. Sie stehen hier und nicht in der Geometrie,
* damit die Messseite und die Anzeige dasselbe Gruen fuer den Garten
* nehmen - und damit man eine Farbe an einer Stelle aendert.
*/
function aussenFarbe($name)
{
$farben = [
"Haus" => "#c8cdd6",
"Hof" => "#57585a",
"Garten" => "#2e5c33",
"Garage" => "#8a9099",
"Carport EG" => "#3f4a63", // nur Fundament, mit PV darauf
"Carport OG" => "#6f7680",
"Veranda EG" => "#9a7b52",
"Veranda OG" => "#c9a978",
"Terrasse EG" => "#9a7b52",
"Terrasse OG" => "#c9a978",
"Pergola" => "#7a6a8f",
];
return $farben[$name] ?? "#8a9099";
}
/** Blickrichtung: negativ heißt von Westen her. -35° ist Südwesten. */
const PLAN_WINKEL = -35.0;
/** Wie stark die Tiefe zusammengedrückt wird. 1 wäre die reine Draufsicht. */
const PLAN_STAUCHUNG = 0.55;
/**
* Wie hoch Höhen erscheinen. 1,4 heißt: ein Meter Höhe sieht aus wie 1,4 m
* Grundfläche. Überhöht, damit die drei Meter zwischen Terrasse EG und
* Terrasse OG deutlich werden - bei maßstäblicher Höhe klebten die beiden
* aufeinander, und genau das soll die Schrägsicht ja auflösen.
*/
const PLAN_UEBERHOEHUNG = 1.4;
/** So groß ist die Zeichenfläche, in die alles hineinpassen muss. */
const PLAN_BREITE = 400;
const PLAN_HOEHE = 300;
/** Luft zwischen Zeichnung und Rand, in Einheiten der Zeichenfläche. */
const PLAN_RAND = 12;
/**
* Die Flächen des Außengeländes, in Metern.
*
* Herkunft: tools/lageplan.php - dort werden sie am amtlichen Luftbild
* abgeklickt, die Seite gibt diese Zeilen fertig aus. Von Hand zu ändern
* lohnt nur für Kleinigkeiten; wer etwas verschiebt, misst besser neu.
*
* raum Raumname aus rooms.php, oder null für Flächen, die kein Raum sind
* (Haus, Hof, Garten - sie tragen keine Kachel)
* ecken Grundriss als [x, y] in Metern, x nach Osten, y nach Süden
* z Höhe der Fläche über Gelände (0 = ebenerdig, ~3 = OG-Ebene)
* hoehe Bauhöhe darüber (0 = flache Fläche, sonst ein Körper)
* dach Satteldach über der Bauhöhe, 0 = flach
* farbe Deckfläche; die Seiten werden daraus abgedunkelt
*
* Solange die Liste leer ist, hat AG keinen Grundriss - die Etage taucht dann
* im Menü nicht auf (siehe floorsWithPlan() in rooms.php), Zuordnung und
* Automatiken funktionieren trotzdem.
*/
function aussenFlaechen()
{
return [
// Hier kommen die abgeklickten Flächen hinein.
];
}
/**
* Die Projektion selbst. Liefert eine Funktion, die (x, y, z) in Metern auf
* (X, Y) in projizierten Metern abbildet - noch ohne Bezug zur Zeichenfläche.
*/
function aussenProjektor()
{
$w = deg2rad(PLAN_WINKEL);
$c = cos($w);
$s = sin($w);
return function ($x, $y, $z) use ($c, $s) {
return [$x * $c - $y * $s,
($x * $s + $y * $c) * PLAN_STAUCHUNG - $z * PLAN_UEBERHOEHUNG];
};
}
/**
* Alles Gezeichnete in die Zeichenfläche einpassen.
*
* Erst wird projiziert, dann ein Maßstab gesucht, in den die ganze Zeichnung
* passt - nicht umgekehrt. Die Zeichenfläche ist 400 × 300 und hat mit Metern
* nichts zu tun; wer den Maßstab vorher festlegt, schneidet entweder etwas ab
* oder lässt die Hälfte leer.
*
* Wichtig: Messseite und Anzeige müssen dieselbe Einpassung benutzen, sonst
* sitzen die Kacheln woanders als die Flächen, auf denen sie liegen sollen.
* Deshalb steht sie hier und nicht zweimal.
*/
function aussenEinpassung($flaechen)
{
$P = aussenProjektor();
$xs = [];
$ys = [];
foreach ($flaechen as $f) {
$oben = ($f["z"] ?? 0) + ($f["hoehe"] ?? 0) + ($f["dach"] ?? 0);
foreach ($f["ecken"] as $e) {
foreach ([$f["z"] ?? 0, $oben] as $z) {
list($X, $Y) = $P($e[0], $e[1], $z);
$xs[] = $X;
$ys[] = $Y;
}
}
}
if (!$xs) {
return null;
}
$breite = max($xs) - min($xs);
$hoehe = max($ys) - min($ys);
// Der kleinere der beiden Maßstäbe passt in beide Richtungen.
$massstab = min(($breite > 0 ? (PLAN_BREITE - 2 * PLAN_RAND) / $breite : INF),
($hoehe > 0 ? (PLAN_HOEHE - 2 * PLAN_RAND) / $hoehe : INF));
if (!is_finite($massstab)) {
$massstab = 1.0;
}
return [
"massstab" => $massstab,
// Zusätzlich zentrieren: was schmaler ist als die Fläche, soll nicht
// in der Ecke kleben.
"dx" => (PLAN_BREITE - $breite * $massstab) / 2 - min($xs) * $massstab,
"dy" => (PLAN_HOEHE - $hoehe * $massstab) / 2 - min($ys) * $massstab,
];
}
/** Ein Punkt in Metern zu einem Punkt auf der Zeichenfläche. */
function aussenPunkt($einpassung, $x, $y, $z)
{
$P = aussenProjektor();
list($X, $Y) = $P($x, $y, $z);
return [$X * $einpassung["massstab"] + $einpassung["dx"],
$Y * $einpassung["massstab"] + $einpassung["dy"]];
}
/** Mittelwert einer Punktliste - gut genug als Ankerpunkt einer Fläche. */
function aussenMitte($punkte)
{
$sx = 0.0;
$sy = 0.0;
foreach ($punkte as $p) {
$sx += $p[0];
$sy += $p[1];
}
$n = max(1, count($punkte));
return [$sx / $n, $sy / $n];
}
/** Eine Farbe abdunkeln - die Seitenflächen liegen im Schatten. */
function aussenSchatten($hex, $faktor)
{
$n = hexdec(ltrim($hex, "#"));
return sprintf("#%02x%02x%02x",
(int) round(($n >> 16) * $faktor),
(int) round((($n >> 8) & 255) * $faktor),
(int) round(($n & 255) * $faktor));
}
/**
* Die Zeichnung als Liste von Polygonen, von hinten nach vorn sortiert.
*
* Sortiert wird nach dem hintersten Punkt der Grundfläche (Malerreihenfolge).
* Das reicht hier: die Körper stehen nebeneinander auf dem Boden und
* durchdringen sich nicht. Bei ineinander verschachtelten Körpern bräuchte es
* mehr, aber ein Grundstück ist keine Baugruppe.
*
* Returns:
* ["polygone" => [["punkte" => [[x,y],...], "farbe" => "#rrggbb"], ...],
* "anker" => [Raumname => [x, y], ...]] Mitte der Deckfläche
*/
function aussenZeichnung()
{
$flaechen = aussenFlaechen();
$einpassung = aussenEinpassung($flaechen);
if (!$einpassung) {
return ["polygone" => [], "anker" => []];
}
$stuecke = [];
foreach ($flaechen as $f) {
$z = $f["z"] ?? 0;
$hoehe = $f["hoehe"] ?? 0;
$dach = $f["dach"] ?? 0;
$farbe = $f["farbe"] ?? "#8a9099";
$tiefe = INF;
foreach ($f["ecken"] as $e) {
$p = aussenPunkt($einpassung, $e[0], $e[1], 0);
$tiefe = min($tiefe, $p[1]);
}
$unten = [];
$oben = [];
foreach ($f["ecken"] as $e) {
$unten[] = aussenPunkt($einpassung, $e[0], $e[1], $z);
$oben[] = aussenPunkt($einpassung, $e[0], $e[1], $z + $hoehe);
}
$polygone = [];
if ($hoehe > 0) {
$anzahl = count($f["ecken"]);
for ($i = 0; $i < $anzahl; $i++) {
$j = ($i + 1) % $anzahl;
$polygone[] = ["punkte" => [$unten[$i], $unten[$j], $oben[$j], $oben[$i]],
"farbe" => aussenSchatten($farbe, 0.62)];
}
}
$polygone[] = ["punkte" => $oben, "farbe" => $farbe];
if ($dach > 0 && count($f["ecken"]) === 4) {
// Satteldach mit First über der Mitte der beiden Schmalseiten.
$e = $f["ecken"];
$first1 = aussenPunkt($einpassung, ($e[0][0] + $e[3][0]) / 2,
($e[0][1] + $e[3][1]) / 2, $z + $hoehe + $dach);
$first2 = aussenPunkt($einpassung, ($e[1][0] + $e[2][0]) / 2,
($e[1][1] + $e[2][1]) / 2, $z + $hoehe + $dach);
$polygone[] = ["punkte" => [$oben[0], $oben[1], $first2, $first1],
"farbe" => $f["dachfarbe"] ?? "#b5563a"];
$polygone[] = ["punkte" => [$oben[3], $oben[2], $first2, $first1],
"farbe" => aussenSchatten($f["dachfarbe"] ?? "#b5563a", 0.78)];
}
$stuecke[] = ["tiefe" => $tiefe, "polygone" => $polygone,
"raum" => $f["raum"] ?? null, "mitte" => aussenMitte($oben)];
}
usort($stuecke, function ($a, $b) {
return $a["tiefe"] <=> $b["tiefe"];
});
$polygone = [];
$anker = [];
foreach ($stuecke as $s) {
foreach ($s["polygone"] as $p) {
$polygone[] = $p;
}
if ($s["raum"] !== null) {
$anker[$s["raum"]] = $s["mitte"];
}
}
return ["polygone" => $polygone, "anker" => $anker];
}
/** Die Zeichnung als fertige SVG-Zeilen. */
function aussenSvg()
{
$zeichnung = aussenZeichnung();
$svg = "";
foreach ($zeichnung["polygone"] as $p) {
$punkte = [];
foreach ($p["punkte"] as $pt) {
$punkte[] = round($pt[0], 1) . "," . round($pt[1], 1);
}
$svg .= '<polygon points="' . implode(" ", $punkte) . '" fill="' . $p["farbe"]
. '" stroke="#ffffff30" stroke-width=".4"/>' . "\n";
}
return $svg;
}
+7 -2
View File
@@ -97,9 +97,14 @@ $rooms = [
// restricted/config.php; am Carport OG haengt der go-e Charger. // restricted/config.php; am Carport OG haengt der go-e Charger.
["floor" => "AG", "mqtt" => "Carport EG", "id" => "carport_eg", "werte" => []], ["floor" => "AG", "mqtt" => "Carport EG", "id" => "carport_eg", "werte" => []],
["floor" => "AG", "mqtt" => "Carport OG", "id" => "carport_og", "werte" => []], ["floor" => "AG", "mqtt" => "Carport OG", "id" => "carport_og", "werte" => []],
["floor" => "AG", "mqtt" => "Veranda", "id" => "veranda", "werte" => []], // Veranda und Terrasse gibt es zweimal, je Wohnpartei eine. In der
// Draufsicht laege das obere Paar genau auf dem unteren - deshalb die
// Schraegsicht, siehe restricted/aussenplan.php.
["floor" => "AG", "mqtt" => "Veranda EG", "id" => "veranda_eg", "werte" => []],
["floor" => "AG", "mqtt" => "Veranda OG", "id" => "veranda_og", "werte" => []],
["floor" => "AG", "mqtt" => "Terrasse EG", "id" => "terrasse_eg", "werte" => []],
["floor" => "AG", "mqtt" => "Terrasse OG", "id" => "terrasse_og", "werte" => []],
["floor" => "AG", "mqtt" => "Pergola", "id" => "pergola", "werte" => []], ["floor" => "AG", "mqtt" => "Pergola", "id" => "pergola", "werte" => []],
["floor" => "AG", "mqtt" => "Terrasse", "id" => "terrasse", "werte" => []],
["floor" => "AG", "mqtt" => "Garage", "id" => "garage", "werte" => []], ["floor" => "AG", "mqtt" => "Garage", "id" => "garage", "werte" => []],
// Der Garten ist kein Bauteil wie die anderen, aber ein Ort: hier haengen // Der Garten ist kein Bauteil wie die anderen, aber ein Ort: hier haengen
// der Regensensor und die beiden Ventilsteuerungen. // der Regensensor und die beiden Ventilsteuerungen.
+308 -149
View File
@@ -2,16 +2,21 @@
/** /**
* Messblatt für den Lageplan des Außengeländes. * Messblatt für den Lageplan des Außengeländes.
* *
* OG, EG und UG haben je ein Bild in assets/img - Renderings aus der * OG, EG und UG haben je ein Rendering in assets/img - Puppenhausschnitt von
* Planungssoftware, Puppenhausschnitt von schräg oben. Für das Außengelände * schräg oben. Für das Außengelände gibt es so ein Bild nicht: die
* gibt es so ein Bild nicht: die Software kennt nur das einzelne Gebäude, * Planungssoftware kennt nur das einzelne Gebäude, nicht das Grundstück. Der
* nicht das Grundstück. Der Plan für AG entsteht deshalb als Vektor direkt im * Plan für AG entsteht deshalb als Vektor, gezeichnet aus der Geometrie in
* SVG von restricted/home.php - Haus, Carport, Garage, Pergola, Terrasse und * restricted/aussenplan.php - und die entsteht hier.
* Garten als Flächen.
* *
* Diese Seite liefert die Koordinaten dafür. Als Hintergrund liegt das * Abgeklickt wird auf dem amtlichen Luftbild (DOP20, 20 cm Bodenauflösung).
* amtliche Luftbild (DOP20, 20 cm Bodenauflösung), abgeklickt wird darauf * Daneben steht die Vorschau in derselben Schrägsicht von Südwesten, in der
* Fläche für Fläche; darunter fallen die fertigen <polygon>-Zeilen heraus. * die Etage später gezeichnet wird: nur dort sieht man, ob Terrasse OG und
* Terrasse EG wirklich auseinandergehen.
*
* Warum das überhaupt geht: die Projektion in aussenplan.php ist eine
* Militärprojektion. Sie lässt den Grundriss maßhaltig, also reichen die
* Koordinaten aus der Draufsicht plus eine Höhe je Fläche. Eine echte
* Perspektive müsste man neu vermessen.
* *
* Warum nicht einfach am Bild abmessen: die Zeichenfläche ist 400 × 300 groß * Warum nicht einfach am Bild abmessen: die Zeichenfläche ist 400 × 300 groß
* und hat mit Metern nichts zu tun. Wer das im Kopf umrechnet, verrechnet * und hat mit Metern nichts zu tun. Wer das im Kopf umrechnet, verrechnet
@@ -21,41 +26,31 @@
* liegt also keine Kopie im Repository. Der Ausschnitt lässt sich über die * liegt also keine Kopie im Repository. Der Ausschnitt lässt sich über die
* Adresszeile verschieben, ohne diese Datei anzufassen: * Adresszeile verschieben, ohne diese Datei anzufassen:
* *
* tools/lageplan.php?lat=47.5781&lon=10.2628&breite=120 * tools/lageplan.php?lat=47.5781&lon=10.2628&breite=70
* *
* "breite" ist die Kantenlänge des Ausschnitts in Metern; die Höhe ergibt * "breite" ist die Kantenlänge des Ausschnitts in Metern; die Höhe ergibt
* sich aus dem Seitenverhältnis 4:3 der Zeichenfläche. * sich aus dem Seitenverhältnis 4:3 der Zeichenfläche.
* *
* Aufruf: http://<host>/smart/tools/lageplan.php * Aufruf: http://<host>/smart/tools/lageplan.php
* *
* Bewusst eine eigene Seite und kein Eintrag im Menü: sie wird einmal * Bewusst eine eigene Seite und kein Eintrag im Menü: sie wird gebraucht,
* gebraucht und danach nie wieder - genau wie kachelpositionen.php, mit der * wenn sich draußen etwas ändert, und das ist selten - genau wie
* sie sich die Bauart teilt. * kachelpositionen.php, mit der sie sich die Bauart teilt.
*/ */
require_once(__DIR__ . "/../helper.php"); require_once(__DIR__ . "/../helper.php");
require_once(__DIR__ . "/../restricted/rooms.php"); require_once(__DIR__ . "/../restricted/rooms.php");
require_once(__DIR__ . "/../restricted/aussenplan.php");
if (!checkLogin()) { if (!checkLogin()) {
http_response_code(403); http_response_code(403);
exit; exit;
} }
/** So groß ist die Zeichenfläche - siehe restricted/home.php. */
const PLAN_BREIT = 400;
const PLAN_HOCH = 300;
/** So groß ist eine Kachel, für die vorgeschlagene x/y-Zeile. */
const KACHEL_BREIT = 24;
const KACHEL_HOCH = 19;
/** Welche Etage hier vermessen wird. */ /** Welche Etage hier vermessen wird. */
const ETAGE = "AG"; const ETAGE = "AG";
/** /** Das Luftbild. CC BY 4.0 - die Namensnennung steht unten auf der Seite. */
* Das Luftbild. CC BY 4.0 - die Namensnennung steht unten auf der Seite und
* gehört dort hin, auch wenn die Seite nur intern erreichbar ist.
*/
const WMS_URL = "https://geoservices.bayern.de/od/wms/dop/v1/dop20"; const WMS_URL = "https://geoservices.bayern.de/od/wms/dop/v1/dop20";
const WMS_LAYER = "by_dop20c"; const WMS_LAYER = "by_dop20c";
@@ -70,9 +65,9 @@ const WMS_SCHAERFE = 3;
*/ */
$lat = isset($_GET["lat"]) ? floatval($_GET["lat"]) : 47.5781; $lat = isset($_GET["lat"]) ? floatval($_GET["lat"]) : 47.5781;
$lon = isset($_GET["lon"]) ? floatval($_GET["lon"]) : 10.2628; $lon = isset($_GET["lon"]) ? floatval($_GET["lon"]) : 10.2628;
$breite = isset($_GET["breite"]) ? floatval($_GET["breite"]) : 120.0; $breite = isset($_GET["breite"]) ? floatval($_GET["breite"]) : 70.0;
$breite = max(20.0, min(400.0, $breite)); $breite = max(20.0, min(400.0, $breite));
$hoehe = $breite * PLAN_HOCH / PLAN_BREIT; $hoehe = $breite * PLAN_HOEHE / PLAN_BREITE;
/** /**
* WGS84 nach UTM Zone 32 Nord (EPSG:25832). * WGS84 nach UTM Zone 32 Nord (EPSG:25832).
@@ -122,31 +117,56 @@ $luftbild = WMS_URL . "?" . http_build_query([
"SERVICE" => "WMS", "VERSION" => "1.3.0", "REQUEST" => "GetMap", "SERVICE" => "WMS", "VERSION" => "1.3.0", "REQUEST" => "GetMap",
"LAYERS" => WMS_LAYER, "STYLES" => "", "CRS" => "EPSG:25832", "LAYERS" => WMS_LAYER, "STYLES" => "", "CRS" => "EPSG:25832",
"BBOX" => $bbox, "BBOX" => $bbox,
"WIDTH" => (int) (PLAN_BREIT * WMS_SCHAERFE), "WIDTH" => (int) (PLAN_BREITE * WMS_SCHAERFE),
"HEIGHT" => (int) (PLAN_HOCH * WMS_SCHAERFE), "HEIGHT" => (int) (PLAN_HOEHE * WMS_SCHAERFE),
"FORMAT" => "image/jpeg", "FORMAT" => "image/jpeg",
]); ]);
/** /**
* Die Flächen, die abgeklickt werden. * Die Flächen, die abgeklickt werden.
* *
* Sie kommen aus der Raumtabelle - ein neuer AG-Raum in rooms.php taucht hier * Die Räume kommen aus der Raumtabelle - ein neuer AG-Raum in rooms.php
* von selbst auf. Das Haus steht davor, obwohl es kein Raum ist: es ist der * taucht hier von selbst auf. Davor stehen Haus und Hof: sie sind keine Räume
* Umriss, an dem sich beim Zeichnen alles andere ausrichtet. * und tragen keine Kachel, aber ohne sie wäre der Plan ein paar Flecken im
* Nichts. Das Haus ist zusätzlich der Umriss, an dem sich beim Zeichnen alles
* andere ausrichtet, deshalb steht es ganz vorn.
*
* "z" ist die Höhe über Gelände, "hoehe" die Bauhöhe darüber. Die Vorgaben
* sind das, was am häufigsten stimmt: eine ebenerdige, flache Fläche.
*/ */
$flaechen = [["schluessel" => "haus", "name" => "Haus", "raum" => null]]; $flaechen = [
["schluessel" => "haus", "name" => "Haus", "raum" => null,
"z" => 0, "hoehe" => 6, "dach" => 3],
["schluessel" => "hof", "name" => "Hof", "raum" => null,
"z" => 0, "hoehe" => 0, "dach" => 0],
];
foreach (allRooms() as $raum) { foreach (allRooms() as $raum) {
if ($raum["floor"] !== ETAGE) { if ($raum["floor"] !== ETAGE) {
continue; continue;
} }
$flaechen[] = ["schluessel" => $raum["id"], "name" => $raum["mqtt"], "raum" => $raum]; // Was auf der OG-Ebene liegt, steht drei Meter höher - die Traufe liegt
// auf sechs Metern, bei zwei Geschossen ist das die halbe Höhe. Der
// Carport OG heisst zwar OG, steht aber auf dem Boden; deshalb entscheidet
// die Bauart und nicht der Name.
$obenAuf = in_array($raum["mqtt"], ["Veranda OG", "Terrasse OG"], true);
$mitDach = in_array($raum["mqtt"], ["Carport OG", "Garage"], true);
$flaechen[] = [
"schluessel" => $raum["id"],
"name" => $raum["mqtt"],
"raum" => $raum,
"z" => $obenAuf ? 3.0 : 0.0,
"hoehe" => $mitDach ? 2.5 : 0.0,
"dach" => 0,
];
} }
// Genug unterscheidbare Farben, danach wiederholen sie sich - bei sieben // Genug unterscheidbare Farben für die Bedienung; gezeichnet wird später mit
// Flächen kommt es dazu nicht. // den Farben aus aussenFarbe().
$farben = ["#ffbe3c", "#35b6cf", "#7ed957", "#ff7a7a", "#c58cff", "#ffe066", "#6ee7c8"]; $stifte = ["#ffbe3c", "#35b6cf", "#7ed957", "#ff7a7a", "#c58cff", "#ffe066",
"#6ee7c8", "#ff9f5c", "#9ec5ff", "#f78fd0", "#b5e48c"];
foreach ($flaechen as $i => $f) { foreach ($flaechen as $i => $f) {
$flaechen[$i]["farbe"] = $farben[$i % count($farben)]; $flaechen[$i]["stift"] = $stifte[$i % count($stifte)];
$flaechen[$i]["farbe"] = aussenFarbe($f["name"]);
} }
?><!doctype html> ?><!doctype html>
@@ -171,7 +191,7 @@ foreach ($flaechen as $i => $f) {
background: var(--grund); color: var(--schrift); background: var(--grund); color: var(--schrift);
font: 15px/1.55 -apple-system, "Segoe UI", Roboto, sans-serif; font: 15px/1.55 -apple-system, "Segoe UI", Roboto, sans-serif;
} }
.huelle { max-width: 900px; margin: 0 auto; } .huelle { max-width: 1000px; margin: 0 auto; }
h1 { font-size: 1.35rem; margin: 0 0 .3rem; } h1 { font-size: 1.35rem; margin: 0 0 .3rem; }
.vorwort { color: var(--leise); margin: 0 0 1.2rem; max-width: 62ch; } .vorwort { color: var(--leise); margin: 0 0 1.2rem; max-width: 62ch; }
.vorwort code, .quelle code { .vorwort code, .quelle code {
@@ -182,14 +202,21 @@ foreach ($flaechen as $i => $f) {
background: var(--karte); border: 1px solid var(--linie); background: var(--karte); border: 1px solid var(--linie);
border-radius: .6rem; padding: .8rem; margin-bottom: 1.2rem; border-radius: .6rem; padding: .8rem; margin-bottom: 1.2rem;
} }
.nebeneinander { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
@media (max-width: 720px) { .nebeneinander { grid-template-columns: 1fr; } }
.nebeneinander h3 {
font-size: .82rem; font-weight: 600; color: var(--leise);
margin: 0 0 .3rem; text-transform: uppercase; letter-spacing: .04em;
}
.plan { .plan {
display: block; width: 100%; height: auto; border-radius: .4rem; display: block; width: 100%; height: auto; border-radius: .4rem;
cursor: crosshair; touch-action: none; background: #0b0d10; touch-action: none; background: #0b0d10;
} }
#plan { cursor: crosshair; }
.achsen text { fill: #ffffffaa; font: 7px sans-serif; } .achsen text { fill: #ffffffaa; font: 7px sans-serif; }
.stift { stroke: #ffbe3c; stroke-width: .4; stroke-dasharray: 3 3; } .stift { stroke: #ffbe3c; stroke-width: .4; stroke-dasharray: 3 3; }
.name { font: 5px sans-serif; text-anchor: middle; paint-order: stroke; .name { font: 6px sans-serif; text-anchor: middle; paint-order: stroke;
stroke: #000a; stroke-width: 1.4; } stroke: #000a; stroke-width: 1.8; }
.waehler { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0 .2rem; } .waehler { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0 .2rem; }
.waehler button { .waehler button {
@@ -202,6 +229,13 @@ foreach ($flaechen as $i => $f) {
.waehler .tupfen { width: .7rem; height: .7rem; border-radius: 50%; } .waehler .tupfen { width: .7rem; height: .7rem; border-radius: 50%; }
.waehler .zahl { color: var(--leise); font-variant-numeric: tabular-nums; } .waehler .zahl { color: var(--leise); font-variant-numeric: tabular-nums; }
.hoehen { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
margin: .7rem 0 0; font-size: .88rem; color: var(--leise); }
.hoehen input {
font: inherit; width: 4.2rem; padding: .15rem .35rem; text-align: right;
background: var(--grund); color: var(--schrift);
border: 1px solid var(--linie); border-radius: .3rem;
}
.knoepfe { display: flex; flex-wrap: wrap; gap: .4rem; margin: .6rem 0 0; } .knoepfe { display: flex; flex-wrap: wrap; gap: .4rem; margin: .6rem 0 0; }
.knoepfe button { .knoepfe button {
font: inherit; font-size: .88rem; cursor: pointer; padding: .25rem .6rem; font: inherit; font-size: .88rem; cursor: pointer; padding: .25rem .6rem;
@@ -213,7 +247,7 @@ foreach ($flaechen as $i => $f) {
h2 { font-size: .95rem; margin: 1.4rem 0 .4rem; font-weight: 600; } h2 { font-size: .95rem; margin: 1.4rem 0 .4rem; font-weight: 600; }
textarea { textarea {
width: 100%; box-sizing: border-box; min-height: 9rem; resize: vertical; width: 100%; box-sizing: border-box; min-height: 11rem; resize: vertical;
background: var(--karte); color: var(--schrift); background: var(--karte); color: var(--schrift);
border: 1px solid var(--linie); border-radius: .4rem; padding: .6rem; border: 1px solid var(--linie); border-radius: .4rem; padding: .6rem;
font-family: ui-monospace, Consolas, monospace; font-size: .82rem; line-height: 1.5; font-family: ui-monospace, Consolas, monospace; font-size: .82rem; line-height: 1.5;
@@ -227,47 +261,59 @@ foreach ($flaechen as $i => $f) {
<h1>Lageplan Außengelände</h1> <h1>Lageplan Außengelände</h1>
<p class="vorwort"> <p class="vorwort">
Die Zeichenfläche ist <?= PLAN_BREIT ?> &times; <?= PLAN_HOCH ?> und zeigt hier Links das Luftbild von oben, rechts die Vorschau in der Schrägsicht von
Südwesten, in der die Etage später gezeichnet wird. Der Ausschnitt zeigt
<b><?= rtrim(rtrim(number_format($breite, 1, ",", ""), "0"), ",") ?> &times; <b><?= rtrim(rtrim(number_format($breite, 1, ",", ""), "0"), ",") ?> &times;
<?= rtrim(rtrim(number_format($hoehe, 1, ",", ""), "0"), ",") ?> m</b> &ndash; <?= rtrim(rtrim(number_format($hoehe, 1, ",", ""), "0"), ",") ?> m</b> &ndash;
eine Einheit sind <?= number_format($breite / PLAN_BREIT, 2, ",", "") ?> m. eine Einheit sind <?= number_format($breite / PLAN_BREITE, 2, ",", "") ?> m.
Wähle eine Fläche und klick ihre Ecken der Reihe nach ab; <b>Backspace</b> nimmt Wähle eine Fläche, klick ihre Ecken der Reihe nach ab und stell darunter ihre
den letzten Punkt zurück. Unten stehen die fertigen Zeilen für Höhe ein; <b>Backspace</b> nimmt den letzten Punkt zurück. Unten stehen die
<code>restricted/home.php</code> und <code>restricted/rooms.php</code>. fertigen Zeilen für <code>restricted/aussenplan.php</code>.
Passt der Ausschnitt nicht, verschieb ihn über <code>?lat=</code>, Passt der Ausschnitt nicht, verschieb ihn über <code>?lat=</code>,
<code>?lon=</code> und <code>?breite=</code> in der Adresszeile. <code>?lon=</code> und <code>?breite=</code> in der Adresszeile.
</p> </p>
<section class="etage"> <section class="etage">
<svg viewBox="0 0 <?= PLAN_BREIT ?> <?= PLAN_HOCH ?>" class="plan" id="plan"> <div class="nebeneinander">
<defs> <div>
<pattern id="raster" width="10" height="10" patternUnits="userSpaceOnUse"> <h3>Draufsicht &ndash; hier wird geklickt</h3>
<path d="M10 0H0V10" fill="none" stroke="#ffffff22" stroke-width=".4"/> <svg viewBox="0 0 <?= PLAN_BREITE ?> <?= PLAN_HOEHE ?>" class="plan" id="plan">
</pattern> <defs>
</defs> <pattern id="raster" width="10" height="10" patternUnits="userSpaceOnUse">
<image href="<?= htmlspecialchars($luftbild, ENT_QUOTES) ?>" x="0" y="0" <path d="M10 0H0V10" fill="none" stroke="#ffffff22" stroke-width=".4"/>
width="<?= PLAN_BREIT ?>" height="<?= PLAN_HOCH ?>"/> </pattern>
<rect x="0" y="0" width="<?= PLAN_BREIT ?>" height="<?= PLAN_HOCH ?>" fill="url(#raster)"/> </defs>
<image href="<?= htmlspecialchars($luftbild, ENT_QUOTES) ?>" x="0" y="0"
<g class="achsen"> width="<?= PLAN_BREITE ?>" height="<?= PLAN_HOEHE ?>"/>
<?php for ($v = 50; $v < PLAN_BREIT; $v += 50): ?> <rect x="0" y="0" width="<?= PLAN_BREITE ?>" height="<?= PLAN_HOEHE ?>" fill="url(#raster)"/>
<text x="<?= $v ?>" y="9" text-anchor="middle"><?= $v ?></text> <g class="achsen">
<?php for ($v = 50; $v < PLAN_BREITE; $v += 50): ?>
<text x="<?= $v ?>" y="9" text-anchor="middle"><?= $v ?></text>
<?php endfor; ?> <?php endfor; ?>
<?php for ($v = 50; $v < PLAN_HOCH; $v += 50): ?> <?php for ($v = 50; $v < PLAN_HOEHE; $v += 50): ?>
<text x="4" y="<?= $v + 3 ?>"><?= $v ?></text> <text x="4" y="<?= $v + 3 ?>"><?= $v ?></text>
<?php endfor; ?> <?php endfor; ?>
</g> </g>
<g id="gezeichnet"></g>
<!-- Hierein zeichnet das Skript die abgeklickten Flächen. --> <g id="stift" style="display:none">
<g id="gezeichnet"></g> <line class="stift" x1="<?= -PLAN_BREITE ?>" x2="<?= PLAN_BREITE ?>"/>
<line class="stift" y1="<?= -PLAN_HOEHE ?>" y2="<?= PLAN_HOEHE ?>"/>
<g id="stift" style="display:none"> </g>
<line class="stift" x1="<?= -PLAN_BREIT ?>" x2="<?= PLAN_BREIT ?>"/> </svg>
<line class="stift" y1="<?= -PLAN_HOCH ?>" y2="<?= PLAN_HOCH ?>"/> </div>
</g> <div>
</svg> <h3>Schrägsicht von Südwesten &ndash; so wird es aussehen</h3>
<svg viewBox="0 0 <?= PLAN_BREITE ?> <?= PLAN_HOEHE ?>" class="plan" id="schraeg"></svg>
</div>
</div>
<div class="waehler" id="waehler"></div> <div class="waehler" id="waehler"></div>
<div class="hoehen">
<span id="hoehenName"></span>
<label>Höhe über Gelände <input type="number" id="feldZ" step="0.5" min="0" max="20"> m</label>
<label>Bauhöhe <input type="number" id="feldH" step="0.5" min="0" max="20"> m</label>
<label id="dachHuelle">Dach darüber <input type="number" id="feldDach" step="0.5" min="0" max="20"> m</label>
</div>
<div class="knoepfe"> <div class="knoepfe">
<button type="button" id="btnZurueck">Punkt zurück</button> <button type="button" id="btnZurueck">Punkt zurück</button>
<button type="button" id="btnLeeren">Diese Fläche leeren</button> <button type="button" id="btnLeeren">Diese Fläche leeren</button>
@@ -276,11 +322,8 @@ foreach ($flaechen as $i => $f) {
<p class="hinweis" id="hinweis">Fläche wählen und die erste Ecke anklicken …</p> <p class="hinweis" id="hinweis">Fläche wählen und die erste Ecke anklicken …</p>
</section> </section>
<h2>Für <code>restricted/home.php</code> &ndash; der Plan</h2> <h2>Für <code>restricted/aussenplan.php</code> &ndash; in <code>aussenFlaechen()</code></h2>
<textarea id="ausgabePlan" readonly spellcheck="false"></textarea> <textarea id="ausgabe" readonly spellcheck="false"></textarea>
<h2>Für <code>restricted/rooms.php</code> &ndash; die Kacheln</h2>
<textarea id="ausgabeRaeume" readonly spellcheck="false"></textarea>
<p class="quelle"> <p class="quelle">
Luftbild: <b>Datenquelle Bayerische Vermessungsverwaltung</b> &ndash; Luftbild: <b>Datenquelle Bayerische Vermessungsverwaltung</b> &ndash;
@@ -293,36 +336,42 @@ foreach ($flaechen as $i => $f) {
</div> </div>
<script> <script>
// Der Server schreibt hier hinein, was oben in den Konstanten steht. // Der Server schreibt hier hinein, was in aussenplan.php steht - damit
const PLAN_B = <?= PLAN_BREIT ?>, PLAN_H = <?= PLAN_HOCH ?>; // Vorschau und spaetere Zeichnung dieselbe Projektion benutzen.
const KACHEL_B = <?= KACHEL_BREIT ?>, KACHEL_H = <?= KACHEL_HOCH ?>; const PLAN_B = <?= PLAN_BREITE ?>, PLAN_H = <?= PLAN_HOEHE ?>, RAND = <?= PLAN_RAND ?>;
const METER_JE_EINHEIT = <?= json_encode(round($breite / PLAN_BREIT, 4)) ?>; const WINKEL = <?= PLAN_WINKEL ?>, STAUCHUNG = <?= PLAN_STAUCHUNG ?>,
UEBERHOEHUNG = <?= PLAN_UEBERHOEHUNG ?>;
const METER_JE_EINHEIT = <?= json_encode(round($breite / PLAN_BREITE, 4)) ?>;
const FLAECHEN = <?= json_encode($flaechen, JSON_UNESCAPED_UNICODE) ?>; const FLAECHEN = <?= json_encode($flaechen, JSON_UNESCAPED_UNICODE) ?>;
const ETAGE = <?= json_encode(ETAGE) ?>;
// Abgeklicktes überlebt einen Neuladen - sieben Flächen sind Arbeit, die // Abgeklicktes ueberlebt einen Neuladen - neun Flaechen sind Arbeit, die
// man nicht wegen eines Fehlklicks auf F5 noch einmal machen will. // man nicht wegen eines Fehlklicks auf F5 noch einmal machen will.
const SPEICHER = "lageplan-" + ETAGE; const SPEICHER = "lageplan-<?= ETAGE ?>";
let punkte = {}; let daten = {};
try { punkte = JSON.parse(localStorage.getItem(SPEICHER)) || {}; } catch (e) { punkte = {}; } try { daten = JSON.parse(localStorage.getItem(SPEICHER)) || {}; } catch (e) { daten = {}; }
FLAECHEN.forEach(f => { if (!Array.isArray(punkte[f.schluessel])) punkte[f.schluessel] = []; }); FLAECHEN.forEach(f => {
const d = daten[f.schluessel] || {};
daten[f.schluessel] = {
punkte: Array.isArray(d.punkte) ? d.punkte : [],
z: typeof d.z === "number" ? d.z : f.z,
hoehe: typeof d.hoehe === "number" ? d.hoehe : f.hoehe,
dach: typeof d.dach === "number" ? d.dach : f.dach
};
});
let aktiv = FLAECHEN[0].schluessel; let aktiv = FLAECHEN[0].schluessel;
const svg = document.getElementById("plan"); const svg = document.getElementById("plan");
const gezeichnet = document.getElementById("gezeichnet"); const gezeichnet = document.getElementById("gezeichnet");
const schraeg = document.getElementById("schraeg");
const stift = document.getElementById("stift"); const stift = document.getElementById("stift");
const hinweis = document.getElementById("hinweis"); const hinweis = document.getElementById("hinweis");
function flaeche(schluessel) { const flaeche = s => FLAECHEN.find(f => f.schluessel === s);
return FLAECHEN.find(f => f.schluessel === schluessel); const mitte = l => {
} const s = l.reduce((a, p) => [a[0] + p[0], a[1] + p[1]], [0, 0]);
return [s[0] / l.length, s[1] / l.length];
/** Mittelpunkt einer Fläche - Mittel der Ecken, gut genug für eine Kachel. */ };
function mitte(liste) {
const s = liste.reduce((a, p) => [a[0] + p[0], a[1] + p[1]], [0, 0]);
return [s[0] / liste.length, s[1] / liste.length];
}
function element(name, attribute) { function element(name, attribute) {
const e = document.createElementNS("http://www.w3.org/2000/svg", name); const e = document.createElementNS("http://www.w3.org/2000/svg", name);
@@ -330,34 +379,126 @@ foreach ($flaechen as $i => $f) {
return e; return e;
} }
function zeichne() { // --- Draufsicht ---------------------------------------------------------
function zeichnePlan() {
gezeichnet.innerHTML = ""; gezeichnet.innerHTML = "";
FLAECHEN.forEach(f => { FLAECHEN.forEach(f => {
const liste = punkte[f.schluessel]; const liste = daten[f.schluessel].punkte;
if (!liste.length) return; if (!liste.length) return;
const zug = liste.map(p => p.join(",")).join(" "); const zug = liste.map(p => p.join(",")).join(" ");
// Solange eine Fläche noch nicht drei Ecken hat, ist sie ein Linienzug // Unter drei Ecken ist es ein Linienzug und keine Flaeche - ein Polygon
// und keine Fläche - ein Polygon sähe dann nach etwas aus, was es // saehe nach etwas aus, was es noch nicht ist.
// noch nicht ist.
gezeichnet.appendChild(element(liste.length > 2 ? "polygon" : "polyline", { gezeichnet.appendChild(element(liste.length > 2 ? "polygon" : "polyline", {
points: zug, fill: liste.length > 2 ? f.farbe + "44" : "none", points: zug, fill: liste.length > 2 ? f.stift + "44" : "none",
stroke: f.farbe, "stroke-width": f.schluessel === aktiv ? 1.1 : 0.7, stroke: f.stift, "stroke-width": f.schluessel === aktiv ? 1.1 : 0.7,
"stroke-linejoin": "round" "stroke-linejoin": "round"
})); }));
liste.forEach(p => gezeichnet.appendChild( liste.forEach(p => gezeichnet.appendChild(
element("circle", { cx: p[0], cy: p[1], r: 1.2, fill: f.farbe }))); element("circle", { cx: p[0], cy: p[1], r: 1.2, fill: f.stift })));
if (liste.length > 2) {
const m = mitte(liste);
const t = element("text", { x: m[0], y: m[1] + 1.8, class: "name", fill: f.farbe });
t.textContent = f.name;
gezeichnet.appendChild(t);
}
}); });
zeichneWaehler();
schreibeAusgabe();
try { localStorage.setItem(SPEICHER, JSON.stringify(punkte)); } catch (e) { /* egal */ }
} }
// --- Schraegsicht -------------------------------------------------------
//
// Dieselben Formeln wie aussenProjektor() und aussenEinpassung() in
// restricted/aussenplan.php. Massgeblich ist der PHP-Code: von dort kommen
// spaeter die gezeichnete Etage und die Kachelpositionen. Hier geht es nur
// darum, waehrend des Abklickens zu sehen, ob die Paare auseinandergehen.
function projiziere(x, y, z) {
const w = WINKEL * Math.PI / 180, c = Math.cos(w), s = Math.sin(w);
return [x * c - y * s, (x * s + y * c) * STAUCHUNG - z * UEBERHOEHUNG];
}
function inMeter(punkt) {
return [punkt[0] * METER_JE_EINHEIT, punkt[1] * METER_JE_EINHEIT];
}
function koerper() {
return FLAECHEN
.filter(f => daten[f.schluessel].punkte.length > 2)
.map(f => {
const d = daten[f.schluessel];
return { f: f, ecken: d.punkte.map(inMeter), z: d.z, hoehe: d.hoehe, dach: d.dach };
});
}
function einpassung(liste) {
const xs = [], ys = [];
liste.forEach(k => k.ecken.forEach(([x, y]) => {
[k.z, k.z + k.hoehe + k.dach].forEach(z => {
const p = projiziere(x, y, z); xs.push(p[0]); ys.push(p[1]);
});
}));
if (!xs.length) return null;
const bx = Math.max(...xs) - Math.min(...xs), by = Math.max(...ys) - Math.min(...ys);
const m = Math.min(bx > 0 ? (PLAN_B - 2 * RAND) / bx : Infinity,
by > 0 ? (PLAN_H - 2 * RAND) / by : Infinity);
const ms = isFinite(m) ? m : 1;
return { ms: ms,
dx: (PLAN_B - bx * ms) / 2 - Math.min(...xs) * ms,
dy: (PLAN_H - by * ms) / 2 - Math.min(...ys) * ms };
}
function dunkler(hex, faktor) {
const n = parseInt(hex.slice(1), 16);
return "rgb(" + [n >> 16, (n >> 8) & 255, n & 255]
.map(v => Math.round(v * faktor)).join(",") + ")";
}
function zeichneSchraeg() {
schraeg.innerHTML = "";
const liste = koerper();
const e = einpassung(liste);
if (!e) {
schraeg.appendChild(element("text", { x: PLAN_B / 2, y: PLAN_H / 2,
fill: "#9aa0a6", "font-size": 9, "text-anchor": "middle" }))
.textContent = "Noch keine Fläche mit drei Ecken.";
return;
}
const P = (x, y, z) => {
const p = projiziere(x, y, z);
return [p[0] * e.ms + e.dx, p[1] * e.ms + e.dy];
};
const stuecke = liste.map(k => {
const tiefe = Math.min(...k.ecken.map(([x, y]) => P(x, y, 0)[1]));
const unten = k.ecken.map(([x, y]) => P(x, y, k.z));
const oben = k.ecken.map(([x, y]) => P(x, y, k.z + k.hoehe));
const teile = [];
if (k.hoehe > 0) {
for (let i = 0; i < k.ecken.length; i++) {
const j = (i + 1) % k.ecken.length;
teile.push({ pkt: [unten[i], unten[j], oben[j], oben[i]],
farbe: dunkler(k.f.farbe, .62) });
}
}
teile.push({ pkt: oben, farbe: k.f.farbe });
if (k.dach > 0 && k.ecken.length === 4) {
const c = k.ecken;
const f1 = P((c[0][0] + c[3][0]) / 2, (c[0][1] + c[3][1]) / 2, k.z + k.hoehe + k.dach);
const f2 = P((c[1][0] + c[2][0]) / 2, (c[1][1] + c[2][1]) / 2, k.z + k.hoehe + k.dach);
teile.push({ pkt: [oben[0], oben[1], f2, f1], farbe: "#b5563a" });
teile.push({ pkt: [oben[3], oben[2], f2, f1], farbe: dunkler("#b5563a", .78) });
}
return { tiefe: tiefe, teile: teile, name: k.f.name, mitte: mitte(oben) };
}).sort((a, b) => a.tiefe - b.tiefe);
stuecke.forEach(s => s.teile.forEach(t => schraeg.appendChild(element("polygon", {
points: t.pkt.map(p => p[0].toFixed(1) + "," + p[1].toFixed(1)).join(" "),
fill: t.farbe, stroke: "#ffffff30", "stroke-width": .4
}))));
stuecke.forEach(s => {
const t = element("text", { x: s.mitte[0].toFixed(1), y: (s.mitte[1] + 2).toFixed(1),
class: "name", fill: "#fff" });
t.textContent = s.name;
schraeg.appendChild(t);
});
}
// --- Bedienung ----------------------------------------------------------
function zeichneWaehler() { function zeichneWaehler() {
const ziel = document.getElementById("waehler"); const ziel = document.getElementById("waehler");
ziel.innerHTML = ""; ziel.innerHTML = "";
@@ -365,34 +506,43 @@ foreach ($flaechen as $i => $f) {
const b = document.createElement("button"); const b = document.createElement("button");
b.type = "button"; b.type = "button";
b.setAttribute("aria-pressed", f.schluessel === aktiv ? "true" : "false"); b.setAttribute("aria-pressed", f.schluessel === aktiv ? "true" : "false");
b.innerHTML = "<span class='tupfen' style='background:" + f.farbe + "'></span>" b.innerHTML = "<span class='tupfen' style='background:" + f.stift + "'></span>"
+ f.name + " <span class='zahl'>" + punkte[f.schluessel].length + "</span>"; + f.name + " <span class='zahl'>" + daten[f.schluessel].punkte.length + "</span>";
b.onclick = () => { aktiv = f.schluessel; zeichne(); }; b.onclick = () => { aktiv = f.schluessel; alles(); };
ziel.appendChild(b); ziel.appendChild(b);
}); });
const d = daten[aktiv];
document.getElementById("hoehenName").innerHTML = "<b>" + flaeche(aktiv).name + "</b>:";
document.getElementById("feldZ").value = d.z;
document.getElementById("feldH").value = d.hoehe;
document.getElementById("feldDach").value = d.dach;
// Ein Dach hat nur, was auch eine Bauhoehe hat - sonst schwebte es.
document.getElementById("dachHuelle").style.display = d.hoehe > 0 ? "" : "none";
} }
function schreibeAusgabe() { function schreibeAusgabe() {
const plan = []; const zeilen = [];
const raeume = [];
FLAECHEN.forEach(f => { FLAECHEN.forEach(f => {
const liste = punkte[f.schluessel]; const d = daten[f.schluessel];
if (liste.length < 3) return; if (d.punkte.length < 3) return;
plan.push('<polygon class="ag-' + f.schluessel + '" points="' const ecken = d.punkte.map(inMeter)
+ liste.map(p => p.join(",")).join(" ") + '"/>'); .map(p => "[" + p[0].toFixed(1) + ", " + p[1].toFixed(1) + "]").join(", ");
if (!f.raum) return; // das Haus ist kein Raum und bekommt keine Kachel zeilen.push(' ["raum" => ' + (f.raum ? '"' + f.raum.mqtt + '"' : "null")
const m = mitte(liste); + ', "z" => ' + d.z + ', "hoehe" => ' + d.hoehe + ', "dach" => ' + d.dach
raeume.push(' ["floor" => "' + ETAGE + '", "mqtt" => "' + f.raum.mqtt + ',\n "farbe" => "' + f.farbe + '",\n'
+ '", "id" => "' + f.raum.id + '",' + ' "ecken" => [' + ecken + ']],');
+ ' "x" => ' + Math.round(m[0] - KACHEL_B / 2)
+ ', "y" => ' + Math.round(m[1] - KACHEL_H / 2) + ', "werte" => []],');
}); });
document.getElementById("ausgabePlan").value = plan.length document.getElementById("ausgabe").value = zeilen.length
? plan.join("\n") ? zeilen.join("\n")
: "Noch nichts abgeklickt - eine Fläche braucht mindestens drei Ecken."; : "Noch nichts abgeklickt - eine Fläche braucht mindestens drei Ecken.";
document.getElementById("ausgabeRaeume").value = raeume.length }
? raeume.join("\n")
: "Noch nichts abgeklickt."; function alles() {
zeichnePlan();
zeichneSchraeg();
zeichneWaehler();
schreibeAusgabe();
try { localStorage.setItem(SPEICHER, JSON.stringify(daten)); } catch (e) { /* egal */ }
} }
function stelle(ereignis) { function stelle(ereignis) {
@@ -406,11 +556,10 @@ foreach ($flaechen as $i => $f) {
svg.addEventListener("pointermove", e => { svg.addEventListener("pointermove", e => {
const [x, y] = stelle(e); const [x, y] = stelle(e);
stift.style.display = ""; stift.style.display = "";
stift.querySelector("line:first-child").setAttribute("y1", y); const zeilen = stift.querySelectorAll("line");
stift.querySelector("line:first-child").setAttribute("y2", y); zeilen[0].setAttribute("y1", y); zeilen[0].setAttribute("y2", y);
stift.querySelector("line:last-child").setAttribute("x1", x); zeilen[1].setAttribute("x1", x); zeilen[1].setAttribute("x2", x);
stift.querySelector("line:last-child").setAttribute("x2", x); const liste = daten[aktiv].punkte;
const liste = punkte[aktiv];
let text = "<b>" + flaeche(aktiv).name + "</b> &ndash; " + x + "," + y let text = "<b>" + flaeche(aktiv).name + "</b> &ndash; " + x + "," + y
+ " &nbsp;(" + (x * METER_JE_EINHEIT).toFixed(1) + " m, " + " &nbsp;(" + (x * METER_JE_EINHEIT).toFixed(1) + " m, "
+ (y * METER_JE_EINHEIT).toFixed(1) + " m)"; + (y * METER_JE_EINHEIT).toFixed(1) + " m)";
@@ -427,24 +576,34 @@ foreach ($flaechen as $i => $f) {
hinweis.textContent = "Fläche wählen und die erste Ecke anklicken …"; hinweis.textContent = "Fläche wählen und die erste Ecke anklicken …";
}); });
svg.addEventListener("click", e => { svg.addEventListener("click", e => { daten[aktiv].punkte.push(stelle(e)); alles(); });
punkte[aktiv].push(stelle(e));
zeichne();
});
document.addEventListener("keydown", e => { document.addEventListener("keydown", e => {
if (e.key === "Backspace") { e.preventDefault(); punkte[aktiv].pop(); zeichne(); } if (e.key === "Backspace" && e.target.tagName !== "INPUT") {
e.preventDefault(); daten[aktiv].punkte.pop(); alles();
}
}); });
document.getElementById("btnZurueck").onclick = () => { punkte[aktiv].pop(); zeichne(); }; ["feldZ", "feldH", "feldDach"].forEach((id, i) => {
document.getElementById("btnLeeren").onclick = () => { punkte[aktiv] = []; zeichne(); }; document.getElementById(id).addEventListener("input", e => {
const wert = parseFloat(e.target.value);
daten[aktiv][["z", "hoehe", "dach"][i]] = isNaN(wert) ? 0 : wert;
zeichneSchraeg(); schreibeAusgabe();
if (i === 1) document.getElementById("dachHuelle").style.display =
daten[aktiv].hoehe > 0 ? "" : "none";
try { localStorage.setItem(SPEICHER, JSON.stringify(daten)); } catch (err) { /* egal */ }
});
});
document.getElementById("btnZurueck").onclick = () => { daten[aktiv].punkte.pop(); alles(); };
document.getElementById("btnLeeren").onclick = () => { daten[aktiv].punkte = []; alles(); };
document.getElementById("btnAllesLeeren").onclick = () => { document.getElementById("btnAllesLeeren").onclick = () => {
if (!confirm("Alle abgeklickten Flächen verwerfen?")) return; if (!confirm("Alle abgeklickten Flächen verwerfen?")) return;
FLAECHEN.forEach(f => { punkte[f.schluessel] = []; }); FLAECHEN.forEach(f => { daten[f.schluessel].punkte = []; });
zeichne(); alles();
}; };
zeichne(); alles();
</script> </script>
</body> </body>
</html> </html>