Claude Codeで平面直角座標へ一括変換 — 国土地理院の測量計算API
点名ごとの緯度経度と系番号が並ぶCSVを、国土地理院の測量計算APIで平面直角座標に一括変換します。往復変換では見抜けない系番号の取り違えと、API側の拒否応答までを実測で扱います。
点名と緯度経度が並んだCSVを、測量や設計の図面で使う平面直角座標に直す作業は、表計算ソフトの関数では手間がかかります。国土地理院の測量計算サイトには、この換算をREST APIで呼べる口が用意されています。Claude Codeにスクリプトを書かせれば、数百点のCSVでも一度に変換できます。
ただし、変換が通ったことと、結果が正しいことは別です。手元で試すと、系番号を一つ間違えても、緯度経度に戻す往復変換はぴたりと一致しました。この記事は、APIの返り方、一括変換のスクリプト、往復変換では拾えない取り違えの見つけ方、Claude Codeに書かせるときの指示の順に進めます。
測量計算APIのbl2xyとxy2blは何を換算するか
測量計算APIは、国土地理院の測量計算サイトの機能をHTTPのGETで呼べるようにしたものです。URLにパラメータを付けて送ると、結果がXMLかJSONで返ります。平面直角座標に関わるのは次の2本です。
| やりたいこと | エンドポイント | 必須パラメータ |
|---|---|---|
| 緯度経度から平面直角座標 | エンドポイントbl2xy.pl | 必須パラメータlatitude longitude zone refFrame |
| 平面直角座標から緯度経度 | エンドポイントxy2bl.pl | 必須パラメータpublicX publicY zone refFrame |
どちらも、出力形式の outputType(json か xml)が毎回必須です。zone は平面直角座標系の系番号で、1から19を指定します。refFrame は測地系で、1が日本測地系、2が世界測地系です。URLの共通部分は https://vldb.gsi.go.jp/sokuchi/surveycalc/surveycalc/ で、仕様のページにはhttpで書かれていますが、httpsでも同じ応答が返りました。
平面直角座標のX軸は北向き、Y軸は東向きです。数学で習うX(東)・Y(北)とは逆なので、CSVの列名を x y と付けたまま取り違える事故が起きやすい箇所です。また、各系の原点は国土交通省の告示(平成14年第9号)で定義されています。全国19の系それぞれに原点があり、その近くの地点ほど座標値は小さくなります。
呼び出しの上限も決まっています。同一IPアドレスからのリクエストは、10秒間で10回までです。1点を往復で変換すると2回使うので、2秒に1点を超えるペースで回すと混雑の応答が返ります。
返り値のフィールド
実際に東京駅付近を9系で変換すると、次のJSONが返りました。
curl -s "https://vldb.gsi.go.jp/sokuchi/surveycalc/surveycalc/bl2xy.pl\
?outputType=json&refFrame=2&zone=9\
&latitude=35.681236&longitude=139.767125"{"OutputData":{"publicX":"-35363.2377","publicY":"-5992.9196",
"gridConv":"0.038616667","scaleFactor":"0.99990044"}}座標のほかに、gridConv(真北方向角)と scaleFactor(縮尺係数)も付いてきます。値はすべて文字列で、数値に直して使います。9系の原点は北緯36度、東経139度50分で、そこを変換するとX・Yとも0が返りました。この性質は、後で検算に使います。
点名CSVを一括変換するスクリプト
入力は、点名・系番号・緯度・経度の4列です。点によって系が違っても、1行ごとに zone を持たせておけば同じスクリプトで流せます。緯度経度を住所から用意するなら、国土地理院の住所検索APIで住所を検証する手順の結果をそのままlat/lon列に使えます。
name,zone,lat,lon
東京駅,9,35.681236,139.767125
札幌市役所,12,43.062096,141.354376
大阪城,6,34.687315,135.526201
那覇空港,15,26.195800,127.646000標準ライブラリだけで書いたスクリプトです。1点につき、bl2xyで平面直角座標を求め、その値をxy2blに戻して元の緯度経度との差を測ります。
import csv, json, math, re, sys, time, urllib.parse, urllib.request
BASE = "https://vldb.gsi.go.jp/sokuchi/surveycalc/surveycalc/"
ORIGIN_9 = (36.0, 139 + 50 / 60) # 9系の原点(36度, 139度50分)
def call(endpoint, **params):
params.update(outputType="json", refFrame=2) # 2 = 世界測地系
url = BASE + endpoint + "?" + urllib.parse.urlencode(params)
for _ in range(3):
with urllib.request.urlopen(url, timeout=20) as r:
body = r.read().decode("utf-8")
try:
data = json.loads(body)
except ValueError: # 歪み超過などはHTMLで返る
text = re.sub(r"<[^>]+>", " ", body.split("<UL>")[-1])
raise RuntimeError(" ".join(text.split()))
if "OutputData" in data:
return data["OutputData"]
msg = data.get("ExportData", {}).get("ErrMsg", "")
if msg.startswith("001"): # サーバ混雑: 待って再試行
time.sleep(12)
continue
raise RuntimeError(msg or str(data))
raise RuntimeError("混雑が続きました")
def convert(row):
zone = int(row["zone"])
lat, lon = float(row["lat"]), float(row["lon"])
try:
fwd = call("bl2xy.pl", zone=zone, latitude=lat, longitude=lon)
except RuntimeError as e:
return {**row, "x": "", "y": "", "err_m": "",
"note": "API拒否: " + str(e)}
x, y = float(fwd["publicX"]), float(fwd["publicY"])
back = call("xy2bl.pl", zone=zone, publicX=x, publicY=y)
dlat = float(back["latitude"]) - lat
dlon = float(back["longitude"]) - lon
err_m = math.hypot(dlat * 111000,
dlon * 111000 * math.cos(math.radians(lat)))
note = "往復差あり" if err_m > 0.01 else "OK"
return {**row, "x": x, "y": y, "err_m": round(err_m, 4), "note": note}
def main(src, dst):
# 既知点: 9系の原点は(0, 0)に変換されるはず
o = call("bl2xy.pl", zone=9, latitude=ORIGIN_9[0], longitude=ORIGIN_9[1])
assert float(o["publicX"]) == 0 and float(o["publicY"]) == 0, o
rows = list(csv.DictReader(open(src, encoding="utf-8")))
out = []
for row in rows:
out.append(convert(row))
time.sleep(2.2) # 10秒10回の内側に収める
with open(dst, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=list(out[0]))
w.writeheader()
w.writerows(out)
main(sys.argv[1], sys.argv[2])bl2xy_batch.py として保存し、次のように実行します。
python3 bl2xy_batch.py points.csv out.csv上の4点を流すと、out.csv は次の内容になりました。
name,zone,lat,lon,x,y,err_m,note
東京駅,9,35.681236,139.767125,-35363.2377,-5992.9196,0.0,OK
札幌市役所,12,43.062096,141.354376,-103804.2987,-72949.3733,0.0,OK
大阪城,6,34.687315,135.526201,-145521.6133,-43411.9672,0.0,OK
那覇空港,15,26.195800,127.646000,21698.6206,14591.4202,0.0,OK往復の差は、計算した範囲ではメートル単位で0でした。同じ4点を3回繰り返した12点(24回の呼び出し)も、約30秒でエラーなく終わっています。待ち時間を入れているのは、前述の上限を避けるためです。
小数点以下の桁数にも触れておきます。仕様では publicX publicY を小数点以下3桁まで指定できると書かれています。bl2xyは4桁で返しますが、4桁のまま渡しても3桁に丸めて渡しても、逆変換の緯度は同じ値(35.68123600)でした。一方、小数点以下1桁まで切ると緯度は35.68123634に動きます。切り詰める前処理を挟むなら、3桁を下回らないようにします。
APIが返すエラーは3種類ある
入力が不正なときの返り方は、JSONのエラー、HTMLの拒否、混雑の3通りでした。スクリプトの call 関数は、この3つを区別して扱っています。
| 入力 | 返り方 |
|---|---|
| 系番号に20を指定 | 返り方JSONで ErrMsg に 100: 始まりの文(系番号が不正) |
緯度に abc を指定 | 返り方JSONで「緯度の値は20~50の範囲で指定してください」 |
| 経度を省略 | 返り方JSONで「経度の値は120~160の範囲で指定してください」 |
| 6系で東京駅(139.77度)を指定 | 返り方HTTP 200のまま、JSONでないHTML |
| 短時間に連続して呼ぶ | 返り方JSONで 001:サーバが混み合っております |
注意したいのは4行目です。系番号が遠すぎると、HTTPステータスは200で、本文だけがHTMLの「計算結果」ページになります。中身は「歪みが1/1万 を大きく超えました。縮尺係数 = 1.00133329入力値を確認してください」という文でした。json.loads をそのまま通すとここで落ちるので、スクリプトは落ちる前にタグを外して文を取り出し、その行の note 欄に書いています。
落とし穴
歪み超過のHTMLを例外に任せると、バッチ全体が止まる
一括処理で1点でも系の遠い行が混ざると、JSONの解析で例外が出て、残りの点が変換されません。行ごとに拒否を受け止めて note に残し、次の行へ進む作りにしておくと、どの点が弾かれたかを後で表で見られます。
往復変換では、系番号の取り違えが見えない
スクリプトの err_m は、往復で元に戻るかを見る列です。ここに落とし穴があります。同じ系番号で行って帰る限り、系が間違っていても差は0になります。実際に、東京駅を隣の8系で変換して確かめました。
| 系番号 | 結果 | 往復の差 |
|---|---|---|
| 9(正しい) | 結果X=-35363.2377 Y=-5992.9196 | 往復の差0 |
| 8(取り違え) | 結果X=-34625.4163 Y=114698.2094 | 往復の差0 |
| 6(遠すぎる) | 結果API拒否(歪み超過) | 往復の差— |
8系の結果は、API側から見れば正当な換算です。東京駅は8系の区域外ですが、原点から東に約115km離れた位置として座標が返り、歪みも1/1万の枠に収まります。この座標を8系のまま逆変換すると、元の緯度経度に戻ります。往復の差だけを合格条件にすると、8系の誤りは通り抜けます。
さらに、逆方向の取り違えも手元で確かめました。9系の座標を8系として緯度経度に戻すと、経度は138.43度になりました。東京駅から約120km西へずれた位置です。
XとYを入れ替えた場合も同じ型です。9系の publicX=-5992.9196、publicY=-35363.2377 を渡すと、緯度35.945、経度139.441が返りました。エラーにはならず、関東内陸の値に収まるので、見た目では気づきません。約40km北西へずれています。
往復以外に置く3つの検算
往復とは別の物差しを、スクリプトの外に3つ置くと取り違えを拾えます。
- 原点の検算: 9系の原点(北緯36度、東経139度50分)が(0, 0)に変換されること。スクリプトの先頭で
assertしているのがこれです。API側の仕様や応答の変化にも気づけます - 入力側の範囲確認: 系番号と点の緯度経度が、告示で定める適用区域に入っているか。区域の一覧は国土地理院の「わかりやすい平面直角座標系」のページから辿れます
- 手元の既知点との照合: 業務で使っている控えの座標値(基準点成果など)を数点混ぜ、API結果との差を見る。系の取り違えを座標値で拾えるのは、この方法です
測地系の指定も、同じ種類の見落としです。ただし refFrame がするのは、渡した緯度経度をどちらの測地系の値として扱うかの指定だけで、測地系そのものの変換ではありません。東京駅の同じ数値を refFrame=1(日本測地系)で換算するとX=-35359.4833、Y=-5992.2040、refFrame=2(世界測地系)ではX=-35363.2377、Y=-5992.9196でした。この約3.75mと約0.72mは、二つの測地系で楕円体が違うことによる計算上の差にすぎません。日本測地系と世界測地系では、同じ地点の緯度経度の値が東京付近で約450mずれます。日本測地系の点を世界測地系に直すには、測量計算APIにある測地系変換(TKY2JGD)を別に通す必要があります。スクリプトは refFrame=2 に固定してあるので、日本測地系で記された古い図面の点を混ぜるときは、先に測地系変換をかけてから流します。
図面側の座標から緯度経度へ戻す方向と、縮尺係数の使い道
逆方向の変換が要るのは、図面や台帳に載っている平面直角座標を、地図に重ねたいときです。入力は点名・系番号・X・Yの4列になり、スクリプトは bl2xy.pl を xy2bl.pl に、latitude longitude を publicX publicY に替えるだけで流用できます。このとき、列名を x y と付けてCSVを作る人と、publicX を東西方向と読む人が混在すると、前節のX・Y入れ替えが起きます。入力CSVの列名は publicX_north publicY_east のように、向きまで書いておくと取り違えが減ります。
返り値の scaleFactor(縮尺係数)は、平面上の距離と地表の距離の比です。9系の原点では0.99990000、原点から離れた東京駅では0.99990044でした。系の原点付近は0.9999、離れるほど1に近づき、ある距離を超えると1を上回ります。前節で6系を拒否した「歪みが1/1万を大きく超えました」は、この係数が1から1/1万を超えて離れた状態を指しています。長い距離を平面座標で測るときは、scaleFactor を列に残しておくと、後から補正するか判断できます。
Claude Codeにスクリプトと検算を書かせる
ここまでの内容を、Claude Codeへの依頼に落とします。Claude Code自体の使い方はClaude Codeの全体像にまとめてあるので、ここでは座標変換の依頼に絞ります。APIの仕様と、往復変換の限界を最初に渡しておくと、往復差0だけで完了を宣言する書き方を避けられます。
毎回の指示はCLAUDE.mdに置く
プロジェクトの規約は、CLAUDE.md に書いておくとセッションの開始時に毎回読み込まれます。ただし、これは強制ではなくコンテキストとして扱われます。次のような断片を置くと、座標変換の作業で共通して効きます。
## 座標変換の規約
- 平面直角座標は国土地理院の測量計算API(bl2xy.pl / xy2bl.pl)で換算する
- refFrame は 2(世界測地系)。X は北向き、Y は東向き
- 同一IPで10秒10回まで。1点の往復で2回使う。待ち時間を入れる
- 往復変換の差0は合格条件にしない。系の取り違えは往復で見えない
- 検算は「原点が(0,0)になる」と「手元の既知点との差」の2つで行う依頼の文面
その上で、依頼はこのくらいの具体さで十分です。
points.csv(name,zone,lat,lon)を平面直角座標に一括変換するスクリプトを
書いて。bl2xy.plを使い、結果をout.csvに書く。
HTMLで返る拒否応答も行ごとに受け止めて、noteに残すこと。
書いたら、次の2点を自分で実行して結果を読んで報告して。
1. 9系の原点が(0,0)に変換されること
2. known.csv の既知点とAPI結果の差(メートル)「自分で実行して結果を読む」まで依頼に入れておくのがポイントです。差が出たら、Claude Codeが out.csv の note 列と差の列を読み、どの行の系番号が怪しいかを挙げてくれます。取り違えが疑われる行は、表の形で一覧にさせると確かめやすくなります。
返ってきたスクリプトは、この記事の call 関数の分岐(JSONのエラー・HTMLの拒否・混雑)に相当する処理が入っているか、目で確かめてから流します。レート制限の待ち時間がないスクリプトを数百点に流すと、混雑の応答が続いて途中で止まります。
変換の前後で確かめておくこと
最後に、運用でつまずきやすい点を並べます。
- 入力CSVに系番号の列を持たせ、点ごとに明示する。「全点同じ系」という暗黙の前提を置かない
- 点が属する系を、変換の前に決めておく。県境や島しょ部の点は、告示の適用区域で確かめる
- 往復差が0でも合格にしない。既知点との差を見てから結果を採用する
まとめ
bl2xyとxy2blの組み合わせで、点名CSVの平面直角座標への一括変換は数十行のスクリプトで済みます。ただし往復変換の一致は、換算の計算が整合している証拠にとどまり、系番号が正しい証拠ではありません。手元の既知点を数点混ぜておくことが、この作業で最も効く確認です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。