Category: Android 2.1

Android 2.1 SQLite3で緯度経度から距離検索

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) で求まるが、誤差が問題になる場合はあわせて記録。

// テーブル登録例
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関数がラジアン基準なので記録の際には注意。

Math.sin(Math.toRadians(loc.getLatitude()));

▼SQLite用 Location距離検索クエリの作成
上述のテーブルを距離条件で検索する場合は、基準とするLocation (latitude, longitude)を元に検索用のクエリを動的に作成する。

// 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単位で距離へ変換。

if(distcos>=1.0) distance_km = 0.0;	// 誤差の都合、同一座標で1.0を超える場合があるため(※)
else distance_km = (6371*Math.acos(distcos));	// miの場合は6371の代わりに3959
// ※厳密には地球の裏側で-1を超える場合もある。

Android 2.1/3.2 ステータスバー通知の取得

NotificatioinManagerからは現在の通知を取得できないが、AccessibilityServiceを使用すれば他のアプリケーションから通知されるNotifycationをイベントとして取得できる。

▼ステータスバー通知 Notification / NotificationManager の特徴
・NotificationMaganerにNotificationを追加するとステータスバーに通知される
・NotificationにはViewを指定できる
・NofiticationManagerでは追加/上書/削除のみを行える
他アプリの通知を取得できないように制限されている。

▼ユーザ補助サービス AccessibilityServiceの特徴
・他アプリでの操作の一部や通知内容の一部を取得できる
・テキスト入力内容まで取得できてしまうため安易に利用できない
・通常のアプリケーションとは別に設定→ユーザ補助からサービスを許可/実行する必要がある

▼AccessibilityServiceのサンプルコード
・必要最低限のサンプルコード (理解には最良のサンプル)
本家和訳サイト
・Android 3.2で実装したところ、Toastも Notificationとして受け取って自己ループに陥ったので修正

▼AccessibilityServiceからの受け渡し
・SharedPreferencesを介してStringで受け渡す例 (抜粋)
サービスとの通信が不要で、ユーザ補助のON/OFF切り替えを認識せずに受信可能。

public class StrAccessibilityService extends AccessibilityService {

	public static final String SHARED_ACCESS_PREFS="str_access";
	public static final String ACCESS_PREFS_KEY="access_key";

	private SharedPreferences mPrefAccess;
	public static final boolean DEBUG=true;	// 通知のToast表示フラグ

	@Override
	public void onCreate() {
		super.onCreate();
	}

	@Override
	public void onServiceConnected() {
		mPrefAccess = getSharedPreferences(SHARED_ACCESS_PREFS, MODE_PRIVATE);

		AccessibilityServiceInfo info = new AccessibilityServiceInfo();
		info.eventTypes = AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED;
		info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
		info.notificationTimeout = NOTIFICATION_TIMEOUT_MILLIS;
		info.flags = AccessibilityServiceInfo.DEFAULT;
		setServiceInfo(info);
	}

	@Override
	public void onAccessibilityEvent(AccessibilityEvent event) {
		int eventType = event.getEventType();
		switch (eventType) {
		case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED :
			if(DEBUG) Log.d("StrAccessibilityService", "Access NOTIFIFY:"+event.getText());
			putPreferences(event);
			break;
		}
	}

	private void putPreferences(AccessibilityEvent event){// Preferenceを変更
		SharedPreferences.Editor editor = mPrefAccess.edit();
		editor.putString(ACCESS_PREFS_KEY, ""+event.getText());
		editor.commit();
	}

}

取得側クラスは SharedPreferences.OnSharedPreferenceChangeListener を implement し、 onSharedPreferenceChanged で取得する。

	public static final String SHARED_ACCESS_PREFS="str_access";
	public static final String ACCESS_PREFS_KEY="access_key";
	private SharedPreferences mPrefAccess;

	@Override
	public void onCreate() {
		super.onCreate();
		mPrefAccess = getSharedPreferences(SHARED_ACCESS_PREFS, MODE_PRIVATE);
		mPrefAccess.registerOnSharedPreferenceChangeListener(this);
	}

	@Override	// StrAccessibilityService#putPreferencesによりコールされる
	public void onSharedPreferenceChanged(SharedPreferences prefs,String key) {
		String access_text = prefs.getString(ACCESS_PREFS_KEY,null);
		// (ここから処理)
	}

Android 2.1でのSQLite FTS3テーブル

SQLiteでFTS3テーブルを構築する際のポイントがいくつかあるようなのでまとめ。
構築済みのテーブルから全文検索用にFTS3テーブルへ移行/FTS3テーブルを追加する場合の注意点リストにも。

▼SQLite FTS3テーブルの特徴
サイズが膨張するためテキスト検索が不要な情報はテーブルを分ける
・FTS3テーブルの値は文字列扱いになり条件に不等号が使えない
_id もTEXTになりPRIMARY KEYですらなくなる → 代わりに docid が自動的に作られる

▼設計
1エントリにテキストと数値が混在し、検索しないテキストが多い場合や数値の条件検索をSQLiteで行う場合はテーブルを切り分ける。

// 通常のテーブル
create table normaltable ( _id integer primary key autoincrement, i_dat0 integer, ... );

// FTS3テーブル (自動的にdocid integer primary key autoincrementが追加される)
create virtual table ftstable USING fts3( stext text, ... );

▼データ管理
設計にもよるが1つのデータを通常のテーブル+FTS3テーブルに分けて登録する場合は、下記の検索方法を使用するため、insert、deleteなどの操作をする場合は両方のテーブルで行う。

public long registerEntry(ContentValues cv) {
	// ContentValues を通常テーブル用+FTS3テーブル用に分割
	ContentValues cv_n=new ContentValues(cv);
	ContentValues cv_fts=new ContentValues();
	cv_fts.put("stext", cv.getAsString("stext")); cv_n.remove("stext");
	cv_fts.put("s_dat0", cv.getAsString("s_dat0")); cv_n.remove("s_dat0");
	cv_fts.put("s_dat1", cv.getAsString("s_dat1")); cv_n.remove("s_dat1");
	return registerEntry(cv_n, cv_fts);
}

public long registerEntry(ContentValues cv_n, ContentValues cv_fts) {
	mDb.insert("ftstable", null, cv_fts);
	return mDb.insert("normaltable", null, cv_n);
}

▼検索
通常のテーブルとFTS3テーブルを _id と docid で参照して両方のデータを含むCursorを取得/Adapter処理できる。

// mDb.rawQuery用検索クエリ例. MATCH部分は*ワイルドカード可
select dt._id, dt.i_dat0, vt.stext
	from normaltable dt, ftstable, vt
	where dt._id=vt.docid AND vt.stext MATCH "Keyword";

 

 

Android 2.1でのBuffer速度

Java VMでのBuffer速度ベンチマークを測定している方がいたので、Xperia (SO-01B/Android 2.1)でどれくらいの差があるかを大ざっぱにチェックした記録。ソース(javaのみ)

結果の値は全てmsec. (/L:ByteOrder.LITTLE_ENDIAN, /B:ByteOrder.BIG_ENDIAN)
※Xperia (Android 2.1)のByteOrder.nativeOrder() は LITTLE_ENDIAN

▼領域確保 配列長(32768, 131072)のint[]、IntBuffer、ByteBuffer (*4bytes)の確保時間

領域確保(整数型32,768分) 1回目 2回目 3回目 4回目 5回目
int[] new 1 94 1 1 1
IntBuffer allocateDirect/L 15 4 1 1 2
IntBuffer allocateDirect/B 287 1 2 1 1
IntBuffer allocate/L 2 1 91 1 1
IntBuffer allocate/B 1 1 1 92 1
IntBuffer wrap 8 1 1 0 91
ByteBuffer allocateDirect/L 1 1 2 2 2
ByteBuffer allocateDirect/B 1 1 1 1 1
ByteBuffer allocate/L 69 91 1 1 0
ByteBuffer allocate/B 1 0 92 1 1
ByteBuffer wrap 0 0 0 91 0
領域確保(整数型131,072分) 1回目 2回目 3回目 4回目 5回目
int[] new 98 120 93 91 90
IntBuffer allocateDirect/L 94 96 95 94 144
IntBuffer allocateDirect/B 3 3 3 3 3
IntBuffer allocate/L 1 1 0 1 1
IntBuffer allocate/B 91 91 31 91 91
IntBuffer wrap 92 92 90 90 90
ByteBuffer allocateDirect/L 96 96 93 94 95
ByteBuffer allocateDirect/B 3 3 3 3 3
ByteBuffer allocate/L 91 92 90 90 90
ByteBuffer allocate/B 91 90 90 90 90
ByteBuffer wrap 92 89 90 91 89

メモリ確保なのでGCが動いてあまり数値が一定しないが基本的には高速。またwrap自体は0~1msで行われるが表の値は new int[] もしくは new byte[] の時間も含む。
allocateDirect(/L)とallocate/Bが比較的遅め。確保法/バイトオーダーで速さが入れ替わっている。

▼処理速度 (個別に値をセット/一括コピー)

int[] array, IntBuffer ib, ByteBuffer bb の以下の処理時間を測定。

for (int i=0; i<SIZE; i++) array[i]=i;
for (int i=0; i<SIZE; i++) ib.put(i);
for (int i=0; i<SIZE; i++) bb.putInt(i);
セット速度(整数型32,768分) 1回目 2回目 3回目 4回目 5回目 最長を除いた4回の平均*
int[] new 16 15 17 18 17 16
IntBuffer allocateDirect/L 317 317 345 315 314 316
IntBuffer allocateDirect/B 444 464 442 371 422 420
IntBuffer allocate/L 282 284 311 280 281 282
IntBuffer allocate/B 342 270 759 147 268 257
IntBuffer wrap 62 25 85 63 64 54
ByteBuffer allocateDirect/L 354 268 264 264 264 265
ByteBuffer allocateDirect/B 405 386 246 372 371 344
ByteBuffer allocate/L 56 232 229 230 254 187
ByteBuffer allocate/B 232 220 216 215 137 197
ByteBuffer wrap 217 229 216 216 217 217

値のセットはどのBufferも遅い。

System.arraycopy(array, 0, array2, 0, SIZE);
ib.put(array);
bb.put(bb_src); // bb_srcはByteBuffer.wrap[bytearray2]
コピー速度(整数型131,072分) 1回目 2回目 3回目 4回目 5回目 最長を除いた4回の平均*
int[] new 2 3 2 2 2 2
IntBuffer allocateDirect/L 2 2 2 2 3 2
IntBuffer allocateDirect/B 9 9 9 9 9 9
IntBuffer allocate/L 988 824 862 581 835 776
IntBuffer allocate/B 519 878 270 820 500 527
IntBuffer wrap 12 12 13 12 12 12
ByteBuffer allocateDirect/L 5 4 5 5 5 5
ByteBuffer allocateDirect/B 97 99 94 96 94 95
ByteBuffer allocate/L 94 94 95 39 94 80
ByteBuffer allocate/B 97 95 94 94 94 94
ByteBuffer wrap 94 109 94 94 95 94

コピーであれば allocateDirect(/L) が高速。

*裏でGCが動くなど、極端に遅い外れ値を除くため5回中もっとも遅かった値を除いて4回を平均