Tags: , , , ,


In der Trigonometrie und Geometrie ist die Triangulation der Prozess der Bestimmung der Lage eines Punktes durch Bildung eines Dreiecks zu diesem Punkt von einem bekannten Punkt aus.

In diesem Artikel wird beschrieben, wie man mit Vanilla JavaScript eine Web-App erstellt, die die ungefähre Höhe eines Objekts misst, indem man das Smartphone auf die Oberseite des Objekts richtet (oder auf die Unterseite, falls die zu messende Höhe vom Boden bis zur Höhe des Smartphones reicht). Dazu entfernt sich der Nutzer in einem bestimmten Abstand von dem Objekt und richtet die Rückseitenkamera des Smartphones auf die Oberseite des Objekts, während er den Stream der Rückseitenkamera auf dem Bildschirm beobachtet. Die App misst den Winkel zur Oberseite (oder Unterseite) mithilfe des Orientierungssensors des Geräts, und da sie den Winkel und die Entfernung zum Objekt kennt, wird die ungefähre Höhe von der Smartphone-Ebene nach oben (oder nach unten zur Unterseite des Objekts) gemessen und auf dem Bildschirm angezeigt. Diese Methode wird als Triangulation bezeichnet.

Das Triangulationsprinzip wird in der Forstwirtschaft verwendet, um die Höhe eines Baumes mit einem Gerät namens Neigungsmesser zu messen, Abbildung 1.

Figure 1
Abbildung 1: Neigungsmesser

Der Neigungsmesser misst den Winkel zwischen einer horizontalen Linie und der Sichtlinie zur Baumkrone. Um die Höhe des Baumes zu messen, wird der horizontale Abstand zwischen dem Auge des Beobachters und dem Baum mit einem Maßband gemessen. Anschließend wird mit Hilfe des Triangulationsprinzips die Höhe des Baums berechnet, wie in Abbildung 2 dargestellt.

 Abbildung 2
Abbildung 2: Berechnung der Höhe eines Objekts mit Hilfe des Triangulationsprinzips

Die in diesem Artikel beschriebene Web-App verwendet dasselbe Triangulationsprinzip wie oben beschrieben, aber anstelle eines Neigungsmessers wird ein Smartphone zur Messung und Berechnung verwendet.

ZIELE

Erstellung einer mobilen Webanwendung unter Verwendung der Programmiersprache JavaScript, um die ungefähre Höhe eines Objekts mit Hilfe des Triangulationsprinzips, des Orientierungssensors des Geräts und des Streams seiner Rückseitenkamera zu messen.

ERWORBENE FÄHIGKEITEN

  • Programmieren einer mobilen Webanwendung in JavaScript, Debuggen und Gestalten der Anwendung.
  • Interpretieren und Verwenden der Eingabedaten des Orientierungssensors eines Geräts.
  • Zugriff auf den Stream der Rückseitenkamera eines Geräts.
  • Anwendung des Triangulationsprinzips zur Messung.

KODIERUNG

Zunächst wird ein neues HTML-Dokument mit dem Namen index.html erstellt und ein Titel für die Seite vergeben (hier: “Höhenmessgerät”). Eine leere CSS-Datei style.css für die Stile und eine leere JavaScript-Datei script.js für die Logik werden mit dem Dokument index.html verknüpft.

Im <body>-Element des Dokuments wird ein neuer Bereich mit dem Namen id="heightInfo" hinzugefügt, der derzeit noch leer ist, aber später die Höhe des Objekts anzeigen wird, auf das er zeigt.

Ein onload-Ereignis wird innerhalb des <body>-Elements hinzugefügt, um eine JavaScript-Funktion main() auszuführen, sobald das Dokument geladen ist.

<!-- index.html -->

<html>
    <head>
        <title>Tool zur Höhenmessung</title>
        <link rel="stylesheet" href="style.css">
        <script src="script.js"></script>
    </head>
    <body onload="main()">
        <div id="heightInfo"></div>
    </body>
</html>

Dann wird die main() in der script.js definiert. Der Schlüssel zu dieser Anwendung ist das Herausfinden der Geräteausrichtung. Dazu wird ein Ereignis-Listener zum Fenster window.addEventListener() hinzugefügt, der auf das Ereignis deviceorientation wartet. Wenn sich diese Ausrichtung ändert, wird eine Callback-Funktion namens onOrientationChange() ausgelöst.

/* script.js */

function main(){
    window.addEventListener("deviceorientation", onOrientationChange)
}

function onOrientationChange(event){
    
}

Die statische Methode console.log(), die eine Meldung an die Konsole ausgibt, wird verwendet, um die mit dem Ereignis verbundenen Informationen zu protokollieren. Ein Laptop oder ein PC sind jedoch nicht mit einem Orientierungssensor ausgestattet, der die Ausrichtung eines Geräts relativ zu einem orthogonalen Koordinatensystem $X$, $Y$ und $Z$ misst, so dass ein Kippen des Laptops oder PCs das Ereignis nicht auslösen kann. Wie man ein Gerät, das keinen Orientierungssensor hat, debuggt, wird im nächsten Abschnitt erklärt.

DEBUGGEN

Zum Debuggen eines Geräts, das keinen Orientierungssensor hat, werden die Entwicklertools (DevTools) in der Webbrowser-App verwendet. Im Chrome-Browser können die Entwicklertools beispielsweise wie in Abbildung 3 dargestellt aufgerufen werden.

Figure 3
Abbildung 3: Schritte zum Öffnen der Entwicklertools im Chrome-Browser

Es öffnet sich ein Fenster innerhalb des Browsers, wie in Abbildung 4 dargestellt. Dann drückt man auf “Weitere Werkzeuge”, die drei vertikalen Punkte ganz links in der Registerkartenleiste im unteren Bereich, und wählt dann das Werkzeug “Sensoren” aus der angezeigten Liste aus, Abbildung 5. Es öffnet sich das Feld “Sensoren” (Abbildung 6), in dem die Geolokalisierung außer Kraft gesetzt, die Geräteausrichtung simuliert, die Berührung erzwungen und der Ruhezustand emuliert werden kann. In diesem Projekt wird nur der Abschnitt “Orientierung” verwendet.

Figure 4
Abbildung 4: Das Feld Entwicklertools, angezeigt im Chrome-Browser


Figure 5
Abbildung 5: Auswahl des Werkzeugs "Sensoren" aus der Liste "Weitere Werkzeuge"


Figure 6
Abbildung 6: Das Feld "Sensoren" wird im Fenster der Entwicklungswerkzeuge geöffnet

Unter der Annahme eines kartesischen Koordinatensystems $X$, $Y$ und $Z$, wie in Abbildung 7 dargestellt, liegt das Gerät flach auf einer ebenen Fläche, z. B. einem Tisch, und der Bildschirm zeigt nach oben.

Figure 7
Abbildung 7: Das kartesische Koordinatensystem X, Y und Z in der Spezifikation für die Geräteausrichtung

Die Geräteausrichtung definiert drei Arten der Drehung, die wie folgt sind:

  • $\alpha$ (alpha): Der Drehwinkel um die $Z$-Achse, Abbildung 8, reicht von $-180$ bis $180$ Grad oder $[-180°, 180°)$.
  • $\beta$ (beta): Der Drehwinkel um die $X$-Achse, Abbildung 9, reicht von $-180$ bis $180$ Grad oder $[-180°, 180°)$.
  • $\gamma$ (gamma): Der Drehwinkel um die $Y$-Achse, Abbildung 10, reicht von $-90$ bis $90$ Grad oder $[-90°, 90°)$.

Figure 8
Abbildung 8: Drehwinkel (α) des Geräts um die Z-Achse


Figure 9
Abbildung 9: Drehwinkel (β) des Geräts um die X-Achse


Figure 10
Abbildung 10: Drehwinkel (γ) des Geräts um die Y-Achse

Durch Einstellen einer benutzerdefinierten Ausrichtung für das virtuelle Gerät in den Abschnitten der Entwicklertools, entweder durch Ziehen des Bildes des Geräts oder durch Ändern der Werte von $\alpha$, $\beta$ und $\gamma$, ändert sich das Protokoll im Konsolenfeld entsprechend, wie in Abbildung 11 gezeigt.

Figure 11
Abbildung 11: Das Protokoll der Konsole wird entsprechend der Ausrichtungsänderung des Geräts aktualisiert

In diesem Projekt ist jedoch nur die Änderung des Wertes von $\beta$ erforderlich. Aus diesem Grund könnte in der Datei script.js das console.log(event) in console.log(event.beta) geändert werden, um sich auf den Wert von $\beta$ zu konzentrieren.

$\beta = 0$ Grad, wenn das Gerät flach auf einer ebenen Fläche wie einem Tisch liegt und der Bildschirm nach oben zeigt, und wenn sich das Gerät in einer vertikalen Position befindet und der Bildschirm dem Benutzer zugewandt ist, dann ist $\beta = 90$ Grad. Für dieses Projekt ist es jedoch erforderlich, dass $\beta = 0$ Grad ist, wenn sich das Gerät in einer vertikalen Position befindet und der Bildschirm dem Benutzer zugewandt ist, und $\beta = -270$ Grad, wenn das Gerät flach auf einer ebenen Fläche liegt und der Bildschirm nach unten zeigt; Bereich von $\beta$: $[-270°, 90°)$. Um dies zu erreichen, wird $90$ Grad vom $\beta$ abgezogen. Um die negativen Gradzahlen innerhalb des Bereichs zu verwerfen, wird ein if hinzugefügt, das prüft, ob $\beta$ kleiner als Null ist; in diesem Fall multipliziert es $\beta$ mit $-1$. Auf diese Weise wird der Bereich von $\beta$ zu: $[0°, 90°)$, wie im folgenden Code gezeigt:

/* script.js */

function main(){
    window.addEventListener("deviceorientation", onOrientationChange)
}

function onOrientationChange(event){
    let angle = event.beta-90;
    if(angle<0){
        angle = -angle;
    }
    console.log(angle);
}

Um diesen Winkel $\beta$ in Höhe umzurechnen, wird die Entfernung zum Objekt benötigt. Hierfür werden in der Datei script.js neue Variablen definiert: distToObject, ein zuvor vom Benutzer gemessener Wert, und heightOfObject, die Höhe des zu messenden Objekts, die durch die Triangulationsmethode wie folgt berechnet wird:

const heightOfObject = Math.tan(angle*Math.PI/180)*distToObject;

Der Winkel $\beta$ wird in Bogenmaß umgerechnet, indem er mit $\frac{\pi}{180}$ multipliziert wird.

Die Datei script.js sieht jetzt so aus, wenn man eine Entfernung von $20$ Metern zum Objekt berücksichtigt:

/* script.js */

function main(){
    window.addEventListener("deviceorientation", onOrientationChange)
}

function onOrientationChange(event){
    let angle = event.beta-90;
    if(angle<0){
        angle = -angle;
    }

    const distToObject = 20;
    const heightOfObject = Math.tan(angle*Math.PI/180)*distToObject;
    document.getElementById("heightInfo").innerHTML =
        heightOfObject.toFixed(1)+" m (" +angle.toFixed(1)+"&deg;)";
}

Um die Entfernung zum Objekt als Benutzereingabe in der App zu ermöglichen, wird ein Schieberegler mit einem Bereich zwischen $1$ bis $50$ Metern und einem Standardwert von $20$ Metern in die App eingefügt, indem er in der index.html kodiert wird. Dann wird dem Schieberegler mit Hilfe des <div>-Tags ein Infofeld hinzugefügt.

<!-- index.html -->

<html>
    <head>
        <title>Tool zur Höhenmessung</title>
        <link rel="stylesheet" href="style.css">
        <script src="script.js"></script>
    </head>
    <body onload="main()">
        <input id="mySlider" type="range" min="1" max="50" value="20">
        <div id="myLabel"></div>
        <div id="heightInfo"></div>
    </body>
</html>

Um den Wert des Schiebereglers an das distToObject zu übergeben, wird in der Datei script.js die Methode getElementById() verwendet, um den Wert des Elements mySlider zurückzugeben:

const distToObject = document.getElementById("mySlider").value;

Um diesen Abstand auf dem Bildschirm auszudrucken, wird in script.js die Eigenschaft innerHTML der Methode getElementById() verwendet, um den HTML-Inhalt (inneres HTML) des Elements myLabel zurückzugeben, das eine bestimmte id im zuvor erstellten <div>-Container hat:

document.getElementById("myLabel").innerHTML = "Distance to object: "+distToObject+" meters";

Abbildung 12 zeigt die aktuelle Ausgabeseite der Anwendung im Browser und im Bereich Entwicklertools.

Figure 12
Abbildung 12: Der Schieberegler und das Infofeld auf der Ausgabeseite und im Bereich Entwicklertools

Wenn man den Schieberegler in seiner aktuellen Position bewegt, wird der Wert darunter nicht aktualisiert. Dazu ist ein oninput-Ereignis erforderlich, das ausgelöst wird, wenn der Wert des Schiebereglers geändert wird. Dies ist hier jedoch nicht notwendig, da der Orientierungssensor in einem Gerät recht empfindlich ist und das Orientierungsereignis ständig ausgelöst wird und das Bewegen des Schiebereglers den Wert darunter ständig aktualisiert.

Im nächsten Abschnitt wird die Kameraeingabe von der Rückseitenkamera des Geräts hinzugefügt, die es dem Benutzer ermöglicht, das Gerät richtig auf die Oberseite (oder Unterseite) des Objekts auszurichten.

KAMERA-STREAM

Eine neue Funktion wurde hinzugefügt: die Kameraeingabe von der Rückseitenkamera des Geräts, die es dem Benutzer ermöglicht, das Gerät an der Oberseite (oder Unterseite) des Objekts auszurichten. Dazu wird auf die Eigenschaft navigator.mediaDevices zugegriffen, die verschiedene Methoden für den Zugriff auf die Kamera, das Mikrofon und die Bildschirmfreigabe bietet, und die Methode getUserMedia() mit der auf true gesetzten Eigenschaft video aufgerufen.

Wenn getUserMedia() aufgerufen wird, gibt es ein Promise-Objekt video:true zurück. Dieses Promise-Objekt hat zwei Instanzmethoden: then() und catch(). Die then()-Methode nimmt zwei Argumente entgegen: Callback-Funktionen für die erfüllten (Erfolg) und abgelehnten (Fehler) Fälle des Promise. In diesem Fall wird nur eine Callback-Funktion verwendet, function(signal) , die für den erfüllten Fall des Promise gilt. Der Browser fragt den Benutzer nach der Erlaubnis, auf die Kamera des verfügbaren Geräts zuzugreifen ( Abbildung 13). Wenn der Benutzer die Erlaubnis erteilt, kann die erfüllte (erfolgreiche) Callback-Funktion, die Zugriff auf das Videosignal (MediaStream) hat, das Versprechen zurückgeben. Dieses Signal (MediaStream) wird an ein neu erstelltes video-Element mit der id myVideo übergeben, das ebenfalls in der index.html hinzugefügt wird: <video id="myVideo"></video>. Die Eigenschaft srcObject gibt das MediaStream-Objekt zurück und spielt (play) das Video ab.

Die catch()-Methode der Promise-Instanz sieht eine Funktion vor, die aufgerufen wird, wenn das Promise abgelehnt wird, hier: der Zugriff auf die zurückgegebenen Fehlerinformationen, Abbildung 14.

Figure 13
Abbildung 13: Der Browser fordert den Benutzer auf, den Zugriff auf die Kamera des verfügbaren Geräts zu erlauben

Figure 14
Abbildung 14: Die Erlaubnis zum Zugriff auf die Kamera des verfügbaren Geräts wird verweigert; Promise wird abgelehnt

Das Aktualisieren der Seite zeigt nun das Video der Webcam, Abbildung 15, die Werte erscheinen, wenn sich das Gerät bewegt (hier das virtuelle Telefon in den Entwicklertools).

Figure 15
Abbildung 15: Der Benutzer gibt die Erlaubnis zum Zugriff auf die Kamera. Die Kameraübertragung wird angezeigt (der graue Bereich ist mit Bäumen gefüllt)

Der JavaScript-Code in der Datei (script.js) sieht wie folgt aus:

/* script.js */

function main(){
    window.addEventListener("deviceorientation", onOrientationChange)

    navigator.mediaDevices.getUserMedia({video:true})
        .then(function(signal){
            const video=document.getElementById("myVideo");
            video.srcObject=signal;
            video.play();
        })
        .catch(function (err){
            alert(err);
        })
}

function onOrientationChange(event){
    let angle = event.beta-90;
    if(angle<0){
        angle = -angle;
    }

    const distToObject = document.getElementById("mySlider").value;
    document.getElementById("myLabel").innerHTML =
        "Distance to object: "+distToObject+" meters";
    const heightOfObject = Math.tan(angle*Math.PI/180)*distToObject;
    document.getElementById("heightInfo").innerHTML =
        heightOfObject.toFixed(1)+" m (" +angle.toFixed(1)+"&deg;)";
}

Der nächste Schritt ist die Gestaltung der Anwendung in der Datei style.css.

GESTALTUNG

Zunächst wird der margin des body auf Null gesetzt, die Elemente werden zentriert, der Overflow wird auf hidden gesetzt, um die Scrollbars zu entfernen, die font-size wird vergrößert, font-family wird auf ‘Arial’ gesetzt, seine Farbe color wird auf ‘white’ (weiß) gesetzt und dem Text werden doppelte schwarze Schatten hinzugefügt, um den Schatten zu verstärken.

/* style.css */

body{
    margin:0;
    text-align:center;
    overflow:hidden;
}

Als nächstes wird das video-Objekt an der Mitte des body ausgerichtet, indem es absolut positioniert wird, d. h. es wird aus dem normalen Dokumentfluss entfernt, seine linke obere Ecke wird in die Mitte verschoben, indem links und oben auf $50\%$ des nächstgelegenen übergeordneten Containers, dem Body, gesetzt werden, und dann wird das Objekt um $50\%$ seiner Größe nach links und $50\%$ seiner Größe nach oben verschoben, wodurch es effektiv innerhalb des Bodys zentriert wird. Der z-index wird auf $-1$ gesetzt, damit überlappende Elemente mit größerem z-index dieses überdecken, d.h. damit andere Elemente über diesem erscheinen.

Dasselbe wird für das heightInfo-Objekt gemacht. Aber hier wird die Textfarbe auf rot und die Schriftart auf fett gesetzt, und im Gegensatz zum video-Objekt wird hier $100\%$ seiner Größe nach oben verschoben, so dass sein Boden in der Mitte des Bildschirms liegt. Dann wird ein $3px$ dicker weißer unterer Rand hinzugefügt, mit $100\%$ der Breite des body. Der Benutzer dieser Anwendung sollte diesen Rand an der Oberseite des zu messenden Objekts ausrichten.

/* style.css */

body{
    margin:0;
    text-align:center;
    overflow:hidden;
    font-size:25px;
    font-family: Arial;
    color: white;
    text-shadow: 0  4px #000, 0  4px #000;
}

video{
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
    z-index: -1;
}

#heightInfo{
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -100%);
    border-bottom: 3px solid white;
    width: 100%;
}

Dann werden der Schieberegler und sein “Thumb” gestylt, wie im nächsten CSS-Codeblock gezeigt, der auch das endgültige Stylesheet ist (gespeichert in der Datei style.css). Der ::-webkit-slider-thumb ist ein CSS-“Pseudo-Element”, das den “Thumb” darstellt, den der Benutzer in der “Rille” einer <input> von type="range" verschieben kann, um seinen numerischen Wert zu ändern.

/* style.css */

body{
    margin:0;
    text-align:center;
    overflow:hidden;
    font-size:25px;
    font-family: Arial;
    color: white;
    text-shadow: 0  4px #000, 0  4px #000;
}

#mySlider{
    appearance:none;
    width:90%;
    height:35px;
    background:#47f;
    margin-top:35px;
}

#mySlider::-webkit-slider-thumb{
    appearance: none;
    width:35px;
    height:35px;
    background:white;
}

video{
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
    z-index: -1;
}

#heightInfo{
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -100%);
    border-bottom: 3px solid white;
    width: 100%;
}

Um das Layout für mobile Geräte zu optimieren, wird in der Datei index.html das <meta>-Tag “viewport” verwendet, um die Größe des Ansichtsfensters zu steuern, indem das Attribut width auf die Anzahl der Pixel der Gerätebreite gesetzt wird und das Attribut user-scalable auf no gesetzt wird, um das Ein- und Auszoomen zu deaktivieren. Dies geschieht durch Hinzufügen der folgenden Codezeile in die Datei:

‹meta name="viewport" content="width=device-width, user-scalable=no"›

<!-- index.html -->

<html>
    <head>
        <meta name="viewport" content="width=device-width, user-scalable=no">
        <title>Tool zur Höhenmessung</title>
        <link rel="stylesheet" href="style.css">
        <script src="script.js"></script>
    </head>
    <body onload="main()">
        <input id="mySlider" type="range" min="1" max="50" value="20">
        <div id="myLabel"></div>
        <div id="heightInfo"></div>
        <video id="myVideo"></video>
    </body>
</html>

Die Verwendung der Rückseitenkamera des Geräts wird auf folgende Weise festgelegt: Im video-Objekt innerhalb der JavaScript-Funktion main() in der Datei script.js, die an getUserMedia() übergeben wird, wird geändert von:

navigator.mediaDevices.getUserMedia({video:true})

zu:

navigator.mediaDevices.getUserMedia({video:{ facingMode: 'environment' }})

Der facingMode ist auf den String-Wert environment gesetzt, was bedeutet, dass die Videoquelle vom Benutzer weg gerichtet ist und somit seine Umgebung betrachtet. Dies ist die Rückseitenkamera des Geräts.

Der endgültige JavaScript-Code in der Datei (script.js) sieht wie folgt:

/* script.js */

function main(){
    window.addEventListener("deviceorientation", onOrientationChange)

    navigator.mediaDevices.getUserMedia({video:{
        facingMode: 'environment'
    }})
        .then(function(signal){
            const video=document.getElementById("myVideo");
            video.srcObject=signal;
            video.play();
        })
        .catch(function (err){
            alert(err);
        })
}

function onOrientationChange(event){
    let angle = event.beta-90;
    if(angle<0){
        angle = -angle;
    }

    const distToObject = document.getElementById("mySlider").value;
    document.getElementById("myLabel").innerHTML =
        "Distance to object: "+distToObject+" meters";
    const heightOfObject = Math.tan(angle*Math.PI/180)*distToObject;
    document.getElementById("heightInfo").innerHTML =
        heightOfObject.toFixed(1)+" m (" +angle.toFixed(1)+"&deg;)";
}

In der nächsten GIF-Animation, Abbildung 16, wird die resultierende App in Aktion gezeigt, wobei die Höhe einer Tür gemessen wird. Der Beobachter steht in einer Entfernung von $2$ Metern von der Tür, stellt das Smartphone so ein, dass der angezeigte Höhenwert $0$ beträgt, und neigt dann das Gerät, bis die vertikale Linie die Oberkante der Tür erreicht, wo eine Höhe von $0,6$ Metern angezeigt wird. Dann wird das Gerät nach unten gekippt, bis die vertikale Linie auf dem Bildschirm den unteren Rand der Tür erreicht, wo eine Höhe von $1,6$ Metern angezeigt wird. Die Höhe der Tür ergibt sich aus der Addition der beiden Ergebnisse, $0,6 + 1,6 = 2,2$ Meter.

Figure 16
Abbildung 16: Die App in Aktion, Messung der Höhe einer Tür

Aktualisiert: