Touch and Go

Orientierung mit dem Windows Phone-Kompass

Charles Petzold

Charles PetzoldWir Menschen nutzen unsere Sinne selten isoliert voneinander: Die Kombination von Sehen und Hören ermöglicht uns, ein mentales Bild unserer Umgebung aufzubauen, wenn wir unser Essen genießen, verwenden wir die Kombination von Geschmacks-, Geruchs- und Tastsinn, und Tast-, Gesichtssinn und Gehör ermöglichen uns, Musikinstrumente zu spielen.

Bei einem Smartphone ist dies nicht anders: En Smartphone kann mit seinem Kameraobjektiv "sehen", mit seinem Mikrofon "hören", mit seinem Touchscreen "fühlen" und GPS und der Orientierungssensor erlauben ihm, seine Position zu bestimmen. Wenn man den Input aller dieser Sensoren zusammennimmt, lässt sich das Ergebnis nur mit dem Begriff "Synergie" bezeichnen.

Das Beschleunigungsmesserproblem

Der Beschleunigungsmesser in Windows Phone ist ein gutes Beispiel für einen Sensor, der grundlegende Informationen liefert, jedoch noch wertvoller wird, wenn er mit einem anderen Sensor, etwa dem Kompass, kombiniert wird.

Die Hardware des Beschleunigungsmessers misst eigentlich Kraft. Wie wir jedoch aus dem Physikunterricht wissen, ist Kraft = Masse x Beschleunigung, der Beschleunigungsmesser reagiert daher auf jede Art von Beschleunigung. Wenn das Telefon ruhig gehalten wird, misst der Beschleunigungsmesser die Schwerkraft und stellt einen 3D-Vektor zur Verfügung, der zum Erdmittelpunkt zeigt. Dieser Vektor ist relativ zu einem 3D-Koordinatensystem, wie in Abbildung 1 gezeigt. Dieses Koordinatensystem bleibt immer gleich, ob Sie ein Silverlight- oder ein XNA-Programm kodieren, oder ob Sie die Anwendung im Hoch- oder Querformat betreiben.

The Phone’s Sensor Coordinate System
Abbildung 1 Das Koordinatensystem des Telefonsensors

Die Klasse des Beschleunigungsmessers stellt den Beschleunigungsvektor in Form eines Vector3-Werts bereit. Dies ist ein XNA-Typ, zur Verwendung in einem Silverlight-Programm benötigen Sie daher eine Referenz zum Microsoft.Xna.Framework-Assembly.

Obwohl der Beschleunigungsvektor die Ausführung eines Programms auf einem Telefon zur Bestimmung der Orientierung des Telefons relativ zur Erde erlaubt, fehlen einige wichtige Informationen. Lassen Sie mich genauer erläutern, was ich meine.

In den Codebeispielen zu dieser Kolumne (verfügbar unter archive.msdn.microsoft.com/mag201206TouchAndGo) finden Sie eine Visual Studio-Lösung mit dem Namen 3DPointers, die vier XNA 3D-Projekte enthält, die einander ziemlich ähnlich sehen. Das Accelerometer 3D-Programm zeichnet eine 3D-"Nadel", die im Raum in Richtung des Beschleunigungsmesservektors schwebt, wie in Abbildung 2 gezeigt.

The Accelerometer 3D Display
Abbildung 2 Die Accelerometer 3D-Anzeige

Jedes Programm, das einen Windows Phone-Sensor verwendet, benötigt eine Referenz zum Microsoft.Devices.Sensors-Assembly. Normalerweise laufen XNA-Programme auf dem Telefon im Querformat. Dies kann ein Problem sein, da es nicht zu dem Koordinatensystem passt, das die Sensoren verwenden. Um mir die Dinge leichter zu machen, habe ich das XNA-Koordinatensystem im Constructor des Game-Derivative des Programms in das Hochformat umorientiert.

graphics.IsFullScreen = true;
graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 800;

In Windows Phone 7.1 wurde das Sensor-API etwas verändert, um für Konsistenz zwischen den einzelnen Sensoren zu sorgen. Der Constructor des Accelerometer 3D-Programms verwendet dieses neue API zur Erstellung einer Beschleunigungsmesserinstanz, die als Feld gespeichert wird:

if (Accelerometer.IsSupported)
{
  accelerometer = new Accelerometer
  {
    TimeBetweenUpdates = this.TargetElapsedTime
  };
}

Der Standardwert der Eigenschaft TimeBetweenUpdates beträgt 25 Millisekunden; hier ist er auf die Bildrate des Programms, 33 Millisekunden, eingestellt.

Das Programm verwendet zum Starten des Beschleunigungsmessers ein Override der OnActivated-Methode:

if (accelerometer != null)
{
  try { accelerometer.Start(); }
  catch { }
}

Obwohl man sich nur schwer ein Szenario vorstellen kann, in dem die Start-Methode an diesem Punkt fehlschlägt, empfehle ich, sie in einen "Try"-Block zu setzen. In OnDeactivated wird der Kompass angehalten:

if (accelerometer != null)
    accelerometer.Stop();

Das Programm verwendet die LoadContent-Methode zum Aufbau von 3D-Scheitelpunkten für die "Nadel" und die Definition eines BasicEffect für die Speicherung der Kamera- und Beleuchtungsinformationen. Die Nadel wird so definiert, dass sich ihre Basis am Ursprungspunkt befindet und sich eine Einheit entlang der positiven Y-Achse erstreckt. Die Kamera zeigt direkt von der positiven Z-Achse zum Ursprungspunkt.

Anschließend verwendet die Update-Methode des Programms den Beschleunigungsmesservektor zur Definition eines World Transform. Dieser World Transform sorgt dafür, dass sich die Nadel relativ zum Ursprungspunkt bewegt. Abbildung 3 zeigt den Code.

Abbildung 3 Die Update-Methode in Accelerometer 3D

protected override void Update(GameTime gameTime)
{
  if (GamePad.GetState(PlayerIndex.One).Buttons.Back 
    == ButtonState.Pressed)
      this.Exit();
  if (accelerometer != null && accelerometer.IsDataValid)
  {
    Vector3 acceleration = accelerometer.CurrentValue.Acceleration;
    text = String.Format("X = {0:F3}\nY = {1:F3}\nZ = {2:F3}",
                            acceleration.X,
                            acceleration.Y,
                            acceleration.Z);
    textPosition = new Vector2(0, this.GraphicsDevice.Viewport.Height -
                                    segoe24Font.MeasureString(text).Y);
    acceleration.Normalize();
    Vector3 axis = Vector3.Cross(acceleration, Vector3.UnitY);
    // Special case for magnetometer equal to (0, 1, 0) or (0, -1, 0)
    if (axis.LengthSquared() == 0)
        axis = Vector3.UnitX;
    else
        axis.Normalize();
    float angle = -(float)Math.Acos(Vector3.Dot(Vector3.UnitY, 
      acceleration));
    basicEffect.World = Matrix.CreateFromAxisAngle(axis, angle);
  }
  else
  {
    basicEffect.World = Matrix.Identity;
    text = "";
  }
  base.Update(gameTime);
}

Die Methode erhält den Beschleunigungswert direkt vom Beschleunigungsmesserobjekt, wenn die Eigenschaft IsDataValid den Wert "True" hat. Die Nadel muss auf der Grundlage des Winkels zwischen dem Beschleunigungsvektor und der positiven Y-Achse gedreht werden. Das skalare Produkt dieser beiden Vektoren stellt diesen Winkel zur Verfügung, während die Rotation von dem Vektorprodukt bereitgestellt wird.

Aber versuchen Sie Folgendes: Stellen Sie sich aufrecht hin, und halten Sie das Telefon in einem bestimmten Winkel in der Hand. Die Nadel zeigt nach unten. Drehen Sie sich jetzt auf der Stelle um 360 Grad. Während der Drehung bleibt die Nadel (wenigstens annähernd) in ihrer Position. Der Beschleunigungsmesser kann nicht erkennen, wenn sich das Telefon um eine Achse bewegt, die parallel zum Beschleunigungsvektor liegt. Wenn Sie eine Anwendung schreiben, die die vollständige 3D-Orientierung des Telefons kennen muss, bietet Ihnen der Beschleunigungsmesser nur einen Teil dessen, was Sie brauchen.

Der Kompass ist die Rettung

Bei der ersten Einführung von Windows Phone war noch nicht klar, ob die Telefone jemals einen Kompass enthalten würden. Niemand schien die endgültige Antwort auf diese Frage zu haben. Für Anwendungsprogrammierer war die Situation jedoch ganz einfach: Es gab kein Programmierinterface für einen Kompass, das heißt, wir könnten ihn nicht nutzen, selbst wenn er vorhanden wäre.

Mit der Einführung von Windows Phone 7.1 wurde dieses Problem gelöst: Obwohl ein Windows Phone-Gerät keinen Kompass haben muss, hatten einige Geräte bereits einen, darunter ein Smartphone mit Windows Phone, das ich im Dezember 2010 gekauft habe. Und das Beste: Programmierer können diesen Kompass bereits verwenden. Windows Phone 7.1 führte eine neue Kompass-Klasse ein, die den Programmierer informiert, ob ein Kompass vorhanden ist (über die statische Eigenschaft Compass.IsSupported), und ihm Zugang zu seinen Messwerten gibt. Auf dem Windows Phone-Emulator gibt Compass.IsSupported den Wert "False" aus.

Die Basis des Telefonkompasses ist ein Hardwareteil, das als Magnetometer bezeichnet wird. Dieser Magnetometer wird von magnetischen Kräften in der Nähe des Telefons beeinflusst, auch von solchen, die etwa von Lautsprechern oder Ihrem Computer ausgehen. Wenn Sie diese Magnetquellen von ihrem Telefon fernhalten, misst der Magnetometer die Stärke und Richtung des Magnetfelds der Erde.

Die Compass-Klasse stellt Daten in Form einer CompassReading-Struktur bereit. Die Magnetometerrohdaten sind in einer Eigenschaft mit der Bezeichnung MagnetometerReading verfügbar. Dies ist ein weiterer Vector3 relativ zu dem in Abbildung 1 gezeigten Koordinatensystem. Wenn keine Magnetquellen in der Nähe sind, ist der Vektor am Magnetfeld der Erde ausgerichtet. Der Vektor zeigt natürlich in Nordrichtung, an den meisten Orten der Nordhalbkugel aber auch zum Erdmittelpunkt. Wenn Sie das Telefon mit dem Bildschirm nach oben halten, hat dieser Vektor eine signifikante -Z-Komponente.

Die 3DPojters-Lösung enthält ein Projekt mit dem Namen Magnetometer 3D, das dem Accelerometer 3D-Projekt ähnelt, jedoch mit dem Unterschied, dass es die Compass-Klasse anstelle der Accelerometer-Klasse verwendet. Abbildung 4zeigt die Anzeige dieses Programms, wenn ich das Telefon in meiner Wohnung in Manhattan mit dem Bildschirm nach oben halte und die Oberseite des Telefons in "Uptown"-Richtung zeigt, d. h. so, dass die linke und die rechte Seite des Telefons (so gut es geht) parallel mit den Avenues von New York City ist. (Eine Karte zeigt, dass diese Avenues um etwa 30 Grad von der Nordrichtung abweichen).


Abbildung 4 Das Magnetometer 3D-Display

Die Dokumentation sagt, dass der MagnetometerReading-Vektor die Einheit Mikrotesla verwendet. Auf einem der zwei kommerziellen Windows Phone-Geräte, die ich besitze, hat dieser Vektor normalerweise eine Größe im 30-er-Bereich, was etwa korrekt ist. (Die in Abbildung 4 gezeigten Vektorwerte haben eine kombinierte Größe von 43.) bei einem dritten Telefon, das ich besitze, ist der MagnetometerReading-Vektor normalisiert und hat immer die Größe 1.

Versuchen Sie jetzt Folgendes: Halten Sie das Telefon so, dass der Magnetometer 3D-Vektor praktisch mit der positiven Y-Achse in Abbildung 1 zusammenfällt. Drehen Sie das Telefon jetzt um eine Achse parallel zu diesem Vektor. Der Vektor bleibt (annähernd) gleich, was anzeigt, dass der Magnetometer des Telefons auch nicht über vollständige Informationen zur Orientierung des Telefons verfügt.

Kompasskursrichtungen

Normalerweise wollen wir bei der Verwendung des Kompasses nicht, dass der 3D-Vektor am Magnetfeld der Erde ausgerichtet ist. Viel nützlicher wäre ein 2D-Vektor, der tangential zur Erdoberfläche verläuft.

Wie Sie wahrscheinlich wissen, fällt das Magnetfeld der Erde nicht mit der Rotationsachse des Planeten zusammen. Die Richtung der Erdachse wird als geographischer (oder "wahrer") Norden bezeichnet. Diese Nordrichtung wird auf Karten und für praktisch alle weiteren Zwecke verwendet. Winkel auf einer zweidimensionalen Oberfläche dienen häufig zur Repräsentation der Nordrichtung. Diese Richtung wird oft als Kursrichtung oder Peilung bezeichnet.

Die Abweichung zwischen magnetischem und wahrem Norden ist überall auf der Welt verschieden. In New York City müssen etwa 13 Grad von der magnetischen ("missweisenden") Peilung abgezogen werden, um die wahre ("rechtweisende") Peilung zu erhalten, in Seattle müssen jedoch 21 Grad zu der magnetischen Peilung addiert werden. Die Compass-Klasse führt für Sie auf der Grundlage des Standorts des Telefons diese Berechnungen durch. Neben dem MagnetometerReading-Vektor stellt CompassReading auch die beiden Eigenschaften des Typs mit dem doppelten Namen MagneticHeading und TrueHeading bereit. Bei beiden handelt es sich um Winkel zwischen 0 und 360 Grad, gemessen entgegen dem Uhrzeigersinn von der in Abbildung 1 gezeigten positiven Y-Achse.

TrueHeading sollte immer als Annäherungswert interpretiert werden – selbst dann sollten Sie diesem Wert nicht vollständig vertrauen. Bei einem meiner zwei Telefone ist TrueHeading normalerweise etwa richtig, auf dem anderen zeigt sich jedoch eine Abweichung von etwa 70 Grad.

Mit dem MagneticHeading-Wert konnte ich noch nichts Rechtes anfangen. An einem bestimmten Ort sollte die Abweichung zwischen den Werten für TrueHeading und MagneticHeading konstant sein. An meinem Wohnort sollte der Wert von TrueHeading minus MagneticHeading bei etwa -13 Grad liegen. Auf allen meinen drei Telefonen wechselt die Abweichung zwischen TrueHeading und MagneticHeading jedoch sporadisch zwischen zwei Werten, je nach der Orientierung des Geräts. Manchmal beträgt die Abweichung -12 (was etwa korrekt ist), meistens ist sie jedoch 92. Dies sind die einzigen beiden Abweichungswerte, die ich gesehen habe. Auf einem meiner Telefone ist der MagneticHeading-Wert mit dem Winkel konsistent, der aus den X- und Y-Werten des MagnetometerReading-Vektors abgeleitet ist.

In einem XNA-Programm können Sie, wie Sie gesehen haben, mit der Update-Methode einfach einen aktuellen Wert von einem Sensor abrufen. Wenn Sie in einem Silverlight-Programm eine Sensor-Klasse verwenden, sollten Sie einen Handle für das CurrentValueChanged-Ereignis einrichten. Sie erhalten dann ein Sensorwertobjekt aus den Ereignisargumenten.

Der Beispielcode zu diesem Artikel enthält zwei Silverlight-Programme, Compass und Dial Compass, die die Eigenschaft TrueHeading verwenden, um die Nordrichtung anzuzeigen. Alle Grafiken sind in XAML definiert. Wie bei den XNA-Programmen erstellen diese Silverlight-Programme das Compass-Objekt direkt ihren Constructors, es wird aber auch ein Handle für die Eigenschaft CurrentValueChanged eingerichtet.

if (Compass.IsSupported)
{
  compass = new Compass();
  compass.TimeBetweenUpdates = TimeSpan.FromMilliseconds(33);
  compass.CurrentValueChanged += OnCompassCurrentValueChanged;
}

In Compass setzt dieser Handle den Winkel auf einem RotateTransform-Objekt, das mit einer Pfeilgrafik verbunden ist:

this.Dispatcher.BeginInvoke(() =>
{
  arrowRotate.Angle = -args.SensorReading.TrueHeading;
  accuracyText.Text = String.Format("±{0}°",
                      args.SensorReading.HeadingAccuracy);
});

Der CurrentValueChanged-Handle wird in einem separaten Thread aufgerufen, Sie müssen also einen Dispatcher verwenden, um eventuelle UI-Objekte zu aktualisieren. Da der TrueHeading-Winkel eine Verschiebung gegen den Uhrzeigersinn anzeigt und Silverlight-Rotationen im Uhrzeigersinn ablaufen, verwendet der Code für die Rotation den negativen Wert des Peilungswinkels.

Das Ergebnis zeigt Abbildung 5, wiederum für ein Telefon, das in New York City in "Uptown"-Richtung zeigt.

The Arrow Compass Display
Abbildung 5 Das Compass-Display

In Dial Compass bleibt der Pfeil unbeweglich, während die Skala sich zur Anzeige der Richtung dreht, wie in Abbildung 6 gezeigt. Sie verwenden diese Variation, wenn Sie wissen möchten, in welche Richtung die Oberkante des Telefons zeigt, und nicht, wo in Bezug zum Telefon die Nordrichtung ist.

The Dial Compass Display
Abbildung 6 Das Dial Compass-Display

Wenn Sie eines dieser Programme ausführen und das Telefon mit dem Bildschirm nach unten halten, funktioniert der Kompass nicht mehr korrekt. Die Rotation verläuft umgekehrt zur gewünschten Richtung. Wenn Sie dies korrigieren müssen, sollten Sie den positiven TrueHeading-Wert verwenden, wenn der Z-Wert des Beschleunigungsvektors positiv ist.

Kalibrierung des Kompasses

In der rechten unteren Ecke zeigt das Compass-Programm die Eigenschaft HeadingAccuracy des CompassReading-Werts an. Theoretisch zeigt dieser Wert die Genauigkeit der Peilungswerte. In der Praxis habe ich jedoch HeadingAccuracy-Werte zwischen 5 und 30 Prozent gesehen. (Ich habe jedoch auf dem Telefon, das die größte Abweichung zeigt, nur 5 Prozent gesehen!)

Die Compass-Klasse definiert auch ein Ereignis mit der Bezeichnung Calibrate, das ausgelöst wird, wenn der HeadingAccuracy-Wert 20 Prozent überschreitet.

Sie können diesen HeadingAccuracy-Wert mit einer Kalibrierungsaktion senken. Halten Sie das Telefon vom Körper weg mit dem Bildschirm nach links oder rechts und schwingen Sie dann Ihren Arm mehrmals in einer "8"-Figur. Einige der Codebeispiele zu den MSDN-Lernprogrammen (unter bit.ly/yYrHrL) haben sogar eine Grafik, die Sie anzeigen können, um die Benutzer darauf hinzuweisen, dass der Kompass kalibriert werden muss.

Kombination von Kompass und Beschleunigungsmesser

Der Beschleunigungsmesser des Telefons ist ein perfektes Beispiel für einen Sensor, der offensichtlich selbst Nutzen bringt – besonders, wenn Sie sich im Wald verlaufen haben –, der aber noch viel wertvoller wird, wenn er mit anderen Sensoren kombiniert wird, etwa mit dem Beschleunigungsmesser. Gemeinsam können diese beiden Sensoren das Telefon vollständig über seine Orientierung im dreidimensionalen Raum informieren.

Tatsächlich gibt es in Windows Phone 7.1 eine neue Klasse, die genau dies tut. Neben der Bereitstellung der Telefonorientierung auf drei verschiedenen Wegen enthält die Motion-Klasse auch Informationen von der neuen Gyroscope-Klasse, wenn sie auf dem Telefon vorhanden ist.

Aber die Motion-Klasse tut noch mehr: Sie glättet die Daten vom Beschleunigungsmesser und vom Kompass. Wenn Sie die bisher vorgestellten Programme ausgeführt haben, haben Sie vielleicht eine erhebliche Inkonsistenz der Daten von diesen Klassen bemerkt. Die Motion-Klasser macht dem ein Ende.

Da ich jedoch persönlich keiner wirklich interessanten Herausforderung aus dem Weg gehe, dachte ich mir, ich sollte versuchen, die Beschleunigungsmesser- und die Kompassdaten "manuell" zu kombinieren. Ich muss gestehen, dass ich nach diesem Versuch die Motion-Klasse umso mehr schätze!

Das Compass 3D-Programm zeigt vier verschiedenfarbige Nadeln in einer Kreisanordnung, die nach Norden (Silber), Osten (Rot), Süden (Grün) und Westen (Blau) zeigen. Das Programm versucht, diese Ebene mit Nadeln parallel zur Erdoberfläche und korrekt an den vier Himmelsrichtungen ausgerichtet anzuzeigen.

Meine Strategie bestand in der Ableitung von Eulerwinkeln. Dies sind die drei Winkel, die die Rotation um die X-, Y- und Z-Achse repräsentieren. gemeinsam beschreiben Sie eine Orientierung im dreidimensionalen Raum. In der Flugdynamik werden diese drei Winkel als Pitch, Roll und Yaw bezeichnet. Aus der Perspektive eines Flugzeugs gibt der "Pitch"-Wert an, ob (und um wie viel) die Nase des Flugzeugs nach oben oder unten zeigt, während der "Roll"-Wert die Neigung nach links oder rechts anzeigt. Diese Rotationswinkel können relativ zu zwei Achsen visualisiert werden: Pitch ist die Rotation um eine Achse durch die Tragflächen, und Roll basiert auf einer Achse, die von vorn nach hinten durch das Flugzeug verläuft. "Yaw" ist die Rotation um eine Achse, die senkrecht zur Erdoberfläche verläuft. Dieser Wert zeigt die Kompassrichtung für das Flugzeug an.

Stellen Sie sich zur Visualisierung dieser Winkel in Bezug auf das Koordinatensystem des Telefons in Abbildung 1 vor, Sie sitzen auf dem Telefon wie auf einem fliegenden Teppich mit der Vorderkante des Telefons in Blickrichtung und den drei Knöpfen hinter Ihnen. Pitch ist die Rotation um die X-Achse, Roll um die Y-Achse und Yaw ist die Rotation um die Z-Achse.

Die Berechnung von Roll und Pitch aus dem Beschleunigungsvektor erwies sich als recht einfach; dazu gehören Standardformeln:

float roll = (float)Math.Asin(-acceleration.X);
float pitch = (float)Math.Atan2(acceleration.Y, -acceleration.Z);

Wenn das Telefon mit dem Bildschirm nach oben flach auf einem Tisch liegt, sind Pitch und Roll gleich Null. Der Roll-Wert variiert zwischen –π/2 und π/2, und die Werte gehen zurück zu Null, wenn der Bildschirm nach unten gedreht wird. Der Pitch-Wert variiert zwischen –π und π, mit dem Maximalwert, wenn der Bildschirm nach unten zeigt. Wenn der Bildschirm des Telefons nach oben zeigt, sollte der Yaw-Wert mehr oder weniger dem der Eigenschaft TrueHeading des Kompasses entsprechen, jedoch für XNA-Zwecke in Bogenmaß konvertiert:

float yaw = MathHelper.ToRadians((float)compass.CurrentValue.TrueHeading);

Wie Sie jedoch bei den beiden Silverlight-Kompassprogrammen gesehen haben, funktioniert TrueHeading nicht mehr korrekt, wenn der Bildschirm des Telefons zur Erde zeigt, der Yaw-Wert muss daher dahingehend korrigiert werden. Nach einigen theoretischen und praktischen versuchen ließ ich dies sein und konstruierte einen World Transform aus den drei Winkeln:

basicEffect.World = Matrix.CreateRotationZ(yaw) *
                    Matrix.CreateRotationY(roll) *
                    Matrix.CreateRotationX(pitch);

Die Ergebnisse sind in Abbildung 7 dargestellt.

The Compass 3D Program
Abbildung 7 Das Compass 3D-Programm

Ich habe auch ein Orientation 3D-Programm hinzugefügt, das diese drei Winkel aus der Motion-Klasse enthält. Sie sehen selbst, um wie viel eleganter die Ergebnisse sind (und dazu kommt noch, dass das Telefon besser funktioniert, wenn sein Bildschirm nach unten zeigt).

Die Motion-Klasse ist eine zu wichtige Ergänzung für das Sensor-API- um nur in diesem einen Programm verwendet zu werden. In der nächsten Folge dieser Kolumne sehen Sie, dass diese Klasse geradezu als Portal in die 3D-Welt dienen kann.

Charles Petzold schreibt seit langem redaktionelle Beiträge für das MSDN Magazin. Die Adresse seiner Website lautet charlespetzold.com.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Drew Batchelor