Home > po polsku, technicznie > Wybieranie obiektów znajdujących się w pewnej odległości od punktu

Wybieranie obiektów znajdujących się w pewnej odległości od punktu

Jakiś czas temu przyszło mi się zmierzyć z zagadnieniem prezentowania obiektów znajdujących się w pewnej odległości od zadanego punktu. Jako dane wejściowe miałem do dyspozycji współrzędne geograficzne, a moim zadaniem było oprogramowanie prostego interface`u opartego o mapę. Pogooglałem, poczytałem, popytałem i doszedłem do pewnego rozwiązania, którym dziś (zachęcony wpisem znalzionym na DZone) chcę się podzielić.

Obliczanie odległości

Ponieważ obliczam odległość nie na płaskiej powierzchni, ale na sferze, zwykłe twierdznie Pitagorasa nie wystarczy ;) Ponieważ wzór na odległość pomiędzy punktami na kuli ziemskiej (polecam zapoznać się z artykułem na Wikipedii oraz stronie Movable Type) nie należy do najkrótszych, postanowiłem ułatwić sobie życie poprzez napisanie funkcji w PGPLSQLu (w projekcie korzystamy z bazy danych PostgreSQL):

CREATE OR REPLACE FUNCTION distance(_lat1 NUMERIC, _lon1 NUMERIC, _lat2 NUMERIC, _lon2 NUMERIC, _unit CHARACTER)
  RETURNS NUMERIC AS
$BODY$
DECLARE
	dist	DECIMAL;
	miles	DECIMAL;
	unit	CHARACTER;
BEGIN
	dist	= sin(radians(_lat1)) * sin(radians(_lat2)) + cos(radians(_lat1)) * cos(radians(_lat2)) * cos(radians(_lon1 - _lon2));
	dist	= acos(dist);
	dist	= degrees(dist);
	miles	= dist * 60 * 1.1515;
	unit	= UPPER(_unit);
 
	IF ( unit = 'K') THEN
		RETURN miles * 1.609344;
	elsif ( unit = 'N') THEN
		RETURN miles * 1.8684;
	ELSE
		RETURN miles;
	END IF;
END;
$BODY$
  LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION distance(NUMERIC, NUMERIC, NUMERIC, NUMERIC, CHARACTER) OWNER TO test_pg;

Funkcja wylicza wynik w różnych jednostkach, w zależności od podanego parametru :) I wszystko byłoby pięknie i cacy, gdyby nie fakt, że w standardowym zapytaniu ($1 i $2 to szerokość i wysokość punktu centralnego):

SELECT * FROM TABLE WHERE distance(lat,lng,$1,$2,'K') < 10

wartość odległości obliczana jest dla każdego wiersza. Przy 100 rekordach nic się nie dzieje, podobnie przy 1000 czy 10000. Jednak przy 100000 czy 1000000 wpisów pojawia się spory problem (je testowałem na bazie zawierającej ponad 1.2 mln rekordów i musiałem ubijać proces :P). Okazuje się jednak, że problem można całkiem prosto rozwiązać! Wystarczy wstępnie zawęzić obszar przeszukiwania!

Wstępne zawężanie obszaru

Załóżmy, że chcemy znaleźć osoby mieszkające w promieniu 10km od centrum Warszawy. Interesuje Nas taki obszar:

Mapa Warszawy z zaznaczonym obszarem znajdującym się 10km od centrum miasta

Mapa Warszawy z zaznaczonym obszarem znajdującym się 10km od centrum miasta

Rozwiązanie polega na ograniczeniu wybieranych rekordów do tych, które mają wysokość pomiędzy “skrajnymi” wysokościami okręgu oraz szerokość pomiędzy “skrajnymi” szrokościami okręgu, czyli znajdujących się w prostokątnym obszarze jak na rysunku:

Mapa Warszawy z zaznaczonym obszarem wyznaczonym przez "skrajne" wartości długości i szerokości

Mapa Warszawy z zaznaczonym obszarem wyznaczonym przez skrajne wartości długości i szerokości

Mapa Warszawy z zaznaczonym obszarem wyznaczonym przez “skrajne” wartości długości i szerokościWyznaczenie takiego obszaru to trywialne zadanie:

SELECT COUNT(*) FROM TABLE WHERE (lat BETWEEN $1 AND $2) AND (lng BETWEEN $3 AND $4)

Do zapytania przekazujemy 4 parametry- 2x wysokość oraz 2x szerokość, które będą ograniczały wstępnie wyniki. Otrzymane wyniki musimy jeszcze dodatkowo zawęzić, aby pozbyć się obiektów “na rogach”, co obrazuje poniższy rysunek:

Warszawa z nałożonymi obszarami- zawężonym i docelowym

Warszawa z nałożonymi obszarami- zawężonym i docelowym

Zatem wynikowe zapytanie wygląda następująco:

SELECT * FROM TABLE
WHERE (lat BETWEEN $1 AND $2) AND (lng BETWEEN $3 AND $4) AND
distance(lat,lng,($5)::NUMERIC,($6)::NUMERIC,($7)::CHARACTER)<=$8

Parametrów jest w sumie 8, ale dzięki temu otrzymujemy wydajny sposób na pobranie obiektów oddalonych od podanego punktu o zadaną odległość w wybranej jednostce ;)

Mam nadzieję, że komuś się przyda :)

  1. No comments yet.
  1. No trackbacks yet.