📍

住所から緯度経度を取得し、地図上に可視化するアプリの最小構成検討

2024/02/25に公開

第一に、地図上に表示したいポイントの住所を「Location」というカラムに記載したCSVファイルを用意する。
今回は美味しいアイスでお馴染み、シャトレーゼの工場の住所をお借りした。

# Factory_name Location
1 白州工場 山梨県北杜市白州町白須大原8383−1
2 豊富工場 山梨県中央市高部1111-1

CSVには住所しか含まれておらず、地図上に可視化するために使用する緯度経度の情報を付与したい。
そこで、今回は下記のコードで地理座標を付与する。
(今回はMapboxのGeocoding APIを使用したが、ここは任意のAPIを使用すれば良い。)

import pandas as pd
import requests

def get_lat_lng_from_address(address, access_token):
    """
    Mapbox Geocoding APIを使用して、与えられた住所の緯度経度を取得する。
    """
    endpoint_url = "https://api.mapbox.com/geocoding/v5/mapbox.places/"
    params = {
        'access_token': access_token,
        'limit': 1,  # 最も関連性の高い結果のみを返す
        'query': address
    }
    response = requests.get(f"{endpoint_url}{address}.json", params=params)
    if response.status_code == 200:
        results = response.json()['features']
        if results:
            location = results[0]['geometry']['coordinates']  # 経度、緯度の順番で返される
            return location[1], location[0]  # 緯度、経度の順番に変更して返す
    return None, None

def add_lat_lng_to_csv(input_csv_path, output_csv_path, access_token):
    """
    CSVファイルを読み込み、各行の住所から緯度経度を取得して新しいカラムに追加し、
    結果を新しいCSVファイルに保存する。
    """
    df = pd.read_csv(input_csv_path)
    lat_lng = df['Location'].apply(lambda x: get_lat_lng_from_address(x, access_token))
    df['緯度'], df['経度'] = zip(*lat_lng)
    df.to_csv(output_csv_path, index=False)

# 使用例
ACCESS_TOKEN = 'APIkey'  
input_csv_path = 'factory.csv'  # 入力CSVファイルのパス
output_csv_path = 'output.csv'  # 出力CSVファイルのパス

add_lat_lng_to_csv(input_csv_path, output_csv_path, ACCESS_TOKEN)

次に、可視化部に移る。
まず、全体のディレクトリ構造は以下の通り。
backend
L main.py
L output.csv
frontend
L index.html

バックエンドのmain.pyはFastAPIを使用した。

import requests
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, JSONResponse
import pandas as pd
from fastapi.middleware.cors import CORSMiddleware

def get_weather_info(latitude, longitude, api_key):
    url = f"https://api.tomorrow.io/v4/timelines"
    querystring = {
        "location": f"{latitude},{longitude}",
        "fields": ["temperature", "weatherCode"],
        "units": "metric",
        "timesteps": "1h",
        "apikey": api_key
    }

    response = requests.request("GET", url, params=querystring)
    if response.status_code == 200:
        data = response.json()
        # 天気情報の取得、ここでは最初の時間のデータを取得しています。
        weather_info = data['data']['timelines'][0]['intervals'][0]['values']
        return weather_info
    else:
        print(response.status_code)
        return None

# APIキーを設定
API_KEY = 'Tomorrow.io API key' 

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 安全な設定にするためには、"*"の代わりに具体的なオリジンを指定してください。
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/", response_class=HTMLResponse)
async def read_root():
    # CSVファイルから緯度経度データを読み込む
    df = pd.read_csv('output.csv')
    
    # HTMLの内容を構築
    html_content = "<h1>工場の天気情報</h1>"
    for index, row in df.iterrows():
        weather_info = get_weather_info(row['緯度'], row['経度'], API_KEY)
        html_content += f"<p>{row['Factory_name']}: {weather_info}</p>"
    
    return html_content


@app.get("/weather")
async def get_weather():
    df = pd.read_csv('output.csv')  # CSVファイルからデータを読み込む
    # 緯度、経度、工場名、天気情報を含むJSONを生成
    data = []
    for index, row in df.iterrows():
        temp = get_weather_info(row['緯度'], row['経度'], API_KEY)
        print("temp:",temp)
        data.append({
            "latitude": row["緯度"],
            "longitude": row["経度"],
            "factoryName": row["Factory_name"],
            "weather": "sunny"
        })

    print("Response : ",data)
    return JSONResponse(content=data)

backendのHTMLファイルは下記。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>工場の位置と天気情報</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
    <link href="https://api.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.css" rel="stylesheet">
    <style>
        body { margin: 0; padding: 0; }
        #map { position: absolute; top: 0; bottom: 0; width: 100%; }
    </style>
</head>
<body>

<div id="map"></div>

<script src="https://api.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.js"></script>
<script>
    mapboxgl.accessToken = 'Mapbox token';
    var map = new mapboxgl.Map({
        container: 'map', // container ID
        style: 'mapbox://styles/mapbox/streets-v11', // style URL
        center: [139.6917, 35.6895], // 初期位置を日本に設定
        zoom: 5 // 初期ズームレベル
    });

    map.on('load', function () {
        // FastAPIから天気情報を取得し、地図上にマーカーを追加
        fetch('http://localhost:8000/weather') // FastAPIのエンドポイントに合わせて変更
        .then(response => response.json())
        .then(data => {
            data.forEach(item => {
                // マーカーを作成して地図上に追加
                new mapboxgl.Marker()
                    .setLngLat([item.longitude, item.latitude])
                    .setPopup(new mapboxgl.Popup({ offset: 25 }) // ポップアップの追加
                        .setHTML(`<h3>${item.factoryName}</h3><p>${item.weather}</p>`))
                    .addTo(map);
            });
        })
        .catch(error => console.log('Error:', error));
    });
</script>

</body>
</html>

#一部天気を取得しようとして諦めた痕跡が残っているが、これは成功したら更新する予定。

上記の配置が完了したら、下記のコマンドでFastAPIを起動。

uvicorn backend.main:app --reload

サーバ起動後、backend/index.htmlを起動すると、地図が表示される。

Discussion