Gestern Abend sprach mit meiner Bruder über die vielen Ideen, die wir in den letzten Jahren hatten. Manche von denen sind gleich in Vergessenheit geraten und manche habe ich heute noch im Kopf. Aber wirklich viele davon, habe wir nicht umgesetzt. Um ehrlich zu sein nur wenige. Es ist dieses Gefühl, wenn eine Idee geboren wird und bei einem guten Gespräch wächst, das ich wirklich spannend finde. Zu beginn nur eine Waage Vorstellung, dann immer detaillierter und irgendwann eine finale Version mit dem Nachsatz:
So machen wir es.
Wenn es dann an die Arbeit gehen soll, fühlt sich es wie ein Rückschlag an. In der Realität kommen dummerweise einige “Vorher muss ich noch…” ’s oder “Gleich nachdem…” ’s zuvor und das stark brennende Feuer von eben wird mit einem kräftigen Schank Wasser abgeschreckt. Ich dem Augenblick wünschte ich mir mich klonen zu können, damit sich mein geklontes Ich voll und ganz auf das neue Projekt konzentrieren kann. Keine Arbeit bei der er jeden Morgen antreten, keine Familie und Freunde um die er sich kümmern muss, und auch keine aktuell laufenden Projekte, die einen beträchtlichen Teil seiner Zeit in Anspruch nehmen. Mit der Voraussetzung müsste es mein Klon packen.
Im Moment kann ich keine Klone erschaffen und muss mir daher anders helfen. Interessant fand ich einen Blog-Eintrag, in dem sich jemand für eine Ideenbörse ausgesprochen hat. Er empfand es als schade, dass viele gute Ideen verloren gehen, weil die Menschen nicht die Zeit oder nicht den Ehrgeiz besitzen diese umzusetzen. Wieso also nicht mit jemandem teilen und der Idee eine Chance geben verwirklicht zu werden? Mein erster Gedanke war: “Ja klar, damit sich jemand mir meiner Idee die Taschen vollstopfen kann”.
Heute denke ich ganz anders darüber.
- Nicht jede Idee hat Potenzial. Sie kann ganz nett sein, aber ohne einen echten Zuspruch, der sich erst in der Praxis zeigt, leider nicht überlebensfähig.
- Im Nachhinein bin ich immer schlauer. Wenn also jemand meine Idee umsetzen sollte, können zwei Dinge passieren. Entweder die Umsetzung wird erfolgreich, oder nicht. Wenn sie erfolgreich wird, dann ärgere ich mich über mich selbst, weil nicht ich die Idee umgesetzt habe. Sollte sie kein Erfolg haben, dann tue ich so, als ob ich es geahnt und deshalb mit dem Nichtstun richtig gelegen habe.
- Wir leben den Kapitalismus. Wir werden dazu erzogen, dass mit jeder unser Bewegung, jeder unserer Tat, Geld verbunden ist - egal ob Einnahme oder Ausgabe. Dadurch herrscht eine - Jeder ist sich selbst der Nächste - Mentalität. Das hat einen interessanten Effekt, der mir erst kürzlich bewusst wurde: Wenn ich jemanden eine Idee erzähle, und der wirtschaftliche Aspekt nicht deutlich wird, dann kommt in 90% der Fälle die Frage: “Und wie verdienst du damit Geld?”. Wenn ich mit “Ich weis es noch nicht” antworte, dann ist die Idee schlecht.
In meiner Vorstellung einer Ideenbörse sind also auch Ideen erlaubt, die nicht sofort die Millionen bringen - oder auch gar nicht. Denn genau genommen ist eine Idee eine potenzielle Lösung für ein vorangegangenes Problem. Sie ist das Ergebnis einer Kette von Gedankengängen, die sich ein Mensch auf der Suche nach einer Lösung baut.
Deshalb würde ich auch mal behaupten, dass der Satz “Man muss nur die richtige Idee haben..” nichts als eine bequeme Ausrede für jeden ist, der Angst vor der Arbeit hat. Denn, Probleme hat ja wahrscheinlich jeder.
Ich baue derzeit an einer Webanwendung in Grails und bin an einem Punkt angekommen, an dem ich unheimlich gern auf GWT zurückgreifen würde. Ich sehe in beiden Technologien viele Vorteile und würde nicht behaupten, dass es sich zwischen den beiden entscheiden sollte. Vielmehr denke ich, dass sich Grails und GWT sehr gut ergänzen können.
Es existiert ein Grails / GWT Plugin mit dem es möglich ist GWT im Frontend und Grails im Backend zu verwenden. Aber es nicht komplett das was ich suche. Denn ich will keine reine GWT-Applikation bauen. Was mir vorschwebt ist eine vollfunktionsfähige, in Grails geschriebene Webseite, die ohne JavaScript auskommt. Auch wenn die breite Masse der Browser JavaScript unterstützt, gibt einige Benutzergruppen die nicht von der clientseitigen Dynamik profitieren können - Nutzer eines Screen-Readers z.B.
Meine Idee ist es nun die Anwendung in Grails zu schreiben und die zusätzliche clientseitige Funktionalität durch GWT zu ergänzen. Ich habe mir ein Szenario ausgedacht, das wie ich denke, ziemlich deutlich macht, was ich will: Meine Webanwendung stellt eine Liste von POI’s dar. Also geo-referenzierte Punkte, die bestimmte Lokalitäten markieren. Ein Klick auf einen der POI Namen öffnet mir eine Karte mit einem Marker auf der geografischen Position des POI’s. Je nach dem ob JavaScript aktiviert ist oder nicht, erscheint die Karte als ein in JS erstelltes Popup oder es folgt der Verweis auf eine seperate URL, auf der eine statische Karte gezeigt wird.
Für das Beispielprojekt verwende ich Grails 1.2.2, GWT 2.0.3, GWT-Query 1.0.0, GWT-Maps 1.0.4, Maven2 und schreibe den Code in IntelliJ Idea. Die Grails Anwendung erzeuge ich mir mit Hilfe des Grails-Maven-Plugins:
$> mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-4:generate \
-DarchetypeGroupId=org.grails\
-DarchetypeArtifactId=grails-maven-archetype \
-DarchetypeVersion=1.2.2 \
-DgroupId=org.example \
-DartifactId=gwt-with-grails
$> cd gwt-with-grails
$> mvn initializeDie erzeuge pom.xml beinhaltet ein Minimum an Konfiguration und zumindest in meinem Fall musste ich aufgrund einiger Exceptions noch einige Abhängigkeiten nach pflegen.
<dependency> <groupId>org.grails</groupId> <artifactId>grails-test</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.5.8</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency>
Laut der Maven Konvention gehören die Quellen in das ${basedir}/src/main/java Verzeichnis, in dem ich mir das org.example.gwt.share Package anlege und eine Poi Klasse definiere. Der Aufbau ist denkbar einfach: Eine eindeutige Id, ein Name und die geografischen Koordinaten.
public class Poi { private final Long id; private String name; private double lat; private double lng; /* Konstruktoren, getter und setter */ ... }
Der Einfachheit halber arbeite ich in diesem Beispiel mit Dummy-Daten und erzeuge mir ein Grails Controller mit einigen, statischen POI’s. In dem index Closure wird die index View gerendert und alle POI’s an die View weitergegeben.
class PoiController { static def POIS = [:] static { POIS[1] = new Poi(1, "Hannover", 52.423, 9.72) POIS[2] = new Poi(2, "Dortmund", 51.516, 7.43) POIS[3] = new Poi(3, "Dresden", 51.048, 13.75) POIS[4] = new Poi(4, "Stuttgart", 48.800, 9.20) } def index = { render view: 'index', model: [pois: POIS.collect { k, v -> v }] } ... }
Damit ich zu einem späteren Zeitpunkt auf die Daten der einzelnen POIs zugreifen kann, hinterlege ich mir die wichtigen Informationen im Markup - und zwar in versteckten Feldern, denen ich die display:none; CSS Eigenschaft über eine Klasse zuweise. Ich bin gerade dabei zu evaluieren, wie man Elemente vor Screen-Readern versteckt…
Die versteckten Elemente benötige ich später, um von GWT aus an die wirklich interessanten Informationen zu gelangen. Diese Vorgehensweise erscheint mir besser zu sein, als inline-JavaScript zu platzieren und damit den Code über mehrere Views zu verteilen. Um den POI Namen habe ich ein Link gezogen, der auf die show-Action des selben Controllers verweist. Bei einem Klick auf den Link wird eine statische Karte mit der Position des POI’s gezeigt. Dieser Link spielt später noch eine wichtige Rolle.
<%@ page contentType="text/html;charset=UTF-8" %> <%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <meta name="layout" content="poi" /> <title>Some interesting Poi's</title> ... </head> <body> <g:each in="${pois}" var="poi"> <div class="poi"> <span class="hidden id" >${poi.id}</span> <span class="hidden name">${poi.name}</span> <span class="hidden lng" >${poi.lng}</span> <span class="hidden lat" >${poi.lat}</span> <g:link class="link" controller="poi" action="show" id="${poi.id}"> ${poi.name} </g:link> </div> </g:each> </body> </html>
Klickt der Anwender auf den Link, landet der Request in der show-Action. Hier wird anhand des POI’s der Link zu dem Google Static Maps Server gebaut und zusammen mit dem POI an die View übergeben - die entsprechend schlicht gestrickt ist.
class PoiController { ... static def KEY = "MY_GOOGLE_MAPS_KEY" def show = { def poi = POIS[params.id ? params.id.toInteger() : 1] def link = new StringBuffer() link << "http://maps.google.com/staticmap" link << "?zoom=13" link << "&size=512x512" link << "&markers=${poi.lat},${poi.lng},blue" link << "&key=${KEY}" link << "&sensor=false" render view: 'show', model: [link: link, poi: poi] } }
<%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <meta name="layout" content="poi" /> <title>${poi.name}</title> </head> <body> <img src="${link}" alt="Map of ${poi.name}" title="${poi.lng},${poi.lat}" /> </body> </html>
Nun starte ich die Grails Anwendung über den grails:run-app Goal und sehe im Browser wie erwartet eine Liste der POI’s. Klicke ich auf einen der Einträge, wird die statische Karte als Bild im Browser geladen. Bis hier verhält sich meine Anwendung wie gewollt und ein Anwender mit deaktiviertem JavaScript wird die Anwendung bedienen können.
Nun geht es darum den Klick-Handler des zuvor erwähnten Links mit JavaScript zu überschreiben. Nicht die show-Action soll aufgerufen werden, sondern es soll ein mit JavaScript erzeugtes Popup erscheinen, in dem eine dynamische Google Maps Karte mit einem Marker geladen ist.
Ich erweitere meine pom.xml Datei um die GWT-Abhängigkeiten:
... <dependency> <groupId>com.google.gwt</groupId> <artifactId>gwt-servlet</artifactId> <version>2.0.3</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.gwt</groupId> <artifactId>gwt-user</artifactId> <version>2.0.3</version> </dependency> ... <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>gwt-maven-plugin</artifactId> <version>1.2</version> </plugin> ...
Nachdem das erledigt ist, erstelle ich eine neues GWT Modul und nenne es org.example.gwt.Poi. Hier ist drauf zu achten, dass die Java-Quellen für Client und Server sich im ${basedir}/src/main/java-Verzeichnis und die Modulbeschreibungen *.gwt.xml im ${basedir}/src/main/resources-Verzeichnis befinden. So sieht momentan meine Verzeichnisstruktur aus:

(Übrigens, die HTML und CSS Datei, die für ein GWT Modul normalerweise erzeugt wird, benötigen wir nicht.) In meinem GWT Modul habe ich mir ein EntryPoint erstellt und es unter PoiEntryPoint gespeichert. Nun kommt ein wichtiger Schritt. Das GWT Modul muss einmal übersetzt werden. Dafür rufe ich den gwt:compile Goal auf und kopiere anschliessend das Kompilat, das sich irgendwo unter ${basdir}/target befindet nach ${basedir}/web-app/js. Das muss nur ein mal gemacht werden, damit der Browser die Einstiegs-JavaScript Datei findet und die GWT Anwendung sich auf dem gewohnten Weg starten und debuggen lässt. Die einzelnen Schritte können auf der GWT Seite nachgelesen werden.
In Grails muss ich nun dafür sorgen, dass der Einstiegspunkt in die GWT Anwendung geladen wird. Das ist die org.example.gwt.Poi.nocache.js Datei, die ich zuvor kopiert habe. Es bietet sich an diesen Import in einer Layout-View durchzuführen.
<html> <head> <title><g:layoutTitle default="My POIs"/></title> <g:layoutHead/> </head> <body> <g:layoutBody/> <!-- Wichtiger Schritt --> <g:javascript library="org.example.gwt.Poi/org.example.gwt.Poi.nocache"/> </body> </html>
Starte ich meine Grails-Anwendung erneut, muss der Browser die org.example.gwt.Poi.nocache.js Datei geladen haben. Dafür am besten mal mit dem Safari Inspector oder Firebug den Inhalt der Datei betrachten. Befindet sich minifizierter JavaScript Code im Inhalt, ist alles gut gegangen.
An dieser Stelle ist die Brücke zu GWT bereits geschlagen und ich kann mich an die Implementierung des EntryPoints machen. Für GWT existiert ein jQuery Nachbau, mit dem es unheimlich einfach ist Elemente aus dem DOM abzugreifen und Operationen wie Event-Handler, Animationen oder CSS auf den Elementen durchzuführen. Das Modul heißt GwtQuery. (Ich habe das Modul in mein lokales Maven Repository installiert, weil ich kein öffentliches Repository fand, aus dem ich das ziehen konnte.)
public class PoiEntryPoint implements EntryPoint { private MapWidget map = null; private PopupPanel mapsPanel; public void onModuleLoad() { $(".poi").each(new Function() { public void f(Element e) { final Long id = Long.parseLong($(e).find("span.id").text()); final Double lat = Double.parseDouble($(e).find("span.lat").text()); final Double lng = Double.parseDouble($(e).find("span.lng").text()); final String name = $(e).find("span.name").text(); $(e).find("a.link").click(new Function() { public boolean f(Event e) { displayPoi(new Poi(id, name, lat, lng)); return false; } }); } }); } ...
In dem EntryPoint iteriere ich über alle DIV-Container mit der Klasse poi und lese die versteckten Informationen aus. Der Klick-Handler des Links, der eigentlich auf die show Action verweist überschreibe ich und sorge mit dem return false; dafür, dass das Ziel nicht angesprungen wird. Stattdessen soll der POI auf einer Karte in einem Popup-Dialog erscheinen.
Dadurch, dass meine Grails Anwendung bereits in einem Container läuft und mir die HTML Seiten mit Inhalt rendert, benötige ich keinen weiteren Server, den GWT im Normalfall starten würde. Deshalb erstelle ich mir eine GWT Konfiguration mit der -noserver Direktive und übergebe als Start-URL die URL meines Grails-Servers. In meinem Fall die http://localhost:8080/gwt-with-grails/poi/index
Nun setzte ich in dem EntryPoint einen Breakpoint, starte die GWT Konfiguration und öffne den Browser. Alles läuft gut, wenn in der onModuleLoad Methode über meine POI Elemente iteriert wird und die Klick-Handler erfolgreich überschrieben sind.
Der Rest des Codes ist trivial, aber ich poste ihn der Vollständigkeit wegen:
... private void displayPoi(final Poi poi) { loadGoogleMaps(new MapsCallback() { public void doWithMap(MapWidget mapWidget) { final LatLng coords = LatLng.newInstance(poi.getLat(), poi.getLng()); map.clearOverlays(); map.setCenter(coords); map.setZoomLevel(12); map.addOverlay(new Marker(coords)); map.getInfoWindow().open(map.getCenter(), new InfoWindowContent(poi.getName())); mapsPanel.show(); } }); } private void loadGoogleMaps(final MapsCallback mapsCallback) { if (map != null) { mapsCallback.doWithMap(map); } else { mapsPanel = new PopupPanel(true, true); mapsPanel.setGlassEnabled(true); mapsPanel.setWidget(new Label("Loading...")); mapsPanel.setWidth("500px"); mapsPanel.setHeight("500px"); mapsPanel.setPopupPositionAndShow(new PopupPanel.PositionCallback() { public void setPosition(int offsetWidth, int offsetHeight) { int left = (Window.getClientWidth() - offsetWidth) / 2; int top = (Window.getClientHeight() - offsetHeight) / 2; mapsPanel.setPopupPosition(left, top); } }); Maps.loadMapsApi("", "2", false, new Runnable() { public void run() { map = new MapWidget(); map.setSize("100%", "100%"); map.addControl(new LargeMapControl()); mapsPanel.setWidget(map); mapsCallback.doWithMap(map); } }); } } private interface MapsCallback { public void doWithMap(MapWidget mapWidget); }
Klicke ich nun auf einen POI Namen, öffnet sich ein modaler Dialog mit der Karte und dem Marker.
In meiner App möchte ich ein Diagramm darstellen und benötige dafür eine Möglichkeit 2D Grafiken zu erzeugen. Die API von Titanium bietet derzeit keine Möglichkeit dies zu tun, weshalb ich mir überlegt habe WebKit zu nutzen und dann auf einem Canvas zu zeichnen. Die ersten Versuche im iPhone Simulator waren sehr vielversprechend, aber leider wird der Code auch mit der Geschwindigkeit meines MacBooks ausgeführt. Die Performanz, mit der die Diagramme auf dem Gerät erzeugt werden, ist eher ernüchternd.
Bevor ich auf die einzelnen Werte eingehe, erkläre ich erstmal einen möglichen Weg Diagramme mit Titanium darzustellen. Meine Ordnerstruktur sieht wie im folgenden Bild aus:

Die Titanium Skripte liegen bei mir alle in der Wurzel. In diesem Fall ist es nur die app.js. Sie ist einfach aufgebaut und erzeugt nur eine WebView, in der eine URL geladen wird.
1 2 3 4 5 6 7 8 9 10 | var window = Ti.UI.createWindow({ orientationModes: [Ti.UI.LANDSCAPE_RIGHT] }); var webkit = Ti.UI.createWebView({ url: 'web/chart.html' }); window.add(webkit); window.open(); |
Die WebView Komponente lädt eine lokale HTML Seite. Die HTML Seiten befinden sich in dem Ordner web und relativ dazu liegen die Skripte und die Stylesheets. Ein wesentlicher Unterschied zu einer normalen Webanwendung ist die Benennung der JavaScript Dateien. Sie enden bei mir mit *.javascript. Titanium liest in seinem Build-Vorgang rekursiv alle Dateien mit der Endung *.js ein und nimmt an, dass dies Titanium Skripte sein müssen. Der Parser geht strikt vor und erlaubt keine Fehler. In Bibliotheken wie jQuery oder Prototype fehlt aber ab und an ein Semikolon. Damit kommt Titanium nicht klar. Eine Möglichkeit das Problem zu umgehen ist die Umbenennung der Endung. Auf diese Weise müssen externe Bibliotheken nicht in die HTML Seiten eingebettet werden.
Wie zu sehen ist, nutze ich für dieses Beispiel Flot. Ein jQuery Plugin zur Erzeugung von Diagrammen. Dafür muss zuerst jQuery, dann Flot, dann mein Code geladen werden. Die HTML Datei, die die WebView Komponente also lädt, sieht wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 | <html> <head> <link rel="stylesheet" type="text/css" href="css/style.css" /> <script type="text/javascript" src="scripts/jquery.min.javascript"></script> <script type="text/javascript" src="scripts/jquery.flot.min.javascript"></script> <script type="text/javascript" src="scripts/chart.javascript"></script> </head> <body> <!-- Render the chart in this container --> <div id="placeholder"></div> </body> </html> |
Was nun fehlt ist die Brücke zwischen Flot in der WebView und Titanium. Das Schöne ist, dass die WebView eine Methode bereitstellt, mit der sich JavaScript Code evaluieren lässt: evalJS. Ich bevorzuge den JavaScript Code, der für die WebView gedacht ist, an einer Stelle zu halten und habe daher mir die chart.javascript angelegt:
1 2 3 4 5 6 7 8 9 10 | function runChartDemo() { var d1 = []; for (var i = 0; i < 14; i += 0.5) { d1.push([i, Math.sin(i)]); } var d2 = [[0, 3], [4, 8], [8, 5], [9, 13]]; var d3 = [ [0, 12], [7, 12], null, [7, 2.5], [12, 2.5]]; $.plot($("#placeholder"), [ d1, d2, d3 ]); } |
Die runChartDemo() Funktion steht mir spätestens dann zu Verfügung, wenn die WebView geladen und Einsatzbereit ist. Und das bekomme ich in Titanium durch einen EventListener mitgeteilt. Nebenbei: In den Docs steht geschrieben, dass entweder nur die Titanium Events oder die Events in dem JavaScript Universum der WebView genutzt werden sollen. Nach Möglichkeit nicht gemischt. Auf jeden Fall kann ich nun in der app.js das load-Event der WebView abgreifen und meine Funktionen ausführen:
1 2 3 | webkit.addEventListener('load', function() { webkit.evalJS('runChartDemo()'); }); |
Starte ich Titanium, sehe ich das folgende Ergebnis:
Nun zu den Nachteilen. Es ist langsam. Zumindest so langsam, dass es mir als Anwender keinen Spass machen wird, auf die Grafik zu warten. Alleine die nackte WebView wird auf meinem iPhone 3G in ca. 1000ms erzeugt. Dann wird jQuery und Flot geladen. Wie lange das dauert, hängt davon ab, ob es zum ersten mal in der App passiert oder zum x-ten mal (das iPhone scheint das zu cachen). Ich habe Werte zwischen 2000ms und 8000ms gehabt. Dann wird das Diagramm erzeugt. Ist es ein einfaches, dauert die Erzeugung 800ms, bei einem komplexeren mit vielen Daten auch schonmal 2000ms. Soll in der Summe heissen, dass diese Lösung im besten Fall nach 3500ms und im schlimmsten Fall nach 11000ms ein Diagramm zeigt.
Ich habe einige Diagramm-Bibliotheken gefunden, aber die meisten, die von der Optik in Betracht kommen, basieren auf einer anderen Bibliothek wie jQuery, MochiKit oder MooTools. Für mich persönlich sind 3500ms akzeptabel, aber 11000ms nicht. Deshalb denke ich, dass noch einiges an Forschungsbedarf auf mich zukommt.
Christian hat in einem Artikel das Titanium Framework von Appcelerator erwähnt. Ich kannte es vorher nicht und habe es mit die letzten Tage einwenig näher angesehen. Aktuell habe ich ein konkretes Projekt, das ich eigentlich wieder mit GWT umsetzen wollte, aber Titanium scheint mir eine echte, und vielleicht sogar bessere Alternative zu sein.
In Titanium wird der Programmcode in JavaScript geschrieben und als nativer Code auf der jeweiligen Platform (Android oder iPhone) ausgeführt. Das ist der große Vorteil. Während ich den Code nur einmal schreibe, ist er dank dem Framework auf mehreren Plattformen lauffähig.
Titanium übernimmt dabei die Rolle eines Proxies. Während bei GWT der Java Code von einem echten Cross-Compiler in JavaScript Code übersetzt wird, interpretiert Titanium den JavaScript Code auf der jeweiligen Plattform und delegiert die Funktionsaufrufe an Module. Diese Module setzen dann die Funktionsaufrufe nativ um. An sich kein schlechter Ansatz. Doch der Code wird interpretiert, deshalb ist er nicht sonderlich schnell.
Das Ergebnis ist, dass mit relativ wenig Code Standardfunktionalität umgesetzt werden kann. Standard deshalb, weil natürlich nur die Funktionen genutzt werden können, die dem Titanium Framework als Modul zu Verfügung stehen. Möchte der Entwickler Funktionen nutzen, die es in Titanium nicht gibt, kann er eigene Module implementieren und damit Titanium erweitern.
Ich habe beim Entwickeln sowohl ein freudiges Grinsen als auch Verzweiflung im Gesicht gehabt und bis jetzt bin ich mir immer noch nicht schlüssig.
Vorteile
- Schnelle Erfolgserlebnisse und Ergebnisse
- Code ist komplett in JavaScript. Daher kein Erlernen einer neuen Sprache
- Native Anwendung
- Großer Funktionsumfang, viele Module
- Automatisches Deployment auf das Gerät und den Simulator
- Schnelles Bauen, da nur wenig übersetzt werden muss
- Konsole mit Log-Ausgaben
Nachteile
- Keine Möglichkeit zum Debuggen Ich schreibe den Code in IDEA, wechsele dann in den Titanium Developer und führe mein Programm aus. Um den Code zu Debuggen, müsste es eine Integrationsmöglichkeit für die IDE geben. Da Titanium noch sehr jung ist, kann es aber durchaus sein, dass dies bald kommt.
-
Wenig Transparenz Bis auf die Log-Ausgaben sehe ich leider nicht, was mein Programm macht. Manchmal ist mein Simulator abgestürzt oder startete das Programm nicht. In vielen Fällen gab es keinen Anhaltspunkt zu der Ursache. Ein
exit(null)reicht als Fehlerbeschreibung nicht. -
Hohe Fehlertoleranz, wenig Feedback In Titanium werden statische “create” Methoden genutzt, um Instanzen anzulegen. Diese Instanzen werden durch Parameter konfiguriert. z.B.
1 2 3 4 5 6
var view = Titanium.UI.createView({ borderRadius:10, backgroundColor:'red', width:50, height:50 });
Titanium schluckt fast alles. Es gibt leider keine Überprüfung darüber, ob ein Parameter erlaubt ist, oder ob er einen gültigen Wert bekommen hat. Dadurch weis ich als Entwickler nicht, aber ich das Objekt falsch konfiguriert habe, oder ob der Fehler eine andere Ursache hat.
- Organisation von JavaScript Dateien in Ordnern ist zwar möglich, bringt aber Probleme mit sich. Ich habe meine “Views” in einem Unterordner liegen gehabt. Relativ zu der
app.js. Die Dateien wurden alle richtig gefunden und interpretiert. Als ich das Programm jedoch ausgeführt, von einem Fenster zu einem anderen und wieder zurück, navigiert habe, wurde auf einmal der Pfad zu der JavaScript Datei falsch zusammengebaut. Daraufhin stürzte mein Programm ab. - Performanz Obwohl die Ausführungszeit nicht schlecht ist, gibt es einige Stolperfallen. Beispiel TableView. In der
createTableViewMethode wird ein Array mit den Zeilen der Tabelle übergeben. Diese Zeilen sollten zuvor erzeugt und dann der TableView auf einen Schlag übergeben werden - was auch klappt. Leider ist die Erzeugung der Zeilen in einer Schleife alles andere als performant. Handelt es sich um einfache Texte, dann geht dies relativ zügig. In meinem Fall bestand eine Zeile aus einer View mit drei Labels. Für die Erzeugung von nur 25 Zeilen mit statischem Inhalt hat auf meinem iPhone 3g 2-3 Sekunden gedauert. Kommen die Daten von einem Server, muss die Latenz und Serverzeit addiert werden. Ich kann mir vorstellen, dass sich dieser Wert verdoppeln kann.
Insgesamt kann ich aber sagen, dass die Vorteile knapp überwiegen, so dass ich auf jeden Fall meine aktuelle App mit Titanium realisieren werde.
Um die Entfernung zweier Objekte in Metern zu berechnen eignet sich die distance Funktion leider nicht. Denn die Entfernung wird in der Einheit berechnet, in der die Punkte gespeichert sind. In diesem Fall als Lat/Lng Koordinaten, also Winkel in Grad. Das die Kalkulation nicht aufgeht, zeigt ein simples Experiment:
SELECT distance(geometry('POINT(0 -170)'), geometry('POINT(0 170)')) SELECT distance(geometry('POINT(0 -160)'), geometry('POINT(0 160)'))
Das Ergebnis der ersten Berechnung ist 340 und das der zweiten 320. Mathematisch richtig, bezogen auf unsere Erde aber leider falsch. Denn die Punkte der zweiten Berechnung liegen in der Realität - auf dem Globus betrachtet - weiter auseinander als die Punkte der ersten Kalkulation. Das liegt daran das die Punkte -180/0 und +180/0 ein und denselben Punkt markieren. Denn es handelt sich bei unserem Planeten um eine Kugel.
Um nun die Entfernung zweier Punkte zu einander zu berechnen, wird die kürsteste Strecke auf einer Kugel ermittelt. Mehr dazu unter Orthodrome
Die Implementierung sieht so aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class GeoUtils { private static final double RADIUS = 6378137.0d; /** * Calculates the distance between two points in meters and returns * the value as result. * * @param p1 The first point * @param p2 The second point * @return The distance in meters */ public static double distance(Point p1, Point p2) { double lng1 = p1.getX() * Math.PI / 180; double lat1 = p1.getY() * Math.PI / 180; double lng2 = p2.getX() * Math.PI / 180; double lat2 = p2.getY() * Math.PI / 180; return RADIUS * Math.acos( Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1) ); } } |
Postgis bietet dafür die distance_sphere Funktion. Leider definiert Hibernate Spatial diese Funktion nicht im Dialekt. Das führt dazu, dass die Funktion per native SQL aufgerufen werden muss - möchte man sie nutzen.
Die einfachste Möglichkeit dies zu umgehen ist die Erweiterung des Postgis Dialektes:
1 2 3 4 5 6 7 8 9 10 | import org.hibernate.Hibernate; import org.hibernate.dialect.function.StandardSQLFunction; import org.hibernatespatial.postgis.PostgisDialect; public class MyPostgisDialect extends PostgisDialect { public MyPostgisDialect() { super(); registerFunction("distance_sphere", new StandardSQLFunction("distance_sphere", Hibernate.DOUBLE)); } } |
Die Verwendung ist dann in HQL denkbar einfach. In dem folgendem Beispiel habe ich eine Funktion definiert, die mir zu einem bestimmten Punkt den nächst möglichen Flughafen findet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * Returns the nearest airport to the given point. * * @param point The reference point * @return The nearest airport */ static Airport findNearestTo(Point point) { return withSession { session -> def hql = """from Airport a order by distance_sphere(setsrid(geometry(:point), 0), a.location)""" session.createQuery(hql) .setParameter("point", point.toText()) .setMaxResults(1) .uniqueResult() } } |
Habe heute was Neues über Groovy Collections gelernt. Wenn ich in Java ein mit Werten initialisiertes Array benötige, dann mache ich das wie folgt:
1 | final String[] array = new String[] { "1", "2", "3" }; |
In Groovy werden die Collection über literale Konstruktoren erzeugt. Listen mit [] und Maps mit [:]. Dies beißt sich einwenig mit Java. Um in Groovy ein Array zu konstruieren gibt zwei einfache Wege:
1 2 3 | def array = ["1", "2", "3"] as String[] /* oder durch einen Cast */ def array = (String[]) ["1", "2", "3"] |
Puh, habe heute einwenig gebraucht, bis auch der letzte Integrationstest lief. Das letzte Mal, als ich mit Postgresql und PostGIS gearbeitet habe, liegt nun drei Jahre her und mein Wissen ist nicht mehr so frisch. In diesem Projekt ist es nicht Ruby on Rails, sondern Hibernate und Grails.
Ich habe heute eine Funktion benötigt, die mir ausgehend von einem bestimmten Punkt, die nächst-mögliche Entität liefert. Dafür habe ich Postgresql aufgesetzt und meine Datenbanken mit den PostGIS Funktionen erweitert.
In Grails ist es erstaunlich einfach eine Domain Klasse um ein Attribut zu erweitern, das georeferenzierte Informationen speichert:
1 2 3 4 5 6 7 8 9 10 11 12 | import com.vividsolutions.jts.geom.Point import org.hibernatespatial.GeometryUserType class Station { Point location static mapping = { columns { location type: GeometryUserType } } } |
Damit die Imports aufgelöst werden können liegen im Classpath folgende Bibliotheken: (Versionen können abweichen)
* hibernate-spatial-1.0-M2.jar
* hibernate-spatial-postgis-1.0-M2.jar
* jts-1.8.jar
* postgis-2.0.0SVN.jar
* postgresql-8.4-701.jdbc4.jar
In der DataSource.groovy muss der Dialekt angepasst werden, wodurch eine menge cooler PostGIS Funktionen dazu kommen. Dazu später mehr.
1 2 3 4 5 6 7 8 | environments { test { dataSource { driverClassName = "org.postgresql.Driver" dialect = org.hibernatespatial.postgis.PostgisDialect } } } |
Jetzt geht es um die eigentliche Funktion. In meinem ersten Entwurf habe ich den Code so geschrieben, wie ich es von Grails gewohnt bin.
1 2 3 4 5 6 7 | class Station { Point location def static findNearest(Point point) { Station.executeQuery("from Station order by distance(location, :p)", [p: point], [max: 1]) } } |
Das hat nicht ganz geklappt und ich habe eine SQLException mit der Begründung You must specify a valid OGC WKT geometry type such as POINT, LINESTRING or POLYGON bekommen. Ich habe einiges ausprobiert, aber wirklich weit bin ich nicht gekommen. Aber wie heißt es schön: RTFM
Das Tutorial auf der Hibernate Spatial Seite brachte mich weiter. Im Abschnitt Spatial Queries ist ganz kurz beschrieben wie das Problem in HQL gelöst wird. Der Trick ist Hibernate mitzuteilen, dass der Wert des Referenzpunktes vom Typ GeometryUserType ist, wodurch Hibernate die Query sauber zusammenbauen kann:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Station { Point location def static findNearest(Point point) { Station.withSession {Session session -> final String q = "from Station order by distance(location, :point) asc" final Query query = session.createQuery(q) final Type geometryType = new CustomType(GeometryUserType.class, null); query.setParameter("point", point, geometryType) query.setMaxResults(1) return query.uniqueResult() } } } |
distance ist übrigens eine der vielen HQL Funktionen, die der PostGIS Dialekt mit sich bringt. In einem Post habe ich eine Liste weiterer Funktionen gefunden: dimension, srid, envelope, astext, asbinary, isempty, issimple, boundary, overlaps, intersects, equals, contains, crosses, disjoint, touches, within, relate, distance, buffer, convexhull, difference, intersection, symdifference, geomunion, extent
Roulette Strategies sieht zwar aus wie ein iPhone App, fühlt sich aber nicht ganz so an, weil die Transitions fehlen (Das sind die animierten Übergänge von einer View zu einer anderen). Für meine neue GWT App auf dem iPhone habe ich mir überlegt, dass die Übergänge nicht fehlen dürfen. Meine Recherche ergab, dass es einige Bibliotheken gibt, die das Verhalten implementieren. z.B. jQTouch oder iPhone UI. Was mir persönlich nicht gefällt ist der erhebliche JavaScript Anteil, den die Bibliotheken mitbringen.
Zwar verträgt sich GWT mit anderen Libs, aber die Fehlersuche ist oft mühsam, wenn es mal nicht so sein sollte. Oftmals haben die Libs Initialisierungs-Routinen, mit denen sie über Elemente des DOMs iterieren und z.B. Click-Handler überschreiben. Wenn man aber die Handler zuvor unter GWT verdrahtet hat, ist die Verwunderung groß, wen sie nicht greifen oder sich anders als erwartet verhalten.
Mein Wunsch war es Transitions nur mit GWT und WebKit Mitteln zu realisieren und ich denke, dass ich ein ganz brauchbares Ergebnis erzielt habe. Hier erstmal eine Demo. Die nächste Tage veröffentliche ich bei Google Code das Projekt und werde im nächsten Artikel genauer auf die Realisierung eingehen.
(Die Demo funktioniert nur in WebKit Browsern.)
Zugegeben, die Downloadzahlen der letzten beiden Tage sind ernüchternd. Zwar habe ich mir bei einer Nischenanwendung nicht die großen Downloads ausgemalt, aber die Hoffnung stirbt zuletzt. Jetzt heisst es Marketing zu betreiben. Meine erste Maßname ist es eine freie Version rauszubringen.
Da freie Apps charakteristisch öfters geladen werden als kostenpflichtige, habe ich auf die schnelle eine kleine Einschränkung eingebaut und das Icon der App getunt. Langsam macht mir das Submitten in den AppStore richtig Spass. Einmal überwunden, wird es zur Routine.
Die freie Version kann alles, allerdings ist die Größe der Permanenz eingeschränkt. Pro Tisch können 15 Treffer hinzugefügt werden, so dass zwar die Funktion ersichtlich, aber die App eigentlich nicht zu gebrauchen ist.
Parallel schreibt mein Bruder ein paar Artikel aus der Sicht eines Roulettespielers. Es ist auch ein kleiner Screencast geplant, der ein Online-Spiel zeigt. Mein persönliches Ziel ist es, bzw. war es noch bevor ich mit der Entwicklung angefangen habe, nach 6 Monaten die 5000 Downloads zu schaffen. Mal sehen
Heute habe ich die App um zwei Bugs erleichtert und ein neues Feature implementiert. Zu jedem Spieltisch kann man sich nun die Verteilung in Form eines hübschen Balkendiagramms ausgeben lassen. So lässt sich auf anhieb erkennen, welche Felder wie oft gespielt werden. Neben den einzelnen Felder, zeigt das Diagram auch wie oft die Dutzende, die Farben, die Geraden/Ungeraden usw. gespielt wurden. Das Update mache ich morgen fertig und reiche es zur Durchsicht ein. Wenn Apple wieder genauso schnell arbeitet, wie beim ersten mal, dann ist es Ende nächster Woche online.


