OpenStreetMap Radkarte


Für meinen Radsportverein habe ich vor einiger Zeit ein paar PHP-Skripte erstellt, mit denen man GPX-Dateien von gefahrenen Touren auf die Website hochladen und ansehen kann.

Viele Radler habe bei ihren Touren Navis oder Handys dabei, die die gefahren Tour aufzeichnen können. Die Daten landen auf dem Rechner als GPX-Datei. Diese Datei enthält im einfachsten Fall eine Liste von Koordinaten, welche die Strecke abstecken. Zusätzlich können Information wie Höhe, Uhrzeit, Genauigkeit und weiteres enthalten sein.

Auf der Einstiegsseite werden mehrere Listen (für die verschiedenen Radsport-Abteilungen) mit gefahrenen Touren angezeigt. Die Listen enthalten neben dem Namen auch das Datum und die Länge der Strecke. Diese Daten werden beim Laden ermittelt. Da der Aufbau der Seite bei vielen und großen GPX-Dateien recht lange gedauert hat, habe ich die Infos in metadata-Tags verpackt. Wird die Datei geladen, prüfe ich, ob diese Tags vorhanden sind. Fehlen sie, starte ich eine komplette Analyse und speichere die metadata-Tags in der Datei.

public function analyzeFile($Short) {
  if (file_exists($this->GPXFileName)) {
    // kurze (schnelle) Analyse durchführen
    $ShortInfos = $this->analyzeFileShort($this->GPXFileName);
 
    if (!$ShortInfos OR !$Short) {
      // Infos konnten nicht ermittelt werden
      $dom = new DOMDocument;
 
      // GPX-Datei in DOM laden
      if ($dom->load($this->GPXFileName) == false) {
        throw new Exception('Invalid XML-File!');
      }
 
      // gründliche Analyse durchführen
      $this->analyzeFileFull($dom);
 
      if ($Short and !$ShortInfos) {
        $this->addShortInfo($dom);
        $dom->save($this->GPXFileName);
      }
    }
  } else {
    throw new Exception("File not found!");
  }
}

Dies ist die Methode, die die Analyse der GPX-Datei startet. Wenn der Parameter $Short False ist, wird immer eine ausführliche Analyse durchgeführt.

Die ausführliche Analyse lädt die komplette GPX-Datei in den Speicher und ermittelt die Uhrzeit, die Strecke, minimale und maximale Höhe und die maximale Geschwindigkeit.

public function analyzeFileFull($dom) {
  $this->Distance = 0;
  $this->AltitudeUp = 0;
  $this->AltitudeDown = 0;
  $this->Startdate = null;
  $this->startPoint = null;
  $this->endPoint = null;
  $this->minPoint = null;
  $this->maxPoint = null;
  $this->minH = null;
  $this->maxH = null;
  $lastPoint = null;
 
  foreach ($dom->getElementsByTagName("trk") as $trk) {
    foreach ($trk->getElementsByTagName("trkseg") as $trkSeg) {
      $lastEle = null;
 
      foreach ($trkSeg->getElementsByTagName("trkpt") as $trkPt) {
        $currentPoint = $this->pointFromXMLNode($trkPt);
 
        // Start- und Endpunkt  
        if ($this->startPoint == null) {
          $this->startPoint = clone $currentPoint;
        }
        $this->endPoint = $currentPoint;
 
        // Bounding box
        if ($this->minPoint == null) {
          $this->minPoint = clone $currentPoint;
        } else {
          $this->minPoint->lat = min($this->minPoint->lat, $currentPoint->lat);
          $this->minPoint->lon = min($this->minPoint->lon, $currentPoint->lon);
        }
 
        if ($this->maxPoint == null) {
          $this->maxPoint = clone $currentPoint;
        } else {
          $this->maxPoint->lat = max($this->maxPoint->lat, $currentPoint->lat);
          $this->maxPoint->lon = max($this->maxPoint->lon, $currentPoint->lon);
        }
 
        // Distanz und Geschwindigkeit              
        if ($lastPoint) {
          $line = new GPXLine($lastPoint, $currentPoint);
          $this->Distance += $line->distance;
 
          if ($line->speed != null) {
            if ($this->maxspeed == null) {
              $this->maxspeed = $line;
            } else {
              if ($this->maxspeed->speed < $line->speed) {
                $this->maxspeed = $line;
              }
            }
          }
        }
 
        // Höhe
        if ($currentPoint->ele != null) {
          if ($lastEle == null) {
            $lastEle = $currentPoint->ele;
            $this->maxH = clone $currentPoint;
            $this->minH = clone $currentPoint;
          } else {
            $eleDiff = $currentPoint->ele - $lastEle;
            if ($eleDiff > 0) {
              $this->AltitudeUp += $eleDiff;
            } else {
              $this->AltitudeDown += $eleDiff;
            }
 
            if ($currentPoint->ele > $this->maxH->ele) {
              $this->maxH = $currentPoint;
            }
 
            if ($currentPoint->ele < $this->minH->ele) {
              $this->minH = $currentPoint;
            }
 
            $lastEle = $currentPoint->ele;
          }
        }
 
        // Startzeit
        if ($this->Startdate == null AND $currentPoint->time != null) {
          $this->Startdate = clone $currentPoint->time;
        }
 
        $lastPoint = $currentPoint;
      }
    }
  }
}

Die kurze bzw. schnelle Analyse ist etwas komplizierter, da ein anderer XML-Parser verwendet wird. Es wird der XML-Parser mit Callback-Funktionen initialisiert. Die werden beim Öffnen und Schließen von Tags bzw. mit den entsprechenden Inhalten aufgerufen. Zeile für Zeile wird dann an den Parser übergeben. Hierdurch kann der Parsevorgang an beliebiger Stelle abgebrochen werden, ohne die ganze Datei zu laden.

public function analyzeFileShort($filename) {
  $parser = xml_parser_create();
 
  xml_set_object($parser, $this);
  xml_set_element_handler($parser, "afsOpenTag", "afsCloseTag");
  xml_set_character_data_handler($parser, "afsTagData");
 
  $this->ShortInfoExit = false;
  $this->ShortInfoFound = false;
  $this->ShortInfoData = 0;
  $GPX_File = fopen($filename, "r");
 
  while (!feof($GPX_File) AND $this->ShortInfoExit == false) {
    $line = fgets($GPX_File, 4096);
    xml_parse($parser, $line);
  }
 
  xml_parser_free($parser);
 
  return $this->ShortInfoFound;
}
 
function afsOpenTag($parser, $name, $attrs) {
  $this->ShortInfoData = 0;
 
  switch ($name) {
    case "DISTANCE":
      $this->ShortInfoData = 1;
      break;
    case "STARTDATE":
      $this->ShortInfoData = 2;
      break;
    case "TRK":
    case "RTE":
      $this->ShortInfoExit = true;
      break;
  }
}
 
function afsCloseTag($parser, $name) {
   
}
 
function afsTagData($parser, $tagData) {
  switch ($this->ShortInfoData) {
    case 1:
      $this->Distance = floatval($tagData);
      $this->ShortInfoFound = true;
      break;
    case 2:
      $this->Startdate = DateTime::createFromFormat("Y-m-d", $tagData);
      break;
  }
 
  $this->ShortInfoData = 0;
}

Die Methode afsOpenTag wird immer bei einem öffnenden Tag aufgerufen. Je nach Tag merke ich mir in der Variablen ShortInfoData, welche Daten im nächsten Aufruf von afsTagData ausgelesen werden sollen. Das ganze ist also ein Zustandsautomat (in diesem Fall nach Moore) und schreit nach einer Umsetzung als Zustandsentwurfmuster (State-Pattern).