OSMにアクセスせず(オフラインで)Leaflet地図を表示する」ための、タイル事前ダウンロード手順メモ
以下、「OSMにアクセスせず(オフラインで)Leaflet地図を表示する」ための、タイル事前ダウンロード手順メモ。プログラム/コマンド込み。
(server22-1_v3.go+fetch_tiles.py を前提)
0. ゴール
-
事前にタイルPNGを
tiles/{z}/{x}/{y}.png形式で保存 -
Goサーバが
/tiles/を静的配信 -
Leaflet は
L.tileLayer('/tiles/{z}/{x}/{y}.png')を参照 -
オフライン環境でも地図が表示される
1. server22-1_v3.go の修正点
1-1. Leaflet のタイルURLをローカルに変更
homeTemplate の地図初期化で、オンラインOSMの行をコメントアウトし、ローカルにする。
1-2. 右クリックで座標とズームを表示(bbox取得用)
(Goテンプレート内なので JS の `...${}` を使わず、文字列連結にする)
HTML側(<div id="map"></div> の直後):
<div id="coordBox"
style="position:absolute; left:10px; bottom:10px; z-index:9999;
background:#fff; padding:6px 10px; border-radius:6px;
opacity:0.9; font-family:monospace;">
right-click: lat,lng
</div>
JS側(L.tileLayer(...).addTo(map); の直後):
1-3. Goサーバで /tiles/ を配信(必須)
main() に1行追加。これが無いと /tiles/*.png がHTMLで返って破綻する。
最終的に main() は概ねこの形:
2. bbox(範囲)と zoom の取得方法(DevTools不要)
-
ブラウザで地図を表示
-
欲しいズームレベルに合わせる
-
画面の左下を右クリック →
zoom=Z lat=... lng=...をメモ(SouthWest) -
画面の右上を右クリック → 同様にメモ(NorthEast)
bbox(minLon,minLat,maxLon,maxLat)は次で作る:
-
minLon = 左下 lng
-
minLat = 左下 lat
-
maxLon = 右上 lng
-
maxLat = 右上 lat
3. タイル一括ダウンロード用スクリプト(fetch_tiles.py)
目的:--bbox と --zooms を指定して tiles/z/x/y.png を保存する。
(既に作成済みの fetch_tiles.py を利用)
4. タイルのダウンロード(実行コマンド)
4-1. 作業ディレクトリ確認(重要)
tiles/ が存在するディレクトリで実行する。
無ければ作る:
4-2. 実行(あなたが確定した bbox/zoom 一覧)
[fetch_tiles.py]
#!/usr/bin/env python3
# fetch_tiles.py
#
# 指定した bbox / zoom 範囲の OSM タイルを
# tiles/{z}/{x}/{y}.png 形式でダウンロードする
#
# 使用例:
# python3 fetch_tiles.py \
# --bbox "130.38,33.58,130.44,33.60" \
# --zooms 16 17 18 \
# --sleep 0.3 --retries 3 --verbose
import math
import os
import time
import argparse
import urllib.request
import urllib.error
OSM_TILE_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
# ------------------------------------------------------------
# 座標変換
# ------------------------------------------------------------
def lonlat_to_tile(lon, lat, z):
lat = max(min(lat, 85.05112878), -85.05112878)
n = 2 ** z
x = int((lon + 180.0) / 360.0 * n)
lat_rad = math.radians(lat)
y = int(
(1.0 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi)
/ 2.0 * n
)
return x, y
# ------------------------------------------------------------
# メイン
# ------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="Download OSM tiles to local directory")
parser.add_argument("--bbox", required=True,
help="minLon,minLat,maxLon,maxLat")
parser.add_argument("--zooms", required=True, nargs="+", type=int,
help="zoom levels (e.g. 12 13 14)")
parser.add_argument("--sleep", type=float, default=0.3,
help="sleep seconds between downloads")
parser.add_argument("--retries", type=int, default=3,
help="retry count per tile")
parser.add_argument("--verbose", action="store_true",
help="verbose output")
args = parser.parse_args()
minLon, minLat, maxLon, maxLat = map(float, args.bbox.split(","))
for z in args.zooms:
x_min, y_max = lonlat_to_tile(minLon, minLat, z)
x_max, y_min = lonlat_to_tile(maxLon, maxLat, z)
if args.verbose:
print(f"[zoom {z}] x:{x_min}-{x_max} y:{y_min}-{y_max}")
for x in range(x_min, x_max + 1):
for y in range(y_min, y_max + 1):
out_dir = os.path.join("tiles", str(z), str(x))
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, f"{y}.png")
if os.path.exists(out_path):
continue
url = OSM_TILE_URL.format(z=z, x=x, y=y)
success = False
for attempt in range(args.retries):
try:
if args.verbose:
print(f"GET {url}")
urllib.request.urlretrieve(url, out_path)
success = True
break
except urllib.error.HTTPError as e:
if args.verbose:
print(f"HTTP error {e.code} for {url}")
except urllib.error.URLError as e:
if args.verbose:
print(f"URL error {e.reason} for {url}")
time.sleep(args.sleep)
if not success:
print(f"FAILED: {url}")
time.sleep(args.sleep)
print("Done.")
if __name__ == "__main__":
main()
補足(重要ポイント)
-
保存形式
tiles/
└─ z/
└─ x/
└─ y.png
ズームごとに bbox が違うため、ズームごとに1回ずつ実行する。
zoom=12
zoom=13
zoom=14
zoom=15
zoom=16
zoom=17
zoom=18(メモの2行目は右上として扱う)
5. ダウンロード結果の確認
5-1. タイル総数
5-2. 代表ファイルの存在確認
6. 「サーバがPNGを返しているか」の確認(最重要)
サーバ起動後、タイルを1つ直接叩く。
期待:
-
HTTP/1.1 200 OK -
Content-Type: image/png
ここが text/html なら、/tiles/ が FileServer に到達していない(main()の設定ミス)か、./tiles の相対パスがズレている。
7. 実運用(オフラインデモ)
-
オンライン環境で
tiles/を作る -
オフライン環境へ
tiles/を丸ごとコピー -
オフライン環境で
go run server22-1_v3.go(またはビルドした実行ファイル) -
ブラウザで
http://localhost:8080/を開く -
ズーム12〜18の範囲内で、地図が欠けずに表示されることを確認
8. よくある失敗と対処
8-1. 画面が灰色+壊れた画像
curl -I ...png が Content-Type: text/html になっている。
→ main() に /tiles/ の http.Handle(...) が無い、または ./tiles が存在しないディレクトリでサーバを起動している。
8-2. 404 が返る
→ tiles/z/x/y.png のパスが足りていない(bbox・zoom不足)か、サーバ起動ディレクトリが違う。
8-3. タイル枚数が多すぎる
→ bbox を縮める(ズームが高いほど爆発する)。特に z=18 は狭いbboxに限定する。
このメモの手順で、オンライン依存を切った状態の地図表示が成立する。