Donnerstag, 21. Januar 2010

Angebotskalkulator

Ich programmier gerade ein Programm (bzw. ein SnapIn für meinen Nothegger Butler, mehr dazu später einmal) welches die Angebotskalkulation für Disponenten erleichtert. Im Prinzip soll der Disponent Start-Land, Start-PLZ, Ziel-Land und Ziel-PLZ eingeben und das Programm berechnet ihm aufgrund von einem auf Regeln basierenden System den Preis und Mindestpreis für diesen Transport.
Zum Anzeigen der Route verwende ich GMaps.NET, ein wirkliches cooles Steuerelement zum Anzeigen von Google Maps Karten.
Da die Angebotskalkulation recht schnell gehen muss, soll der Disponent einfach nur die PLZ des gewünschten Start- oder Zielortes eingeben. Ich hab mir zuerst gedacht dass das sicher kein Problem ist in Google Maps. Jedoch liefert der Google Maps Geocoder bei Angabe einer PLZ nicht unbedingt das richtige Ergebnis. Schon gar nicht, wenn man z.B. eine PLZ in Osteuropa sucht. Als ersten Lösungsansatz habe ich auf http://www.querystring.org/google-maps/google-maps-query-string-parameters den hl-Parameter entdeckt. mit dem kann man die Hostlanguage (als ccTLD) festlegen. Google Maps beachtet diesen Parameter (das Land in dem sich der Browser des Suchenden befindet) bei der Suche nach Orten. Angenommen es gibt zwei Orte mit dem gleichen Namen (oder auch gleich PLZ) in Amerika und Spanien. So wird ein spanischer Browser den spanischen Ort, der amerikanische jedoch den amerikanischen Ort als erstes finden.
Dadurch waren meine Probleme jedoch leider nicht gelöst, da komischerweise die Query vom Geocoder nicht unbedingt als Postleitzahl interpretiert wurde. So fand ich mit der Suche "BY, 220000" und Hostlanguage "BY" leider nicht Minsk in Weißrussland (wie von mir beabsichtigt), sondern irgendeine Universität in Italien.
Ein weiteres Problem war, dass ich nicht für jede Suche Google fragen wollte, deshalb hab ich mir gedacht ich mach einfach eine lokale Tabelle in unserer Datenbank, in der ich PLZ, Name und die Koordinaten zu jedem Ort in Europa speichere. Daraufhin hab ich mir von einem Anbieter solcher Informationen ein Angebot machen lassen.. mit dem Resultat dass diese Daten 19.000!!! Euro kosten würden :-) Ok, daraufhin hab ich die OpenGeoDB gefunden. Das sah für mich schon sehr vielversprechend aus. Nur musste ich nun die Daten aus der OpenGeoDB in mein eigenes Datenformat bringen, da mir die Struktur der OpenGeoDB zwar sehr erweiterungsfähig und gut erscheint, für meine Zwecke (häufiges Abfragen) jedoch nicht ganz geeignet ist.

Der Aufbau meiner GeoTable:
CREATE TABLE `GeoTable`
(
`Id` integer (11) NOT NULL AUTO_INCREMENT ,
`Land` varchar (5),
`LocId` integer (11),
`PLZ` varchar (10),
`Name` varchar (255),
`Lat` double (13,5),
`Lng` double (13,5),
PRIMARY KEY (`Id`)
) TYPE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;


Land ist dabei die ccTLD des Landes und die LocId ist die LocId aus der GeoDb Datenbank. Die Daten brachte ich mit folgendem SQL Script von der GeoDB in meine Tabelle:

SELECT land.text_val AS LAND,
gl.loc_id AS LOC_ID,
zip.text_val AS PLZ,
NAME.text_val AS NAME,
coord.lat AS LAT,
coord.lon AS LNG
FROM geodb_textdata zip
LEFT JOIN geodb_textdata NAME
ON NAME.loc_id = zip.loc_id
LEFT JOIN geodb_locations gl
ON gl.loc_id = zip.loc_id
LEFT JOIN geodb_coordinates coord
ON zip.loc_id = coord.loc_id
LEFT JOIN geodb_hierarchies hier
ON hier.loc_id = zip.loc_id
LEFT JOIN geodb_textdata land
ON land.loc_id = hier.id_lvl2 AND land.text_type=500500000
WHERE zip.text_type = 500300000
AND NAME.text_type =500100000
AND (gl.loc_type =100600000
OR gl.loc_type =100700000 );


Nun hatte ich die Daten der GeoDB (ver. 2.4d) in meiner persönlichen, kleinen und äußerst feinen Geo-Tabelle (wieder mal 19k Euro gespart) :-)
Jedenfalls ist die Tabelle leider nicht vollständig. Deshalb schau ich nun wenn ein Disponent einen Ort sucht erst in der GeoTable Tabelle nach. Ist der Ort vorhanden ists super. Wenn nicht, dann benutze ich den Yahoo-Geocoder. Dieser liefert meiner Meinung nach bessere Ergebnisse für das Suchen nach Postleitzahlen, da man beim Request der Geokoordinaten schon State und ZIP angeben kann. Mit State ist allerdings ein amerikanischer Bundesstaat gemeint. Soweit ich das jetzt aber ausprobiert habe, funktioniert es auch sehr gut wenn man in State den englischen Namen des gesuchten Landes übergibt. Wurde eine Koordinate gefunden, so speichere ich diese in meiner GeoTable Tabelle um beim nächsten Suchvorgang nicht wieder den Yahoo Geocoder belästigen zu müssen.

So sieht das ganze nun in Aktion aus:
Zu beachten ist auch der Umstand, dass der Disponent nicht unbedingt den Vorschlag der Google Maps Route verwenden muss. Er kann durch Doppelklick auf die Karte VIA-Punkte festlegen. Diese Punkte werden dann mit Hilfe des via Parameters der Google Maps API beachtet. Herauszufinden wie das geht hat mich auch ein wenig Zeit gekostet. Der Teufel liegt dabei im Detail. Wie ich vom Namen des Parameters via abgeleitet habe, dacht ich, man kann die Positionen direkt in diesen Parameter schreiben. Jedoch muss man die Adressen in den daddr Parameter schreiben (die erste Adresse ohne, alle weiteren mit einem "to:" vor der Adresse) und im via-Parameter auf den Index der Adressen im daddr String verweisen.
Der fertige Aufruf sieht bei mir wiefolgt aus:


string.Format("http://maps.google.com/maps?f=q&output=dragdir&doflg=p&hl={0}{1}&q=&saddr=@{2},{3}&daddr={4}{5}", language, highway, start.Lat.ToString(CultureInfo.InvariantCulture), start.Lng.ToString(CultureInfo.InvariantCulture), daddr, viaUrl)

Zum daddr String komme ich wiefolgt:

if (viaPoints != null && viaPoints.Length > 0)
{
int count = 1;
viaUrl = "&via=";
foreach (PointLatLng vp in viaPoints)
{
if (count > 1)
{
daddr += "+to:";
}//if
daddr += "@" + vp.Lat.ToString(CultureInfo.InvariantCulture) + "," + vp.Lng.ToString(CultureInfo.InvariantCulture);
viaUrl += count.ToString() + ",";
count++;
}//foreach
viaUrl = viaUrl.Substring(0, viaUrl.Length - 1);
daddr += "+to:@" + end.Lat.ToString(CultureInfo.InvariantCulture) + "," + end.Lng.ToString(CultureInfo.InvariantCulture);
}//if


Das @ vor den Adressen bedeutet nur, dass Google nicht genau diese Position sondern die Umgebung dieser Position in der Routenberechnung verwenden soll.

Dann bleibt nur noch das Problem, dass Google Maps die Via-Punkte genau in der übergebenen Reihenfolge beachtet und ich nicht ahnen kann, in welcher Reihenfolge der Disponent die Via-Punkte durch Doppelklick anlegt. Jetzt sortiere ich die Via-Punkte bevor ich sie an Google Maps übergebe nach ihrer Distanz zum Startpunkt hin aufsteigend. In erster Instanz verwende ich jetzt mal die Luftlinie. Sollte das zu Problemen führen, müsste man für jeden Via-Punkt die Entfernung zum Startpunkt durch Google Maps berechnen lassen. Jedoch glaub ich, dass das mit der Luftlinie auch gut funktionieren sollte.

Zur Sortierung habe ich die Klasse MapDistance geschrieben:


///
/// Diese Klasse beschreibt eine Position auf der Karte und ihre Distanz zu einem Startpunkt.
///

public class MapDistance : IComparable
{
///
/// Der Regerenzpunkt zu dem die Entfernung berechnet wird.
///

public PointLatLng ReferencePoint { get; set; }
///
/// Die betreffende Position auf der Karte deren Entfernung zum Referenzpunkt in DinstanceToStart angegeben ist.
///

public PointLatLng Position { get; set; }
///
/// Die Luftlinien-Entfernung vom Refernzpunkt zur Kartenposition.
///

public double DistanceToStart
{
get
{
double dist_x = Position.Lat - ReferencePoint.Lat;
double dist_y = Position.Lng - ReferencePoint.Lng;
return Math.Sqrt(Math.Pow(dist_x, 2) + Math.Pow(dist_y, 2)); // pythagoras du wiffa hund!
}
}


#region IComparable Member

public int CompareTo(MapDistance other)
{
return this.DistanceToStart.CompareTo(other.DistanceToStart);
}

#endregion


Die Sortierung selbst erledigt mir Dank dem IComparable Interface die List.Sort Methode.

Damit werden die Zwischenpunkte in der richtigen Reihenfolge zur Routenplanung an Google Maps übergeben.

Jetzt mach ich mich dann gleich an die regelbasierende Preiskalkulation. Denn der Preis ist leider nicht nur von der Länge der Strecke abhängig. Weitere Faktoren sind beispielsweise das Ursprungsland, das Zielland, bestimmte Regionen die auf der Route liegen, Teilladungen etc.
Für die Kalkulation hab ich mir ein in XML ausgedrücktes regelbasierendes System vorgestellt. In Kürze mehr dazu :-)


Keine Kommentare:

Kommentar veröffentlichen