Monthly Archives: März 2010

Entfernungsberechnung mit PostGIS

Um die Entfernung zweier Objekte in Metern zu berechnen eignet sich die distance Funktion leider nicht. Denn die Entfernung wird in der Einheit berechnet, in der die Punkte gespeichert sind. In diesem Fall als Lat/Lng Koordinaten, also Winkel in Grad. Das die Kalkulation nicht aufgeht, zeigt ein simples Experiment:

SELECT distance(geometry('POINT(0 -170)'), geometry('POINT(0 170)'))
SELECT distance(geometry('POINT(0 -160)'), geometry('POINT(0 160)'))

Das Ergebnis der ersten Berechnung ist 340 und das der zweiten 320. Mathematisch richtig, bezogen auf unsere Erde aber leider falsch. Denn die Punkte der zweiten Berechnung liegen in der Realität – auf dem Globus betrachtet – weiter auseinander als die Punkte der ersten Kalkulation. Das liegt daran das die Punkte -180/0 und +180/0 ein und denselben Punkt markieren. Denn es handelt sich bei unserem Planeten um eine Kugel.

Um nun die Entfernung zweier Punkte zu einander zu berechnen, wird die kürsteste Strecke auf einer Kugel ermittelt. Mehr dazu unter Orthodrome

Die Implementierung sieht so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GeoUtils {
  private static final double RADIUS = 6378137.0d;
 
  /**
   * Calculates the distance between two points in meters and returns
   * the value as result.
   *
   * @param p1 The first point
   * @param p2 The second point
   * @return The distance in meters
   */
  public static double distance(Point p1, Point p2) {
    double lng1 = p1.getX() * Math.PI / 180;
    double lat1 = p1.getY() * Math.PI / 180;
 
    double lng2 = p2.getX() * Math.PI / 180;
    double lat2 = p2.getY() * Math.PI / 180;
 
    return RADIUS * Math.acos(
            Math.sin(lat1) * Math.sin(lat2) +
            Math.cos(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1)
    );
  }
}

Postgis bietet dafür die distance_sphere Funktion. Leider definiert Hibernate Spatial diese Funktion nicht im Dialekt. Das führt dazu, dass die Funktion per native SQL aufgerufen werden muss – möchte man sie nutzen.

Die einfachste Möglichkeit dies zu umgehen ist die Erweiterung des Postgis Dialektes:

1
2
3
4
5
6
7
8
9
10
import org.hibernate.Hibernate;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernatespatial.postgis.PostgisDialect;
 
public class MyPostgisDialect extends PostgisDialect {
  public MyPostgisDialect() {
    super();
    registerFunction("distance_sphere", new StandardSQLFunction("distance_sphere", Hibernate.DOUBLE));
  }
}

Die Verwendung ist dann in HQL denkbar einfach. In dem folgendem Beispiel habe ich eine Funktion definiert, die mir zu einem bestimmten Punkt den nächst möglichen Flughafen findet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
   * Returns the nearest airport to the given point.
   *
   * @param point The reference point
   * @return The nearest airport
   */
  static Airport findNearestTo(Point point) {
    return withSession { session ->
      def hql = """from Airport a
                   order by distance_sphere(setsrid(geometry(:point), 0), a.location)"""
      session.createQuery(hql)
             .setParameter("point", point.toText())
             .setMaxResults(1)
             .uniqueResult()
    }
  }