web-dev-qa-db-ja.com

SQlite最寄りの場所の取得(緯度と経度を使用)

SQLiteデータベースに格納されている緯度と経度のデータがあり、入力したパラメーターに最も近い場所(現在の場所-緯度/経度など)を取得したい。

私はこれがMySQLで可能であることを知っており、SQLiteにはHaversine式(球体上の距離を計算する)のカスタム外部関数が必要であるというかなりの研究を行いましたが、Javaと動作します。

また、カスタム関数を追加する場合は、org.sqlite .jar(org.sqlite.Functionの場合)が必要です。これにより、アプリに不要なサイズが追加されます。

これの反対側は、距離だけを表示することはそれほど問題ではないので、SQLのOrder by関数が必要です-私はすでにカスタムSimpleCursorAdapterでそれをしましたが、私はデータをソートできません私のデータベースには距離列がありません。それは場所が変わるたびにデータベースを更新することを意味し、それはバッテリーとパフォーマンスの無駄です。だから誰かがデータベースにない列でカーソルをソートする方法を知っているなら、私も感謝するでしょう!

この機能を使用するAndroidアプリがたくさんあることは知っていますが、魔法について説明してください。

ところで、私はこの代替案を見つけました: SQLiteのRadiusに基づいてレコードを取得するためのクエリ?

Latとlngのcosとsinの値に対して4つの新しい列を作成することを提案していますが、他にそれほど冗長ではない方法はありますか?

81
Jure

1)最初にSQLiteデータを適切な近似値でフィルタリングし、Javaコードで評価する必要があるデータ量を減らします。この目的には、次の手順を使用します。

確定的なthresholdおよびデータのより正確なフィルターを使用するには、radius中心点の北、西、東、南のメートルをJavaコードでしてから、で簡単に確認するデータベース内のポイントがその長方形内にあるかどうかを判断するためのSQL演算子(>、<)より小さいおよびより大きい。

メソッドcalculateDerivedPosition(...)は、これらのポイントを計算します(図のp1、p2、p3、p4)。

enter image description here

/**
* 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;
    }

楽しい!

このリファレンス を使用してカスタマイズし、完成させました。

106
Bobs

Chrisの答えは本当に役に立ちます(ありがとう!)が、直線座標(UTMまたはOSグリッド参照など)を使用している場合にのみ機能します。緯度/経度に度を使用する場合(例:WGS84)、上記は赤道でのみ機能します。他の緯度では、ソート順に対する経度の影響を減らす必要があります。 (あなたが北極に近いと想像してください...緯度はまだどこでも同じですが、経度はほんの数フィートかもしれません。これはソート順が間違っていることを意味します)。

赤道にいない場合は、現在の緯度に基づいてファッジファクターを事前に計算します。

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

次に注文する:

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

まだ近似値にすぎませんが、最初のものよりもはるかに優れているため、ソート順の不正確さは非常にまれです。

67
Teasel

これは回答され受け入れられましたが、私の経験と解決策を追加すると思いました。

ユーザーの現在位置と特定のターゲット位置との間の正確な距離を計算するためにデバイス上でヘイバーシン機能を実行するのはうれしいことですが、クエリ結果を距離順にソートして制限する必要がありました。

満足のいく解決策とは言えませんが、ロットを返して、事後のソートとフィルター処理を行いますが、これにより2番目のカーソルが生成され、多くの不要な結果が返されて破棄されます。

私の好ましい解決策は、longとlatのデルタ値の2乗のソート順で渡すことでした。

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

ソート順のためだけに完全なHaversineを実行する必要はありません。また、結果を平方根する必要がないため、SQLiteは計算を処理できます。

編集:

この答えはまだ愛を受けています。ほとんどの場合は問題なく動作しますが、もう少し精度が必要な場合は、以下の@Teaselの回答をご覧ください。緯度が90に近づくにつれて増加する不正確さを修正する「ファッジ」係数が追加されます。

66
Chris Simpson

結果セットのサイズを小さくして適切な関数を適用するために、エントリの Geohash タグ/インデックスを検討したことがありますか。

同様の分野での別のstackoverflowの質問: finding-the-closest-point-to-a-given-point

2
Morrison Chang

可能な限りパフォーマンスを向上させるために、次のORDER BY句を使用して@Chris Simpsonのアイデアを改善することをお勧めします。

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をデータベースの追加列として保存する必要もあります。エンティティをデータベースに挿入してデータを取り込みます。これにより、大量のデータを抽出しながらパフォーマンスがわずかに向上します。

0
Sergey Metlov