国土地理院地図もOSM(OpenStreetMap)も Google Map に準拠している。 zoom 0 では世界地図(メルカトル地図)が256x256画素の一つのタイル画像で表される。 zoom 1 では縦横に2分した4(=2x2)枚のタイル画像となる。 zoom 2 では縦横に4分した16(=4x4)枚のタイル画像となる。 以下同様である。
zoom 0の世界地図の座標は左上(西北)が (0,0)、右下(東南)が (1,1) と考える。 地図の描画では極座標を世界XY平面座標への変換が必要となる。
Google Map は世界地図を zoom 0 では1枚で表す[1]。
国土地理院の地図や OpenStreetMap など多くのデジタル地図がこの Google Map 方式を採用している。
左上(西北)端の座標を (0, 0)、右下(東南)端の座標を(1, 1)とする。
極座標との関係は以下の通りである。
世界座標:(0,0) => 緯度経度:(85.05112877980659,-180) 世界座標:(1,1) => 緯度経度:(-85.05112877980659,180)
zoom 1 では、上下左右に2分割して、合計4枚の地図(タイル)で表す。
このときのタイル座標は (0, 0)、(1, 0)、(0, 1)、(1, 1) となる。
(0,0) | (1,0) |
(0,1) | (1,1) |
zoom 2、3、4、... とこれを繰り返す単純なしくみになっている。
Java言語による極座標からXY平面座標への変換メソッドを下に示す。 経度からX座標への変換はシンプルであるが、緯度からY座標への変換は少し複雑である。
極座標で表された地点を地図に表示するには極座標からXY平面座標への変換が必要となる。
double Lon2X(double lon, int zoom) { return (lon + 180.0) / 360.0 * (1<<zoom); } double Lat2Y(double lat, int zoom) { return (1 - Math.log(Math.tan(lat*Math.PI/180.0) + 1 / Math.cos(lat*Math.PI/180.0))/Math.PI)/2.0 * (1<<zoom); }
地図上でクリックした地点の極座標を求めるにはXY平面座標から極座標への変換が必要となる。
上記の逆変換であり、当然、X座標から経度への変換は簡単であり、Y座標から緯度への変換は複雑である。 これがメルカトル図法の特徴である。
double X2Lon(double x, int z) { return x / Math.pow(2.0, z) * 360.0 - 180.0; } double Y2Lat(double y, int z) { double n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, z); return 180.0 / Math.PI * Math.atan(Math.sinh(n)); }
OSMでは極座標は 10の7乗をかけて、32ビット整数化することが多い。 double では 8バイトであるが、この整数化では、小数点以下7桁の精度が保たれ、 メモリやファイルサイズが double型の半分で済む。
XY平面座標の場合、unsigned int型の場合、最高精度としては double型の (0.0, 0.0) ~ (1.0, 1.0) を (0, 0) ~ (232, 232) に変換することである。 ただし、経度180度は-180度と同じであるから、232(経度180度)は 0 (経度-180度)とする。 すなわち、数値の範囲は (0, 0) ~ (232-1, 232-1) である。
C/C++ や C# など多くのプログラミング言語に unsigned int 型があるが、Java には unsigned int型はない。 Java でも int型で数値の範囲 (0, 0) ~ (232-1, 232-1) を表現することはできるが、 少し分かりずらい。 1ビット精度を下げて double型の (0.0, 0.0) ~ (1.0, 1.0) を (0, 0) ~ (231-1, 231-1) とした方が無難である。
自作地図アプリでは、更に精度を1ビットさげて double型の (0.0, 0.0) ~ (1.0, 1.0) を (0, 0) ~ (230-1, 230-1) としている。 日常使う地図としては十二分な精度である。
将来的には double型の (0.0, 0.0) ~ (1.0, 1.0) を (0, 0) ~ (231-1, 231-1) とするかも知れない。この場合には 231(経度180度) を 0 (経度-180度)とする必要がある。
更には、最高精度の(0, 0) ~ (232-1, 232-1) とするかも知れない。
現在は 230 を掛けた値を使う。
これまではGPSログは整数化した極座標で管理していた。これを整数化した世界XY平面座標による管理に変更する。 この方が描画時の座標計算が簡単になり、GPS軌跡データの描画に関わるCPU負荷が軽くなる。
class GPSLog { final static int E7 = 10000000; static ListlistLogs; // 今日のGPSログ byte hh, mm, ss; int time; // 0時からの経過秒数 int ilon, ilat, ialt; float speed, accuracy = -1; //19:52:17,139.5044180,35.5452761,96.4,0.0,9.9 public GPSLog(String line) { String[] v = line.split("[,:]"); hh = (byte)Integer.parseInt(v[0]); mm = (byte)Integer.parseInt(v[1]); ss = (byte)Integer.parseInt(v[2]); time = hh*60*60 + mm*60 + ss; ilon = (int)Math.round(Double.parseDouble(v[3]) * E7); ilat = (int)Math.round(Double.parseDouble(v[4]) * E7); ialt = (int)Math.round(Double.parseDouble(v[5])); if (v.length >= 8) { speed = Float.parseFloat(v[6]); accuracy = Float.parseFloat(v[7]); } } } float getX(int ilon) { return (float) (Lon2X(ilon / E7, zoom) * PX - (CX - W / 2)); } float getY(int ilat) { return (float) (Lat2Y(ilat / E7, zoom) * PX - (CY - H / 2)); } void drawToday(Canvas canvas) { if (!drawHistoryToday) return; paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(2.5f*Map.Scale); List logs = GPSLog.listLogs; if (logs == null || logs.size() == 0) return; float xB = getX(logs.get(0).ilon); float yB = getY(logs.get(0).ilat); int tB = logs.get(0).time; for (int n = 1; n < logs.size(); n++) { GPSLog log = logs.get(n); float xE = getX(log.ilon); float yE = getY(log.ilat); int tE = log.time; if (tE - tB < 60*3) { paint.setColor(log.speed < 3 ? Color.BLUE : Color.BLACK); canvas.drawLine(xB, yB, xE, yE, paint); } xB = xE; yB = yE; tB = tE; } }
プログラム行数は大差ないが、描画時の座標変換が簡単になる。
オブジェクトサイズをなるべく小さくしたい場合は byte hh, mm, ss はローカル変数にする。
極座標がほしいときがあるため、lon、lat をオブジェクトに入れているが、 座標変換で求めるならば、ローカル変数にできる。
ここでの処理時間上の効果は小さいが、レンダリング用データと座標系が同じになるので、 分かりやすい。0~1.0 の世界座標に 230 をかけているので、zoom 0 の場合には 230 で割る必要がある。ズームの値が zoom の場合は 230-zoomで割ることになる。
レンダリングでも同様の処理がある。 使用頻度が高い場合は、double f = 1.0 / (1<<(30 - zoom)) を Map の変数として、zoom を変えるごとに 変更する。
class GPSLog { final static double B30 = 1<<30; static ListlistLogs; // 今日のGPSログ byte hh, mm, ss; int time; // 0時からの経過秒数 double lon, lat; int x, y, ialt; float speed, accuracy = -1; //19:52:17,139.5044180,35.5452761,96.4,0.0,9.9 public GPSLog(String line) { String[] v = line.split("[,:]"); hh = (byte)Integer.parseInt(v[0]); mm = (byte)Integer.parseInt(v[1]); ss = (byte)Integer.parseInt(v[2]); time = hh*60*60 + mm*60 + ss; lon = Double.parseDouble(v[3]); lat = Double.parseDouble(v[4]); x = (int)Math.round(Map.Lon2X(lon, 0) * B30); y = (int)Math.round(Map.Lat2Y(lat, 0) * B30); ialt = (int)Math.round(Double.parseDouble(v[5])); if (v.length >= 8) { speed = Float.parseFloat(v[6]); accuracy = Float.parseFloat(v[7]); } } } float getX(int x, double f) { return (float)(x * f * PX - (CX - W / 2)); } float getY(int y, double f) { return (float)(y * f * PX - (CY - H / 2)); } void drawToday(Canvas canvas) { if (!drawHistoryToday) return; paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(2.5f*Map.Scale); List logs = GPSLog.listLogs; if (logs == null || logs.size() == 0) return; double f = 1.0 / (1<<(30 - zoom)); float xB = getX(logs.get(0).x, f); float yB = getY(logs.get(0).y, f); int tB = logs.get(0).time; for (int n = 1; n < logs.size(); n++) { GPSLog log = logs.get(n); float xE = getX(log.x, f); float yE = getY(log.y, f); int tE = log.time; if (tE - tB < 60*3) { paint.setColor(log.speed < 3 ? Color.BLUE : Color.BLACK); canvas.drawLine(xB, yB, xE, yE, paint); } xB = xE; yB = yE; tB = tE; } }