Open7

OpenStreetMapで地図を表示して、対象の駅を示す

tadokunotadokuno

オムクエの地図と駅リストの作成

オムクエで最も重要な要素となる、地図の表示と駅リストの表示を行う。

駅リストの入手

駅データ.jpがフリーのデータを提供しているので、これを利用する。
駅名だけではなく、住所や緯度経度の情報も含まれているので、これを利用して、station_masterを整備しておく。そうすることで、OpenStreetMapに駅を表示する際に、表示している範囲の駅をリスト表示する際には、SQLでのデータベースアクセスとなり、外部APIの呼び出しが不要になる。

現時点での、station_masterのスキーマはこうなる。

結果、マスターには1万レコードの駅情報が入っている。

tadokunotadokuno

ChatGPTは嘘ばかり

①Webブラウザで開いたHTMLから、.envの変数を参照できると言い張る。
②const supabase = supabase.createClient(env.supabaseUrl, env.supabaseKey);で、右辺と左辺のsupabaseが異なるので(右辺はsupabaseのオブジェクトだと言い張る)問題は無いと言い張る。

結局、.envを参照するためのサーバレス関数を定義して、それをfetchで呼び出すことにした。
また、const supabase は、const supabaseClient と変数名を変えて動作することができた。

.envを参照するためには、ローカルのファイルシステムにアクセスする必要があって、Webブラウザからはそれは無理なので出来るはずはないとか、言語仕様上、そんなことはあり得ないとか、そういう原則から思考することがLLMにはできない。

自分も、もしかしたらできるのでは、とやってみたのが馬鹿でした。

tadokunotadokuno

サーバーレス関数の実装

functions で実装しているデータベースやファイルを参照する関数をHTMLファイルから呼び出すのに、fetchを使って使えるようにしたい。

HTMLから直接呼び出さないのは、LINE Botからの処理とロジックの部分は共有したかったから。

手始めに、環境変数を取り出すものを作る。

functions/getEnv.js
export async function handler(event, context) {
  return {
    statusCode: 200,
    body: JSON.stringify({
      supabaseUrl: process.env.SUPABASE_API_URL,
      supabaseKey: process.env.SUPABASE_API_KEY
    }),
  };
};

呼び出し側

const response = await fetch('/.netlify/functions/getEnv');  // サーバーレス関数を呼び出す
const env = await response.json();
supabaseClient = supabase.createClient(env.supabaseUrl, env.supabaseKey);

これで、HTMLファイルからsupabaseのDBにアクセスできるようになった。

次に、

export async function fetchOmuriceIndexData(stationName,station_id,lat,lng) {
  // DB を検索して、必要な情報を得る。
  // 検索できない場合は、nullを返す。
  const result = {
    index,
    stationName: stationName,
    lat,
    lng,
    ....
  };
  return result;
}

と定義されている関数を呼び出す。

先ほどの、環境変数を取得するものと同じように作る。

functions/fetchOmuIndexData.js
import { fetchOmuriceIndexData } from './regOmuIndex.js';

export async function handler(event, context) {
  // クエリパラメータを取得
  const params = event.queryStringParameters;
  const station_name = params.station_name;
  const station_id = params.station_id;
  const lat = params.lat;
  const lng = params.lng;

  const result = await fetchOmuriceIndexData(station_name,station_id,lat,lng);  

  return {
    statusCode: 200,
    body: JSON.stringify(result),
  };
};

呼び出し側は、この関数を使用する。

newmap.html
async function fetchOmuIndexData(station_name,station_id,lat,lng) {
      const result = await fetch(`/.netlify/functions/fetchOmuIndexData?station_name=${station_name}&station_id=${station_id}&lat=${lat}&lng=${lng}`);
      console.log(`RESULT::: ${result}`);
      return await result.json();
}

この関数は、必要な情報をセットして、

newmap.html
const result = await fetchOmuIndexData(station.station_name,station.station_id,station.lat,station.lng);

と呼び出される。

実際に実行してみると、GETリクエストはサーバーに出ており、fetchOmuriceIndexData()が正しい引数で呼び出されることは確認したが、戻り値が返ってこない。また、fetch()の後にデバッグ文として入れているconsole.log(`RESULT::: ${result}`); が実行されている形跡が無い。

サーバーからは、検索結果がJSON形式で返っていることは確認している。

tadokunotadokuno

本当に検索結果が戻っているのか

実際には、fetchの呼び出しは連続している。
コンソールのログを見ると、fetch()はつづけさまに呼び出されている。6回のfetch()が呼び出されてから、そのあとに、Response with status 200 in 117 ms. というログが出ている所を見ると、GETリクエストのレスポンスを待つことなく、次のGETリクエストをしているように見える。

おそらく、これが問題で戻り値が得られていないのだろうと今のところは考えている。

現在、デバッグ中のものは、
https://github.com/tadokuno/omu-rice-index.git

に入っている。

作成中のファイルはこちら

newmap.html
newmap.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simplebar@5.3.6/dist/simplebar.min.css"/>
  <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/simplebar@5.3.6/dist/simplebar.min.js"></script>

  <title>オムライス指数マップ</title>
  <style>
    .window {
      border: 2px solid black;
      background-color: #FFFFFF;
/*      box-shadow: 3px 3px #808080; */
      position: relative;
      overflow: hidden; 
    }

    .title-bar {
      background-color: #000080;
      color: white;
      padding: 5px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .title {
      font-size: 14px;
    }

    /* マップのサイズを指定 */
    #map {
      height: 50vh;
      width: 100%;
    }

    /* オムライス指数リストのスタイル */
    #omurice-list {
      overflow-y: scroll;
      border: 1px solid #ccc; /* 見やすさのためにボーダーを追加(任意) */
      padding: 10px;
      background-color: #f0f0f0;
    }

    .content {
      padding: 10px;
      height: 260px; /* タイトルバーの分を引いた高さ */
      overflow-y: scroll; /* 常にスクロールバーを表示 */
      font-size: 12px;
      background-color: white;
      box-sizing: border-box;
      -webkit-overflow-scrolling: touch; /* iOSでのスムーズスクロール */
    }

    table {
      width: 100%;
      border-collapse: collapse;
    }

    th, td {
      padding: 8px;
      text-align: left;
      border-bottom: 1px solid #ddd;
    }

    th {
      background-color: #333;
      color: white;
    }
  </style>
  <!-- Leaflet.jsのCSSを読み込み -->
  <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
</head>
<body>
  <div class="window">
    <div class="title-bar">
      <span class="title">OmuQue Manager</span>
      <div class="buttons">
        <button class="minimize">-</button>
        <button class="close">x</button>
      </div>
    </div>
    <div id="map"></div>
    <div class="content" data-simplebar data-simplebar-auto-hide="false">
      <div id="omurice-list">
      <table>
        <thead>
          <tr>
            <th>駅名</th>
            <th>オムライス指数</th>
          </tr>
        </thead>
        <tbody id="station-list">
          <!-- 駅とオムライス指数がここに動的に表示されます -->
        </tbody>
      </table>
      </div>
    </div>
  </div>

  <!-- Leaflet.jsのJavaScriptを読み込み -->
  <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
  <!-- Supabase用のスクリプト -->
  <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js"></script>

  <script>
    let map;
    let markerGroup;
    let supabaseClient;

    // Leafletマップの初期化
    async function initializeMap(lat, lng) {
      map = L.map('map').setView([lat, lng], 12);

      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap contributors'
      }).addTo(map);

      // 地図が移動したりリサイズされたときに駅リストを更新
      map.on('moveend', fetchStationData);   // 地図の位置が変更されたとき
      map.on('resize', fetchStationData);    // 地図がリサイズされたとき
      // 駅データを取得してマップに表示
      await fetchStationData();
    }

    async function initMap() {
      const response = await fetch('/.netlify/functions/getEnv');  // サーバーレス関数を呼び出す
      const env = await response.json();
      supabaseClient = supabase.createClient(env.supabaseUrl, env.supabaseKey);

      // 東京駅をデフォルトとして、現在地を中心に設定
      let lat = 35.681236;
      let lng = 139.767125;
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(function (position) {
          console.log(position.coords);
          const lat = position.coords.latitude;
          const lng = position.coords.longitude;
          initializeMap(lat,lng);
        }, function () {
          initializeMap(35.681236, 139.767125); // 東京駅
        });
      } else {
        initializeMap(35.681236, 139.767125); // 東京駅
      }
    }
    // 駅のデータを基にマップとリストを更新
    function updateMapAndList(stations) {
      const stationList = document.getElementById('station-list');
      stationList.innerHTML = ''; // 既存のリストをクリア

      stations.forEach(station => {
        // マップにマーカーを追加
        const marker = L.marker([station.lat, station.lng]).addTo(map);
        marker.bindPopup(`<b>${station.station_name}</b><br>オムライス指数: ${station.index}`);

        // リストにオムライス指数を追加
        const row = document.createElement('tr');
        row.innerHTML = `<td>${station.station_name}</td><td>${station.index}</td>`;
        stationList.appendChild(row);
      });
    }
    async function fetchStationData() {
      const bounds = map.getBounds();
      const ne = bounds.getNorthEast();
      const sw = bounds.getSouthWest();

      // Supabaseからstation_masterテーブルの駅データを取得
      const { data, error } = await supabaseClient
        .from('station_master')
        .select('*')
        .gte('lat', sw.lat)
        .lte('lat', ne.lat)
        .gte('lng', sw.lng)
        .lte('lng', ne.lng);

      // オムライス指数を取得
      data.forEach( async station => {
        const result = await fetchOmuIndexData(station.station_name,station.station_id,station.lat,station.lng);
        if( result != null ) {
          station.index = result.index;
        }
      })

      if (error) {
        console.error('Error fetching station data:', error);
        return;
      }
      updateMapAndList(data);
    }
    async function fetchOmuIndexData(station_name,station_id,lat,lng) {
      const result = await fetch(`/.netlify/functions/fetchOmuIndexData?station_name=${station_name}&station_id=${station_id}&lat=${lat}&lng=${lng}`);
      console.log(`RESULT::: ${result}`);
      return await result.json();
    }
    // ページ読み込み時にマップを初期化
    window.onload = initMap;
  </script>
</body>
</html>
tadokunotadokuno

単に、戻り値の取り方がが間違っていたのかも。
ちょっと分からなくなってきた。

tadokunotadokuno

環境変数を取り出すサーバーレス関数を実装するのは、セキュリティリスクになることに気が付いた。
API_KEYなど隠蔽する情報が必要な処理は、フロントエンドに書くことができない。
サーバーサイドに実装するよう修正し、getEnvは破棄する。

tadokunotadokuno
      data.forEach( async station => {
        const result = await fetchOmuIndexData(station.station_name,station.station_id,station.lat,station.lng);
        if( result != null ) {
          station.index = result.index;
        }
      })

この、forEachで呼び出している関数はawaitで呼び出していない。

ここを、

      for (const station of data) {
        const result = await fetchOmuIndexData(station.station_name, station.station_id, station.lat, station.lng);
        if (result != null) {
          station.index = result.index?result.index:0;
        }
      }

と書き換えて、無事解決した。
ChatGPTは無名関数を使いまくって、可読性の低いコードを出してくるので、ちょっと困る。