Python x Google maps APIで考えるラボ旅行計画

こんにちは。ラボ旅行係・隊長のわたしです。　

わたしの研究室での仕事といえば、半年に一回あるラボ旅行について、日々考えることなわけです。
　　
例えば、予算とか、（そこそこ）男女平等に楽しめるのかとか、誰かの趣味に偏りすぎてないかとか、みんなが楽しめそうか？、とか定量評価しにくい変数について思い悩んだ結果、いつも結構わたしのやりたいことにみんなを付き合わせることになるわけですよね。

しかしそろそろ、普通にネタ切れなわけで。行く場所も、やることも。

そこでこの記事では、次に行くべき都市を楽に選ぶ方法を考えました。
まあ、chatgptと対話した方が速そうですけどね。

# 前提
（本記事において大事ではない前提）
予算：1人3万円くらい
人数：20人くらい

（本記事において大事かもしれない前提）
工程：一泊二日
また、簡単のため、東京駅をスタート地点として考える。

# 1. Google maps APIキーの取得
この記事を参考に、Places API (New)のキーを取得した。

https://qiita.com/Haruka-Ogawa/items/997401a2edcd20e61037

参考：[Google maps platform](https://developers.google.com/maps?hl=ja)

# 2.都市名リストの取得
地理データベース[GeoNames](https://www.geonames.org/)より、
https://download.geonames.org/export/dump/ から allCountries.txt をダウンロードした。

# 3.コードを書く。

まずは、上記都市名リストを用いて、東京駅から50km以上150km以内の都市をリストアップする。観光するにあたって、そこそこ大きい都市が良いかなと思ったので、人口5万人以上の都市でフィルタリングした。

```python
import csv
from geopy.distance import geodesic

#東京駅の緯度経度
tokyo_station = (35.681236, 139.767125)

min_dist_km = 50
max_dist_km = 150

potential_cities = []

with open("allCountries.txt", "r", encoding="utf-8") as f:
    reader = csv.reader(f, delimiter='\t')
    for row in reader:
        if len(row) < 19:
            continue
        name = row[1]
        lat = float(row[4])
        lon = float(row[5])
        population = int(row[14])
        
        # 人口5万人以上の都市を選択
        if population >= 50000:
            city_coord = (lat, lon)
            dist = geodesic(tokyo_station, city_coord).kilometers
            if min_dist_km <= dist <= max_dist_km:
                potential_cities.append({
                    "name": name, "lat": lat, "lon": lon,
                    "distance_km": dist, "population": population, "location": (lat, lon) })

potential_cities.sort(key=lambda x: x["distance_km"])

print("東京駅から50km以上150km以内の主要都市候補:")
print(len(potential_cities))
```
これを実行すると次のように出力された。どうやら155都市が選択されたらしい。
```
東京駅から50km以上150km以内の主要都市候補:
155
```

次に、これらの都市がどこにあるのか可視化してみる。

```python
import folium

m = folium.Map(location=tokyo_station, zoom_start=7)

# 東京駅を青マーカーで表示
folium.Marker(
    location=tokyo_station,
    popup="Tokyo Station",
    icon=folium.Icon(color='blue', icon='info-sign')
).add_to(m)

# potential_citiesに含まれる都市を赤マーカーで表示
for city in potential_cities:
    folium.CircleMarker(
        location=(city["lat"], city["lon"]),
        radius=5, color='red',fill=True,
        fill_color='red',fill_opacity=0.7,
        popup=f"{city['name']} (距離: {city['distance_km']:.1f}km, 人口: {city['population']})").add_to(m)

#これまでにラボ旅行で行った都市をオレンジマーカーで表示
visited_cities = [{"name": "Nikko", "lat": 36.7190, "lon": 139.6983},{"name": "Atami", "lat": 35.0962, "lon": 139.0773},
    {"name": "Chiba", "lat": 35.6073, "lon": 140.1065},{"name": "Karuizawa", "lat": 36.3552, "lon": 138.5974}]

for city in visited_cities:
    folium.CircleMarker(
        location=(city["lat"], city["lon"]),
        radius=5,
        color='orange',
        fill=True,
        fill_color='orange',
        fill_opacity=0.7,
        popup=city["name"]
    ).add_to(m)

m.save("cities_map.html")
```

このhtmlを開くと以下のような感じになっている。
オレンジが過去に行ったところなので、そこそこいい感じの距離感の都市が選択できていそうであることを確認した。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929126/f95a5313-cc23-a7f0-d194-030d6f1c0417.png)

流石に155都市なんて考えたくないので、これらの都市のうち、「動物園」、「遊園地」、「水族館」のいずれかを持っている都市に絞ってみることにした。

```python
import googlemaps

API_KEY = "<Google Maps APIキー>"
gmaps = googlemaps.Client(key=API_KEY)

def find_activities_in_city(city_name, city_location=None, activity_types=["zoo", "amusement_park", "aquarium"]):
    query = f"{city_name} sightseeing"
    response = gmaps.places_nearby(location=city_location, radius=5000, keyword="sightseeing")
    results = response.get("results", [])
    
    # 観光スポットの抽出
    activities = [{"name": result["name"], "types": result.get("types", [])}
        for result in results
        if any(t in result.get("types", []) for t in activity_types)]
    
    return activities

def normalize_city_name(city_name):
    name = city_name.strip()
    suffixes = [" shi", "-shi"]
    
    lower_name = name.lower()
    for s in suffixes:
        if lower_name.endswith(s):
            name = name[: -len(s)].strip()
            break
    return name.capitalize()

#都市名の重複確認
unique_cities = set()

# 訪問済み都市の除外と観光スポットを用いた都市のフィルタリング
filtered_cities = []
for city in potential_cities:
    city_name = city["name"]
    
    activities = find_activities_in_city(city_name, city_location=city["location"])
    normalized = normalize_city_name(city_name)
    if activities:
        unique_cities.add(normalized)
        if normalized in visited_cities:
            continue
        else:
            filtered_cities.append({"city": normalized, "activities": activities})
        
print("\n次の旅行先候補:")
if not filtered_cities:
    print("条件に合う都市が見つかりませんでした。")
    
else:
    for city_name in unique_cities:
        city = next((c for c in filtered_cities if c['city'] == city_name), None)
        print(f"都市: {city['city']}")
        print("観光スポット:")
        for activity in city["activities"]:
            print(f" - {activity['name']} (types: {', '.join(activity['types'])})")
        print("---")
```

これの出力は以下のよう。4都市が候補になったようです！
```
次の旅行先候補:
都市: Shizuoka
観光スポット:
 - Nihondaira Zoo (types: zoo, tourist_attraction, point_of_interest, establishment)
---
都市: Hitachi
観光スポット:
 - かみね遊園地 (types: amusement_park, tourist_attraction, point_of_interest, establishment)
---
都市: Choshi
観光スポット:
 - Chōshi Marine Research Institute Co., Ltd. (types: aquarium, travel_agency, point_of_interest, establishment)
---
都市: Koga
観光スポット:
 - Navel Park (types: park, amusement_park, tourist_attraction, point_of_interest, establishment)
---
```

# さいごに

この中だったら、静岡かなあ。