Sergiy Krutykov

Die Anzeige der Karten auf iPhone ist grundsätzlich sehr einfach: Man muss lediglich eigene Klasse von einer speziellen Klasse (MKMapView) ableiten. Wenn man dann die Satellitenansicht aktiviert und die Karte zu dem Landscape um -3/2 PI dreht, dann bekommt man bereits die Ansicht, wie in der Abbildung:

Annotationen

Zum Anzeigen verschiedener Gegenstände auf der Karte können spezielle Objekte, die so genannten Annotationen verwendet werden. Die von Google Maps API bekannten Stecknadeln, sind ein Beispiel für solche Annotationen. Der Umgang mit Annotationen auf iPhone ist etwas umständlich. Als eine Annotation gilt jedes Objekt einer Klasse, die das Protokoll MKAnnotation adoptiert. Laut diesem Protokoll besteht eine Annotation aus einer Koordinate (in Längen- und Breitengrad), einem Titel und einem Untertitel. Darüber hinaus beinhaltet sie noch als Attribut eine spezielle UIView die MKMapAnnotationView, die ihr beim Hinzufügen zu einer Karte von dieser Karte zugewiesen wird:

Es gibt Möglichkeit eigene Views für Annotationen zu schreiben. Es gibt aber eine bereits implementierte Klasse, MKPinAnnotationView, die nämlich die oben erwähnte berühmte Stecknadel repräsentiert. Dabei kann eine solche Stecknadel eine der drei Farben, rot, grün oder violett haben. In der folgenden Abbildung sind beispielhaft drei Nadeln in unterschiedlicher Farbe dargestellt:

In der Abbildung sieht man, wofür die Attribute Titel und Untertitel stehen: Das sind die Aufschriften in der Sprechblase der entsprechenden Annotation (der Titel ist "Pin" und der Untertitel ist "Ich bin rot.").

Das Platzieren von einer Annotation auf der Karte geschieht nach dem folgenden Szenario: Nachdem eine Annotation erzeugt wurde, werden ihre Koordinaten mit gewünschten Werten gesetzt und zu einer Karte (also einer Unterklasse von MKMapView) mit dem Aufruf von addAnnotation: auf dieser Karte hinzugefügt. Kurz danach (kann aber durchaus eine Sekunde dauern) wird die Instanzmethode dieser Karte

- (MKAnnotationView *) mapView:(MKMapView *)mapView
                       viewForAnnotation:(id <MKAnnotation>)mapAnnotation

automatisch aufgerufen, die eine spezielle UIView, nämlich MKAnnotationView zurück liefert (das Argument der Methode mapView ist dabei die Karte, auf welcher sich alles ereignet, und mapAnnotation ist die hinzugefügte Annotation). Man muss also innerhalb von dieser Methode die View zurück liefern, die das Aussehen der Annotation bestimmen soll. Wenn man für diese View eine Instanz von MKPinAnnotationView benutzt, dann bekommt die Annotation das Aussehen einer Stecknadel wie oben. Alternativ kann man eine Instanz von MKAnnotationView direkt benutzen. Allerdings ist diese View initial leer. Sie hat aber ein Image als Instanzvariable, welchem Pixelgrafiken zugewiesen werden können, wie in den folgenden zwei Abbildungen dargestellt ist.

Skalierung der Bitmap-Annotationen

Wie aus den obigen Abbildungen ersichtlich ist, wird die Größe der Annotation nicht automatisch an die Zoomstufe angepasst, sondern bleibt immer konstant, was optisch (und gerade für ein Spiel) sehr unpassend ist. Um dies zu ändern, muss man das Image der Annotation-View selbst skalieren. Dies geschieht dadurch, dass das Image in einen graphischen Kontext gezeichnet wird, der eine andere Größe hat und dann der Inhalt dieses Kontextes wieder zu einem RGBA-Bild gerendert wird. Danach reicht es, einfach das alte Image der View durch das neue zu ersetzen. Diese Skalierung und Ersetzung der View kann man einfach mit Hilfe von einem Timer in regelmäßigen kurzen Abständen immer wieder wiederholen, damit die Größe der Annotation zu der aktuellen Zoomstufe immer passt. Aber die Skalierung ist ein aufwändiger Prozess, deshalb ist eine Lösung ziemlich unpraktikabel. Viel bessere Lösung wäre, die Größe nur dann zu ändern, wenn die Zoomstufe sich ändert. Abhilfe schafft hier eine Listener-Methode der Mapview, die gerade dann aufgerufen wird, wenn das Zoom sich ändert. Die Mapview hat eine Auswahl an solchen Methoden: Die Methode mapView:viewForAnnotation: von oben ist auch von dieser Art. In Bezug auf das Zoomen werden vor allem die beiden Methoden aufgerufen:

- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated

mapView:regionWillChangeAnimated: wird aufgerufen kurz bevor die Region (also die Größe und/oder die Position des auf dem Bildschirm sichtbaren Kartenausschnitts) sich ändert und mapView:regionDidChangeAnimated: direkt danach. Damit ist garantiert, dass, wenn sich die Zoomstufe ändert, die zweite Methode aufgerufen wird und dass sie aber nicht permanent ausgerufen wird, sondern nur wenn der Benutzer die Karte scrollt oder zoomt. Es bleibt dann nur zu bestimmen, auf welche Weise die Größe der Region die Skalierung der Annotation-View beeinflussen soll. Dafür gibt es keine fest definierte Formel. Was die Karte diesbezüglich zur Verfügung stellt, ist eben nur diese Region mit ihrer Position und der Größe. Die Größe wird dabei in Längen- und Breitengrad angegeben. Da die Länge sich von Polen bis hin zum Äquator sehr stark ändert, bietet sich an, die Breite zum Bestimmen der Zoomstufe zu benutzen. Man kann zum Beispiel die Breite eine Region, bei welcher die Annotaiton-View ohne Skalierung von der Größe her passt, als Ausgangslänge definieren, und dann jedes Mal die neue Breite durch diese fest definierte teilen, um den Skalierungsfaktor zu bestimmen. Auf diese Weise bekommt man das Ergebnis wie in der folgenden Abbildung:

Bei der Skalierung ist es wichtig darauf zu achten, dass wenn die Zoomstufe zu klein ist, dann werden die Annotationen entsprechend viel zu klein und verschwindet eventuell gänzlich. Dafür muss man die minimale und maximale Größe definieren, die nicht überschritten werden dürfen.

Annotationen als Vektorgrafik

Die oben beschriebene Skalierung der Images hat die Nachteile, dass sie immer noch nicht ganz effizient ist und auch starke Pixeleffekte aufweist. Da aber die Views der Annotationen auch UIView sind, kann man sie mit Hilfe von Vektorgrafik zeichnen. Dies ermöglicht nicht nur verlustfreie Skalierung der Views sondern auch das Animieren von diesen. In den folgenden drei Abbildungen sind einige Vorteile der Vektorgrafik beispielhaft vorgestellt:

Beim Vergleich des linken Bildes mit den beiden anderen, sieht man, dass die Skalierung verlustfrei verläuft. Und der Vergleich des mittleren Bildes mit dem rechten aufweist, dass die Polizisten und die Diebe ihre Augen bewegen, und dass die Scheibe, die den Spieler selbst darstellen soll, leuchtend blinkt. Während die Bewegung der Augen nur dazu dient, die Applikation "lebhafter" zu gestalten, ist das blinkende Leuchten hilfreich, um eine sonst unauffällige Annotation auf der Karte besser erkennbar zu machen. Aus dem gleichen Grund erscheint hinter den Köpfen der Polizisten und Diebe ein farbliches halbdurchsichtiges abgerundetes Rechteck, welches um so weniger transparent wird, je kleiner die Annotationen werden. Beim Vergleich des mittleren Bildes mit dem rechten sieht man sonst das der Licht-Keil, der den Blickwinkel des Spielers (gemäß der Richtung des Kompasses) symbolisieren soll, drehbar ist, womit nicht nur Position des Spielers sondern auch seine Orientierung visualisiert wird.

Automatische Drehung der Karte

Die Orientierung des Spielers kann man nicht nur, wie oben beschrieben, mit einem Keil visualisieren, sondern man kann auch die Karte selbst drehen, so dass die Orientierung der Karte genau zu der Umgebung passt. Offensichtlich macht dieses Vorgehen nur dann Sinn, wenn der Spieler sich exakt in dem Zentrum der Karte befindet. Die Karte kann man als jede UIView beliebig transformieren, zum Beispiel um ihren Mittelpunkt drehen. Allerdings reicht das einfache Drehen nicht aus, wie die folgende Abbildung zeigt:

Damit man die schwarzen Ränder nicht sieht, muss man zu einem Trick greifen, nämlich die Karte so vergrößern, dass sie auch beim Drehen immer noch den ganzen Bildschirm ausfüllt. Problematisch ist dann nur der Google-Logo, der dann am Rande dieser vergrößerten Karte bleibt und nicht mehr sichtbar wird. Dies ist natürlich ein Urheberrecht-Verstoß, der möglichst umzugehen ist. Da hilft noch ein Trick. Es stellt sich heraus, dass der Google-Logo einfach eine (und dabei die einzige) Subview auf der Karte ist, so dass man diese einfach manipulieren kann. Dafür muss man nur das Array von Subviews subvies von der Karte mit Hilfe von einer for-each-Schleife durchgehen, und bereits den Verweis auf die erste Subview (die gerade der Logo ist) in eine Instanzvariable speichern. Dieser Logo ist dann auf eine passende Stelle zu platzieren. Man sollte dies allerdings nicht auf der Karte selbst machen, da sie ja immer wieder gedreht wird und dadurch der Logo sich immer wieder verschieben würde, sondern auf eine Parent-View wie zum Beispiel auf der View des Viewcontrollers, der diese Karte verwaltet. Es bleibt dann nur noch eine Kleinigkeit: Man sieht auf der Abbildung, dass die Annotationen mit Dieben und Polizisten auch mitgedreht werden. Deshalb muss man diese um den gleichen Winkel in die umgekehrte Richtung (in Bezug auf die Drehung der Karte) drehen, damit sie vertikal ausgerichtet sind, wie in der folgenden Abbildung:

Im Vergleich zu den oberen Abbildungen sieht man, dass die Karte zwar gedreht ist, aber es keine schwarzen Ränder gibt, Google-Logo an dem richtigen Platz ist und die Bilder von Polizisten und Dieben vertikal ausgerichtet sind.

Overlays

Außer einzelne Gegenstände auf der Karte zu platzieren, besteht noch die Möglichkeit, eine Fläche auf der Karte auf eine besondere Weise zu färben, zum Beispiel, wenn man analog zu den Strategiespielen einen "Fog of War" über einen Bereich der Karte legen will. Dies kann man auch mit Hilfe von einer (dann aber ziemlich großen) Annotation machen, indem man diese in den Mittelpunkt des Bereichs legt. Man muss dann die Größe der Annotation zu der Zoomstufe sehr genau anpassen, was sehr umständlich ist. Abhilfe schaffen hier die Overlays, die zwar zu Annotationen sehr ähnlich sind, aber für diese Probleme besser geeignet sind. Analog zu Annotationen gelten als Overlays alle Objekte, die das Protokoll MKOverlay adoptieren, laut welchem ein Overlay wie auch jede Annotation eine Koordinate aber keinen Titel oder Untertitel haben muss, dafür aber das Attribut boundingMapRect, welches die Position und die Größe des Overlays aber nicht in Längen- und Breitengrad sondern in einem speziellen 2D-projizierten Koordinatensystem beschreibt:

Das Hinzufügen eines Overlays geschieht nach dem gleichen Schema wie auch bei Annotationen: Nach dem Aufruf von der Instanzmethode addOverlay: auf einem Kartenobjekt wird in kurzer Zeit die folgende Methode aufgerufen:

(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay

In dieser Methode muss man analog zu mapView:viewForAnnotation dem Overlay eine View, die eine Unterklasse von MKOverlayView ist, zuweisen. Im Gegensatz zu Annotationen beinhalten allerdings Overlayviews keine Images, so dass man keine Pixelgrafiken mit einem Overlay verknüpfen kann und man muss mit der Vektorgrafik arbeiten. Allerdings wird dabei nicht die übliche Methode drawRect: (siehe Unterabschnitt Vektorgrafik in dem Abschnitt Smartphones) benutzt, sondern in einer von MKOverlayView abgeleiteten Klasse die Methode

- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context

implementiert. Diese Methode wird ebenfalls automatisch aufgerufen, sobald, die View die Aufforderung, sich zu erneuern, bekommt. Dies passiert normalerweise beim Zoomen und Scrollen auf der Karte. Im Gegensatz zu drawRect: bekommt sie offensichtlich als Parameter die Zeichnungsfläche nicht in Bildschirm-Koordinaten sondern in speziell projizierten Kartenkoordinaten und auch einen grafischen Kontext, welchen man zum Zeichnen auch benutzen muss. In diesem Kontext kann man alle Zeichnungen wie etwa in dem Unterabschnitt Vektorgrafik machen, wie die folgende Abbildung zeigt:

In der linken Abbildung wird einfach die Zeichnungsroutine eines Toggle-Buttons benutzt, wobei sich das Bild natürlich an alle Zoomänderungen und Kartenverschiebungen automatisch anpasst. Rechts wird der "Fog of War" gezeichnet, der aus vielen abgedunkelten Plättchen besteht und einer etwas dunkleren Umrandung des Gebiets. Wenn die Plättchen ihre Koordinaten und ihre Größe in Längen- und Breitengrad haben, dann müssen diese noch in die oben genannte Kartenkoordinaten und diese dann in die Bildschirmkoordinaten umgewandelt werden. Dabei ist der erste Schritt sehr aufwendig, weshalb, solange sich die Plättchen nicht permanent ändern, diese Berechnung vorab passieren muss und danach schon die konvertierten Kartenkoordinaten in der Routine drawMapRect:zoomScale:inContext: selbst verwendet werden sollen.

Beschränkungen der Kartenanzeige auf die vorgegebene Fläche

Da häufig, wie zum Beispiel bei einem Navigationsspiel, die Spielfläche begrenzt ist, der Benutzer aber dir Möglichkeit hat, beliebig zu zoomen und zu scrollen (dies kann auch ungewollt durch versehentliches Berühren des Bildschirms geschehen), so dass er das Spielgebiet aus den Augen verlieren kann, wenn er dies zu weit treibt, stellt sich die Frage, wie man diese Aktionen von Benutzer kontrollieren und gegebenenfalls einschränken könnte. Als erste Hilfe gibt es die Möglichkeit der Karte Gesten-Listener zuzuweisen, so dass zum Beispiel per Doppeltap die Karte auf dem Spieler zentriert wird. Damit kann der Benutzer auch bei viel zu wildem Scrollen immer noch zu seiner eigenen Position schnell springen. Allerdings ist die beste Lösung, dem Benutzer ganz zu verbieten, zu weit zu scrollen oder zu zoomen. Gerade in Bezug auf das Zoomen, könnte damit das Problem vermieden werden, dass bei Google Maps in der Satellitenansicht bei zu großen Zoomstufen manchmal anstatt von Satellitenfotos einfach nur grauer Hintergrund mit den Aufschriften "No Images" darauf erscheint, was für manche Applikationen, vor allem Spiele, inakzeptabel ist:

Die Gesten des Benutzers für das Zoomen und Scrollen werden durch die Karte selbst verarbeitet und man kann auf sie kaum Einfluss nehmen. Allerdings könnten hier die oben genannten Methode behilflich sein:

- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated

Die Idee ist dabei, dass man in der Methode mapView:regionWillChangeAnimated:, also noch bevor die Änderung der Ansicht passiert, die Parameter der Region zwischenspeichert und danach in mapView:regionDidChangeAnimated: zuerst überprüft, ob die neue Region noch zulässig ist und wenn ja, dann wird nichts gemacht, sonst wird die Region der Karte auf den alten Wert zurückgesetzt.

Diese Methode hat den Nachteil, dass der Benutzer beim Scrollen keine zu langen Züge macht, denn die Methode mapView:regionDidChangeAnimated: wird aufgerufen, nur wenn der Benutzer den Finger loslässt und wenn er durch zu langes Ziehen in eine nicht erlaubte Region "gerät", dann springt die Ansicht zu der Ausgangsregion, so dass der Benutzer praktisch an der gleichen Stelle bleibt. Das gleiche Problem hat man auch bei Zoomen: Es besteht nach wie vor die Möglichkeit, dass der graue Hintergrund mit den Aufschriften "No Images" zwischendurch erscheint und erst bei Loslassen der Finger wieder verschwindet.

Gegen diese Probleme kann man nur etwas unternehmen, wenn man die Gesten selbst verarbeitet. Dafür muss man allerdings in erster Linie in der Karte das Zoomen und Scrollen ausschalten. Dann muss man in den entsprechenden Listener-Methoden analog zu dem oben Beschriebenen überprüfen, ob die neue Region noch passt. Schwierig ist dabei diese neue Region zu bestimmen. Währen bei der "pinch-to-zoom" Geste der Skalierungsfaktor zur Verfügung steht, welcher dazu benutzt werden kann, in Verbindung mit der alter Zoomstufe die neue Zoomstufe zu berechnen, muss man beim Scrollen die neue Region anhand der Bewegung des Fingers auf dem Bildschirm bestimmen, wobei die Bildschirmkoordinaten zuerst zu den geographischen und dann zu den Kartenkoordinaten konvertiert werden müssen.


Page last modified on March 21, 2011, at 08:26 PM