地理院標高タイルダウンローダー(ソースコードと実行結果)


Python開発環境,ライブラリ類
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。
Python 3.12 のインストール
インストール済みの場合は実行不要。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
【関連する外部ページ】
Python の公式ページ: https://www.python.org/
AI エディタ Windsurf のインストール
Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
winget install --scope machine Codeium.Windsurf -e --silent
【関連する外部ページ】
Windsurf の公式ページ: https://windsurf.com/
必要なライブラリのインストール
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する
pip install pillow numpy httpx
地理院標高タイルダウンローダープログラム
概要
概要
このプログラムは,国土地理院が提供するPNG形式の標高タイルを参考となる地図を見ながら,緯度,軽度,ズームレベルを指定してダウンロードする.ダウンロード後に標高値のブレビューと保存ができる[1]。
主要技術
地理情報システム: 位置情報を持ったデータを管理・加工し、視覚的な表示,分析などを可能にする技術である[1][2]。
主要技術
地理情報システム: 位置情報を持ったデータを管理・加工し、視覚的な表示,分析などを可能にする技術である[1][2]。
参考文献
- [1] 国土地理院. GISとは. https://www.gsi.go.jp/GIS/whatisgis.html
- [2] 国土地理院. 標高タイルの詳細仕様. https://maps.gsi.go.jp/development/demtile.html
ソースコード
"""
地理院標高タイルダウンローダープログラム
特徴技術名: 地理院タイル(標高タイル)
出典: 国土地理院. (2025). 地理院タイル仕様. https://maps.gsi.go.jp/development/siyou.html
特徴機能: PNG形式による標高データの効率的配信
RGB値を用いて標高値をエンコードし、256x256ピクセルのタイルとして配信。
計算式: x = R×65536 + G×256 + B, h = (x < 8388608) ? x×0.01 : (x-16777216)×0.01
学習済みモデル: なし
方式設計:
関連利用技術:
- tkinter: GUIフレームワーク(ウィンドウ、ボタン、入力フィールド)
- PIL/Pillow: 画像処理ライブラリ(画像読み込み、表示、変換)
- NumPy: 数値計算ライブラリ(標高データの配列処理)
- HTTPX: HTTP通信ライブラリ(タイルデータの取得、HTTP/2対応)
入力と出力:
入力: 緯度・経度座標、ズームレベル、データセット選択
出力: 標高タイル画像のダウンロードと表示、標高値の数値データ
処理手順:
1. 緯度・経度からタイル座標への変換
2. 指定されたデータセットの標高タイルをHTTPで取得
3. PNG画像のRGB値を標高値にデコード
4. 可視化とメタデータ表示
前処理: 座標系変換(世界測地系からWebメルカトル投影への変換)
後処理: 標高データの統計情報計算(最小値・最大値)
追加処理: 地図プレビュー機能(現在位置の地図タイル表示と範囲指示)
調整を必要とする設定値:
- default_zoom (デフォルトズームレベル): 取得する標高データの解像度を決定(推奨値15)
将来方策: ズームレベル自動最適化機能(指定座標での利用可能な最高精度データセットの自動選択)
その他の重要事項:
- 出典明示が必要: 「国土地理院」または「地理院タイル」
- 利用規約: https://maps.gsi.go.jp/help/use.html
前準備:
pip install pillow numpy httpx
"""
import math
import httpx
from PIL import Image, ImageTk, ImageDraw
import numpy as np
from io import BytesIO
import os
from datetime import datetime
import tkinter as tk
from tkinter import ttk, messagebox
import threading
# 定数定義
TILE_SIZE = 256 # タイルサイズ(ピクセル)
DEFAULT_LAT = 35.6812 # デフォルト緯度(東京)
DEFAULT_LON = 139.7671 # デフォルト経度(東京)
DEFAULT_ZOOM = 15 # デフォルトズームレベル(推奨値)
MIN_ZOOM = 10 # 最小ズームレベル
MAX_ZOOM = 18 # 最大ズームレベル
MAP_TILE_ZOOM = 8 # 地図プレビュー用ズームレベル
PREVIEW_SIZE = 256 # プレビュー表示サイズ(ピクセル)
TILE_RANGE = 4 # 地図プレビューでの表示タイル範囲(4x4)
# デフォルト値(東京)
default_lat = DEFAULT_LAT
default_lon = DEFAULT_LON
default_zoom = DEFAULT_ZOOM
# データセット情報
datasets = {
'dem5a_png': '航空レーザ測量(最高精度: 標準偏差0.3m以内)',
'dem5b_png': '写真測量(高精度: 標準偏差0.7m以内)',
'dem5c_png': '既存資料活用(中精度: 標準偏差2.5m以内)',
'dem_png': '10mメッシュ(標準精度: 標準偏差5m以内)'
}
# グローバル変数
current_tile_data = None
current_tile_coords = None
current_elevation = None
current_dataset = None
japan_map = None
map_info = {}
def lat_lon_to_tile(lat, lon, zoom):
"""Webメルカトル投影による緯度経度→タイル座標変換(境界処理修正版)"""
lat_rad = math.radians(lat)
n = 2.0 ** zoom
x = math.floor((lon + 180.0) / 360.0 * n)
y = math.floor((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
# タイル座標の境界チェック
x = max(0, min(int(n - 1), x))
y = max(0, min(int(n - 1), y))
return x, y
def tile_to_lat_lon(x, y, zoom):
"""タイル座標→緯度経度変換"""
n = 2.0 ** zoom
lon = (x + 0.5) / n * 360.0 - 180.0
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * (y + 0.5) / n)))
lat = math.degrees(lat_rad)
return lat, lon
def decode_elevation_png(image_array):
"""地理院標高タイル公式仕様に基づくPNGデコード(精度最適化版)"""
# RGB値を取得
R = image_array[:, :, 0].astype(np.uint32)
G = image_array[:, :, 1].astype(np.uint32)
B = image_array[:, :, 2].astype(np.uint32)
# 無効値マスク: (R,G,B)=(128,0,0)
invalid_mask = (R == 128) & (G == 0) & (B == 0)
# 標高計算: x = R*65536 + G*256 + B
x = R * 65536 + G * 256 + B
u = 0.01 # 標高分解能(0.01m)
# h = x*u (x<2^23), (x-2^24)*u (x>=2^23)
elevation = np.where(x < 8388608, x.astype(np.float64) * u, (x.astype(np.float64) - 16777216) * u)
elevation[invalid_mask] = np.nan
return elevation
def download_map_tiles():
"""日本地図タイルのダウンロード(エラーハンドリング改善版)"""
global japan_map, map_info
try:
zoom = MAP_TILE_ZOOM
tiles = []
failed_tiles = []
# 日本全体をカバーするタイル範囲
japan_tiles = []
for x in range(220, 232):
for y in range(98, 110):
japan_tiles.append((x, y))
total_tiles = len(japan_tiles)
for i, (x, y) in enumerate(japan_tiles):
url = f'https://cyberjapandata.gsi.go.jp/xyz/std/{zoom}/{x}/{y}.png'
try:
response = httpx.get(url, timeout=10.0)
if response.status_code == 200:
img = Image.open(BytesIO(response.content))
tiles.append((x, y, img))
else:
# ダウンロード失敗時は灰色のタイルを作成
img = Image.new('RGB', (TILE_SIZE, TILE_SIZE), (100, 100, 100))
tiles.append((x, y, img))
failed_tiles.append((x, y))
except Exception:
# エラー時も灰色のタイルを作成して継続
img = Image.new('RGB', (TILE_SIZE, TILE_SIZE), (100, 100, 100))
tiles.append((x, y, img))
failed_tiles.append((x, y))
status_var.set(f'地図データをダウンロード中... ({i+1}/{total_tiles})')
root.update_idletasks()
if tiles:
create_japan_map(tiles, zoom)
if failed_tiles:
status_var.set(f'準備完了(一部タイル取得失敗: {len(failed_tiles)}件)')
else:
status_var.set('準備完了')
update_map_preview()
except Exception as e:
status_var.set('地図ダウンロード失敗')
# 失敗時でも灰色の地図を作成
create_fallback_map()
def create_fallback_map():
"""フォールバック用の地図作成"""
global japan_map, map_info
width = 12 * TILE_SIZE
height = 12 * TILE_SIZE
japan_map = Image.new('RGB', (width, height), (150, 150, 150))
map_info = {
'zoom': MAP_TILE_ZOOM,
'min_x': 220,
'max_x': 231,
'min_y': 98,
'max_y': 109,
'width': width,
'height': height
}
def create_japan_map(tiles, zoom):
"""日本地図の作成"""
global japan_map, map_info
x_coords = [t[0] for t in tiles]
y_coords = [t[1] for t in tiles]
min_x, max_x = min(x_coords), max(x_coords)
min_y, max_y = min(y_coords), max(y_coords)
width = (max_x - min_x + 1) * TILE_SIZE
height = (max_y - min_y + 1) * TILE_SIZE
# 背景を灰色にして未取得部分を明確化
japan_map = Image.new('RGB', (width, height), (150, 150, 150))
for x, y, img in tiles:
pos_x = (x - min_x) * TILE_SIZE
pos_y = (y - min_y) * TILE_SIZE
japan_map.paste(img, (pos_x, pos_y))
map_info = {
'zoom': zoom,
'min_x': min_x,
'max_x': max_x,
'min_y': min_y,
'max_y': max_y,
'width': width,
'height': height
}
def move_tile(direction):
"""タイル移動処理"""
try:
lat = float(lat_var.get())
lon = float(lon_var.get())
zoom = zoom_var.get()
x, y = lat_lon_to_tile(lat, lon, zoom)
if direction == 'north':
y -= 1
elif direction == 'south':
y += 1
elif direction == 'east':
x += 1
elif direction == 'west':
x -= 1
# 境界チェック
n = 2 ** zoom
if x < 0 or x >= n or y < 0 or y >= n:
return
new_lat, new_lon = tile_to_lat_lon(x, y, zoom)
lat_var.set(f'{new_lat:.6f}')
lon_var.set(f'{new_lon:.6f}')
except ValueError:
pass
def update_map_preview():
"""地図プレビューの更新"""
if not japan_map:
return
try:
lat = float(lat_var.get())
lon = float(lon_var.get())
zoom = zoom_var.get()
current_x, current_y = lat_lon_to_tile(lat, lon, zoom)
# スケール計算
scale = 2 ** (zoom - MAP_TILE_ZOOM)
map_center_x = current_x / scale
map_center_y = current_y / scale
pixel_x = (map_center_x - map_info['min_x']) * TILE_SIZE
pixel_y = (map_center_y - map_info['min_y']) * TILE_SIZE
if (pixel_x < 0 or pixel_x > map_info['width'] or
pixel_y < 0 or pixel_y > map_info['height']):
map_canvas.delete('all')
map_canvas.create_text(PREVIEW_SIZE // 2, PREVIEW_SIZE // 2, text='範囲外', fill='red')
return
view_size = TILE_SIZE * TILE_RANGE / scale
half_size = view_size / 2
left = max(0, int(pixel_x - half_size))
top = max(0, int(pixel_y - half_size))
right = min(map_info['width'], int(pixel_x + half_size))
bottom = min(map_info['height'], int(pixel_y + half_size))
if right > left and bottom > top:
cropped = japan_map.crop((left, top, right, bottom))
preview = cropped.resize((PREVIEW_SIZE, PREVIEW_SIZE), Image.Resampling.LANCZOS)
draw_img = preview.copy()
draw = ImageDraw.Draw(draw_img)
tile_size_on_preview = PREVIEW_SIZE / TILE_RANGE
center_x = PREVIEW_SIZE // 2
center_y = PREVIEW_SIZE // 2
half_tile = tile_size_on_preview / 2
draw.rectangle([
center_x - half_tile,
center_y - half_tile,
center_x + half_tile,
center_y + half_tile
], outline='red', width=2)
draw.line([center_x - 10, center_y, center_x + 10, center_y], fill='blue', width=2)
draw.line([center_x, center_y - 10, center_x, center_y + 10], fill='blue', width=2)
map_photo = ImageTk.PhotoImage(draw_img)
map_canvas.delete('all')
map_canvas.create_image(center_x, center_y, image=map_photo)
map_canvas.image = map_photo
map_canvas.create_text(center_x, 240,
text=f'Tile: ({current_x}, {current_y}) Z{zoom}',
fill='black', anchor='center')
except ValueError:
pass
except Exception:
pass
def update_zoom_label(value):
"""ズームレベルラベルの更新"""
zoom = int(float(value))
zoom_label.config(text=f'{zoom} (推奨: 15)')
def download_tile():
"""標高タイルのダウンロード(エラーハンドリング改善版)"""
global current_tile_data, current_tile_coords, current_elevation, current_dataset
try:
lat = float(lat_var.get())
lon = float(lon_var.get())
zoom = zoom_var.get()
dataset = dataset_var.get()
x, y = lat_lon_to_tile(lat, lon, zoom)
current_tile_coords = (x, y, zoom)
url = f'https://cyberjapandata.gsi.go.jp/xyz/{dataset}/{zoom}/{x}/{y}.png'
status_var.set(f'ダウンロード中: {dataset} タイル({x}, {y})...')
root.update()
try:
response = httpx.get(url, timeout=30.0)
except httpx.TimeoutException:
error_msg = f'タイムアウト: サーバーからの応答がありません\nタイル座標: ({x}, {y}), ズーム: {zoom}'
status_var.set('エラー: タイムアウト')
info_label.config(text=error_msg)
return
except Exception as e:
error_msg = f'通信エラー: {str(e)}\nタイル座標: ({x}, {y}), ズーム: {zoom}'
status_var.set('エラー: 通信失敗')
info_label.config(text=error_msg)
return
if response.status_code == 200:
current_tile_data = response.content
current_dataset = dataset
try:
img = Image.open(BytesIO(current_tile_data))
img_array = np.array(img)
current_elevation = decode_elevation_png(img_array)
display_image(img)
update_info(lat, lon, zoom, x, y, dataset)
save_button.config(state=tk.NORMAL)
status_var.set('ダウンロード完了')
except Exception as e:
error_msg = f'画像処理エラー: {str(e)}\nデータが破損している可能性があります'
status_var.set('エラー: 画像処理失敗')
info_label.config(text=error_msg)
elif response.status_code == 404:
error_msg = f'データなし: 指定した位置に{dataset}のデータが存在しません\nタイル座標: ({x}, {y}), ズーム: {zoom}\n別のデータセットまたはズームレベルをお試しください'
status_var.set('エラー: データなし')
info_label.config(text=error_msg)
else:
error_msg = f'ダウンロード失敗: HTTP {response.status_code}\nタイル座標: ({x}, {y}), ズーム: {zoom}'
status_var.set(f'エラー: HTTP {response.status_code}')
info_label.config(text=error_msg)
except ValueError:
error_msg = 'エラー: 緯度・経度は数値で入力してください'
status_var.set('エラー: 入力値が不正です')
info_label.config(text=error_msg)
except Exception as e:
error_msg = f'予期しないエラー: {str(e)}'
status_var.set('エラー')
info_label.config(text=error_msg)
def display_image(pil_image):
"""標高データの可視化表示"""
img_array = np.array(pil_image)
elevation = decode_elevation_png(img_array)
valid_elevation = elevation[~np.isnan(elevation)]
if len(valid_elevation) > 0:
min_elev = np.min(valid_elevation)
max_elev = np.max(valid_elevation)
if max_elev > min_elev:
normalized = (elevation - min_elev) / (max_elev - min_elev) * 255
else:
normalized = np.full_like(elevation, 127)
normalized[np.isnan(normalized)] = 0
normalized = normalized.astype(np.uint8)
vis_image = Image.fromarray(normalized, mode='L')
photo = ImageTk.PhotoImage(vis_image)
else:
photo = ImageTk.PhotoImage(pil_image)
canvas.delete('all')
canvas.create_image(TILE_SIZE//2, TILE_SIZE//2, image=photo)
canvas.image = photo
def update_info(lat, lon, zoom, x, y, dataset):
"""情報表示の更新"""
if current_elevation is not None:
valid_elevation = current_elevation[~np.isnan(current_elevation)]
if len(valid_elevation) > 0:
min_elev = np.nanmin(current_elevation)
max_elev = np.nanmax(current_elevation)
info_text = f"""座標: {lat:.4f}, {lon:.4f}
ズームレベル: {zoom}
タイル座標: ({x}, {y})
データセット: {dataset}
標高範囲:
最小: {min_elev:.1f}m
最大: {max_elev:.1f}m
データソース: 国土地理院
地理院タイル(標高タイル)"""
else:
info_text = f"""座標: {lat:.4f}, {lon:.4f}
ズームレベル: {zoom}
タイル座標: ({x}, {y})
データセット: {dataset}
標高範囲: データなし
データソース: 国土地理院
地理院タイル(標高タイル)"""
else:
info_text = 'データ取得エラー'
info_label.config(text=info_text)
def save_tile():
"""タイルデータの保存(エラーハンドリング改善版)"""
if current_tile_data and current_tile_coords:
x, y, zoom = current_tile_coords
dataset = current_dataset.replace('_png', '')
filename = f'tile_{dataset}_{zoom}_{x}_{y}.png'
try:
with open(filename, 'wb') as f:
f.write(current_tile_data)
save_metadata(filename, x, y, zoom)
status_var.set(f'保存完了: {filename}')
messagebox.showinfo('保存完了', f'ファイルを保存しました:\n{filename}')
except PermissionError:
status_var.set('保存エラー: アクセス権限がありません')
messagebox.showerror('保存エラー', 'ファイルの保存に失敗しました。\n書き込み権限を確認してください。')
except Exception as e:
status_var.set('保存エラー')
messagebox.showerror('保存エラー', f'ファイルの保存に失敗しました。\nエラー: {str(e)}')
def save_metadata(image_filename, x, y, zoom):
"""メタデータファイルの保存"""
metadata_filename = image_filename.replace('.png', '_metadata.txt')
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
lat = float(lat_var.get())
lon = float(lon_var.get())
dataset = current_dataset
try:
with open(metadata_filename, 'w', encoding='utf-8') as f:
f.write('=== 地理院タイル(標高)メタデータ ===\n')
f.write(f'保存日時: {timestamp}\n')
f.write(f'データソース: 国土地理院\n')
f.write(f'データセット: {dataset}\n')
f.write(f'元座標: {lat}, {lon}\n')
f.write(f'タイル座標: ({x}, {y})\n')
f.write(f'ズームレベル: {zoom}\n')
if current_elevation is not None:
valid_elevation = current_elevation[~np.isnan(current_elevation)]
if len(valid_elevation) > 0:
f.write(f'標高範囲: {np.min(valid_elevation):.1f}m - {np.max(valid_elevation):.1f}m\n')
f.write(f'\n【利用時の出典明示】\n')
f.write(f'「国土地理院」または「地理院タイル」\n')
f.write(f'\n利用規約: https://maps.gsi.go.jp/help/use.html\n')
except Exception:
# メタデータの保存に失敗しても画像は保存されているため、エラーは無視
pass
def show_license_info():
"""利用条件の表示"""
license_text = """地理院タイル(標高)利用条件
• 出典明示: 必須(「国土地理院」または「地理院タイル」)
• 商用利用: 可能
• 改変: 可能
• 再配布: 可能(出典明示必須)
詳細: https://maps.gsi.go.jp/help/use.html
この条件に同意してご利用ください。"""
messagebox.showinfo('利用条件', license_text)
# プログラム開始時のガイダンス表示
print("=== 地理院標高タイルダウンローダー ===")
print("概要: 国土地理院の標高タイルをダウンロードし、標高データを可視化するプログラムです")
print("\n操作方法:")
print("1. 緯度・経度を入力(デフォルト: 東京)")
print("2. ズームレベルを選択(推奨: 15)")
print("3. データセットを選択(高精度順: dem5a > dem5b > dem5c > dem)")
print("4. 'ダウンロード開始'ボタンをクリック")
print("5. 結果を確認後、'保存'ボタンで画像とメタデータを保存")
print("\n注意事項:")
print("• 十字ボタンで隣接タイルに移動可能")
print("• 地図プレビューで現在位置を確認可能")
print("• 保存時は出典明示が必要です(国土地理院)")
print("• 利用規約: https://maps.gsi.go.jp/help/use.html")
print("=====================================\n")
# GUI作成
root = tk.Tk()
root.title('地理院標高タイルダウンローダー')
root.geometry('800x800')
main_frame = ttk.Frame(root, padding='5')
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 入力部分
input_frame = ttk.LabelFrame(main_frame, text='座標設定', padding='5')
input_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=2)
# 緯度
ttk.Label(input_frame, text='緯度:').grid(row=0, column=0, sticky=tk.W, padx=5)
lat_var = tk.StringVar(value=str(default_lat))
lat_var.trace('w', lambda *args: update_map_preview())
lat_entry = ttk.Entry(input_frame, textvariable=lat_var, width=15)
lat_entry.grid(row=0, column=1, padx=5)
# 経度
ttk.Label(input_frame, text='経度:').grid(row=0, column=2, sticky=tk.W, padx=5)
lon_var = tk.StringVar(value=str(default_lon))
lon_var.trace_add('write', lambda *args: update_map_preview())
lon_entry = ttk.Entry(input_frame, textvariable=lon_var, width=15)
lon_entry.grid(row=0, column=3, padx=5)
# 移動ボタン(十字配置)
move_frame = ttk.Frame(input_frame)
move_frame.grid(row=0, column=4, rowspan=2, padx=20)
ttk.Button(move_frame, text='↑', width=3, command=lambda: move_tile('north')).grid(row=0, column=1)
ttk.Button(move_frame, text='←', width=3, command=lambda: move_tile('west')).grid(row=1, column=0)
ttk.Button(move_frame, text='→', width=3, command=lambda: move_tile('east')).grid(row=1, column=2)
ttk.Button(move_frame, text='↓', width=3, command=lambda: move_tile('south')).grid(row=2, column=1)
# ズームレベル(スライダー)
zoom_frame = ttk.Frame(input_frame)
zoom_frame.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=5)
ttk.Label(zoom_frame, text='ズームレベル:').grid(row=0, column=0, sticky=tk.W)
zoom_var = tk.IntVar(value=default_zoom)
zoom_var.trace('w', lambda *args: update_map_preview())
zoom_slider = ttk.Scale(
zoom_frame,
from_=MIN_ZOOM,
to=MAX_ZOOM,
orient=tk.HORIZONTAL,
variable=zoom_var,
command=update_zoom_label
)
zoom_slider.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=10)
zoom_frame.columnconfigure(1, weight=1)
zoom_label = ttk.Label(zoom_frame, text=f'{default_zoom} (推奨: 15)')
zoom_label.grid(row=0, column=2)
# データセット選択
dataset_frame = ttk.LabelFrame(main_frame, text='データセット選択', padding='5')
dataset_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=2)
ttk.Label(dataset_frame, text='※より高精度なものを優先(dem5a > dem5b > dem5c > dem)を推奨',
foreground='blue').grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=(0, 5))
dataset_var = tk.StringVar(value='dem5a_png')
for i, (dataset, description) in enumerate(datasets.items()):
ttk.Radiobutton(
dataset_frame,
text=f'{dataset}: {description}',
variable=dataset_var,
value=dataset
).grid(row=i+1, column=0, sticky=tk.W, pady=2)
# ダウンロードボタン
download_button = ttk.Button(
main_frame,
text='ダウンロード開始',
command=download_tile
)
download_button.grid(row=2, column=0, columnspan=2, pady=5)
# 表示部分(標高タイルと地図を横並び)
display_frame = ttk.Frame(main_frame)
display_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=2)
# 標高タイル表示
elevation_frame = ttk.LabelFrame(display_frame, text='ダウンロードしたデータ', padding='5')
elevation_frame.grid(row=0, column=0, padx=2)
canvas = tk.Canvas(elevation_frame, width=TILE_SIZE, height=TILE_SIZE, bg='gray')
canvas.grid(row=0, column=0, padx=5, pady=5)
# 地図プレビュー
map_frame = ttk.LabelFrame(display_frame, text='地図プレビュー', padding='5')
map_frame.grid(row=0, column=1, padx=2)
map_canvas = tk.Canvas(map_frame, width=PREVIEW_SIZE, height=PREVIEW_SIZE, bg='lightgray')
map_canvas.grid(row=0, column=0, padx=5, pady=5)
map_canvas.create_text(PREVIEW_SIZE//2, PREVIEW_SIZE//2, text='地図データ読み込み中...', fill='gray', tags='loading')
# 情報表示
info_frame = ttk.Frame(display_frame)
info_frame.grid(row=0, column=2, sticky=(tk.W, tk.E, tk.N), padx=5)
info_label = ttk.Label(info_frame, text='データ未取得', justify=tk.LEFT, font=('', 8))
info_label.grid(row=0, column=0, sticky=(tk.W, tk.N))
# 保存ボタン
save_button = ttk.Button(
main_frame,
text='保存',
command=save_tile,
state=tk.DISABLED
)
save_button.grid(row=4, column=0, columnspan=2, pady=5)
# ステータスバー
status_var = tk.StringVar(value='準備完了')
status_bar = ttk.Label(main_frame, textvariable=status_var, relief=tk.SUNKEN)
status_bar.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=2)
# 初期処理
show_license_info()
# 地図タイルをバックグラウンドでダウンロード
status_var.set('地図データをダウンロード中...')
threading.Thread(target=download_map_tiles, daemon=True).start()
# アプリケーション実行
root.mainloop()