Files changed (1) hide show
  1. app.py +138 -177
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 PIL import Image
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
- 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")
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
- 返却座標は [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
@@ -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
- time.sleep(GEOCODE_DELAY_SEC)
 
 
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
- # 可視化(matplotlib)
136
  # ----------------------------
137
- def plot_map_png(
138
- gdf_pref: gpd.GeoDataFrame,
139
- gdf_pts: gpd.GeoDataFrame,
140
- line_width: float = 0.6,
141
- marker_size: int = 24,
142
- legend_shrink: float = 0.6,
143
- legend_fontsize: int = 8,
144
- figsize=(7, 7),
145
- ) -> Image.Image:
146
- fig, ax = plt.subplots(figsize=figsize)
147
- gdf_pref.boundary.plot(ax=ax, linewidth=line_width, color="black")
148
-
149
- gdf_pts_valid = gdf_pts[gdf_pts.geometry.notnull()]
150
- if not gdf_pts_valid.empty:
151
- cf_num = pd.to_numeric(
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:
169
- pass
170
-
171
- ax.set_axis_off()
172
- plt.tight_layout()
173
- buf = io.BytesIO()
174
- fig.savefig(buf, format="png", dpi=200)
175
- plt.close(fig)
176
- buf.seek(0)
177
- return Image.open(buf)
178
-
179
- # ----------------------------
180
- # 可視化(folium)
181
- # ----------------------------
182
- def make_folium_html(gdf_pref: gpd.GeoDataFrame, gdf_pts: gpd.GeoDataFrame, marker_size: int = 24):
183
- gdf_pts_valid = gdf_pts[gdf_pts.geometry.notnull()]
184
- if not gdf_pts_valid.empty:
185
- center_lat = gdf_pts_valid.geometry.y.median()
186
- center_lon = gdf_pts_valid.geometry.x.median()
187
- zoom = 6
188
- else:
189
- center_lat, center_lon, zoom = 35.6812, 139.7671, 5
190
-
191
- m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom)
192
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  try:
194
- folium.GeoJson(gdf_pref.to_json(), name="prefecture").add_to(m)
195
  except Exception:
196
- pass
197
-
198
- circle_radius = max(3, int(marker_size // 3))
199
-
200
- for _, r in gdf_pts_valid.iterrows():
201
- lat, lon = r.geometry.y, r.geometry.x
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,
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(zip_file, excel_file, sheet_name, header_row, address_col, power_col,
223
- use_inet, line_width, marker_size, legend_shrink, legend_fontsize):
224
- # Shapefile
225
- if zip_file is not None and hasattr(zip_file, "name") and os.path.exists(zip_file.name):
226
- zip_path = zip_file.name
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
- gdf_pref = load_gdf_from_zip(zip_path)
235
  except Exception as e:
236
  empty_df = pd.DataFrame(columns=["address_input", "CF", "lat", "lon"])
237
- return None, None, "", empty_df, f"行政界の読み込みに失敗しました: {e}"
238
 
239
- # Excel→ジオコーディング
240
- if excel_file is None or not hasattr(excel_file, "name"):
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
- geo_df = geocode_with_cache(addresses, cfs, use_internet=bool(use_inet))
257
- table_df = geo_df[["address_input", "CF", "lat", "lon"]].copy()
258
 
259
- geometry = [
260
- Point(lon, lat) if (pd.notna(lat) and pd.notna(lon)) else None
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
- try:
267
- img = plot_map_png(
268
- gdf_pref, gdf_pts,
269
- line_width=float(line_width),
270
- marker_size=int(marker_size),
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 = make_folium_html(gdf_pref, gdf_pts, marker_size=int(marker_size))
279
  except Exception as e:
280
- html = f"<p>folium描画に失敗しました: {e}</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 img, html, "\n".join(info), table_df, ""
292
 
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():
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
- out_img = gr.Image(label="静的地図(matplotlib)", type="pil")
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
- out_err = gr.Markdown(label="エラー", visible=True)
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(zipf, xls, s, h, a, p, inet, lw, ms, lsh, lfs):
336
  return run(
337
- zipf, xls, s, int(h), _parse(a), _parse(p),
338
- inet, lw, ms, lsh, lfs
339
  )
340
 
341
  run_btn.click(
342
  fn=app_run,
343
- inputs=[zip_in, xlsx_in, sheet, header_row, address_col, power_col, use_inet, line_width, marker_size, legend_shrink, legend_fontsize],
344
- outputs=[out_img, out_html, out_info, out_table, out_err],
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__":