Mittwoch, 7. April 2010

double/float vs. decimal in C#

Vor kurzem habe ich etwas für unsere Buchhaltung programmiert. Dabei ist mir die 2er-Komplement Darstellung bzw. die Ungenauigkeit bei double bzw. float Datentypen wiedermal aufgefallen.
Normalerweise ist die Genauigkeit von float und double eh ausreichens (verbunden mit der Math.Round Funktion), aber gerade bei Berechnungen für das Finanzamt könnten solche Ungenauigkeiten fatale Auswirkungen haben, da solche Rundungsungenauigkeiten die Angewohnheit haben sich zu summieren.

Deshalb hab ich sämtliche Berechnungen mit dem decimal Datentyp gemacht. Natürlich hat auch dieser Datentyp eine begrenzte Genauigkeit, auf die ersten 28-29 signifikante Stellen (Quelle: MSDN) ist eine decimal Gleitkommazahl jedoch genau.

Das Verwenden des decimal Datentyps unterscheidet sich nur minimal vomm double Datentyp.

Eine decimal Zahl gibt man mit dem m Postfix an, zum Beispiel:

decimal d = 1.5m;
decimal d2 = d / 0.25m;

Ansonsten bietet die Decimal Klasse die selben Bequemlichkeiten wie die Double Klasse.

Dienstag, 23. Februar 2010

Angebotskalkulator - Präsentation

Sodala, vorletzte Woche habe ich den Angebotskalkulator vorerst fertiggestellt.

Die Features:
  • Preise für einen Transport werden automatisch berechnet
  • Benutzer gibt Start-, Ziel- und Viapunkte vor
  • Adresswahl entweder durch Eingabe oder durch Doppelklick auf Karte
  • Start-, Ziel- und Zwischenpunkte können mittels Drag&Drop auf der Karte verschoben werden
  • Teilladungspreise können berechnet werden
  • Regelsystem wird in XML Datei gespeichert und ist somit leicht austausch- und optimierbar
  • Durch Integration in mein Nothegger Butler System stehen Standardfunktionen wie beispielsweise Auto-Update oder automatische Sende von Fehlerberichten an mich zur Verfügung
  • Der Benutzer hat die Möglichkeit eigene Wünsche als Feature-Request direkt an mich zu schicken
  • Oft verwendete VIA Punkte (Mont Blanc, Frejus, etc.) sind als Bookmarks unterhalb der Karte hinterlegt
  • Ortsuche entweder über komplette Postleitzahl, oder über eine Kombination aus Postleitzahlenbereich und Stichwörtern
  • Vom Geocoder gefundene Adressen werden gespeichert, sodass bei erneuter Verwendung (entweder durch den selben oder andere Benutzer) die Adresse nicht erneut vom Geocoder bezogen werden muß
  • Sehr intuitives Handling der Karte
  • Anzeige eines absoluten Minimumpreises, der nur in wenigen Ausnahmefällen unterschritten werden darf
Screenshots (Klick auf Bild vergrößert das Bild):

Das Programm in Aktion:


Via-Punkte können einfach durch Doppelklick auf die Karte hinzugefügt werden:


Fährt man mit dem Mauszeiger über einen Marker, so wird er hervorgehoben dargestellt und man kann ihn mittels Drag&Drop verschieben.


Nach der Berechnung kann man sich die Berechnungsdetails ansehen. Dabei ist der "Durchlauf" durch das Regelsystem nachvollziehbar, da ersichtlich ist, welche Regeln greifen und welche damit verbundenen Aktionen ausgeführt wurden.


Entspricht das berechnete Ergebnis nicht den Vorstellungen des Disponenten, so kann er mir dies über einen Klick und Angabe einer Begründung auf einfache Art und Weise mitteilen. Ich halte dann mit dem Chef Rücksprache und passe das Regelsystem ggf. an.


Das Programm ist jetzt seit ca. eineinhalb Wochen im Einsatz und die Resonanz ist sehr positiv.

Jetzt kann ich mich dann wieder anderen Dingen widmen. Das Senden von Ladestellen über den Nothegger Butler ist eines der nächsten Dinge. Zuvor werd ich aber noch ein kleines Tool schreiben, das unserer Buchhaltung hilft, Vorsteueranträge an das Finanzministerium zu übermitteln. Mehr dazu zu einem späteren Zeitpunkt.

Freitag, 5. Februar 2010

Angebotskalkulation - Regelsystem

Nun habe ich mich der Berechnung des Angebotspreises gewidmet. Die Preise sind nämlich von verschiedenen Faktoren (wie zum Beispiel Ausgangsland, Zielland, Wegpunkte etc.) anhängig. Die Preise fix in das Programm zu kodieren wäre sehr unelegant. Aber wie kann man so ein Berechnungssystem möglichst flexibel und trotzdem nicht fix kodiert programmieren? Ich sah von Anfang an zwei vielversprechende Möglichkeiten. Entweder mit einer Scriptsprache (z.B. Script.NET, hab ich schon in meiner Diplomarbeit verwendet) oder über eine XML Datei, in der die Regeln kodiert sind.
Ich hab mich jetzt für die Variante mit der XML Datei entschieden. Dabei soll es verschiedene Rulesets in der Datei geben. Zu jedem RuleSet gehören eine Menge von Regeln. Pro RuleSet kann man sich festlegen, ob man jede Regel ausführen will (evaluate="ALL") oder ob der Interpreter (wie beispielsweise bei iptables) die Regeln nur so lange abarbeiten soll, bis es zu einem Treffer kommt (evaluate="UNTIL_FIRST_HIT"). Wobei erster Treffer nicht ganz zutreffend ist. Jeder Regel sind in meinem Schema nämlich entweder eine variable Anzahl von Kindregeln oder eine Menge von mit der Regel verknüpften Aktionen zugeordnet. Sobald eine Regel erfüllt wurde und ihre Aktionen fertig ausgeführt wurden, wird das in meiner Terminilogie als FIRST_FIT betrachtet.
Das XML Schema für das Regelsystem sieht wie folgt aus:

<?xml version="1.0" encoding="ISO-8859-1"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified">
<xs:element name="pricing_definition" type="rulesets_Type"/>
<xs:complexType name="rulesets_Type">
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="pricetable" type="pricetable_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="geoshape" type="geoshape_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="actionset" type="actionset_Type"/>
</xs:choice>
<xs:choice maxOccurs="unbounded">
<xs:element name="ruleset" type="ruleset_Type"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ruleset_Type">
<xs:sequence maxOccurs="unbounded">
<xs:element name="rule" type="rule_Type"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="evaluate" type="evaluate_type" use="required"/>
</xs:complexType>
<xs:complexType name="rule_Type">
<xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="rule" type="rule_Type"/>
</xs:choice>
<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="action" type="action_Type"/>
<xs:element name="execute" type="execute_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="partial_cargo_pricing" type="partial_cargo_pricing_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="panic" type="panic_Type"/>
</xs:choice>
</xs:sequence>
</xs:choice>
<xs:attribute name="match_target" type="match_target_type" use="required"/>
<xs:attribute name="match_type" type="match_type" use="required"/>
<xs:attribute name="match_value" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="execute_Type">
<xs:attribute name="actionset" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="actionset_Type">
<xs:choice>
<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="action" type="action_Type"/>
<xs:element name="execute" type="execute_Type"/>
</xs:choice>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="partial_cargo_pricing" type="partial_cargo_pricing_Type"/>
</xs:choice>
</xs:sequence>
</xs:choice>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="panic_Type">
<xs:attribute name="desc" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="action_Type">
<xs:attribute name="target" type="action_target_type" use="required"/>
<xs:attribute name="type" type="action_type" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:complexType>
<xs:simpleType name="match_target_type">
<xs:restriction base="xs:string">
<xs:enumeration value="SRC_COUNTRY"/>
<xs:enumeration value="DST_COUNTRY"/>
<xs:enumeration value="SRC_ZIP"/>
<xs:enumeration value="DST_ZIP"/>
<xs:enumeration value="ROUTE"/>
<xs:enumeration value="TRUCKTYPE"/>
<xs:enumeration value="PRICE"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="match_type">
<xs:restriction base="xs:string">
<xs:enumeration value="EQUALS"/>
<xs:enumeration value="STARTS_WITH"/>
<xs:enumeration value="ENDS_WITH"/>
<xs:enumeration value="GREATER"/>
<xs:enumeration value="SMALLER"/>
<xs:enumeration value="PASSES"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="action_target_type">
<xs:restriction base="xs:string">
<xs:enumeration value="PRICE"/>
<xs:enumeration value="MIN_PRICE"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="action_type">
<xs:restriction base="xs:string">
<xs:enumeration value="ADD_ABS"/>
<xs:enumeration value="ADD_REL"/>
<xs:enumeration value="SET"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="evaluate_type">
<xs:restriction base="xs:string">
<xs:enumeration value="ALL"/>
<xs:enumeration value="UNTIL_FIRST_FIT"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="pricetable_Type">
<xs:choice maxOccurs="unbounded">
<xs:element name="pte" type="pricetable_entry_Type"/>
</xs:choice>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="pricing" type="pricing_type" use="required"/>
<xs:attribute name="entity_size" type="xs:double" use="required"/>
</xs:complexType>
<xs:complexType name="pricetable_entry_Type">
<xs:attribute name="count" type="xs:integer" use="required"/>
<xs:attribute name="percentage" type="xs:double" use="required"/>
</xs:complexType>
<xs:simpleType name="pricing_type">
<xs:restriction base="xs:string">
<xs:enumeration value="OVERALL_PERCENTAGE"/>
<xs:enumeration value="PER_ENTITY_PERCENTAGE"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="partial_cargo_pricing_Type">
<xs:attribute name="ldm_table" type="xs:string" use="required"/>
<xs:attribute name="pal_table" type="xs:string" use="required"/>
<xs:attribute name="weight_table" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="geoshape_Type">
<xs:choice>
<xs:element name="geocircle" type="geocircle_Type"/>
</xs:choice>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="geocircle_Type">
<xs:sequence>
<xs:element name="center_lat" type="xs:double"/>
<xs:element name="center_lng" type="xs:double"/>
<xs:element name="radius" type="xs:double"/>
</xs:sequence>
</xs:complexType>
</xs:schema>



PriceTables werden benötigt, damit man, damit man für Teilladungen den prpzentuellen Teilladungspreis berechnen kann. So wird beispielsweise eine Preistabelle mit dem Namen LDM_A definiert. OVERALL_PERCENTAGE gibt an, dass es sich um absolute prozentuelle Werte handelt. Alternativ dazu könnte man PER_ENTITY_PERCENTAGE angeben. Dann würde der Preis pro Entity (also pro Ladungseinheit) berechnet werden. Dazu würde man das Attribut entity_size benötigen.

<pricetable id="LDM_A" pricing="OVERALL_PERCENTAGE" entity_size="1">
<pte percentage="16" count="1"></pte>


Als nächstes werden sogenannte GeoShapes definiert. Ein GeoShape ist ein Bereich auf der Landkarte. Man kann dann in den Regeln überprüfen, ob eine Route durch ein bestimmtes GeoShape verläuft. Ein GeoShape wird folgendermaßen definiert:

<geoshape id="INNSBRUCK">
<geocircle>
<center_lat>47.2496400077265</center_lat>
<center_lng>11.3962554931641</center_lng>
<radius>0.17204397565615925</radius>
</geocircle>
</geoshape>


Dabei handelt es sich um einen GeoCircle, d.h. einen Kreis auf der Landkarte mit gegebenem Mittelpunkt und Radius.
Um dem Ersteller einer solchen Regeldatei Schreibarbeit zu ersparen, habe ich das Konzept der ActionSets eingeführt. Dabei kann man eine Menge von Aktionen zu einem ActionSet zusammenfassen und in einer Regel einfach auf das ActionSet verweisen, anstatt alle darin enthaltenen Aktionen extra an die Regel anhängen zu müssen. Ein ActionSet wird wiefolgt definiert:

<actionset id="AS_150_80">
<action type="ADD_ABS" target="PRICE" value="150"/>
<action type="ADD_ABS" target="MIN_PRICE" value="80"/>
</actionset>

Dabei werden dem ActionSet zwei Aktionen zugeordnet. Man könnte jedoch rekursiv weitere ActionSets einbinden:

<actionset id="AS_400_350_W2_LDMA_PAL2">
<execute actionset="AS_400_350" />
<partial_cargo_pricing weight_table="WEIGHT_2" ldm_table="LDM_A" pal_table="PAL_2"/>
</actionset>

Hier wird ein ActionSet mit Hilfe des execute Elementes aufgerufen und zusätzlich eine partial_cargo_pricing Aktion ausgeführt. Eine solche Aktion bestimmt eine der oben definierten Preistabellen für jeden Teilladungstyp (Lademeter, Gewicht oder Paletten).

Danach folgen die einzelnen RuleSets mit ihren Kindregeln. Solche Kindregeln können weitere Kindregeln haben. Ein Beispiel für eine solche Regel sieht man im folgenden Listing:

<ruleset evaluate="UNTIL_FIRST_FIT" name="Main Actions">
<!-- Ladungen in die Schweiz -->
<rule match_target="DST_COUNTRY" match_type="EQUALS" match_value="CH">
<rule match_target="TRUCKTYPE" match_type="EQUALS" match_value="PLANE">
<execute actionset="AS_400_350_W2_LDMA_PAL2"/>
</rule>

...

Die erste Zeile definiert ein RuleSet mit dem Namen "Main Actions" und die Regeln werden so lange ausgeführt, bis man auf eine gültige Aktion trifft.Die erste Regel wird erfüllt, wenn es sich beim Zielland um die Schweiz handelt. Wird diese Regel nicht erfüllt, springt das Auswertungssystem auf die nächste Schwesterregel. Ist die Regel erfüllt, so wird überprüft ob es sich um ein Planenfahrzeug handelt. Ist diese Regel auch erfüllt, so wird das ActionSet AS_400_350_W2_LDMA_PAL2 ausgeführt. Wird sie nicht erfüllt, wird mit der nächsten Schwesterregel fortgefahren. Wird von diesen auch keine erfüllt, so wird eine Ebene höher mit der nächsten Schwesterregel weitergemacht usw. Es handelt sich also im Prinzip um einen DFS Algorithmus.
Interessant ist auch folgende Regel:

<rule match_target="ROUTE" match_type="PASSES" match_value="MONTBLANC">
<action type="ADD_ABS" value="1150" target="PRICE"/>
<partial_cargo_pricing weight_table="WEIGHT_3" pal_table="PAL_2" ldm_table="LDM_B"/>
</rule>


Hierbei wird überprüft, ob die Route über den Mont Blanc verläuft. ist dies der Fall, wird zum Preis für den Transport ein absoluter Betrag hinzugefügt.

Sodala, das wars dann auch wieder mit meinem kleinen Berechnungssystem. Ich finde dass mir so eine gute Alternative zwischen Flexibilität und Wiederverwendbarkeit gelungen ist. Ich hab unser Regelsystem jetzt mal von Hand zusammengestellt und bin auf eine XML Datei mit ca. 1500 Zeilen gekommen. Vielleicht werd ich mir in Zukunft in ein paar ruhigen Minuten einmal einen grafischen Editor programmieren.. wer weiß ?! :-)

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 :-)


Mittwoch, 20. Januar 2010

Willkommen

Servus!

Mein Name ist Florian Wörter und ich arbeite als Leiter für Softwareentwicklung bei der Firma Nothegger Transport Logistik GmbH. Wie Ihr auf meiner Homepage Homepage von Florian Wörter sehen könnt, habe ich vorher Informatik an der Johannes Kepler Universität in Linz studiert.
Ich habe im Verlauf meiner Laufbahn sehr viele verschiedene Anwendungen programmiert. Eine (leider nicht mehr ganz aktuelle) Liste meiner kommerziellen Projekte findest Du unter Projekte von Florian Wörter. Wie sich das für einen technisch interessierten Menschen gehört, hab ich natürlich nebenbei immer wieder verschiedene Dinge auspro(gramm | b)iert.

Da ich jetzt mitlerweile schon einiges an Programmiererfahrung vorweisen kann, hab ich mich dazu durchgerungen diesen Blog zu erstellen, indem ich einige interessante Aspekte verschiedener Arbeiten präsentieren will. Vielleicht kann ja jemand von meiner Arbeit profitieren.

In Kürze mehr :-)