Bernhard Häussner
Journal: Neueste Artikel erscheinen hier (Seite 11 von 23)

Blogpopst-Previews

26.07.2009, 16:42
Blogpost Tooltips

Blogpost Tooltips

Heute hatte ich mal wieder einen Einfall, was meinem Blog noch fehlt, also außer dem Refactoring der MySQL API. Irgendwie nicht zufriedenstellend waren nämlich die Post-Listen in der Sidebar, da durch das Datum neben dem Titel kaum eine Überschrift in eine Zeile gepasst hat. Nun habe ich das Problem „gelöst“, indem bei Mouseover ein Tooltip mit Meta-Infos angezeigt wird.

Das Ganze läd von einer simplen PHP-Schnittstelle mit AJAX bzw. „AJAJ“ (J für JSON) die Metadaten, also die Anzahl der Kommentare und die Tags, aber auch einen kleinen Textausschnitt („Teaser“). Mit jQuery geht das recht fix und mit wenig Code. Auch die Anzeige als Tooltip war recht einfach. Die runden Ecken werden im Firefox mit -moz-border-radius gemacht und auch die Transparenz wird von der CSS-Eigenschaft opacity geregelt, sodass kein weiteres Hintergrundbild nötig ist. Mit if ($(bla).width()<400) wird verhindert, dass bei genug Platz das Datum entfernt wird, zu sehen auf der Seite Archiv. Dank jQuery bleibt es bei 29 Zeilen JS-Code.

Auch der PHP-Teil ist recht simpel und besteht eigentlich nur aus einer SQL-Abfrage und der Funktion json_encode(), das sind dann 16 Zeilen.

Jetzt bin ich nur gespannt, wie sich das auf den Traffic bzw. die Reqeusts auswirkt, da jedes mal, wenn jemand über einen Link fährt, ein Request an den Server gesendet wird.

Achja, bisher gab es auch keine ausgefeilten 404-Seiten, aber jetzt gibt es eine Fehlerseite, die z.B. auch einen Link zur Suche anbietet.

Update 2009-08-01

Ich habe jetzt auch noch meine Lightbox an das Look & Feel der Blogpopst-Previews angepasst, sodass sie jetzt nicht nur individuell, sondern auch (im Firefox) gut aussehend ist. Außerdem läd sie jetzt keine Graphiken mehr, nur um „prev“, „next“ und „close“ anzuzeigen. Zu testen hier:

Obwohl ich es eigentlich nicht so mag, in jQuery Plugins herumzubasteln, habe ich doch eine ganze Passage gerefactort und hoffe jetzt einfach mal, dass es auch im IE noch halbwegs klappt.

Kommentare: keine

Twitter Bookmarklet III

11.07.2009, 20:16

For twitter users: Here's an easy-to-use toolbar button for your browser that substitutes the URL shortening and copy-pasting for you. There's no installation, I don't want to know your twitter login or password, and it will shorten the URL using bit.ly.

The bookmarklet will:

  • Receive a short URL from the bit.ly API
  • Open up your twitter homepage in a new tab (Firefox) or in a pop up with the short URL and the page title included in the textarea
  • (Or take you to the twitter login page, which will then redirect)

To install just drag this link/button into your browser's bookmarks toolbar:

tweet

The bookmarklet is even supporting flickr-Photos (using their short URL like http://flic.kr/p/6qxGR2) and other web sites (Ars technica, PHP-Manual, this blog...) if they provide own shortening services.

The code is a bit inspired by John Resig's „retweet“ button but it's not on the page you are viewing, but in your browser's toolbar, so you don't rely on webmasters cluttering their designs with social bookmarking services' buttons.

For IE-Users: You will have to right-click on the link and choose save bookmark. After that, you will be warned, because it's not a simple link, but a bookmarklet.

Kommentare: 2 Einträge

MacMahonMosaik Online Game

09.07.2009, 18:05
Mac Mohan Mosaik

Mac Mohan Mosaik

Da ich beim Känguru-Wettbewerb 2009 mitgemacht habe, habe ich auch das MacMahonMosaik in die Hände bekommen. Es ist ein kleines Rätsel ähnlich wie Sudoku. Weil es interessant zu Modellieren ist, habe ich das MacMahonMosaik als Online-Spiel erstellt. Hier ein paar Details zu Implementierung:

Modell

Jedes Quadrat im Spiel hat 4 Kanten, die in 3 verschiedenen Farben markiert sein können. Daher lässt sich jede Anordnung als 4-Tupel einer 3-Menge auffassen. Diese 81 Tupel wiederum können den natürlichen Zahlen von 0-80 zugeordnet werden. Somit habe ich als Objekte zunächst die 24 Quadrate (die anderen fallen weg, da sie durch Drehung erreicht werden können), die jeweils eine Zahl speichern. Sie müssen sich nun z.B. drehen können. Dazu wird die Zahl in das Tupel umgerechnet und die Einträge des Tupels um eine Stelle verschoben, wobei der letzte Eintrag der erste wird. Um zu Überprüfen, ob zwei Quadrate aneinander passen, dreht man eines der beiden zweimal und schaut dann einfach ob in der gewünschten Richtung die selben Farben stehen. Dadurch sieht die Prüffunktion im Javascript sehr kurz aus:

p.checkMatch=function(other,pos){
  var mycodes=this.getColorCodes(); // Tupel aus Zahl
  var o=other.clone(); // Neues Quadrat mit selber Zahl
  var othercodes=o.rotate().rotate().getColorCodes(); // anderes Q. 2x drehen
  return (mycodes.charAt(pos)==othercodes.charAt(pos)); //prüfen
}

Zusätzlich haben die Quadrate noch ein paar Darstellungsrelevante optionale Eigenschaften und Methoden. Ist ihnen ein <Canvas>-Element im HTML-Baum delegiert, können sie ihm die passenden Event-Handler für das interaktive Drehen und Bewegen zuweisen und eine graphische Repräsentation ihrer selbst zeichnen. Und sie speichern einige Daten für drag & drop.

Doch beim Ziehen und Ablegen kommt das zweite Objekt ins Spiel: Das Feld in dem die Quadrate abgelegt werden können. Wann immer man ein Quadrat bewegt, frag dieses Quadrat beim Gitter nach, ob es sich einrasten kann. Das Gitter gäbe dann die Pixel-Koordinaten des Einrastpunkts auf dem Bildschirm und die Koordinaten innerhalb des Gitters zurück. Wird das Quadrat dann fallen gelassen, registriert es sich im Gitter.

Das Gitter hat dazu ein zweidimensionales Feld hinterlegt, in dem es Pointer zu den abgelegten Quadraten speichern kann. Wenn jetzt ein Quadrat im Raster abgelegt wird und sich registriert hat, weist es das Raster an, sich zu revalidieren - Das Gitter ruft für jedes Quadrat und je seine vier Nachbarn die Prüffunktion auf. (Da das „Passen“ symmetrisch ist, ließe sich die Komplexität des Algorithmus vielleicht noch optimieren, doch das hätte wieder mehr Code zur Folge und bei 24 Einheiten...) Da das Gitter nur Zeiger zu den Quadraten speichert, ist bei einer Drehung eines Quadrats keine weitere Registrierung nötig. Falls also ein Quadrat angeklickt wird, rotiert es sich, aktualisiert die eigene Darstellung und gibt den Revalidierungsbefehl an das Raster.

Für die Darstellung hat das Gitter auch eine recht einfache Methode, die den Feldhintergrund rötlich färbt, sollte ein Fehler vorliegen und eine Gratulation meldet, sollten alle Quadrate an einem passenden Ort sein, also das Spiel gewonnen.

Ablaufdiagramm

Hier mal ein kleiner Flowchart. Man beginne das Lesen bei den Event-Handlern: (erstellt mit U+2500-U+257F: Box Drawing)

Quadrat
├ Zahl ◁──────┐ {Umrechnung}
├ (Tupel) ◁───┘
├ Drehen() ◀────────┐◁──┐─┐▷─────┐
├ Prüfen(Quadrat) ──◆───┊─┊──────┊──┐
├ [Darstellung]     │   │ │      │  │
│  ├ Visualisieren◁─┊───┊─┘      │  │
│  └ [Handler]      │   │        │  └──┐
│     ├ Klick ──────┊───┘        │     │
│     ├ Packen  ────┊────────────┊──┐  │
│     ├ Bewegen ◀───┊───┐        │  │  │
│     └ Ablegen ────┊─────────┬▷─┤  │  │
└ Kopieren() ►──────┘   │     │  │  │  ▼ bool
                     [XY,XY]  │  │  │  │
Gitter                  │     │  │  │  │
├ Feld[X,Y]             │     │  │  │  │
├ KoordinatenBei(XY) ───┘     │  │  │  │
├ Registrieren(Quadrat)  ◀─XY─┘  │  │  │
│ └ Un-Registrieren() ◀─────XY───┊──┘  │
├ Alle Prüfen()  ►────────┐─▷────│─────┘
└ Darstellung erneuern() ─┘  ◁───┘
  └ Färben(), Gratulation()

Wie das Gitter überprüft, ob auch am Rand rote Kanten liegen? Statt weiteren Code für den Rand zu basteln, wird das Gitter mit voll-roten Quadraten ohne graphische Repräsentation außerhalb das Gitters vorbelegt. Das hat den kleinen Nebeneffekt, das auch out-of-bounds-Fehler im Prüfalgorithmus vermieden werden, da ja z.B. bei negativen Gitterkoordinaten -1 stets ein solches „virtuelles“ Quadrat liegt.

Wie man also sieht, kann man an diesem Spiel schon einige lustige Code-Spar-Methoden benutzen. Eigentlich sparen sie nicht nur Code, sondern auch Nachdenken, wenn man sie „sieht“.

Javascript Umsetzung

JS-Closures habe ich in letzter Zeit immer mehr zu schätzen gelernt. (Man muss in PHP übrigens auch nicht mehr lange darauf verzichten) Sie lassen den Programmierer Funktionen, also mehr oder weniger Code, wie ein String oder eine Zahl an andere Funktionen übergeben oder in Variablen spichern, sodass mit anonymen Funktionsobjekten als Closures in JS eigene Kontrollstrukturen definiert werden können, wie diese zweifache for-Schleife, um über Koordinaten bzw. Zweidimensionales zu loopen:

function loopXY(w,h,cb) {
  for (var x=0;x<w;x++) {
    for (var y=0;y<h;y++) {
      cb(x,y);
    }
  }
}
var m=[];
loopXY(5,7,function(x,y){
  m.push("("+x+","+y+")");
});
alert(m);

Wie man sieht stehen in Javascript in den Closures alle äußeren Variablen zur Verfügung. Das wiederholte verwenden des Namens einer äußeren Variable führt trozdem noch dazu, dass in der Closure diese lokal verwendet wird und außen der alte Wert erhalten bleibt. Ein bischen gewöhnungsbedürftig ist die Verwendung von this in JS. Im oben gezeigten Code würde z.B. in der Schleife this auf window zeigen. Das lässt sich mit der Function.call oder der Function.apply-Funktion kompensieren, wie in diesen Code-Schnippseln für Event-Handler.

Für die Erstellung von Objekten (außer reinen Datenspeichern, die mit {} einfach erstellt werden können) verwende ich folgende Grundstruktur:

function Square(id) { // constructor
	this.id=id; // Eigenschaften initialisieren
}
(function(){
  var p=Square.prototype; // shortcut zum Prototype
  p.setId=function(id) { // einfacher Setter
    this.id=id;
    return this; // macht Setter chainable
  };
  var privat; // kann nur von den Objektmethoden gesehen werden
  function privatauch() {} // dito
})();
var s=new Square (3);

Allerdings bin ich mir nicht sicher, ob das wirklich besser ist, als alles in Konstruktoren zu packen.

Der Code des Onlinegames ist vielleicht nicht ganz so interessant zu lesen, wie es war ihn zu erstellen, aber ich denke, hinein schauen lohnt sich. Ich bin natürlich immer interessiert an weiteren Javascript-Techniken und Tipps zur Code-Gestaltung.

Kommentare: keine

Friede den Hütten? Google fotografiert wieder...

29.06.2009, 17:58
Google Car in Würzburg
Google Car in Würzburg

Exhibitionisten aller Länder vereinigt euch! ☺ Heute ist in Würzburg wieder mal ein Streetview-Auto unterwegs. Was mich an der Sache allerdings noch etwas irritiert: Es hatte nicht das Google-Logo aufgeklebt. Verkauft Google etwa seine Spielzeugchen an den begüterten Datamining-Landschaftsfotografen? Oder fahren die Kamerawagen schon wieder nach Hause?

Nun kann sich der Würzburger zumindest darauf freuen, schon bald ein Foto von sich im Internet zu entdecken. Warum kann man sich da eigentlich nicht verlinken, wie bei StudiVZ? Oder wie bei iPhoto, sodass man dann gleich mitgeteilt bekommt, wo man noch alles abgelichtet wurde. Noch mal von hinten:

Google Car in Würzburg (von hinten)
Google Car in Würzburg (von hinten)

Angesichts der Tatsache, dass es nicht gerade in der City herum fuhr und ein Franzose am Steuer telefoniert hat (und französische Zulassung), könnte es auch sein, dass es nur eine kleine Durchreise war. Oder der Spaß beginnt erst noch.

Kommentare: 3 Einträge

MySQL-Queries sparen mit GROUP_CONCAT

22.06.2009, 20:10
Ein MySQL-Query mit GROUP_CONCAT

Ein MySQL-Query mit GROUP_CONCAT

Fast jede (Web-)Anwendung, die eine Datenbank wie MySQL benutzt, muss irgendwie das Problem lösen, zu einem Artikel mehrere Tags, zu einem Eintrag mehrere Kommentare, zu einem Wort mehrere Synonyme zu finden, die sich in einer weiteren Tabelle befinden. Jedes Mal, wenn mehrere Zeilen Abgefragt werden sollten, die wieder mehrere untergeordnete Zeilen haben, bedeutet das meistens für jedes Element einen weiteren Query zu starten, oder eine zweite Abfrage mit einer weiteren Liste zu starten, oder eine riesige Reihe Datensätze abzufragen und der Anwendung das Gruppieren zu überlassen. Mit GROUP_CONCAT (Dokumentation) lässt sich in MySQL (fast) alles in ein SQL-Query zusammenfassen.

Einführung

Nehmen wir eine einfache Datenbankstuktur an, wie die eines Blogs. Es gibt blogeintraege mit einem Titel, blogtags mit einem Namen. Dann gibt es noch eine Tabelle, die beides verbindet, z.B. blogtaglinks. Jeder Datensatz hat natürlich eine eindeutige ID. Wenn man jetzt alle Blogeinträge auflisten will, könnte man das wie folgt machen:

SELECT Titel FROM blogeintraege

Um die Anzahl der Verbundenen Tags herauszufinden, wäre ein solcher Join üblich:

SELECT Titel, COUNT(distinct(t.ID))
FROM blogeintraege b, blogtags t, blogtaglinks l
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag
GROUP BY b.ID
+-------------------------+-----------------------+
| Titel                   | COUNT(distinct(t.ID)) |
+-------------------------+-----------------------+
| Hello World!            |                     2 |
| The CMS                 |                     2 |
| To be Released Later... |                     1 |
+-------------------------+-----------------------+
3 rows in set (0.01 sec)

Hier werden jeweils alle Spalten, die den selben Blogeintrag repräsentieren gruppiert und dann aus diesen Gruppen die Anzahl der verschiedenen Tags abgefragt. Dabei ist COUNT die Gruppenfunktion, die die nun versteckten, „weg-gruppierten“, Datensätze verarbeitet - hier zählt.

Die GROUP_CONCAT-Funktion

MySQL und andere RDBMSs kennen für Zahlen einige Funktionen, die Summe, Durchschnitt etc. gruppierter Zahlen berechnen können. MySQL kennt darüber hinaus die GROUP_CONCAT()-Funktion, welche die „versteckten“ Werte (also auch CHAR) in einer Spalte zusammen zu hängen. Mit dieser Funktion wäre es also möglich, die Tags direkt aufzulisten:

SELECT Titel, GROUP_CONCAT(distinct(t.name))
FROM blogeintraege b, blogtags t, blogtaglinks l
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag
GROUP BY b.ID
+-------------------------+---------------------------------+
| Titel                   | GROUP_CONCAT(distinct(t.name))  |
+-------------------------+---------------------------------+
| Hello World!            | Projekt: Mein Blog,Persönliches |
| The CMS                 | PHP,Projekt: Mein Blog          |
| To be Released Later... | Projekt: Mein Blog              |
+-------------------------+---------------------------------+
3 rows in set (0.00 sec)

Jetzt wird anstatt der Anzahl der Tags eine Komma-getrennte Liste der Tags angezeigt. Das ist insofern ein recht großer Fortschritt, da wir nicht nochmal eine zweite Abfrage machen müssen, die uns jeweils die Namen der Tags heraus sucht.

Weiter Ausbauen

Jetzt wird es Zeit ein bisschen zu Optimieren. In diesem Beispiel macht es wenig Sinn, aber theoretisch könnten zwei Tags den gleichen Namen haben. In diesem Fall würde uns MySQL aber den Namen nur einmal anzeigen, da distinct() doppelte Werte aussortiert. Wir würden also weniger Tags bekommen, als ein COUNT(distinct(t.ID)) anzeigt. Das können wir lösen, indem wir die distinct()-Funktion auf die IDs der Tags anwenden:

SELECT Titel, GROUP_CONCAT(distinct(t.ID),'-', t.name)
FROM blogeintraege b, blogtags t, blogtaglinks l
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag
GROUP BY b.ID

Das würde uns für jeden Blogeintrag eine Liste erstellen, wie 42-Webbrowser,28-Productivity,27-Design. Die eigentlich überflüssigen IDs werden später noch entfernt, z.B. mit PHP.

Wenn wir zusätzlich noch die Erstellungsdaten z.B. der letzten drei Kommentare abfragen wollen, zeigt sich, warum wir distinct() verwenden müssen: Sonst wird jeder Tag so oft angezeigt, je nach dem, wie viele Kommentare ein Eintrag hat. Wenn wir also noch eine Tabelle benutzen, wie blogkommentare mit creation_time, und uns jeweils die letzten Erstellungszeiten anzeigen lassen wollen, benutzen wir:

SELECT Titel, GROUP_CONCAT(distinct(t.ID),'-', t.name), 
GROUP_CONCAT(distinct(c.ID),'-',c.creation_time ORDER BY creation_time DESC)
FROM blogeintraege b, blogtags t, blogtaglinks l, blogkommentare c
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag and c.entry_id = b.id
GROUP BY b.ID

Jetzt haben wir noch eine weitere Spalte mit einem Wert, wie 34-2008-10-20 18:28:41,33-2008-10-19 23:22:37. Doch je mehr Spalten wir abfragen, desto unübersichtlicher werden die Listen. Außerdem hängt ja immer noch die ungewollte, aber doch notwendige id davor.

Verarbeitung mit PHP

Diese minderen kosmetischen Makel lassen sich dann in der Anwendung noch beheben. Hier zeige ich, wie ich mit PHP die Listen in Arrays lade und sortiere:

function arrayifyGrpConcats($q,$resultset) {
  $regexp='/\\bgroup_concat\\((.*?) separator \'(?<sep>.*?)\'\\) +as +(?<name>\w+)\\b/i';
  if ( preg_match_all($regexp,$q,$matches,PREG_SET_ORDER) ) {
    foreach ($matches as $grpCoClause) {
      if (isset($grpCoClause["name"],$grpCoClause["sep"])) {
        $name=$grpCoClause["name"];
        if (strpos($name,'_',1)===false) {
          $foldername=$name;
          $subname=false;
        } else {
          $foldername=substr($name,0,strpos($name,'_',1));
          $subname=substr($name,strpos($name,'_',1)+1);
        }
        $s=$grpCoClause["sep"];
        foreach ($resultset as $id=>$row) {
          $val=$row[$name];
          unset($resultset[$id][$name]);
          if ($subname) {
            if (!isset($resultset[$id][$foldername])) {
              $resultset[$id][$foldername]=array();
            }
            $vals=explode($s,$val);
            for ($i=0, $len=count($vals);$i<$len;$i++) {
              if (!isset($resultset[$id][$foldername][$i])) {
                $resultset[$id][$foldername][$i]=array();
              }
              $resultset[$id][$foldername][$i][$subname]=preg_replace('/\\d+-/','',$vals[$i],1);
            }
          } else {
             $resultset[$id][$foldername]=explode($s,$val);
          }
        }
      }
    }
  }
  return $resultset;
}

Diese Funktion kann direkt nach der Abfrage auf die Ergebnisse angewendet werden. Dazu übergibt man ihr das Result als durchnummeriertes Array mit je einem assoziativen Unterarray (Dictionary) pro Datensatz, der als Schlüssel die Spaltennamen und als Werte den jeweiligen Spaltenwert enthält. Die Funktion sucht dann im Query nach GROUP_CONCAT-Aufrufen der From group_concat(... separator ',') as iwas. Das Trennsymbol wird explizit angegeben, damit es hier keine Missverständnisse gibt.

Man sollte bedenken, dass MySQL mit dem Separator im Wert nichts macht. Wie man im SQL sieht habe ich das Problem mit dem Trennzeichen im Wert „gelöst“, indem ich es aus dem Kommentartext einfach gelöscht habe. Eine ausgefeiltere Methode habe ich bei stackoverflow gefunden.

Als Alias nach as kann man fast alles angeben. Die Funktion wird diesen Namen als Spaltennamen im Result-Array verwenden.

Als kleines Extra habe ich noch eingebaut, dass die Funktion optional mehrere Spalten zusammen nimmt, wenn sie ein Alias der Form gruppe_name erkennt: Sie wird dann im Resultset-Array unter gruppe ein nummeriertes Array anlegen, das ein assoziatives Arrays enthält mit den verschieden name. Wenn wir also zu einem Kommentar nicht nur das Datum, sondern auch den Autor wissen wollen, könnten wir group_by... as comm_time, group_by... as comm_name verwenden, um die Daten für jeden Kommentar zusammen zu halten.

Im Endeffekt kann das dann so aussehen:

SELECT b.id, b.Titel AS Titel,
group_concat(DISTINCT t.ID,'-', t.name separator ', ') as tags_Name, 
group_concat(DISTINCT t.id separator ', ') as tags_id, 
group_concat(DISTINCT c.ID,'-',c.creation_time separator ', ') as comm_time, 
group_concat(DISTINCT c.ID,'-',REPLACE(LEFT(c.Text,20),',','') 
     separator ', ') as comm_text 
FROM blogeintraege b, blogtaglinks l, blogtags t, blogkommentare c
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag and c.entry_id=b.id
GROUP BY b.ID LIMIT 1,2

Was uns dann dieses Ergebnis liefert: (print_r)

Array
(
    [0] => Array
        (
            [id] => 2
            [Titel] => The CMS
            [tags] => Array
                (
                    [0] => Array
                        (
                            [Name] => PHP
                            [id] => 3
                        )

                    [1] => Array
                        (
                            [Name] => Projekt: Mein Blog
                            [id] => 2
                        )

                )

            [comm] => Array
                (
                    [0] => Array
                        (
                            [time] => 2008-10-19 23:22:37
                            [name] => Bernhard H.
                            [text] => Also das Kommentarsy
                        )

                    [1] => Array
                        (
                            [time] => 2008-10-20 18:28:41
                            [name] => Bernhard H.
                            [text] => So ich hab es jetz 
                        )

                )

        )

    [1] => Array
        (
            [id] => 10
            [Titel] => To be Released Later...
            [tags] => Array
                (
                    [0] => Array
                        (
                            [Name] => Projekt: Mein Blog
                            [id] => 2
                        )

                )

            [comm] => Array
                (
                    [0] => Array
                        (
                            [time] => 2008-10-20 19:48:50
                            [name] => Bernhard H.
                            [text] => Cool das klappt auc
                        )

                )

        )

)

Ich hoffe mit dieser Methode in Zukunft Datenbankabfragen zu vermindern und damit die Geschwindigkeit der Anwendungen weiter zu erhöhen. Zwei kleine Sachen noch:

SELECT @@group_concat_max_len

Dieser Wert ist die maximale Länger der mit GROUP_CONCAT gebildeten Spaltenwerte und lieget meist bei 1024. Außerdem kann es nötig sein den Spaltenwerte von BLOB zu konvertieren: CAST(... as CHAR).

Kommentare: keine
 
Χρόνογραφ
© 2008-2018 by Bernhard Häussner - Impressum - Login
Kurz-Link zu dieser Seite: http://1-co.de/bj