# 20250706_App上で各ダムの訪問履歴を記録する機能を追加
%%writefile app.py
# -*- coding: utf-8 -*-
import streamlit as st
import geopandas as gpd
import pandas as pd
import random # サンプルデータ作成のために使用
import folium # Folium地図のために追加
from streamlit_folium import st_folium # StreamlitでFolium地図を表示するために追加
# --- 47都道府県と地方の定義 ---
# 全都道府県のリスト
prefectures_all = [
'北海道', '青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県',
'茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県',
'新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県',
'三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県',
'鳥取県', '島根県', '岡山県', '広島県', '山口県',
'徳島県', '香川県', '愛媛県', '高知県',
'福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県'
]
# 都道府県と地方の辞書 (北海道地方と東北地方を結合)
regions = {
'北海道・東北地方': ['北海道', '青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県'], # 結合されたグループ
'関東地方': ['茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県'],
'中部地方': ['新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県'],
'近畿地方': ['三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県'],
'中国地方': ['鳥取県', '島根県', '岡山県', '広島県', '山口県'],
'四国地方': ['徳島県', '香川県', '愛媛県', '高知県'],
'九州・沖縄地方': ['福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県']
}
# --- サンプルデータの作成 ---
random.seed(42) # 再現性のためにシードを設定
num_dams = 100 # サンプルダムの数を増やします
data = {
'都道府県名': [random.choice(prefectures_all) for _ in range(num_dams)],
'ダム名': [f'サンプルダム{i+1}' for i in range(num_dams)],
'ダム型式': [random.choice(['重力式コンクリートダム', 'ロックフィルダム', 'アーチ式コンクリートダム', 'アースダム', '複合ダム']) for _ in range(num_dams)],
'ダム目的': [random.choice(['治水', '利水', '発電', '治水・利水', '工業用水']) for _ in range(num_dams)],
'geometry': gpd.points_from_xy(
[130 + i * 0.2 + random.uniform(-0.1, 0.1) for i in range(num_dams)], # 経度
[33 + i * 0.1 + random.uniform(-0.05, 0.05) for i in range(num_dams)] # 緯度
)
}
gdf = gpd.GeoDataFrame(data, crs="EPSG:4326")
# 各ダムにユニークなIDを付与 (FirestoreのドキュメントIDとして使用することを想定)
# ダム名と都道府県名を組み合わせることで、サンプルデータ内でユニークなIDを生成
gdf['dam_id'] = gdf['ダム名'] + "_" + gdf['都道府県名']
# --- サンプルデータの作成ここまで ---
# --- Streamlit ページ設定 ---
st.set_page_config(layout="wide", page_title="ダム情報ビューア") # 画面全体を使う設定
st.title("ダム情報ビューア")
# --- セッションステートの初期化 ---
# ユーザーが選択した都道府県のリスト
if 'selected_prefectures' not in st.session_state:
st.session_state.selected_prefectures = []
# サイドバーのフィルタリング状態を保持する (複数選択用リストに変更)
if 'selected_dam_types' not in st.session_state:
st.session_state.selected_dam_types = []
if 'selected_dam_purposes' not in st.session_state:
st.session_state.selected_dam_purposes = []
# ダムの訪問状態を保持する辞書 (dam_id: 状態)
# アプリ起動時に全てのダムを「未訪問」で初期化
if 'dam_visit_statuses' not in st.session_state:
st.session_state.dam_visit_statuses = {
dam_id: '未訪問' for dam_id in gdf['dam_id'].unique()
}
# --- メイン画面の表示関数 ---
# この関数は常にメインコンテンツを表示します
def show_main_page_content(): # 関数名を変更して、ページ遷移がないことを明確に
st.header("ダム一覧")
# 表示するGeoDataFrameを準備 (元のgdfを変更しないようにコピー)
display_gdf = gdf.copy()
# 「訪問状態」列をdisplay_gdfに追加または更新
# セッションステートから現在の訪問状態をマージ
display_gdf['訪問状態'] = display_gdf['dam_id'].map(st.session_state.dam_visit_statuses).fillna('未訪問')
# フィルタリング条件を累積的に適用する
# 1. 都道府県によるフィルタリング
if st.session_state.selected_prefectures:
display_gdf = display_gdf[display_gdf['都道府県名'].isin(st.session_state.selected_prefectures)]
st.subheader(f"選択された都道府県 ({', '.join(st.session_state.selected_prefectures)}) のダム")
else:
st.subheader("全てのダムデータ")
# 2. ダム型式によるフィルタリング (複数選択に対応)
if st.session_state.selected_dam_types:
display_gdf = display_gdf[display_gdf['ダム型式'].isin(st.session_state.selected_dam_types)]
st.info(f"「ダム型式: {', '.join(st.session_state.selected_dam_types)}」でフィルタリング中")
# 3. ダム目的によるフィルタリング (複数選択に対応)
if st.session_state.selected_dam_purposes:
display_gdf = display_gdf[display_gdf['ダム目的'].isin(st.session_state.selected_dam_purposes)]
st.info(f"「ダム目的: {', '.join(st.session_state.selected_dam_purposes)}」でフィルタリング中")
# --- 表表示 ---
st.subheader("ダム一覧表")
# geometry列をWKT形式の文字列に変換して表示 (StreamlitのdataframeでGeoPandasのgeometryを直接表示するとエラーになる場合があるため)
gdf_display_wkt = display_gdf.copy()
gdf_display_wkt['geometry'] = gdf_display_wkt['geometry'].apply(lambda x: x.wkt)
# 「dam_id」列は内部的なものなので表示しない
columns_to_show = [col for col in gdf_display_wkt.columns if col != 'dam_id']
# フィルタリング結果の表示
if not gdf_display_wkt.empty:
st.dataframe(
gdf_display_wkt[columns_to_show], # 表示する列を指定
use_container_width=True, # コンテナの幅に合わせる
height=300, # 表示行数を調整
key="main_dam_data_table" # データフレームにユニークなキーを設定
)
else:
st.info("選択された条件に一致するダムは見つかりませんでした。")
st.markdown("---") # ダム状態記録セクションとの間に区切り線
# --- ダム訪問状態の記録セクション (地図の上に移動) ---
st.subheader("ダム訪問状態の記録")
st.write("訪問状態を記録したいダムを選択し、状態を更新してください。")
# 選択可能なダムのリストを作成 (表示されている display_gdf から)
# dam_id と表示名を組み合わせる
dam_options = [f"{row['ダム名']} ({row['都道府県名']})" for idx, row in display_gdf.iterrows()]
dam_id_map = {f"{row['ダム名']} ({row['都道府県名']})": row['dam_id'] for idx, row in display_gdf.iterrows()}
if dam_options:
selected_dam_display_name = st.selectbox(
"状態を更新するダムを選択",
options=dam_options,
key="select_dam_for_status"
)
selected_dam_id = dam_id_map[selected_dam_display_name]
current_status = st.session_state.dam_visit_statuses.get(selected_dam_id, '未訪問')
# 選択されたダムの情報を表示
# gdfからdam_idで該当する行を検索
selected_dam_row = gdf[gdf['dam_id'] == selected_dam_id].iloc[0]
st.write(f"**選択中のダム情報:**")
st.write(f" **都道府県:** {selected_dam_row['都道府県名']}")
st.write(f" **ダム名:** {selected_dam_row['ダム名']}")
st.write(f" **ダム型式:** {selected_dam_row['ダム型式']}")
st.write(f" **ダム目的:** {selected_dam_row['ダム目的']}")
new_status = st.radio(
"訪問状態",
options=['未訪問', '訪問済'],
index=0 if current_status == '未訪問' else 1,
key="dam_visit_status_radio"
)
if st.button("状態を更新", key="update_dam_status_button"):
st.session_state.dam_visit_statuses[selected_dam_id] = new_status
st.success(f"'{selected_dam_display_name}' の状態を '{new_status}' に更新しました!")
st.rerun() # 状態更新を反映するために再実行
else:
st.info("表示するダムがないため、状態を記録できません。")
st.markdown("---") # 地図とダム状態記録セクションとの間に区切り線
# --- 地図表示 (Foliumを使用) ---
if not display_gdf.empty:
st.subheader("ダム位置情報マップ (Folium)")
# マップの中心座標を計算 (フィルタリングされたダムの中央)
if not display_gdf.empty:
center_lat = display_gdf.geometry.y.mean()
center_lon = display_gdf.geometry.x.mean()
else: # データがない場合は日本の中心付近に設定
center_lat = 36.2048
center_lon = 138.2529
# Foliumマップを作成
m = folium.Map(location=[center_lat, center_lon], zoom_start=6, tiles="OpenStreetMap")
# 各ダムの位置にマーカーを追加
for idx, row in display_gdf.iterrows():
popup_html = f"""
<b>ダム名:</b> {row['ダム名']}<br>
<b>都道府県:</b> {row['都道府県名']}<br>
<b>型式:</b> {row['ダム型式']}<br>
<b>目的:</b> {row['ダム目的']}<br>
<b>訪問状態:</b> {row['訪問状態']}<br>
<b>緯度:</b> {row.geometry.y:.4f}<br>
<b>経度:</b> {row.geometry.x:.4f}
"""
# FoliumのPopupはHTMLを直接受け取れる
folium.Marker(
location=[row.geometry.y, row.geometry.x],
popup=folium.Popup(popup_html, max_width=300), # ポップアップの最大幅を設定
tooltip=row['ダム名'] # マーカーにマウスオーバーしたときに表示されるテキスト
).add_to(m)
# StreamlitでFoliumマップを表示
st_folium(m, width=700, height=500, key="folium_map")
else:
st.info("地図上に表示するダムがありません。")
# --- サイドバーのナビゲーションとフィルタオプション ---
with st.sidebar:
st.header("操作メニュー")
# 「全ての選択を解除」ボタンの追加
if st.button("全ての選択を解除", key="clear_all_filters"):
st.session_state.selected_prefectures = []
st.session_state.selected_dam_types = [] # 複数選択用リストをクリア
st.session_state.selected_dam_purposes = [] # 複数選択用リストをクリア
# 訪問状態はフィルタとは別なので、ここではリセットしない
st.info("全てのフィルタリング条件を解除しました。")
st.rerun() # 変更を反映するために再実行
st.subheader("都道府県の選択 (複数選択可能)")
st.write("地方ごとに開閉して選択してください。") # 説明を追加
# 各地方をst.expanderで囲み、開閉可能にする
for region, prefectures in regions.items():
# expanderのキーをユニークにする
with st.expander(region, expanded=False): # デフォルトで閉じる
# 現在の選択状態を一時リストにコピー(チェックボックスの変更を即座に反映するため)
current_selected_prefectures_in_page = list(st.session_state.selected_prefectures)
# 列を削除し、チェックボックスを縦一列に配置
for pref in prefectures:
checkbox_key = f"sidebar_checkbox_pref_{pref}" # ユニークキーをより明確に
initial_value = pref in current_selected_prefectures_in_page
if st.checkbox(pref, value=initial_value, key=checkbox_key):
if pref not in current_selected_prefectures_in_page:
current_selected_prefectures_in_page.append(pref)
else:
if pref in current_selected_prefectures_in_page:
current_selected_prefectures_in_page.remove(pref)
# サイドバー内のチェックボックス変更を即座にセッションステートに反映し、メイン画面を更新
# ここで変更がなければrerunしないようにすることで、不要な再実行を減らす
if set(current_selected_prefectures_in_page) != set(st.session_state.selected_prefectures):
st.session_state.selected_prefectures = current_selected_prefectures_in_page
st.rerun() # 変更を反映するために再実行
st.markdown("---") # 区切り線
st.subheader("その他のフィルタオプション")
# ダム型式で選択 (複数選択に対応)
all_dam_types = sorted(gdf['ダム型式'].unique().tolist()) # '全て'オプションは不要
with st.expander("ダム型式で選択 (複数選択)", expanded=False):
current_selected_dam_types_sidebar = list(st.session_state.selected_dam_types)
# 列を削除し、チェックボックスを縦一列に配置
for dam_type in all_dam_types:
checkbox_key_type = f"sidebar_checkbox_dam_type_{dam_type}"
initial_value_type = dam_type in current_selected_dam_types_sidebar
if st.checkbox(dam_type, value=initial_value_type, key=checkbox_key_type):
if dam_type not in current_selected_dam_types_sidebar:
current_selected_dam_types_sidebar.append(dam_type)
else:
if dam_type in current_selected_dam_types_sidebar:
current_selected_dam_types_sidebar.remove(dam_type)
if set(current_selected_dam_types_sidebar) != set(st.session_state.selected_dam_types):
st.session_state.selected_dam_types = current_selected_dam_types_sidebar
st.rerun()
# ダムの目的で選択 (複数選択に対応)
all_dam_purposes = sorted(gdf['ダム目的'].unique().tolist()) # '全て'オプションは不要
with st.expander("ダム目的で選択 (複数選択)", expanded=False):
current_selected_dam_purposes_sidebar = list(st.session_state.selected_dam_purposes)
# 列を削除し、チェックボックスを縦一列に配置
for dam_purpose in all_dam_purposes:
checkbox_key_purpose = f"sidebar_checkbox_dam_purpose_{dam_purpose}"
initial_value_purpose = dam_purpose in current_selected_dam_purposes_sidebar
if st.checkbox(dam_purpose, value=initial_value_purpose, key=checkbox_key_purpose):
if dam_purpose not in current_selected_dam_purposes_sidebar:
current_selected_dam_purposes_sidebar.append(dam_purpose)
else:
if dam_purpose in current_selected_dam_purposes_sidebar:
current_selected_dam_purposes_sidebar.remove(dam_purpose)
if set(current_selected_dam_purposes_sidebar) != set(st.session_state.selected_dam_purposes):
st.session_state.selected_dam_purposes = current_selected_dam_purposes_sidebar
st.rerun()
st.subheader("アプリの操作")
if st.button("アプリを終了"):
st.stop()
# --- メインロジック: 常にメインコンテンツを表示 ---
show_main_page_content()
Streamlitで永続性を確保するためのSkillを身に着ける必要がありそうですが、とりあえず今回はここまでに留めて、次のStepでは実際に国内に存在するダムの情報を国土数値情報からGetして、TrialCodeに実装する方法を検討します。
コメント
コメントを投稿