SlowCycle.de » 2008 » Februar


Um die Ergebnismenge doch noch zu verfeinern (MBR Problem)
bzw. den Abstand der gefundenen Ergebnisse zum Ursprungspunkt anzuzeigen hilft folgende
Funktion:

DROP FUNCTION IF EXISTS GEODISTANCE;

DELIMITER $$

CREATE FUNCTION GEODISTANCE (lat1 DOUBLE, lon1 DOUBLE, lat2 DOUBLE, lon2 DOUBLE) RETURNS DOUBLE UNSIGNED
DETERMINISTIC NO SQL
BEGIN
	DECLARE rlat1, rlat2, rlon1, rlon2 DOUBLE;
	DECLARE EARTH DOUBLE UNSIGNED DEFAULT 6371.0;

	SET rlat1 = RADIANS(lat1),
		rlon1 = RADIANS(lon1),
		rlat2 = RADIANS(lat2),
		rlon2 = RADIANS(lon2);

	RETURN ROUND(COALESCE(ACOS(
		(SIN(rlat2)*SIN(rlat1)) +
		(COS(rlat2)*COS(rlat1)*COS(rlon1-rlon2))
	) * EARTH, 0), 2);

END;

$$
DELIMITER ;

Geo Koordinaten Umkreis Suche unter Verwendung von Spatial (R-Tree) Indizes
und der Mysql Geometry Funktionen.

Für die Suche wird eine Tabelle mit folgender oder ähnlicher Struktur benötigt:
Dump incl. Testdaten (geo.sql)

CREATE TABLE `geo` (
   `id` int(10) unsigned NOT NULL auto_increment,
   `coordinate` geometry NOT NULL,
   `zip` int(5) unsigned zerofill NOT NULL,
   PRIMARY KEY  (`id`),
   SPATIAL KEY `coordinate` (`coordinate`),
   KEY `zip` (`zip`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1;

Die folgende SQL Function bekommt die Geographische Länge und Breite, den Umkreisradius in KM und die Anzahl
der Ecken des Umkreises (Genauigkeit) übergeben. Als Rückgabe liefert die Funktion ein MySQL GEOMETRY Object
welches auf die Tabelle `geo` angewendet werden kann.

DROP FUNCTION IF EXISTS GEOPOLYGON;
DELIMITER $$

CREATE FUNCTION GEOPOLYGON(lat DOUBLE,lon DOUBLE,radius MEDIUMINT,edges TINYINT) RETURNS GEOMETRY
DETERMINISTIC
BEGIN

DECLARE i MEDIUMINT DEFAULT 1;
DECLARE a,b,c DOUBLE DEFAULT NULL;
DECLARE result VARCHAR (2048) DEFAULT NULL;

IF (edges<3 || edges>360 || radius>500) THEN
    RETURN NULL;
END IF;

SET result=CONCAT(lat+radius/111.12,' ',lon,',');

WHILE (i<edges)
    SET c=RADIANS(360/edges*i);
    SET a=lat+COS(c)*radius/111.12;
    SET b=lon+SIN(c)*radius/(COS(RADIANS(lat + COS(c)*radius/111.12/111.12))*111.12);
    SET result=CONCAT(result,a,' ',b, ',');
    SET i=i+1;
END WHILE;

RETURN GEOMFROMTEXT(CONCAT('POLYGON((',result,lat+radius/111.12,' ',lon,'))'));
END;

$$
DELIMITER ;

Der Query in dem die Function GEOPOLYGON() verwendet wird, liefert
als Ergebnis eine Liste mit geo.id und geo.zip.
Im Funktionsaufruf kann Umkreis und in der WHERE Bedingung des sub- selects die Postleitzahl
des Mittelpunktes vom Umkreis definiert werden.

SELECT
	id,	zip
FROM geo
WHERE CONTAINS
(
	(
	SELECT
		GEOPOLYGON(X(coordinate),Y(coordinate), 5, 12)
	FROM geo
	WHERE zip = 06116 LIMIT 1
	), coordinate
);
+-------+-------+
| id    | zip   |
+-------+-------+
| 18617 | 06188 |
| 10723 | 06184 |
|  8090 | 06124 |
| 11434 | 06116 |
| 11435 | 06128 |
| 11436 | 06130 |
| 15246 | 06122 |
| 15991 | 06118 |
| 15992 | 06126 |
| 17664 | 06120 |
| 17665 | 06110 |
| 19574 | 06114 |
| 19947 | 06112 |
| 15654 | 06188 |
|  8092 | 06108 |
| 20785 | 06132 |
+-------+-------+
16 rows in set (0.00 sec)

MySQL implementiert die Funktion CONTAINS() unter anderen zurzeit noch nicht entsprechend der Spezifikation. Die, die implementiert werden, geben dasselbe Ergebnis wie die entsprechenden MBR-basierten Funktionen zurück.

MBR steht für “minimum bounding rectangle” (kleinstes umschließendes Rechteck).
Das berechnete Polygon welches die GEOPOLYGON Funktion zurück liefert ist also eigentlich nur das kleinstmögliche Rechteck, zumindest solange bis MySQL die Funktionen entsprechend implementiert hat, was gerade so um das Polygon herum passt.