SQlite Отримання найближчих місць (з широтою та довготою)


86

У мене є дані з широтою та довготою, що зберігаються у моїй базі даних SQLite, і я хочу отримати найближчі місця до параметрів, які я ввожу (наприклад, моє поточне місцезнаходження - lat / lng тощо).

Я знаю, що це можливо в MySQL, і я провів досить багато досліджень, що SQLite потребує власної зовнішньої функції для формули Haversine (обчислення відстані на кулі), але я не знайшов нічого, що написано на Java і працює .

Крім того, якщо я хочу додати власні функції, мені потрібен org.sqliteфайл .jar (дляorg.sqlite.Function ), який додає непотрібний розмір додатку.

Інша сторона цього полягає в тому, що мені потрібна функція "Порядок за SQL", оскільки відображення відстані не є великою проблемою - я це вже зробив у своєму користувацькому SimpleCursorAdapter, але я не можу сортувати дані, оскільки я немає стовпця відстані в моїй базі даних. Це означало б оновлювати базу даних щоразу, коли місцезнаходження змінюється, а це витрата батареї та продуктивності. Отже, якщо хтось має ідею щодо сортування курсора за стовпцем, якого немає в базі даних, я також буду вдячний!

Я знаю, що існує маса програм для Android, які використовують цю функцію, але хтось може пояснити магію.

До речі, я знайшов таку альтернативу: Запит на отримання записів на основі Radius в SQLite?

Пропонується створити 4 нові стовпці для значень cos та sin lat і lng, але чи існує інший, не такий надмірний спосіб?


Ви перевірили, чи працює org.sqlite.Function для вас (навіть якщо формула неправильна)?
Thomas Mueller,

Ні, я знайшов (зайву) альтернативу (відредагований допис), яка звучить краще, ніж додавання 2,6 МБ .jar у програму. Але я все ще шукаю кращого рішення. Дякую!
Jure

Який тип одиниці виміру зворотної відстані?

Ось повна реалізація для побудови запиту SQlite на Android на основі відстані між вашим місцезнаходженням і розташуванням об’єкта.
EricLarch 02.03.12

Відповіді:


112

1) Спочатку відфільтруйте дані SQLite з хорошим наближенням та зменште кількість даних, які вам потрібно оцінити у вашому коді Java. Для цього використовуйте наступну процедуру:

Щоб мати детермінований поріг і більш точний фільтр даних, краще розрахувати 4 місця, які знаходяться в radiusметрі півночі, заходу, сходу та півдня від вашої центральної точки у вашому коді Java, а потім легко перевірити менше ніж і більше, ніж Оператори SQL (>, <), щоб визначити, чи знаходяться ваші точки в базі даних у цьому прямокутнику чи ні.

Метод calculateDerivedPosition(...)обчислює ці бали для вас (p1, p2, p3, p4 на малюнку).

введіть тут опис зображення

/**
* Calculates the end-point from a given source at a given range (meters)
* and bearing (degrees). This methods uses simple geometry equations to
* calculate the end-point.
* 
* @param point
*           Point of origin
* @param range
*           Range in meters
* @param bearing
*           Bearing in degrees
* @return End-point from the source given the desired range and bearing.
*/
public static PointF calculateDerivedPosition(PointF point,
            double range, double bearing)
    {
        double EarthRadius = 6371000; // m

        double latA = Math.toRadians(point.x);
        double lonA = Math.toRadians(point.y);
        double angularDistance = range / EarthRadius;
        double trueCourse = Math.toRadians(bearing);

        double lat = Math.asin(
                Math.sin(latA) * Math.cos(angularDistance) +
                        Math.cos(latA) * Math.sin(angularDistance)
                        * Math.cos(trueCourse));

        double dlon = Math.atan2(
                Math.sin(trueCourse) * Math.sin(angularDistance)
                        * Math.cos(latA),
                Math.cos(angularDistance) - Math.sin(latA) * Math.sin(lat));

        double lon = ((lonA + dlon + Math.PI) % (Math.PI * 2)) - Math.PI;

        lat = Math.toDegrees(lat);
        lon = Math.toDegrees(lon);

        PointF newPoint = new PointF((float) lat, (float) lon);

        return newPoint;

    }

А тепер створіть свій запит:

PointF center = new PointF(x, y);
final double mult = 1; // mult = 1.1; is more reliable
PointF p1 = calculateDerivedPosition(center, mult * radius, 0);
PointF p2 = calculateDerivedPosition(center, mult * radius, 90);
PointF p3 = calculateDerivedPosition(center, mult * radius, 180);
PointF p4 = calculateDerivedPosition(center, mult * radius, 270);

strWhere =  " WHERE "
        + COL_X + " > " + String.valueOf(p3.x) + " AND "
        + COL_X + " < " + String.valueOf(p1.x) + " AND "
        + COL_Y + " < " + String.valueOf(p2.y) + " AND "
        + COL_Y + " > " + String.valueOf(p4.y);

COL_X - ім'я стовпця в базі даних, що зберігає значення широти та COL_Y призначене для довготи.

Отже, у вас є деякі дані, які знаходяться біля вашої центральної точки з хорошим наближенням.

2) Тепер ви можете прокручувати ці відфільтровані дані та визначати, чи вони дійсно знаходяться поблизу вашої точки (у колі) чи не використовують такі методи:

public static boolean pointIsInCircle(PointF pointForCheck, PointF center,
            double radius) {
        if (getDistanceBetweenTwoPoints(pointForCheck, center) <= radius)
            return true;
        else
            return false;
    }

public static double getDistanceBetweenTwoPoints(PointF p1, PointF p2) {
        double R = 6371000; // m
        double dLat = Math.toRadians(p2.x - p1.x);
        double dLon = Math.toRadians(p2.y - p1.y);
        double lat1 = Math.toRadians(p1.x);
        double lat2 = Math.toRadians(p2.x);

        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2)
                * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        double d = R * c;

        return d;
    }

Насолоджуйтесь!

Я використав та налаштував це посилання та заповнив його.


Перегляньте чудову веб-сторінку Кріса Венеса, якщо ви шукаєте реалізацію цієї концепції у Javascript вище. movable-type.co.uk/scripts/latlong.html
barneymc

@ Менма x - широта, а y - довгота. радіус: радіус кола, який зображений на малюнку.
Бобс

наведене вище рішення є правильним і працює. Спробуйте ... :)
YS

1
Це приблизне рішення! Це дає вам приблизні результати за допомогою швидкого , зручного для індексів запиту SQL. Це дасть неправильний результат у деяких екстремальних обставинах. Отримавши невелику кількість приблизних результатів в межах кількох кілометрів, використовуйте повільніші, точніші методи для фільтрації цих результатів . Не використовуйте його для фільтрації з дуже великим радіусом або якщо ваш додаток буде часто використовуватися на екваторі!
user1643723

1
Але я не розумію. CalculateDerivedPosition перетворює координату lat, lng в декартову, а потім у запиті SQL ви порівнюєте ці декартові значення з lat, long значення. Дві різні геометричні координати? Як це працює? Дякую.
Misgevolution

70

Відповідь Кріса справді корисна (дякую!), Але вона буде працювати лише в тому випадку, якщо ви використовуєте прямолінійні координати (наприклад, посилання на сітку UTM або OS). Якщо використовується градус для lat / lng (наприклад, WGS84), то вищезазначене працює лише на екваторі. В інших широтах потрібно зменшити вплив довготи на порядок сортування. (Уявіть, що ви близько до північного полюса ... градус широти все ще такий самий, як і де завгодно, але градус довготи може становити лише кілька футів. Це означатиме, що порядок сортування неправильний).

Якщо ви не знаходитесь на екваторі, заздалегідь обчисліть коефіцієнт запобігання на основі вашої поточної широти:

<fudge> = Math.pow(Math.cos(Math.toRadians(<lat>)),2);

Потім упорядкуйте за:

((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)

Це все-таки лише наближення, але набагато краще, ніж перше, тому неточності в порядку сортування будуть набагато рідше.


3
Це справді цікавий момент щодо поздовжніх ліній, що сходяться на полюсах і зсувають результати, чим ближче ви отримуєте. Гарне виправлення.
Chris Simpson,

1
здається, це працює cursor = db.getReadableDatabase (). rawQuery ("Виділити nome, id як _id," + "(" + latitude + "- lat) * (" + latitude + "- lat) + (" + longitude + "- lon) * (" + longitude + "- lon) *" + fudge + "as distanza" + "from cliente" + "order by distanza asc", null);
max4ever

має ((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)бути менше distanceабо distance^2?
Бобс,

Хіба коефіцієнт нечіткості не в радіанах, а стовпці в градусах? Чи не слід їх перетворити в одну одиницю?
Rangel Reale,

1
Ні, коефіцієнт запобігання - це масштабний коефіцієнт, який дорівнює 0 на полюсах і 1 на екваторі. Це не в градусах, ані в радіанах, це просто одиничне число. Функція Java Math.cos вимагає аргументу в радіанах, і я припустив, що <lat> був у градусах, отже, функція Math.toRadians. Але отриманий косинус не має одиниць.
Teasel

68

Я знаю, що на це відповіли та прийняли, але думав, що я додаю свій досвід та рішення.

Хоча я був радий виконати функцію гаверсину на пристрої для обчислення точної відстані між поточною позицією користувача та будь-яким конкретним цільовим місцем розташування, потрібно було сортувати та обмежувати результати запиту в порядку відстані.

Менш задовільним рішенням є повернення партії та сортування та фільтрування за фактом, але це призведе до того, що другий курсор повернеться та відкинеться безліч непотрібних результатів.

Моїм вподобаним рішенням було передавати в порядку сортування квадратичні значення дельти long і lats:

((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
 (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN))

Немає необхідності робити повну загальну інформацію лише для порядку сортування, і немає необхідності виводити результати в квадрат, тому SQLite може обробляти обчислення.

РЕДАГУВАТИ:

Ця відповідь все ще отримує любов. У більшості випадків це чудово працює, але якщо вам потрібна трохи більша точність, будь ласка, ознайомтеся з відповіддю @Teasel нижче, яка додає коефіцієнт "видумки", який виправляє неточності, що зростають із наближенням широти до 90.


Чудова відповідь. Не могли б ви пояснити, як це працювало і як називається цей алгоритм?
iMatoria

3
@iMatoria - Це лише скорочена версія відомої теореми Піфагора. Враховуючи два набори координат, різниця між двома значеннями X представляє одну сторону прямокутного трикутника, а різниця між значеннями Y - іншу. Щоб отримати гіпотенузу (і, отже, відстань між точками), ви додаєте квадрати цих двох значень разом, а потім отримуєте результат із квадратних коренів. У нашому випадку ми не робимо останній біт (квадратне вкорінення), тому що не можемо. На щастя, це не потрібно для порядку сортування.
Кріс Сімпсон,

6
У своєму додатку BostonBusMap я використовував це рішення, щоб показати зупинки, найближчі до поточного місцезнаходження. Однак вам потрібно масштабувати відстань довготи наcos(latitude) щоб мати широту та довготу приблизно однаковими. Дивіться en.wikipedia.org/wiki/…
noisecapella

0

Для того, щоб якомога більше підвищити продуктивність, я пропоную вдосконалити ідею @Chris Simpson за допомогою наступного ORDER BYпункту:

ORDER BY (<L> - <A> * LAT_COL - <B> * LON_COL + LAT_LON_SQ_SUM)

У цьому випадку ви повинні передати наступні значення з коду:

<L> = center_lat^2 + center_lon^2
<A> = 2 * center_lat
<B> = 2 * center_lon

І ви також повинні зберігати LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2як додатковий стовпець у базі даних. Заповніть його, вставляючи свої сутності в базу даних. Це трохи покращує продуктивність при витягуванні великої кількості даних.


-3

Спробуйте щось подібне:

    //locations to calculate difference with 
    Location me   = new Location(""); 
    Location dest = new Location(""); 

    //set lat and long of comparison obj 
    me.setLatitude(_mLat); 
    me.setLongitude(_mLong); 

    //init to circumference of the Earth 
    float smallest = 40008000.0f; //m 

    //var to hold id of db element we want 
    Integer id = 0; 

    //step through results 
    while(_myCursor.moveToNext()){ 

        //set lat and long of destination obj 
        dest.setLatitude(_myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LATITUDE))); 
        dest.setLongitude(_myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LONGITUDE))); 

        //grab distance between me and the destination 
        float dist = me.distanceTo(dest); 

        //if this is the smallest dist so far 
        if(dist < smallest){ 
            //store it 
            smallest = dist; 

            //grab it's id 
            id = _myCursor.getInt(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_ID)); 
        } 
    } 

Після цього ідентифікатор містить потрібний вам елемент із бази даних, щоб ви могли його отримати:

    //now we have traversed all the data, fetch the id of the closest event to us 
    _myCursor = _myDBHelper.fetchID(id); 
    _myCursor.moveToFirst(); 

    //get lat and long of nearest location to user, used to push out to map view 
    _mLatNearest  = _myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LATITUDE)); 
    _mLongNearest = _myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LONGITUDE)); 

Сподіваюся, це допоможе!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.