Files changed (1) hide show
  1. app.py +54 -33
app.py CHANGED
@@ -1,13 +1,13 @@
1
  # app.py
2
  import os
3
  import io
 
 
4
  import pandas as pd
5
  import numpy as np
6
  import geopandas as gpd
7
  import matplotlib.pyplot as plt
8
  from shapely.geometry import Point
9
- from geopy.geocoders import Nominatim
10
- from geopy.extra.rate_limiter import RateLimiter
11
  import folium
12
  import gradio as gr
13
  from PIL import Image
@@ -15,11 +15,14 @@ from PIL import Image
15
  # ----------------------------
16
  # 設定
17
  # ----------------------------
18
- USER_AGENT = os.environ.get(
19
- "NOMINATIM_USER_AGENT",
20
- "jp-geocoding-demo (contact: your_email@example.com)" # 連絡先付きに変更推奨
21
  )
22
- GEOCODE_DELAY_SEC = 1.0
 
 
 
23
  CACHE_DIR = "data/cache"
24
  os.makedirs(CACHE_DIR, exist_ok=True)
25
  CACHE_PATH = os.path.join(CACHE_DIR, "geocode_cache.csv")
@@ -57,21 +60,49 @@ def load_gdf_from_zip(zip_path: str) -> gpd.GeoDataFrame:
57
  return gdf
58
 
59
  # ----------------------------
60
- # ジオコーダ
61
  # ----------------------------
62
- def make_geocoder():
63
- geolocator = Nominatim(user_agent=USER_AGENT, timeout=10)
64
- geocode = RateLimiter(geolocator.geocode, min_delay_seconds=GEOCODE_DELAY_SEC)
65
- return geocode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  def geocode_with_cache(addresses, CFs, use_internet=True):
68
  cache = load_cache()
69
  cache_map = {row["address_input"]: (row["lat"], row["lon"], row["CF"]) for _, row in cache.iterrows()}
70
  results = []
71
- geocode = make_geocoder() if use_internet else None
72
 
73
  for a, cf in zip(addresses, CFs):
74
- a = str(a)
75
  cf = "" if (cf is None or (isinstance(cf, float) and np.isnan(cf))) else str(cf)
76
 
77
  # cache hit
@@ -85,15 +116,11 @@ def geocode_with_cache(addresses, CFs, use_internet=True):
85
  results.append({"address_input": a, "CF": cf, "lat": np.nan, "lon": np.nan})
86
  continue
87
 
88
- try:
89
- loc = geocode(a, country_codes="jp", addressdetails=True)
90
- if loc:
91
- lat, lon = loc.latitude, loc.longitude
92
- else:
93
- lat, lon = np.nan, np.nan
94
- except Exception:
95
- lat, lon = np.nan, np.nan
96
 
 
97
  cache = cache[cache["address_input"] != a]
98
  cache = pd.concat(
99
  [cache, pd.DataFrame([{"address_input": a, "lat": lat, "lon": lon, "CF": cf}])],
@@ -125,22 +152,17 @@ def plot_map_png(
125
  gdf_pts_valid.get("CF", pd.Series([np.nan]*len(gdf_pts_valid))),
126
  errors="coerce"
127
  )
128
- # 凡例縮小: legend_kwds={'shrink': ...}
129
  gdf_pts_valid.assign(CF_num=cf_num).plot(
130
  ax=ax,
131
  column="CF_num",
132
  cmap="OrRd",
133
- markersize=max(2, int(marker_size)), # ★ ポイント大きさ
134
  alpha=0.85,
135
  legend=True,
136
- legend_kwds={"shrink": legend_shrink}, # ★ 凡例を小さく
137
  )
138
-
139
- # カラーバーの目盛フォントを小さく
140
  try:
141
- # 図内の axes のうち、カラーバー(凡例)軸を見つけてフォント縮小
142
  for _ax in fig.axes:
143
- # メイン地図の ax を除外して残りをカラーバーとみなす
144
  if _ax is not ax:
145
  _ax.tick_params(labelsize=legend_fontsize)
146
  except Exception:
@@ -173,7 +195,6 @@ def make_folium_html(gdf_pref: gpd.GeoDataFrame, gdf_pts: gpd.GeoDataFrame, mark
173
  except Exception:
174
  pass
175
 
176
- # foliumの点サイズも拡大(matplotlibのmarker_sizeに概ね追随)
177
  circle_radius = max(3, int(marker_size // 3))
178
 
179
  for _, r in gdf_pts_valid.iterrows():
@@ -181,7 +202,7 @@ def make_folium_html(gdf_pref: gpd.GeoDataFrame, gdf_pts: gpd.GeoDataFrame, mark
181
  popup = f"{r.get('address_input','(no addr)')}<br>CF:{r.get('CF','')}"
182
  folium.CircleMarker(
183
  location=(float(lat), float(lon)),
184
- radius=circle_radius, # ★ 丸の半径
185
  fill=True,
186
  fill_opacity=0.9,
187
  popup=popup,
@@ -272,7 +293,7 @@ def run(zip_file, excel_file, sheet_name, header_row, address_col, power_col,
272
  # ----------------------------
273
  # Gradio UI
274
  # ----------------------------
275
- with gr.Blocks(title="Japan Shapefile + Excel Geocoding Plotter") as demo:
276
  gr.Markdown("## japan_ver85.shp(ZIP) + Excel住所 → 日本地図にプロット(凡例小・点大の調整可)")
277
 
278
  with gr.Row():
@@ -288,10 +309,10 @@ with gr.Blocks(title="Japan Shapefile + Excel Geocoding Plotter") as demo:
288
  power_col = gr.Textbox(label="数値列(任意:列名 or 0始まり列番号)", value="発電出力(kW)")
289
 
290
  with gr.Row():
291
- use_inet = gr.Checkbox(label="Nominatimに問い合わせ(オフでキャッシュのみ使用)", value=True)
292
  line_width = gr.Slider(0.2, 2.0, value=0.6, step=0.1, label="境界線の太さ")
293
 
294
- # ★ 新しい見た目調整スライダ
295
  with gr.Row():
296
  marker_size = gr.Slider(4, 64, value=24, step=2, label="ポイントサイズ(matplotlib / folium)")
297
  legend_shrink = gr.Slider(0.3, 1.0, value=0.6, step=0.05, label="凡例の縮小率(小さいほど小さく)")
 
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 PIL import Image
 
15
  # ----------------------------
16
  # 設定
17
  # ----------------------------
18
+ GSI_USER_AGENT = os.environ.get(
19
+ "GSI_USER_AGENT",
20
+ "jp-gsi-geocoding-demo (contact: your_email@example.com)" # 連絡先付き推奨
21
  )
22
+ GSI_TIMEOUT_SEC = float(os.environ.get("GSI_TIMEOUT_SEC", "10"))
23
+ GEOCODE_DELAY_SEC = float(os.environ.get("GSI_RATE_LIMIT_SEC", "0.5")) # マナーとして少し待機
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")
 
60
  return gdf
61
 
62
  # ----------------------------
63
+ # 国土地理院 ジオコーダ
64
  # ----------------------------
65
+ def make_gsi_session() -> requests.Session:
66
+ s = requests.Session()
67
+ s.headers.update({"User-Agent": GSI_USER_AGENT})
68
+ return s
69
+
70
+ def gsi_geocode_once(address: str, session: requests.Session) -> tuple[float, float]:
71
+ """
72
+ 国土地理院 住所検索APIを1回呼び出し、(lat, lon) を返す。失敗時は (nan, nan)。
73
+ 返却座標は [lon, lat] なので順を入れ替えて返す。
74
+ """
75
+ try:
76
+ # 空やnan文字列はスキップ
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
96
+ return (np.nan, np.nan)
97
 
98
  def geocode_with_cache(addresses, CFs, use_internet=True):
99
  cache = load_cache()
100
  cache_map = {row["address_input"]: (row["lat"], row["lon"], row["CF"]) for _, row in cache.iterrows()}
101
  results = []
102
+ session = make_gsi_session() if use_internet else None
103
 
104
  for a, cf in zip(addresses, CFs):
105
+ a = "" if (a is None or (isinstance(a, float) and np.isnan(a))) else str(a).strip()
106
  cf = "" if (cf is None or (isinstance(cf, float) and np.isnan(cf))) else str(cf)
107
 
108
  # cache hit
 
116
  results.append({"address_input": a, "CF": cf, "lat": np.nan, "lon": np.nan})
117
  continue
118
 
119
+ lat, lon = gsi_geocode_once(a, session)
120
+ # マナーとして小休止
121
+ time.sleep(GEOCODE_DELAY_SEC)
 
 
 
 
 
122
 
123
+ # キャッシュ更新
124
  cache = cache[cache["address_input"] != a]
125
  cache = pd.concat(
126
  [cache, pd.DataFrame([{"address_input": a, "lat": lat, "lon": lon, "CF": cf}])],
 
152
  gdf_pts_valid.get("CF", pd.Series([np.nan]*len(gdf_pts_valid))),
153
  errors="coerce"
154
  )
 
155
  gdf_pts_valid.assign(CF_num=cf_num).plot(
156
  ax=ax,
157
  column="CF_num",
158
  cmap="OrRd",
159
+ markersize=max(2, int(marker_size)),
160
  alpha=0.85,
161
  legend=True,
162
+ legend_kwds={"shrink": legend_shrink},
163
  )
 
 
164
  try:
 
165
  for _ax in fig.axes:
 
166
  if _ax is not ax:
167
  _ax.tick_params(labelsize=legend_fontsize)
168
  except Exception:
 
195
  except Exception:
196
  pass
197
 
 
198
  circle_radius = max(3, int(marker_size // 3))
199
 
200
  for _, r in gdf_pts_valid.iterrows():
 
202
  popup = f"{r.get('address_input','(no addr)')}<br>CF:{r.get('CF','')}"
203
  folium.CircleMarker(
204
  location=(float(lat), float(lon)),
205
+ radius=circle_radius,
206
  fill=True,
207
  fill_opacity=0.9,
208
  popup=popup,
 
293
  # ----------------------------
294
  # Gradio UI
295
  # ----------------------------
296
+ with gr.Blocks(title="Japan Shapefile + Excel Geocoding Plotter (GSI)") as demo:
297
  gr.Markdown("## japan_ver85.shp(ZIP) + Excel住所 → 日本地図にプロット(凡例小・点大の調整可)")
298
 
299
  with gr.Row():
 
309
  power_col = gr.Textbox(label="数値列(任意:列名 or 0始まり列番号)", value="発電出力(kW)")
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="凡例の縮小率(小さいほど小さく)")