Vielen Dank für die ausführliche Erklärung. Mal schauen, was geht...![]()
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:
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.Code:userid | plz | land | geoid | x | y | raster_x | raster_y | combi | valid
Das Verfahren der Umrechnung ist schon fast trivial:
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.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;
}
}
}
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.
Karte ausgebenPHP-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);
}
}
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:
Pins ausgebenHTML-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>
Jetzt geht es an den dynamischen Teil, in dem die Pins ausgegeben werden.
Als erstes bauen wir uns die Abfrage mit allen relevanten Daten:
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.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;
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.
Und schon ist alles fertig. Beim Überfahren der Pins mit der Maus sorgt die overlib-Klasse für die Einblendung der Informationen.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";
}
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:
Damit ist das Grundprinzip erklärt. Es sollte jetzt nicht schwerfallen, eigene Projekte mit Kartenunterstützung zu erstellen.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)';
}
}
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
Geändert von steffenk (15.07.2005 um 01:17 Uhr)
TYPO3 · MySQLDumper · dislabs
·
manche Mühlen mahlen schneller ...
"Ich habe Rücken"
Horst Schlämmer
Vielen Dank für die ausführliche Erklärung. Mal schauen, was geht...![]()
Gibt's auch einen Link zur TP-Karte?
Schau mal oben auf dieser Seite, links neben "Willkommen rigo2"
TYPO3 · MySQLDumper · dislabs
·
manche Mühlen mahlen schneller ...
"Ich habe Rücken"
Horst Schlämmer
Hallo St@eff.en,
erstmal herzlichen Dank für Deine Anleitung, nun habe ich einen ungefähren Plan, wie ich mein Vorhaben angehen kann.
Ich bin nicht gerade routiniert im Programmieren aber sehe das als eine großartige und vor allem sinnvolle Aufgabe, hierdurch etwas zu lernen.
Ich hätte auch schon eine ganz simple Frage ganz zu Beginn - und zwar was für Datentypen Du für die 10 Felder in der Tabelle geodb_user benutzt hast. Wäre schön, wenn ich das erfahren könnte um gleich zu Beginn Fehler auszuschließen.
Danke im Voraus, mfg, kWER
PS: Hauptsächlich ist frage ich mich, ob x,y,raster_x, raster_y und valid INTs sein sollten.
Geändert von kWER (19.08.2005 um 21:07 Uhr)
np, hier ist sie:
aber das ist nicht bindend, hier kann man auch Infos eintragen wie man möchte.Code:CREATE TABLE `geodb_user` ( `userid` int(11) unsigned NOT NULL default '0', `gruppe` tinyint(4) NOT NULL default '0', `plz` varchar(5) NOT NULL default '', `land` char(3) NOT NULL default '', `geoid` int(11) NOT NULL default '0', `x` int(4) NOT NULL default '0', `y` int(11) NOT NULL default '0', `raster_x` int(4) NOT NULL default '0', `raster_y` int(4) NOT NULL default '0', `combi` tinyint(4) NOT NULL default '0', `combi1` tinyint(4) NOT NULL default '0', PRIMARY KEY (`userid`) ) TYPE=MyISAM
TYPO3 · MySQLDumper · dislabs
·
manche Mühlen mahlen schneller ...
"Ich habe Rücken"
Horst Schlämmer
Ich bin's nochmal.
Hatte heute endlich mal Zeit, mich wieder an diese Herausforderung zu setzen und bin gewissermaßen verzweifelt
Ich hätte ein paar grundsätzliche Fragen, meine Tabellen verwirren mich nur noch.
Wo ist der Unterschied zwischen den Tabellen 'user' und 'geolib_user'?
Beide müssen scheinbar relevante Geodaten sowie PLZ beinhalten, nur dass die 'user' noch mehr spezielle Daten (wie HP, Avatar) etc. hat.
Anfangs nahm ich an, dass 'user' eben mein vorhandener Userstamm (zB von nem Forum) ist, die große SELECT Abfrage kurz vor Schluss nahm mir aber diesen Glauben.
Ich habe mir die 'geolib_user' wie empfohlen angelegt und zusätzlich eine Tabelle 'user' gemacht, wo eben zusätzliche Infos über Kontaktmöglichkeiten/Schlafplätze etc. gespeichert werden (und ein paar Testwerte eingefügt) - muss ich nun auch noch Felder für die Geodatens schaffen? Wird geolib_user damit nicht überflüssig?
Irgendwie fehlt mir das Verständnis, wie ich den Zusammenhang zwischen PLZ und Koordinaten speichere - beziehungsweise wohin.
Sorry, ich stehe komplett auf dem Schlauch
Würde mich über Hilfe oder evtl. auch größere Quelltextausschnitte sehr freuen.
MfG kWER
Wunderbar, schöne Erklärung. Ist nur noch anzumerken, dass diese Methode in der Nähe der Pole ungenau wirdReicht aber vollkommen, würde ich sagen
@TheToast Danke. Könntest Du das mit der Ungenauigkeit konkretisieren ? Das versteh ich nicht
@kWer - sry, hab erst grad gemerkt, das Du erneut gepostet hast.
Da das ganze mit dem Forum verknüpft ist, ist die Tabelle user die Forumstabelle der User mit userid für die Links zum Profil, die Avatar, Homepage etc.
Das ist eigentlich gar nicht von Belang, man kann da was völlig beliebiges nehmen.
Du kannst ja am vorhandenen Quelltext sehen, welche Felder überhaupt benötigt werden.
Alles was die Koordinaten angeht steht in der Tabelle geodb_user, und eigentlich reicht die auch völlig aus für die Darstellung der Pins.
Wenn Du ein Problem hast, dann poste doch mal Deinen Source und ich kann Dir detaillierte Infos geben, mehr Quelltext möchte ich aus datenschutztechnischen Gründen nicht geben, da es um Internas dieses Forums geht, bitte hab dafür Verständnis.
TYPO3 · MySQLDumper · dislabs
·
manche Mühlen mahlen schneller ...
"Ich habe Rücken"
Horst Schlämmer
Moin!
Erstmal Danke für die Mühe deinen Code vorzustellen.
Nun mal eine Frage, was genau meinst du mit "Range" und mit welcher Methode man das rauskriegen kann? Wenn man die TP-Karte nutzt, kann man dann auch deine Range benutzen und muss sich darum keine sorgen machen? ;-)
Gruß
Anton
mit Range sind die Eckkoordinaten der Karte gemeint, damit die Koordinaten auch an die korrekte Stelle kommen. Der Rangebereich für die TP-Karte ist direkt am Anfang definiert.
TYPO3 · MySQLDumper · dislabs
·
manche Mühlen mahlen schneller ...
"Ich habe Rücken"
Horst Schlämmer
Kann mir jemand sagen, wie das Pear-Package heisst, auf pear.php.net find ich das nämlich nich..
Hallo!
Sieht ja ganz gut aus, das Script
Mir ist aber eine Sache aufgefallen, an der man das ganze noch optimieren kann:
Warum liest du alle Datein des Benutzers aus? Reicht hier nicht auch einfach die Benutzerid? Alles andere verursacht doch extrem viel Traffic und lange Ladezeiten, wenn man alle Datein aller Benutzer ausliestCode://Userdaten und Geodaten in ein Array packen $res=mysql_query("SELECT * FROM `user`"); $i=0; while($row=mysql_fetch_array($res)) { $members[$i++]=$row; }![]()
Hi Sebastian,
im live-script lese ich nicht alle Felder des Benutzers aus, sondern nur die relevanten, also id, avatar, Adressfelder etc.
Da ich bei der Erklärung allgemeingültig bleiben wollte, bin ich hier nicht detalliert drauf eingegangen, da dies auch je nach Forensoftware stark variiert.
TYPO3 · MySQLDumper · dislabs
·
manche Mühlen mahlen schneller ...
"Ich habe Rücken"
Horst Schlämmer
Aktive Benutzer in diesem Thema: 1 (Registrierte Benutzer: 0, Gäste: 1)