トップ地図アプリMap3 > 極座標から世界XY平面座標への変換

極座標から世界XY平面座標への変換

はじめに

国土地理院地図も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世界平面座標系

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、... とこれを繰り返す単純なしくみになっている。

極座標からXY平面座標への変換

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平面座標から極座標への変換

地図上でクリックした地点の極座標を求めるには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));
  }

整数化したXY平面座標

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ログは整数化した極座標で管理していた。これを整数化した世界XY平面座標による管理に変更する。 この方が描画時の座標計算が簡単になり、GPS軌跡データの描画に関わるCPU負荷が軽くなる。

元のプログラム

class GPSLog {
    final static int E7 = 10000000;
    static List listLogs;    // 今日の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 List listLogs;    // 今日の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;
        }
    }

履歴およびメモ

リファレンス

[1] 世界は1枚の画像から : グーグルマップのしくみを探る(1)