Android 2.1のSQLiteにはsin, cos関数がないため例示されているMySQLでの経度緯度からの距離検索はそのままの形では利用できない。解決のヒントをもとに実装したので要点まとめ。
▼元の公式からクエリに利用できる形へ変換
距離 = C * acos ( sin(lat)*sin(qlat)+cos(lat)*cos(qlat)*cos(lng-qlng) )
[lat, lng] 座標1の緯度経度(DB内を想定) …青字はDB格納時に予め算出できる部分
[qlat, qlng] 座標2の緯度経度(基準位置を想定) …緑字はクエリ投入時に算出できる部分
C=6371 (距離の単位がkmの場合) または 3959 (単位がmiの場合)
cos(a-b) = cos(a)*cos(b)+sin(a)*sin(b) なので、
距離 = C * acos ( sin(lat)*sin(qlat)+cos(lat)*cos(qlat)*(cos(lng)*cos(qlng)+sin(lng)*sin(qlng)) )
acos(x) = y は cos(y)=x なので、
cos(距離/C) = sin(lat)*sin(qlat)+cos(lat)*cos(qlat)*(cos(lng)*cos(qlng)+sin(lng)*sin(qlng))
SQLite内のエントリには緯度経度それぞれのsin, cos値を追加で格納しておけば、検索条件とする緯度経度のsin, cos、cos(距離/C)を使った四則演算で距離を評価できる形になる。
このときのcos(距離/C)値は距離として-1が最も遠く+1が最も近い状態。
遠い(地球の裏側) -1 … 0 (地球の1/4) … +1 (同一の緯度経度)
▼SQLiteへのエントリ登録
1つの座標について sin(lat), cos(lat), sin(lng), cos(lng) をそれぞれ real型で登録しておく。元の lat, lng 値はそれぞれ atan(sin/cos) で求まるが、誤差が問題になる場合はあわせて記録。
1 2 3 | // テーブル登録例 create table location ( _id integer primary key autoincrement, dat0 integer , ... , sinlat real not null , coslat real not null , sinlng real not null , coslng real not null ); |
※JavaのMath関数がラジアン基準なので記録の際には注意。
1 | Math.sin(Math.toRadians(loc.getLatitude())); |
▼SQLite用 Location距離検索クエリの作成
上述のテーブルを距離条件で検索する場合は、基準とするLocation (latitude, longitude)を元に検索用のクエリを動的に作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // SQLiteDatabase.rawQuery用クエリ文 public String searchNearQuery(Location loc, double range_km){ double km_cos=Math.cos(range_km/ 6371 ); // 距離基準cos値 double radlat=Math.toRadians(loc.getLatitude()), radlong=Math.toRadians(loc.getLongitude()); double qsinlat=Math.sin(radlat), qcoslat=Math.cos(radlat); double qsinlng=Math.sin(radlong), qcoslng=Math.cos(radlong); StringBuilder sb= new StringBuilder(); sb.append( "SELECT _id, dat0, ..., " ); sb.append( "(sinlat*" +qsinlat+ " + coslat*" +qcoslat+ "*(coslng*" +qcoslng+ "+sinlng*" +qsinlng+ ")) AS distcos " ); sb.append( " FROM location " ); sb.append( " WHERE distcos > " +km_cos); // 値が大きい方が近い sb.append( " ORDER BY distcos DESC " ); // 近い順に出力 // sb.append(" LIMIT 10"); // ←↑↓必要な場合追加 // Log.d("searchNearQuery", sb.toString()); return sb.toString(); } |
▼検索結果からの距離の取得
distcos値をacos角度x単位で距離へ変換。
1 2 3 | if (distcos>= 1.0 ) distance_km = 0.0 ; // 誤差の都合、同一座標で1.0を超える場合があるため(※) else distance_km = ( 6371 *Math.acos(distcos)); // miの場合は6371の代わりに3959 // ※厳密には地球の裏側で-1を超える場合もある。 |