タイトル画像
琴葉葵

前回データベースから周辺店舗の一覧を取得できるようになったけど、一覧表示のままだと、その店がどこにあるか分かりづらいね

琴葉茜

じゃあ今回は、それを使って地図上に「ピン」を立ててみようか!

琴葉葵

「ピン」って、googleマップで場所を検索した時に位置を教えてくれるやつだよね

琴葉茜

そうそう
このピンを立てる機能も「folium」を使えば簡単に設置できるから試してみよう!

琴葉葵

お願いします!

琴葉茜

じゃあまずは、練習として検索する時に指定した座標にピンを立ててみようか

琴葉茜

ピンを立てる方法もシンプルで、「folium.Marker([緯度, 経度], popup='ポップアップメッセージ').add_to(地図を管理している変数名)」で追加できるよ

琴葉葵

ほうほう

琴葉茜

ただ、ピンを立てる場合は地図を表示するプログラムより前に用意する必要があるから注意してね

folium.Marker([lat, lon], popup="検索地点", icon=folium.Icon(color="blue")).add_to(m)
琴葉葵

追加したよ!
ピンをクリックすると、そのピンの情報が表示されるようになってるね!

解説画像1
琴葉茜

バッチリだね!
ちなみに「popup="〇〇"」の部分はHTML形式で書き込むとリンクの作成とかも出来るから覚えておくと良いよ!

琴葉葵

そうなんだ!
まだHTMLはよく分からないけど、使えるようになったら試してみる!!

琴葉茜

じゃあ次は、周辺店舗のピンを立ててみようか!
これは変数の「restaurants」をforループで処理していけばOK!

# 「restaurants = ~」の次の行くらいに追加しよう

for res in restaurants:
    folium.Marker(
        location=[res['lat'], res['lon']], 
        tooltip=res['name'], 
        popup=f"{res['name']} ({res['amenity']})", 
        icon=folium.Icon(color="red", icon="cutlery", prefix="fa")
    ).add_to(m)
解説画像1
琴葉葵

赤色のピンがいっぱい出てきた!!

琴葉茜

良い感じ!
じゃあついでに、検索範囲を調整できる仕組みも追加しておこうか

琴葉葵

おおー良いね!!

琴葉茜

まずは検索範囲を調整できるスライダーを用意しようか
「st.form()」の部分にある「target_place」と「submitted」の間に次のプログラムを追加しよう

search_radius = st.slider("検索範囲 (km)", 0.5, 3.0, 1.0)
琴葉茜

今回は最小範囲を「0.5」、最大範囲を「3.0」、初期値を「1.0」として設定してるけど、好みで調整してみてね!

琴葉葵

オッケー!

琴葉茜

あとは、変数「restaurants」に検索結果を返すプログラムの引数にスライダーの情報を追加するだけでOK!

restaurants = search_restaurants(lat, lon, radius_km=search_radius)
琴葉葵

ちゃんとスライダー機能が使えるようになったよ!!

琴葉茜

これで地図アプリはひとまず完成だね!

琴葉茜

他にも利用できる地理情報には、OpenStreetMapで使われているルート検索エンジン「OSRM」や国土交通省が連携している3Dの都市モデル「PLATEAU」、「国土数値情報」とかもあるから、ぜひ活かしてみてね!

琴葉葵

上手く位置情報を利用できたら、「ポケモンGO」みたいな位置情報を利用したゲームとかも作れるかもね!

全体のプログラム

import sqlite3
import folium
from streamlit_folium import st_folium
import streamlit as st
import pandas as pd
from geopy.geocoders import Nominatim
from geopy.distance import geodesic


# 土地名から緯度経度の取得
def get_coordinates(address):
    geolocator = Nominatim(user_agent="my_geocoding_app")
    geocoder_result = geolocator.geocode(address, timeout=5.0)
    try:
        return float(geocoder_result.latitude), float(geocoder_result.longitude)
    except Exception as e:
        print(f"接続エラー: {e}")
    return None, None

# データベースから周辺の店を探す
def search_restaurants(lat, lon, radius_km=1.0):
    try:
        conn = sqlite3.connect("restaurants.db")
        # 検索を速くするための大まかな絞り込み(緯度経度の差)
        # 距離オブジェクトの作成
        d = geodesic(kilometers=1.0)

        # 各方位の地点を計算
        # destination(始点, 方位角)  方位角: 0=北, 90=東, 180=南, 270=西
        north = d.destination((lat, lon), 0)
        south = d.destination((lat, lon), 180)
        east = d.destination((lat, lon), 90)
        west = d.destination((lat, lon), 270)
        
        query = f"""
        SELECT * FROM restaurants 
        WHERE lat BETWEEN {south.latitude} AND {north.latitude}
          AND lon BETWEEN {west.longitude} AND {east.longitude}
        """

        df = pd.read_sql_query(query, conn)
        conn.close()
        
        # 正確な距離でフィルタリング
        results = []
        for _, row in df.iterrows():
            dist = geodesic((lat, lon), (row['lat'], row['lon'])).km
            if dist <= radius_km:
                res_dict = row.to_dict()
                res_dict['distance_km'] = dist
                results.append(res_dict)
        
        return sorted(results, key=lambda x: x['distance_km'])
    except Exception as e:
        st.error(f"データベースエラー: {e}")
        return []


# 1. st.form を使って、入力中の勝手な更新を防ぐ
with st.form("search_form"):
    target_place = st.text_input("調べたい場所を入力してください", "渋谷駅")
    search_radius = st.slider("検索範囲 (km)", 0.5, 3.0, 1.0)
    submitted = st.form_submit_button("検索する")

# 2. ボタンが押された時に検索を実行し、結果を session_state に保存する
if submitted:
    if target_place:
        with st.spinner("検索中..."):
            lat, lon = get_coordinates(target_place)
            if lat and lon:
                m = folium.Map(location=[lat, lon], zoom_start=15)
                folium.Marker([lat, lon], popup="検索地点", icon=folium.Icon(color="blue")).add_to(m)
                restaurants = search_restaurants(lat, lon, radius_km=search_radius)
                
                for res in restaurants:
                    folium.Marker(
                        location=[res['lat'], res['lon']], 
                        tooltip=res['name'], 
                        popup=f"{res['name']} ({res['amenity']})", 
                        icon=folium.Icon(color="red", icon="cutlery", prefix="fa")
                    ).add_to(m)

                st_folium(m, width=700, height=500, returned_objects=[])

                st.write(f"「{target_place}」の周辺に {len(restaurants)} 件の飲食店が見つかりました。")
                
                # 結果をテーブルで表示
                if restaurants:
                    df_results = pd.DataFrame(restaurants)
                    st.dataframe(df_results[['name', 'amenity', 'distance_km']], use_container_width=True)
                
            else:
                st.error("指定された場所が見つかりませんでした。")
    else:
        st.error("場所を入力してください。")