Spaces:
Sleeping
Sleeping
Update app.py
#3
by
naohiro701
- opened
app.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
| 1 |
# app.py
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
import io
|
| 4 |
import time
|
|
|
|
|
|
|
| 5 |
import requests
|
| 6 |
import pandas as pd
|
| 7 |
import numpy as np
|
| 8 |
import geopandas as gpd
|
| 9 |
-
import matplotlib.pyplot as plt
|
| 10 |
from shapely.geometry import Point
|
| 11 |
-
import folium
|
| 12 |
import gradio as gr
|
| 13 |
-
from
|
| 14 |
|
| 15 |
# ----------------------------
|
| 16 |
# 設定
|
|
@@ -20,13 +22,15 @@ GSI_USER_AGENT = os.environ.get(
|
|
| 20 |
"jp-gsi-geocoding-demo (contact: your_email@example.com)" # 連絡先付き推奨
|
| 21 |
)
|
| 22 |
GSI_TIMEOUT_SEC = float(os.environ.get("GSI_TIMEOUT_SEC", "10"))
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
GSI_GEOCODE_URL = "https://msearch.gsi.go.jp/address-search/AddressSearch"
|
| 25 |
|
| 26 |
CACHE_DIR = "data/cache"
|
| 27 |
os.makedirs(CACHE_DIR, exist_ok=True)
|
| 28 |
CACHE_PATH = os.path.join(CACHE_DIR, "geocode_cache.csv")
|
| 29 |
-
DEFAULT_ZIP = "data/japan_ver85.zip"
|
| 30 |
|
| 31 |
# ----------------------------
|
| 32 |
# キャッシュ
|
|
@@ -47,18 +51,6 @@ def save_cache(df_cache):
|
|
| 47 |
except Exception:
|
| 48 |
pass
|
| 49 |
|
| 50 |
-
# ----------------------------
|
| 51 |
-
# Shapefile 読み込み
|
| 52 |
-
# ----------------------------
|
| 53 |
-
def load_gdf_from_zip(zip_path: str) -> gpd.GeoDataFrame:
|
| 54 |
-
gdf = gpd.read_file(f"zip://{zip_path}") # , engine="pyogrio"
|
| 55 |
-
try:
|
| 56 |
-
if gdf.crs:
|
| 57 |
-
gdf = gdf.to_crs("EPSG:4326")
|
| 58 |
-
except Exception:
|
| 59 |
-
pass
|
| 60 |
-
return gdf
|
| 61 |
-
|
| 62 |
# ----------------------------
|
| 63 |
# 国土地理院 ジオコーダ
|
| 64 |
# ----------------------------
|
|
@@ -70,26 +62,21 @@ def make_gsi_session() -> requests.Session:
|
|
| 70 |
def gsi_geocode_once(address: str, session: requests.Session) -> tuple[float, float]:
|
| 71 |
"""
|
| 72 |
国土地理院 住所検索APIを1回呼び出し、(lat, lon) を返す。失敗時は (nan, nan)。
|
| 73 |
-
|
| 74 |
"""
|
| 75 |
try:
|
| 76 |
-
|
| 77 |
-
if not address or address.strip() == "" or address.strip().lower() in ("nan", "none"):
|
| 78 |
return (np.nan, np.nan)
|
| 79 |
|
| 80 |
resp = session.get(GSI_GEOCODE_URL, params={"q": address}, timeout=GSI_TIMEOUT_SEC)
|
| 81 |
if not resp.ok:
|
| 82 |
return (np.nan, np.nan)
|
| 83 |
data = resp.json()
|
| 84 |
-
# 返り値は配列(候補リスト)。最上位候補を採用
|
| 85 |
if isinstance(data, list) and len(data) > 0:
|
| 86 |
feat = data[0]
|
| 87 |
coords = (feat.get("geometry") or {}).get("coordinates") or []
|
| 88 |
if isinstance(coords, (list, tuple)) and len(coords) >= 2:
|
| 89 |
-
lon, lat = coords[0], coords[1]
|
| 90 |
-
# 数値化チェック
|
| 91 |
-
lat = float(lat)
|
| 92 |
-
lon = float(lon)
|
| 93 |
return (lat, lon)
|
| 94 |
except Exception:
|
| 95 |
pass
|
|
@@ -117,8 +104,10 @@ def geocode_with_cache(addresses, CFs, use_internet=True):
|
|
| 117 |
continue
|
| 118 |
|
| 119 |
lat, lon = gsi_geocode_once(a, session)
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
| 122 |
|
| 123 |
# キャッシュ更新
|
| 124 |
cache = cache[cache["address_input"] != a]
|
|
@@ -132,86 +121,100 @@ def geocode_with_cache(addresses, CFs, use_internet=True):
|
|
| 132 |
return pd.DataFrame(results)
|
| 133 |
|
| 134 |
# ----------------------------
|
| 135 |
-
#
|
| 136 |
# ----------------------------
|
| 137 |
-
def
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
try:
|
| 194 |
-
|
| 195 |
except Exception:
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
folium.CircleMarker(
|
| 204 |
-
location=(float(lat), float(lon)),
|
| 205 |
-
radius=circle_radius,
|
| 206 |
-
fill=True,
|
| 207 |
-
fill_opacity=0.9,
|
| 208 |
-
popup=popup,
|
| 209 |
-
).add_to(m)
|
| 210 |
-
|
| 211 |
-
return m._repr_html_()
|
| 212 |
|
| 213 |
# ----------------------------
|
| 214 |
-
#
|
| 215 |
# ----------------------------
|
| 216 |
def _parse_indexer(x):
|
| 217 |
try:
|
|
@@ -219,85 +222,53 @@ def _parse_indexer(x):
|
|
| 219 |
except Exception:
|
| 220 |
return x
|
| 221 |
|
| 222 |
-
def run(
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
elif os.path.exists(DEFAULT_ZIP):
|
| 228 |
-
zip_path = DEFAULT_ZIP
|
| 229 |
-
else:
|
| 230 |
-
empty_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
|
| 231 |
-
return None, None, "", empty_df, "Shapefile の ZIP をアップロードするか、data/japan_ver85.zip を配置してください。"
|
| 232 |
|
| 233 |
try:
|
| 234 |
-
|
| 235 |
except Exception as e:
|
| 236 |
empty_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
|
| 237 |
-
return
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
gdf_pts = gpd.GeoDataFrame(columns=["address_input", "CF", "lat", "lon"], geometry=[], crs="EPSG:4326")
|
| 242 |
-
table_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
|
| 243 |
-
else:
|
| 244 |
-
try:
|
| 245 |
-
df = pd.read_excel(excel_file.name, sheet_name=sheet_name, header=int(header_row))
|
| 246 |
-
except Exception as e:
|
| 247 |
-
empty_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
|
| 248 |
-
return None, None, "", empty_df, f"Excel の読み込みに失敗しました: {e}"
|
| 249 |
-
|
| 250 |
-
addr_series = df.iloc[:, address_col] if isinstance(address_col, int) else df[address_col]
|
| 251 |
-
cf_series = df.iloc[:, power_col] if isinstance(power_col, int) else df[power_col]
|
| 252 |
-
|
| 253 |
-
addresses = addr_series.astype(str).tolist()
|
| 254 |
-
cfs = cf_series.tolist()
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
for lat, lon in zip(geo_df["lat"], geo_df["lon"])
|
| 262 |
-
]
|
| 263 |
-
gdf_pts = gpd.GeoDataFrame(geo_df, geometry=geometry, crs="EPSG:4326")
|
| 264 |
|
| 265 |
-
#
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
legend_shrink=float(legend_shrink),
|
| 272 |
-
legend_fontsize=int(legend_fontsize),
|
| 273 |
-
)
|
| 274 |
-
except Exception as e:
|
| 275 |
-
return None, None, "", table_df, f"静的描画に失敗しました: {e}"
|
| 276 |
|
|
|
|
| 277 |
try:
|
| 278 |
-
html =
|
| 279 |
except Exception as e:
|
| 280 |
-
html = f"<p>
|
| 281 |
|
| 282 |
-
#
|
| 283 |
info = []
|
| 284 |
-
info.append(f"都道府県レコード数: {len(gdf_pref)}")
|
| 285 |
-
if gdf_pref.crs:
|
| 286 |
-
info.append(f"PREF CRS: {gdf_pref.crs}")
|
| 287 |
info.append(f"ポイント数(有効座標): {int(gdf_pts.geometry.notnull().sum())} / {len(gdf_pts)}")
|
| 288 |
-
if not gdf_pts.empty and gdf_pts.crs:
|
| 289 |
-
info.append(f"PTS CRS: {gdf_pts.crs}")
|
| 290 |
|
| 291 |
-
return
|
| 292 |
|
| 293 |
# ----------------------------
|
| 294 |
-
# Gradio UI
|
| 295 |
# ----------------------------
|
| 296 |
-
with gr.Blocks(title="
|
| 297 |
-
gr.Markdown("##
|
| 298 |
|
| 299 |
with gr.Row():
|
| 300 |
-
zip_in = gr.File(label="Shapefile (ZIP)", file_count="single", file_types=[".zip"])
|
| 301 |
xlsx_in = gr.File(label="Excelファイル(住所付き)", file_count="single", file_types=[".xlsx", ".xls"])
|
| 302 |
|
| 303 |
with gr.Row():
|
|
@@ -310,21 +281,12 @@ with gr.Blocks(title="Japan Shapefile + Excel Geocoding Plotter (GSI)") as demo:
|
|
| 310 |
|
| 311 |
with gr.Row():
|
| 312 |
use_inet = gr.Checkbox(label="国土地理院APIに問い合わせ(オフでキャッシュのみ使用)", value=True)
|
| 313 |
-
line_width = gr.Slider(0.2, 2.0, value=0.6, step=0.1, label="境界線の太さ")
|
| 314 |
-
|
| 315 |
-
# 見た目調整スライダ
|
| 316 |
-
with gr.Row():
|
| 317 |
-
marker_size = gr.Slider(4, 64, value=24, step=2, label="ポイントサイズ(matplotlib / folium)")
|
| 318 |
-
legend_shrink = gr.Slider(0.3, 1.0, value=0.6, step=0.05, label="凡例の縮小率(小さいほど小さく)")
|
| 319 |
-
legend_fontsize = gr.Slider(6, 16, value=8, step=1, label="凡例の目盛フォントサイズ")
|
| 320 |
|
| 321 |
run_btn = gr.Button("描画")
|
| 322 |
|
| 323 |
-
|
| 324 |
-
out_html = gr.HTML(label="インタラクティブ地図(folium)")
|
| 325 |
-
out_info = gr.Textbox(label="メタ情報", lines=4)
|
| 326 |
out_table = gr.Dataframe(label="ジオコーディング結果(住所・緯度・経度・CF)", wrap=True)
|
| 327 |
-
|
| 328 |
|
| 329 |
def _parse(x):
|
| 330 |
try:
|
|
@@ -332,16 +294,15 @@ with gr.Blocks(title="Japan Shapefile + Excel Geocoding Plotter (GSI)") as demo:
|
|
| 332 |
except Exception:
|
| 333 |
return x
|
| 334 |
|
| 335 |
-
def app_run(
|
| 336 |
return run(
|
| 337 |
-
|
| 338 |
-
inet, lw, ms, lsh, lfs
|
| 339 |
)
|
| 340 |
|
| 341 |
run_btn.click(
|
| 342 |
fn=app_run,
|
| 343 |
-
inputs=[
|
| 344 |
-
outputs=[
|
| 345 |
)
|
| 346 |
|
| 347 |
if __name__ == "__main__":
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
# pip install keplergl pandas numpy geopandas shapely gradio requests openpyxl
|
| 3 |
+
|
| 4 |
import os
|
| 5 |
import io
|
| 6 |
import time
|
| 7 |
+
import json
|
| 8 |
+
import tempfile
|
| 9 |
import requests
|
| 10 |
import pandas as pd
|
| 11 |
import numpy as np
|
| 12 |
import geopandas as gpd
|
|
|
|
| 13 |
from shapely.geometry import Point
|
|
|
|
| 14 |
import gradio as gr
|
| 15 |
+
from keplergl import KeplerGl
|
| 16 |
|
| 17 |
# ----------------------------
|
| 18 |
# 設定
|
|
|
|
| 22 |
"jp-gsi-geocoding-demo (contact: your_email@example.com)" # 連絡先付き推奨
|
| 23 |
)
|
| 24 |
GSI_TIMEOUT_SEC = float(os.environ.get("GSI_TIMEOUT_SEC", "10"))
|
| 25 |
+
|
| 26 |
+
# ★ sleep最小(0秒)
|
| 27 |
+
GEOCODE_DELAY_SEC = float(os.environ.get("GSI_RATE_LIMIT_SEC", "0.0"))
|
| 28 |
+
|
| 29 |
GSI_GEOCODE_URL = "https://msearch.gsi.go.jp/address-search/AddressSearch"
|
| 30 |
|
| 31 |
CACHE_DIR = "data/cache"
|
| 32 |
os.makedirs(CACHE_DIR, exist_ok=True)
|
| 33 |
CACHE_PATH = os.path.join(CACHE_DIR, "geocode_cache.csv")
|
|
|
|
| 34 |
|
| 35 |
# ----------------------------
|
| 36 |
# キャッシュ
|
|
|
|
| 51 |
except Exception:
|
| 52 |
pass
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
# ----------------------------
|
| 55 |
# 国土地理院 ジオコーダ
|
| 56 |
# ----------------------------
|
|
|
|
| 62 |
def gsi_geocode_once(address: str, session: requests.Session) -> tuple[float, float]:
|
| 63 |
"""
|
| 64 |
国土地理院 住所検索APIを1回呼び出し、(lat, lon) を返す。失敗時は (nan, nan)。
|
| 65 |
+
APIは [lon, lat] を返すので順を入れ替える。
|
| 66 |
"""
|
| 67 |
try:
|
| 68 |
+
if not address or str(address).strip() == "" or str(address).strip().lower() in ("nan", "none"):
|
|
|
|
| 69 |
return (np.nan, np.nan)
|
| 70 |
|
| 71 |
resp = session.get(GSI_GEOCODE_URL, params={"q": address}, timeout=GSI_TIMEOUT_SEC)
|
| 72 |
if not resp.ok:
|
| 73 |
return (np.nan, np.nan)
|
| 74 |
data = resp.json()
|
|
|
|
| 75 |
if isinstance(data, list) and len(data) > 0:
|
| 76 |
feat = data[0]
|
| 77 |
coords = (feat.get("geometry") or {}).get("coordinates") or []
|
| 78 |
if isinstance(coords, (list, tuple)) and len(coords) >= 2:
|
| 79 |
+
lon, lat = float(coords[0]), float(coords[1])
|
|
|
|
|
|
|
|
|
|
| 80 |
return (lat, lon)
|
| 81 |
except Exception:
|
| 82 |
pass
|
|
|
|
| 104 |
continue
|
| 105 |
|
| 106 |
lat, lon = gsi_geocode_once(a, session)
|
| 107 |
+
|
| 108 |
+
# ★ 最小スリープ(デフォルト0.0秒)
|
| 109 |
+
if GEOCODE_DELAY_SEC > 0:
|
| 110 |
+
time.sleep(GEOCODE_DELAY_SEC)
|
| 111 |
|
| 112 |
# キャッシュ更新
|
| 113 |
cache = cache[cache["address_input"] != a]
|
|
|
|
| 121 |
return pd.DataFrame(results)
|
| 122 |
|
| 123 |
# ----------------------------
|
| 124 |
+
# Kepler.gl HTML 生成(ポイントのみ)
|
| 125 |
# ----------------------------
|
| 126 |
+
def make_kepler_html(df_points: pd.DataFrame, height: int = 640) -> str:
|
| 127 |
+
"""
|
| 128 |
+
df_points は 'lat','lon','address_input','CF' を含む DataFrame を想定。
|
| 129 |
+
ポイントレイヤのみを Kepler.gl で描画し、HTMLを文字列で返す。
|
| 130 |
+
"""
|
| 131 |
+
df_valid = df_points.dropna(subset=["lat", "lon"]).copy()
|
| 132 |
+
if df_valid.empty:
|
| 133 |
+
# 空のKeplerでもHTMLは返す
|
| 134 |
+
m = KeplerGl(height=height)
|
| 135 |
+
return m._repr_html_()
|
| 136 |
+
|
| 137 |
+
# ほどよい初期中心
|
| 138 |
+
center_lat = float(df_valid["lat"].median())
|
| 139 |
+
center_lon = float(df_valid["lon"].median())
|
| 140 |
+
|
| 141 |
+
# Kepler 設定(ポイントレイヤのみ)
|
| 142 |
+
config = {
|
| 143 |
+
"version": "v1",
|
| 144 |
+
"config": {
|
| 145 |
+
"visState": {
|
| 146 |
+
"filters": [],
|
| 147 |
+
"layers": [
|
| 148 |
+
{
|
| 149 |
+
"id": "point_layer",
|
| 150 |
+
"type": "point",
|
| 151 |
+
"config": {
|
| 152 |
+
"dataId": "points",
|
| 153 |
+
"label": "Points",
|
| 154 |
+
"color": [18, 147, 154],
|
| 155 |
+
"columns": {"lat": "lat", "lng": "lon"},
|
| 156 |
+
"isVisible": True,
|
| 157 |
+
"visConfig": {
|
| 158 |
+
"radius": 10, # 基本半径
|
| 159 |
+
"opacity": 0.9,
|
| 160 |
+
"outline": False
|
| 161 |
+
}
|
| 162 |
+
},
|
| 163 |
+
"visualChannels": {
|
| 164 |
+
# CF列が数値ならサイズに反映(なければ自動で固定半径)
|
| 165 |
+
"sizeField": {"name": "CF", "type": "real"} if pd.to_numeric(df_valid.get("CF", pd.Series([])), errors="coerce").notna().any() else None,
|
| 166 |
+
"sizeScale": "sqrt",
|
| 167 |
+
},
|
| 168 |
+
}
|
| 169 |
+
],
|
| 170 |
+
"interactionConfig": {
|
| 171 |
+
"tooltip": {
|
| 172 |
+
"enabled": True,
|
| 173 |
+
"fieldsToShow": {
|
| 174 |
+
"points": [ {"name": "address_input", "format": None},
|
| 175 |
+
{"name": "CF", "format": None},
|
| 176 |
+
{"name": "lat", "format": None},
|
| 177 |
+
{"name": "lon", "format": None} ]
|
| 178 |
+
},
|
| 179 |
+
"compareMode": False,
|
| 180 |
+
"compareType": "absolute"
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
"layerBlending": "normal"
|
| 184 |
+
},
|
| 185 |
+
"mapState": {
|
| 186 |
+
"bearing": 0,
|
| 187 |
+
"pitch": 0,
|
| 188 |
+
"latitude": center_lat,
|
| 189 |
+
"longitude": center_lon,
|
| 190 |
+
"zoom": 6
|
| 191 |
+
},
|
| 192 |
+
"mapStyle": {
|
| 193 |
+
"styleType": "light",
|
| 194 |
+
"topLayerGroups": {},
|
| 195 |
+
"visibleLayerGroups": {"label": True, "road": True, "border": False, "building": False, "water": True, "land": True}
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
m = KeplerGl(height=height, config=config)
|
| 201 |
+
# Kepler は DataFrame の列名で自動解釈(lat/lon)
|
| 202 |
+
m.add_data(data=df_valid[["lat", "lon", "address_input", "CF"]], name="points")
|
| 203 |
+
|
| 204 |
+
# Gradioへは _repr_html_ をそのまま返すのが簡単
|
| 205 |
try:
|
| 206 |
+
return m._repr_html_()
|
| 207 |
except Exception:
|
| 208 |
+
# 万一ノートブック外で不安定な場合はHTMLファイルを生成して読み戻す
|
| 209 |
+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
|
| 210 |
+
tmp = f.name
|
| 211 |
+
m.save_to_html(file_name=tmp, read_only=True)
|
| 212 |
+
with open(tmp, "r", encoding="utf-8") as fh:
|
| 213 |
+
html = fh.read()
|
| 214 |
+
return html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
# ----------------------------
|
| 217 |
+
# 実行パイプライン(ポイントのみ)
|
| 218 |
# ----------------------------
|
| 219 |
def _parse_indexer(x):
|
| 220 |
try:
|
|
|
|
| 222 |
except Exception:
|
| 223 |
return x
|
| 224 |
|
| 225 |
+
def run(excel_file, sheet_name, header_row, address_col, power_col, use_inet):
|
| 226 |
+
# Excel 読み込み
|
| 227 |
+
if excel_file is None or not hasattr(excel_file, "name"):
|
| 228 |
+
table_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
|
| 229 |
+
return "", table_df, "Excelファイルを指定してください。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
try:
|
| 232 |
+
df = pd.read_excel(excel_file.name, sheet_name=sheet_name, header=int(header_row))
|
| 233 |
except Exception as e:
|
| 234 |
empty_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
|
| 235 |
+
return "", empty_df, f"Excel の読み込みに失敗しました: {e}"
|
| 236 |
|
| 237 |
+
addr_series = df.iloc[:, address_col] if isinstance(address_col, int) else df[address_col]
|
| 238 |
+
cf_series = df.iloc[:, power_col] if isinstance(power_col, int) else df[power_col]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
+
addresses = addr_series.astype(str).tolist()
|
| 241 |
+
cfs = cf_series.tolist()
|
| 242 |
|
| 243 |
+
geo_df = geocode_with_cache(addresses, cfs, use_internet=bool(use_inet))
|
| 244 |
+
table_df = geo_df[["address_input", "CF", "lat", "lon"]].copy()
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
+
# GeoDataFrame も一応整備(未使用だが将来の拡張用)
|
| 247 |
+
geometry = [
|
| 248 |
+
Point(lon, lat) if (pd.notna(lat) and pd.notna(lon)) else None
|
| 249 |
+
for lat, lon in zip(geo_df["lat"], geo_df["lon"])
|
| 250 |
+
]
|
| 251 |
+
gdf_pts = gpd.GeoDataFrame(geo_df, geometry=geometry, crs="EPSG:4326")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
+
# Kepler.gl(ポイントのみ)
|
| 254 |
try:
|
| 255 |
+
html = make_kepler_html(table_df, height=640)
|
| 256 |
except Exception as e:
|
| 257 |
+
html = f"<p>Kepler.gl描画に失敗しました: {e}</p>"
|
| 258 |
|
| 259 |
+
# 情報(地物数のみ)
|
| 260 |
info = []
|
|
|
|
|
|
|
|
|
|
| 261 |
info.append(f"ポイント数(有効座標): {int(gdf_pts.geometry.notnull().sum())} / {len(gdf_pts)}")
|
|
|
|
|
|
|
| 262 |
|
| 263 |
+
return html, table_df, "\n".join(info)
|
| 264 |
|
| 265 |
# ----------------------------
|
| 266 |
+
# Gradio UI(ポイントのみ)
|
| 267 |
# ----------------------------
|
| 268 |
+
with gr.Blocks(title="Excel住所 → Kepler.gl(ポイントのみ)") as demo:
|
| 269 |
+
gr.Markdown("## Excelの住所を国土地理院APIでジオコーディング → Kepler.gl に **ポイントのみ** を描画")
|
| 270 |
|
| 271 |
with gr.Row():
|
|
|
|
| 272 |
xlsx_in = gr.File(label="Excelファイル(住所付き)", file_count="single", file_types=[".xlsx", ".xls"])
|
| 273 |
|
| 274 |
with gr.Row():
|
|
|
|
| 281 |
|
| 282 |
with gr.Row():
|
| 283 |
use_inet = gr.Checkbox(label="国土地理院APIに問い合わせ(オフでキャッシュのみ使用)", value=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
run_btn = gr.Button("描画")
|
| 286 |
|
| 287 |
+
out_html = gr.HTML(label="インタラクティブ地図(Kepler.gl:ポイントのみ)")
|
|
|
|
|
|
|
| 288 |
out_table = gr.Dataframe(label="ジオコーディング結果(住所・緯度・経度・CF)", wrap=True)
|
| 289 |
+
out_info = gr.Textbox(label="メタ情報", lines=2)
|
| 290 |
|
| 291 |
def _parse(x):
|
| 292 |
try:
|
|
|
|
| 294 |
except Exception:
|
| 295 |
return x
|
| 296 |
|
| 297 |
+
def app_run(xls, s, h, a, p, inet):
|
| 298 |
return run(
|
| 299 |
+
xls, s, int(h), _parse(a), _parse(p), inet
|
|
|
|
| 300 |
)
|
| 301 |
|
| 302 |
run_btn.click(
|
| 303 |
fn=app_run,
|
| 304 |
+
inputs=[xlsx_in, sheet, header_row, address_col, power_col, use_inet],
|
| 305 |
+
outputs=[out_html, out_table, out_info],
|
| 306 |
)
|
| 307 |
|
| 308 |
if __name__ == "__main__":
|