hiroki0008 commited on
Commit
db0cf7a
·
verified ·
1 Parent(s): b1675ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -58
app.py CHANGED
@@ -1,10 +1,10 @@
1
- # app.py (Kepler.glを使わない安定版:Folium + 無料タイル)
2
  # pip install folium gradio pandas numpy requests openpyxl
3
 
4
  import os
 
5
  import time
6
- import base64
7
- import urllib.parse
8
  import requests
9
  import pandas as pd
10
  import numpy as np
@@ -18,7 +18,7 @@ GSI_USER_AGENT = os.environ.get(
18
  "jp-gsi-geocoding-demo (contact: your_email@example.com)" # 連絡先付き推奨
19
  )
20
  GSI_TIMEOUT_SEC = float(os.environ.get("GSI_TIMEOUT_SEC", "10"))
21
- GEOCODE_DELAY_SEC = float(os.environ.get("GSI_RATE_LIMIT_SEC", "0.0")) # 連続呼び出し間隔(秒)
22
 
23
  GSI_GEOCODE_URL = "https://msearch.gsi.go.jp/address-search/AddressSearch"
24
 
@@ -59,12 +59,13 @@ def make_gsi_session() -> requests.Session:
59
 
60
  def gsi_geocode_once(address: str, session: requests.Session) -> tuple[float, float]:
61
  """
62
- 国土地理院住所検索APIを1回呼び出し、(lat, lon) を返す(失敗時は (nan, nan))。
63
- APIは [lon, lat] を返すため、順を入れ替えて返す。
64
  """
65
  try:
66
  if not address or str(address).strip() == "" or str(address).strip().lower() in ("nan", "none"):
67
  return (np.nan, np.nan)
 
68
  resp = session.get(GSI_GEOCODE_URL, params={"q": address}, timeout=GSI_TIMEOUT_SEC)
69
  if not resp.ok:
70
  return (np.nan, np.nan)
@@ -124,7 +125,6 @@ def geocode_with_cache(addresses, CFs, use_internet=True):
124
  # Folium 地図生成(無料タイル)
125
  # ----------------------------
126
  import folium
127
- from folium.plugins import MarkerCluster
128
 
129
  TILE_CATALOG = {
130
  "GSI 標準地図": "https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png",
@@ -133,21 +133,17 @@ TILE_CATALOG = {
133
  "OpenStreetMap": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
134
  }
135
 
136
- def _make_folium_map(df_points: pd.DataFrame, base_name: str, cluster: bool, height_px: int = 640) -> str:
137
  df_valid = df_points.dropna(subset=["lat", "lon"]).copy()
138
  if df_valid.empty:
139
- # 日本中心
140
  center_lat, center_lon, zoom = 35.0, 135.0, 4
141
  else:
142
  center_lat = float(df_valid["lat"].median())
143
  center_lon = float(df_valid["lon"].median())
144
  zoom = 6
145
 
146
- # ベースマップ
147
- tiles_url = TILE_CATALOG.get(base_name, TILE_CATALOG["GSI 標準地図"])
148
  m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom, control_scale=True, tiles=None)
149
-
150
- # 各タイルをレイヤとして追加(UIから切替可能)
151
  for name, url in TILE_CATALOG.items():
152
  folium.TileLayer(
153
  tiles=url,
@@ -159,13 +155,8 @@ def _make_folium_map(df_points: pd.DataFrame, base_name: str, cluster: bool, hei
159
  ).add_to(m)
160
 
161
  # マーカー(CF でサイズ可変)
162
- if cluster:
163
- mc = MarkerCluster(name="Points").add_to(m)
164
-
165
- # サイズスケーリング
166
  if "CF" in df_valid.columns and df_valid["CF"].notna().any():
167
  cf = df_valid["CF"].clip(lower=0)
168
- # 0~1正規化 → 3~15px
169
  cf_norm = (cf - cf.min()) / (cf.max() - cf.min() + 1e-9)
170
  sizes = (cf_norm * 12 + 3).fillna(6).tolist()
171
  else:
@@ -175,14 +166,9 @@ def _make_folium_map(df_points: pd.DataFrame, base_name: str, cluster: bool, hei
175
  lat, lon = float(row["lat"]), float(row["lon"])
176
  addr = str(row.get("address_input", ""))
177
  cfv = row.get("CF", np.nan)
178
- popup = folium.Popup(
179
- folium.IFrame(
180
- html=f"<b>住所:</b> {addr}<br><b>CF:</b> {'' if pd.isna(cfv) else cfv}",
181
- width=260, height=80
182
- ),
183
- max_width=260
184
- )
185
- marker = folium.CircleMarker(
186
  location=(lat, lon),
187
  radius=float(r),
188
  weight=1,
@@ -190,23 +176,45 @@ def _make_folium_map(df_points: pd.DataFrame, base_name: str, cluster: bool, hei
190
  fill=True,
191
  fill_opacity=0.8,
192
  fill_color="#12939A",
193
- popup=popup,
194
- )
195
- (mc if cluster else m).add_child(marker)
196
 
197
  folium.LayerControl(position="topright").add_to(m)
 
198
 
199
- # HTML 文字列
200
- html = m.get_root().render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
- # iframe 埋め込み(data:URL)+ 新規タブ用リンク
203
- data_url = "data:text/html;charset=utf-8," + urllib.parse.quote(html)
204
- link_url = "data:text/html;base64," + base64.b64encode(html.encode("utf-8")).decode("ascii")
205
- iframe = (
206
- f'<iframe src="{data_url}" style="border:0;width:100%;height:{height_px}px"></iframe>'
207
- f'<div style="margin-top:6px;"><a href="{link_url}" target="_blank" rel="noopener">↗ 地図を新しいタブで開く</a></div>'
208
- )
209
- return iframe
210
 
211
  # ----------------------------
212
  # 実行パイプライン
@@ -217,43 +225,53 @@ def _parse_indexer(x):
217
  except Exception:
218
  return x
219
 
220
- def run(excel_file, sheet_name, header_row, address_col, power_col, use_inet, base_name, cluster):
221
  # Excel 読み込み
222
  if excel_file is None or not hasattr(excel_file, "name"):
223
  table_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
224
- return "", table_df, "Excelファイルを指定してください。"
225
 
226
  try:
227
  df = pd.read_excel(excel_file.name, sheet_name=sheet_name, header=int(header_row))
228
  except Exception as e:
229
  empty_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
230
- return "", empty_df, f"Excel の読み込みに失敗しました: {e}"
231
 
232
- # 列参照
233
  addr_series = df.iloc[:, address_col] if isinstance(address_col, int) else df[address_col]
234
  cf_series = df.iloc[:, power_col] if isinstance(power_col, int) else df[power_col]
235
 
236
  addresses = addr_series.astype(str).tolist()
237
  cfs = cf_series.tolist()
238
 
 
239
  geo_df = geocode_with_cache(addresses, cfs, use_internet=bool(use_inet))
240
  table_df = geo_df[["address_input", "CF", "lat", "lon"]].copy()
241
 
242
- # Folium 地図
243
  try:
244
- html_map = _make_folium_map(table_df, base_name=base_name, cluster=bool(cluster), height_px=640)
245
- info = [f"ポイント数(有効座標): {int(table_df[['lat','lon']].dropna().shape[0])} / {len(table_df)}"]
246
- except Exception as e:
247
- html_map = f"<p>地図描画に失敗しました: {e}</p>"
248
- info = []
249
 
250
- return html_map, table_df, "\n".join(info)
 
 
 
 
 
 
 
251
 
252
  # ----------------------------
253
  # Gradio UI
254
  # ----------------------------
255
- with gr.Blocks(title="Excel住所 → Folium(無料タイル・Mapbox不要)") as demo:
256
- gr.Markdown("## Excelの住所を国土地理院APIでジオコーディング → Folium(Leaflet)で地図表示(無料タイル・Mapbox不要)")
 
 
 
 
257
 
258
  with gr.Row():
259
  xlsx_in = gr.File(label="Excelファイル(住所付き)", file_count="single", file_types=[".xlsx", ".xls"])
@@ -269,13 +287,17 @@ with gr.Blocks(title="Excel住所 → Folium(無料タイル・Mapbox不要)
269
  with gr.Row():
270
  use_inet = gr.Checkbox(label="国土地理院APIに問い合わせ(オフでキャッシュのみ使用)", value=True)
271
  base_name = gr.Dropdown(choices=list(TILE_CATALOG.keys()), value="GSI 標準地図", label="ベースマップ")
272
- cluster = gr.Checkbox(label="マーカーをクラスタ表示", value=True)
 
 
 
273
 
274
  run_btn = gr.Button("描画")
275
 
276
- out_html = gr.HTML(label="地図(Folium / Leaflet)")
277
  out_table = gr.Dataframe(label="ジオコーディング結果(住所・緯度・経度・CF)", wrap=True)
278
  out_info = gr.Textbox(label="メタ情報", lines=2)
 
279
 
280
  def _parse(x):
281
  try:
@@ -283,15 +305,15 @@ with gr.Blocks(title="Excel住所 → Folium(無料タイル・Mapbox不要)
283
  except Exception:
284
  return x
285
 
286
- def app_run(xls, s, h, a, p, inet, base, cl):
287
  return run(
288
- xls, s, int(h), _parse(a), _parse(p), inet, base, cl
289
  )
290
 
291
  run_btn.click(
292
  fn=app_run,
293
- inputs=[xlsx_in, sheet, header_row, address_col, power_col, use_inet, base_name, cluster],
294
- outputs=[out_html, out_table, out_info],
295
  )
296
 
297
  if __name__ == "__main__":
 
1
+ # app.py (Folium + 無料タイル / data:URL不使用 / File出力)
2
  # pip install folium gradio pandas numpy requests openpyxl
3
 
4
  import os
5
+ import re
6
  import time
7
+ import tempfile
 
8
  import requests
9
  import pandas as pd
10
  import numpy as np
 
18
  "jp-gsi-geocoding-demo (contact: your_email@example.com)" # 連絡先付き推奨
19
  )
20
  GSI_TIMEOUT_SEC = float(os.environ.get("GSI_TIMEOUT_SEC", "10"))
21
+ GEOCODE_DELAY_SEC = float(os.environ.get("GSI_RATE_LIMIT_SEC", "0.0"))
22
 
23
  GSI_GEOCODE_URL = "https://msearch.gsi.go.jp/address-search/AddressSearch"
24
 
 
59
 
60
  def gsi_geocode_once(address: str, session: requests.Session) -> tuple[float, float]:
61
  """
62
+ 国土地理院 住所検索APIを1回呼び出し、(lat, lon) を返す(失敗時は (nan, nan))。
63
+ APIは [lon, lat] を返すため、順を入れ替える。
64
  """
65
  try:
66
  if not address or str(address).strip() == "" or str(address).strip().lower() in ("nan", "none"):
67
  return (np.nan, np.nan)
68
+
69
  resp = session.get(GSI_GEOCODE_URL, params={"q": address}, timeout=GSI_TIMEOUT_SEC)
70
  if not resp.ok:
71
  return (np.nan, np.nan)
 
125
  # Folium 地図生成(無料タイル)
126
  # ----------------------------
127
  import folium
 
128
 
129
  TILE_CATALOG = {
130
  "GSI 標準地図": "https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png",
 
133
  "OpenStreetMap": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
134
  }
135
 
136
+ def _build_folium_map_html(df_points: pd.DataFrame, base_name: str) -> str:
137
  df_valid = df_points.dropna(subset=["lat", "lon"]).copy()
138
  if df_valid.empty:
 
139
  center_lat, center_lon, zoom = 35.0, 135.0, 4
140
  else:
141
  center_lat = float(df_valid["lat"].median())
142
  center_lon = float(df_valid["lon"].median())
143
  zoom = 6
144
 
145
+ # ベースマップ(複数切替)
 
146
  m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom, control_scale=True, tiles=None)
 
 
147
  for name, url in TILE_CATALOG.items():
148
  folium.TileLayer(
149
  tiles=url,
 
155
  ).add_to(m)
156
 
157
  # マーカー(CF でサイズ可変)
 
 
 
 
158
  if "CF" in df_valid.columns and df_valid["CF"].notna().any():
159
  cf = df_valid["CF"].clip(lower=0)
 
160
  cf_norm = (cf - cf.min()) / (cf.max() - cf.min() + 1e-9)
161
  sizes = (cf_norm * 12 + 3).fillna(6).tolist()
162
  else:
 
166
  lat, lon = float(row["lat"]), float(row["lon"])
167
  addr = str(row.get("address_input", ""))
168
  cfv = row.get("CF", np.nan)
169
+ popup_html = f"<b>住所:</b> {addr}<br><b>CF:</b> {'' if pd.isna(cfv) else cfv}"
170
+
171
+ folium.CircleMarker(
 
 
 
 
 
172
  location=(lat, lon),
173
  radius=float(r),
174
  weight=1,
 
176
  fill=True,
177
  fill_opacity=0.8,
178
  fill_color="#12939A",
179
+ popup=folium.Popup(popup_html, max_width=260),
180
+ ).add_to(m)
 
181
 
182
  folium.LayerControl(position="topright").add_to(m)
183
+ return m.get_root().render()
184
 
185
+ def _rewrite_leaflet_cdn(html_text: str, host: str) -> str:
186
+ """
187
+ Folium が出力する Leaflet の CDN(通常 jsDelivr)を、必要に応じて置換。
188
+ SRI不整合を避けるため integrity/crossorigin を除去する。
189
+ """
190
+ # integrity / crossorigin を削除(SRIミスマッチ回避)
191
+ html_text = re.sub(r'\sintegrity="[^"]+"', "", html_text)
192
+ html_text = re.sub(r'\scrossorigin="[^"]+"', "", html_text)
193
+
194
+ if host == "jsdelivr":
195
+ return html_text # 置換しない
196
+ elif host == "cdnjs":
197
+ html_text = html_text.replace(
198
+ "https://cdn.jsdelivr.net/npm/leaflet@", "https://cdnjs.cloudflare.com/ajax/libs/leaflet/"
199
+ )
200
+ html_text = html_text.replace("/dist/leaflet.css", "/leaflet.css")
201
+ html_text = html_text.replace("/dist/leaflet.js", "/leaflet.js")
202
+ return html_text
203
+ elif host == "unpkg":
204
+ html_text = html_text.replace(
205
+ "https://cdn.jsdelivr.net/npm/", "https://unpkg.com/"
206
+ )
207
+ return html_text
208
+ else:
209
+ return html_text
210
 
211
+ def _save_map_html_file(html_text: str) -> str:
212
+ """地図HTMLを実ファイルに保存(Gradio Fileに渡すパスを返す)"""
213
+ fd, path = tempfile.mkstemp(suffix=".html")
214
+ os.close(fd)
215
+ with open(path, "w", encoding="utf-8") as f:
216
+ f.write(html_text)
217
+ return path
 
218
 
219
  # ----------------------------
220
  # 実行パイプライン
 
225
  except Exception:
226
  return x
227
 
228
+ def run(excel_file, sheet_name, header_row, address_col, power_col, use_inet, base_name, leaflet_cdn):
229
  # Excel 読み込み
230
  if excel_file is None or not hasattr(excel_file, "name"):
231
  table_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
232
+ return ("Excelファイルを指定してください。", table_df, "", None)
233
 
234
  try:
235
  df = pd.read_excel(excel_file.name, sheet_name=sheet_name, header=int(header_row))
236
  except Exception as e:
237
  empty_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
238
+ return (f"Excel の読み込みに失敗しました: {e}", empty_df, "", None)
239
 
240
+ # 列参照(番号/名前の両対応)
241
  addr_series = df.iloc[:, address_col] if isinstance(address_col, int) else df[address_col]
242
  cf_series = df.iloc[:, power_col] if isinstance(power_col, int) else df[power_col]
243
 
244
  addresses = addr_series.astype(str).tolist()
245
  cfs = cf_series.tolist()
246
 
247
+ # ジオコーディング
248
  geo_df = geocode_with_cache(addresses, cfs, use_internet=bool(use_inet))
249
  table_df = geo_df[["address_input", "CF", "lat", "lon"]].copy()
250
 
251
+ # 地図HTML生成 → CDN書換 → 実ファイル保存 → File出力
252
  try:
253
+ html_text = _build_folium_map_html(table_df, base_name=base_name)
254
+ html_text = _rewrite_leaflet_cdn(html_text, host=leaflet_cdn)
255
+ map_file_path = _save_map_html_file(html_text)
 
 
256
 
257
+ msg = (
258
+ "✅ 地図HTMLを生成しました。下の **地図HTMLファイル** をクリックして新規タブで開いてください。\n"
259
+ "(埋め込みではなく実ファイル配信なので、CSPが厳しい環境でも表示できるはずです)"
260
+ )
261
+ info = f"ポイント数(有効座標): {int(table_df[['lat','lon']].dropna().shape[0])} / {len(table_df)}"
262
+ return (msg, table_df, info, map_file_path)
263
+ except Exception as e:
264
+ return (f"地図描画に失敗しました: {e}", table_df, "", None)
265
 
266
  # ----------------------------
267
  # Gradio UI
268
  # ----------------------------
269
+ with gr.Blocks(title="Excel住所 → Folium(無料タイル・File配信)") as demo:
270
+ gr.Markdown(
271
+ "## Excelの住所を国土地理院APIでジオコーディング → Folium(Leaflet)で地図表示(無料タイル・Mapbox不��)\n"
272
+ "- 地図は **実ファイル(.html)** として配信します(CSPが厳しい環境でもOK)。\n"
273
+ "- タイル=地理院/OSM、CDNは必要に応じて切替できます。"
274
+ )
275
 
276
  with gr.Row():
277
  xlsx_in = gr.File(label="Excelファイル(住所付き)", file_count="single", file_types=[".xlsx", ".xls"])
 
287
  with gr.Row():
288
  use_inet = gr.Checkbox(label="国土地理院APIに問い合わせ(オフでキャッシュのみ使用)", value=True)
289
  base_name = gr.Dropdown(choices=list(TILE_CATALOG.keys()), value="GSI 標準地図", label="ベースマップ")
290
+ leaflet_cdn = gr.Dropdown(
291
+ choices=["jsdelivr", "cdnjs", "unpkg"], value="jsdelivr",
292
+ label="Leaflet CDN(遮断時に切替)"
293
+ )
294
 
295
  run_btn = gr.Button("描画")
296
 
297
+ out_html = gr.HTML(label="案内メッセージ")
298
  out_table = gr.Dataframe(label="ジオコーディング結果(住所・緯度・経度・CF)", wrap=True)
299
  out_info = gr.Textbox(label="メタ情報", lines=2)
300
+ out_file = gr.File(label="地図HTMLファイル(クリックで開く/ダウンロード)")
301
 
302
  def _parse(x):
303
  try:
 
305
  except Exception:
306
  return x
307
 
308
+ def app_run(xls, s, h, a, p, inet, base, cdn):
309
  return run(
310
+ xls, s, int(h), _parse(a), _parse(p), inet, base, cdn
311
  )
312
 
313
  run_btn.click(
314
  fn=app_run,
315
+ inputs=[xlsx_in, sheet, header_row, address_col, power_col, use_inet, base_name, leaflet_cdn],
316
+ outputs=[out_html, out_table, out_info, out_file],
317
  )
318
 
319
  if __name__ == "__main__":