Auf mehrfachen Wunsch stelle ich hier die Technik vor, wie die TP-Karte generiert wird.
Dabei beschränke ich mich auf den PHP-Teil der Berechnung und Darstellung.
Die dynamischen Teile wie Bewegung der Karte kann man ja dem Quelltext entnehmen.
Karten mit Geodaten zeichnen
Ziel dieses Projektes ist es, alle Mitglieder eines Forums in einer Karte anzuzeigen.
Was brauchen wir denn alles, um das umsetzen zu können ?
- eine Karte
- eine Datenbank mit GeoDaten
- PLZ-Angaben der User
- Umrechnungsroutinen für die Geo-Koordinaten
Natürlich stösst man bei einem solchen Projekt unweigerlich auf die openGeoDB[1], ein OpenSource-Projekt für Geodaten. Nach anfänglichem Stöbern gefielen mir ein paar Sachen nicht:
- die Klasse setzt PEAR vorraus
- die Struktur ist sehr verschachtelt und nicht gut kommentiert, es erfordert einiges an Einarbeitung
- die Datenbank hat einen sehr komplexen Aufbau
- die Pins werden in das Bild kopiert, der Vorgang dauert bei vielen Pins sehr lange, da jeder Pin einzeln das Bild kopiert werden muss
Aber für das Projekt braucht man ja lediglich ein paar Angaben - Stadt, PLZ, Land, Koordinaten, und ein paar Kleinigkeiten.
Glücklicherweise gibt es noch eine kleinere Datenbank[2]. Man benötigt lediglich eine Tabelle, die geodb_locations. Hier sind alle benötigten Information enthalten.
Eine Karte kann man sich unter [3] downloaden und entsprechend grafisch bearbeiten. Sie beinhaltet Landes-/Bundeslandgrenzen, Städte, Flüsse und Strassen, es bleibt jedem überlassen, wie die Karte aussehen soll.
Das wichtigste aber ist die Range-Angabe, die die Koordinaten der Eckpunkte der Karte angibt. Spätestens hier kann man sich den Frust holen, da diese Angaben nirgends stehen - die openGeoDBClass nutzt nur eine Deutschlandkarte und hat demzufolge eine andere Range.
Berechnung
Hat man diese Angaben zusammen, kann es auch schon losgehen.
Ich benutze eine Tabelle, in die die Userdaten teinkommen. Die Tabelle heisst `geodb_user` und hat folgende Struktur:
Code:
userid | plz | land | geoid | x | y | raster_x | raster_y | combi | valid
geoid ist die id des Datensatzes in der geodb_locations, x/y die absoluten Bildkoordinaten, raster_x/raster_y die Koordinaten des Raster-Mittelpunktes, und combi gibt an, ob es mehrere mit dieser Koordinate gibt. valid gibt an, ob die Daten gültig sind.
Das Verfahren der Umrechnung ist schon fast trivial:
PHP-Code:
//Range-Bereich
$range_min['x']=5.8;
$range_max['x']=17.2;
$range_min['y']=45.8;
$range_max['y']=55.1;
// Größe der Karte in Pixel
$karte_groesse_x=1400;
$karte_groesse_y=1800;
//Userdaten und Geodaten in ein Array packen
$res=mysql_query("SELECT * FROM `user`");
$i=0;
while($row=mysql_fetch_array($res)) {
$members[$i++]=$row;
}
$anz=sizeof($members);
for ($i=0;$i<$anz;$i++)
{
$sql="SELECT * FROM `geodb_locations` WHERE `plz` like '%".$members[$i]['plz']."%' AND adm0='".$members[$i]['land']."' ORDER BY LENGTH(plz)";
$res=mysql_query($sql);
//gibt es den angegebenen Ort ?
if ($res)
{
$r=mysql_fetch_object($res);
if ($r)
{
// Dann Geokoordinaten auf Bildgröße skalieren und absolute X,Y-Koordinaten speichern
$members[$i]['x']= floor( ($r->laenge - $range_min['x']) * ($karte_groesse_x / ($range_max['x'] - $range_min['x'])));
$members[$i]['y']= floor( ($r->breite - $range_max['y']) * ($karte_groesse_y / ($range_min['y'] - $range_max['y'])));
$members[$i]['valid']=1;
} else {
$members[$i]['x']=$members[$i]['y']=$members[$i]['valid']=0;
}
}
}
Ihr werdet lachen - das wars schon. Die wichtigste Hürde ist geschafft, man liest eine PLZ aus der Datenbank und berechnet mit der Formel die Koordinate auf dem Bild.
Für unsere Karte lesen wir alle relevanten Userdaten und legen sie in einem Array ab Um alle relevanten Daten zur Laufzeit zur Verfügung zu haben, habe ich eine neue Tabelle, in der die User mit ihren Koordinaten eingetragen werden.
Jetzt besteht das Problem, das es u.U. zu einer PLZ mehrere User gibt oder Standorte so nah beieinander liegen, das die Pins sich überlappen würden. Aus diesem Grund habe ich die Karte in ein Raster eingeteilt. Die Grösse des Rasters richtet sich nach der Grösse der Pins.
Sind mehrere User in einem Raster, werden sie zusammengefasst und der Pin in den Mittelpunkt des Rasters verlegt. Diese Raster-Koordinaten werden ebenfalls für jeden User berechnet, so das man nachher leicht erkennen kann, ob es mehrere User im gleichen Raster gibt.
PHP-Code:
//Rastergrösse: 24 x 24 Pixel
$rastergruppierung_x=24;
$rastergruppierung_y=24;
//Für jeden User aus den Koordinaten das Raster berechnen
for ($i=0;$i<$anz;$i++)
if($members[$i]['valid']==1) {
// Mittelpunkt des zugehörigen Rasterquadrats ermitteln
$rest_x=$members[$i]['x'] % $rastergruppierung_x;
$rest_y=$members[$i]['y'] % $rastergruppierung_y;
$members[$i]['raster_x']=$members[$i]['x']-$rest_x+floor($rastergruppierung_x/2);
$members[$i]['raster_y']=$members[$i]['y']-$rest_y+floor($rastergruppierung_y/2);
}
}
Karte ausgeben
Hier hab ich mich für eine dynamische Variante entschieden, die die Pins zur Laufzeit auf der Karte absolut positioniert. Die Karte bleibt in ihrem Originalzustand und es werden keine Imagefunktionen benötigt, was die Performance deutlich erhöht.
Für die Karte brauchen folgenden Aufbau:
HTML-Code:
<div id="originalImgBox" style="position:absolute;top:0px;left:0px">
<img id="originalImg" style="position:relative" src="karte.gif" width="1400" height="1800" />
<!-- Anfang Sektion Pins -->
... dynamischer Code ...
<!-- Ende Sektion Pins -->
</div>
Pins ausgeben
Jetzt geht es an den dynamischen Teil, in dem die Pins ausgegeben werden.
Als erstes bauen wir uns die Abfrage mit allen relevanten Daten:
PHP-Code:
$sql="SELECT `user`.`userid`,`user`.`username`,`user`.`homepage`, `user`.`avatar`,`geodb_user`.*,
IF(`geodb_locations`.`ortsteil`!='',CONCAT(`geodb_locations`.`ort`,' / ',`geodb_locations`.`ortsteil`),`geodb_locations`.`ort`) as `stadt`, `geodb_locations`.`adm1` as `bundesland`,`geodb_locations`.`kfz`,`geodb_locations`.`ort`,`geodb_locations`.`name_int` as `urlort`,
CONCAT(`raster_y`,'_',`raster_x`) as `raster`,
FROM `user`
LEFT JOIN `geodb_user` ON `geodb_user`.`userid`=`user`.`userid`
LEFT JOIN `geodb_locations` ON `geodb_locations`.`id`=`geodb_user`.`geoid`
WHERE `geodb_user`.`valid`=1 ORDER BY `raster` ASC;
Jetzt haben wir die Userdaten nach Rasterkoordinaten sortiert, können die Schleife durchlaufen und die Pins setzen. Die Einblendung der Informationen besorgt die Klasse overlib [4], die das div automatisch bei mouseover einblendet und bei mouseout ausblendet.
Zusätzlich habe ich für x und y Offsetwerte benutzt, die die Lage des Pins verschieben. Diese Offset hängen mit der Grafik des Pins zusammen, schliesslich soll eine Stelle des Pins auf die Koordinate zeigen, und das ist nicht zwingend eine Ecke des gifs.
PHP-Code:
//Query ausführen
$res=mysql_query($sql);
$num=mysql_num_rows($res);
//Variablen initialisieren
$oldraster=$html="";
$pin='images/p.gif';
//Offsets
$offset_x=-4;
$offset_y=-8;
//Jetzt die Daten durchlaufen
for($i=0;$i<$num;$i++)
//Datensatz holen
$a=mysql_fetch_array($res);
$combi=$a['combi'];
//Gruppierung
$raster=$a['raster'];
//Neues Raster ? dann html löschen und alte Gruppierung ausgeben
if($oldraster!=$raster) {
if($i>0 && $html!="") {
//nicht beim ersten mal, daher muss $i grösser als 0 sein
Pin($x+$offset_x,$y+$offset_y,$html,$pin); //Pin zeichnen
}
$html=""; //Ausgabe löschen
}
//Wenn combi=1 gibt es mehrere im Planquadrat
$x=($combi==0) ? $a['x']:$a['raster_x'];
$y=($combi==0) ? $a['y']:$a['raster_y'];
//PopUp-Informationen sammeln
$stadturl='<a class="s" href="http://www.'.strtolower(str_replace(" ","-",$a['urlort'])).'.'.strtolower($a['land']).'" target="_blank">'.$a['plz'].' '.$a['stadt'].'</a>';
$avatar='<img src="/forum/customavatars/avatar'.$a['userid'].'_'.$a['avatarrevision'].'.gif" width="46" height="46" alt="">';
$homepage=($a['homepage']!="") ? '<a href="'.$a['homepage'].'" target="_blank">[HP]</a> ' : '';
// usw. Hier kann man ja beliebige Informationen für den User sammeln
//Infos zu einem Div zusammensetzen
$html.='<div class="p">'.$avatar.$ulink.$stadturl.' ('.$a['bundesland'];
$html.=($a['kfz']!="") ? ' KFZ: '.$a['kfz'].')<br>' : ')<br>'.$homepage;
$html.='</div>';
//schauen, ob ausgegeben werden soll (Einzelpin), sonst aufsparen (Combi)
if($combi==0 || $i==($num-1)) {
Pin($x+$offset_x,$y+$offset_y,$html,$pin);
$html=="";
}
//Raster merken
$oldraster=$raster;
}
function Pin($x,$y,$html,$pin) {
echo '<div style="position:absolute;top:'.$y.'px;left:'.$x.'px;"'
.' onmouseover="overlib(\''.str_replace('"','"',$html).'\',STICKY, MOUSEOFF);" onmouseout="nd();">'
.'<img src="'.$pin.'" alt=""></div>'."\n";
}
Und schon ist alles fertig. Beim Überfahren der Pins mit der Maus sorgt die overlib-Klasse für die Einblendung der Informationen.
Der Grund für die Umhängetabelle begründet sich in der Tatsache, das sehr viele User zusammenkommen und der Aufruf der Seite entsprechend lange dauern würde. hat man weniger User / Objekte, könnte man die relevanten Informationen auch direkt auskesen.
Datenpflege
Leider wird man immer feststellen, das Daten ungenau oder falsch sind oder gänzlich fehlen.
Für diesen Fall muss man in der Lage sein, die Daten zu ergänzen / korrigieren. Hierzu bediene ich mich der PLZ-Suche [5], um den/die genauen Ortsnamen zu ermitteln. Unter GNS Search kann ich mit diesen Angaben die GeoKoordinaten ermitteln.Die Koordinaten sind hier in Breiten- und Längengrad angegeben, so das wir noch eine Umrechnungsfunktion brauchen:
PHP-Code:
function dms2deg($dms) {
$cfgStrings = array('N', 'NO', 'O', 'SO', 'S', 'SW', 'W', 'NW');
$negativeSigns = array($cfgStrings[4], $cfgStrings[6], "-");
$negativeSignsString = $cfgStrings[4].$cfgStrings[6];
if (strlen($dms) == 6) {
$dms = "0".$dms;
} elseif (strlen($dms) == 5) {
$dms = "00".$dms;
}
$searchPattern = "|\s*([$negativeSignsString\-\+]?)\s*(\d{1,3})[\°\s]*(\d{1,2})[\'\s]*(\d{1,2})([\,\.]*)(\d*)[\'\"\s]*([$negativeSignsString\-\+]?)|i";
if(preg_match($searchPattern, $dms, $result)) {
if (in_array(strtoupper($result[1]), $negativeSigns) || in_array(strtoupper($result[7]), $negativeSigns)) {
$algSign = -1.;
} else {
$algSign = 1.;
}
if (((1. * $result[2]) > 360) || ($result[3] >= 60) || ($result[4] >= 60)) {
return 'Values out of range';
}
return $algSign * ($result[2] +(($result[3] + (($result[4].".".$result[6]) * 10/6)/100)*10/6)/100);
} else {
return 'No DMS-Format (Like 51° 24\' 32.123\'\' W)';
}
}
Damit ist das Grundprinzip erklärt. Es sollte jetzt nicht schwerfallen, eigene Projekte mit Kartenunterstützung zu erstellen.
Ich habe bewusst darauf verzichtet, komplette Quelltexte anzugeben, da das individuell verschieden benötigt wir, vielmehr ging es mir um die Erklärung des Prinzips.
... und so schnell wird man zum Geografen :-)
Quellen
[1]
openGeoDB-Homepage
[2]
openGeoDB-Datenbank (Download)
[3]
openGeoDB Karten
[4]
Overlib
[5]
GeoNet Names Server (GNS)
[6]
PLZ-Suche Europa
Das Tutorial findet ihr auch im
dislabs-Labor