見出し画像

AIエージェント元年 第10話

スタンドみたいにパーソナライズされたAIアプリがあると、いいですよね。
今年と言わず今月はまず交通系アプリを開発したいと考えています。

東京だと山手線のように次から次と電車が来るので、何時に公共交通機関が来るというのはあまり重要ではないかもしれませんが、常に徒歩時間を逆算し何時に乗れる予定か教えてくれる、そんなアプリを作りたいと思います。

iPhoneの時計って、リアルタイムで動いてくれるじゃないですか?あんな感じで、次に乗れる可能性のある時間を常時表示してくれるような、ありそうでない、そんなアプリを目指したいと思います。ゆくゆくはインバウンド向けなどサービス拡充していきたいですね。

実装方針は以下です。

  • Google APIを用いてGPSを取得し、最寄り駅を算出

  • 同様に徒歩時間を算出

  • 札幌のオープンデータを用いて、地下鉄の時刻データベースを作る

まずは札幌の地下鉄にフォーカスします。APIを使えば、簡単に乗り物も時刻情報も取得できそうですが、基本的には自前でJson形式のオープンデータを持ちたいので、GPSとか特殊なもの以外は極力データを手元に寄せ、開発していきたいと思います。

2025.01.04時点の成果物

時間は3候補あげるようにしたかったのですが、undefinedになってしまいました。

フロントはReact Nativeで、バックエンドはNode.jsで書きました。

const { Client } = require('pg');

// Database connection configuration
const dbConfig = {
  user: process.env.DB_USER,
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  password: process.env.DB_PASSWORD,
  port: process.env.DB_PORT || 5432,
};

// Main handler function
exports.handler = async (event) => {
  let client = null;

  try {
    // Parse input parameters
    let nearestStation, walkingMinutes;

    if (typeof event === 'string') {
      const parsedEvent = JSON.parse(event);
      nearestStation = parsedEvent.nearestStation;
      walkingMinutes = parsedEvent.walkingMinutes;
    } else if (event.body) {
      const parsedBody = JSON.parse(event.body);
      nearestStation = parsedBody.nearestStation;
      walkingMinutes = parsedBody.walkingMinutes;
    } else {
      nearestStation = event.nearestStation;
      walkingMinutes = event.walkingMinutes;
    }

    // Basic validation
    if (!nearestStation || typeof walkingMinutes !== 'number') {
      return {
        statusCode: 400,
        body: JSON.stringify({
          error: 'Invalid parameters. Required: nearestStation (string) and walkingMinutes (number)',
        }),
      };
    }

    // Get current JST time and calculate arrival time
    const jstNow = new Date(new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }));
    const arrivalTime = new Date(jstNow);
    arrivalTime.setMinutes(arrivalTime.getMinutes() + walkingMinutes);

    // Get the hour part of the arrival time directly
    const validDepartureHour = arrivalTime.getHours();

    // Connect to database
    client = new Client(dbConfig);
    await client.connect();

    const arrivalMinute = arrivalTime.getMinutes();
    const nextHour = validDepartureHour + 1; // 現在の時間に1を足す

    // Simplified query for next available trains (current hour and next hour)
    const query = `
    SELECT 
      "direction", 
      "hour", 
      "minutes", 
      "nearestStation", 
      "weekdayOrEnd"
    FROM "TimeSchedule"
    WHERE "nearestStation" = $1
      AND "hour" = $2
      AND "weekdayOrEnd" = $3
    UNION ALL
    SELECT 
      "direction", 
      "hour", 
      "minutes", 
      "nearestStation", 
      "weekdayOrEnd"
    FROM "TimeSchedule"
    WHERE "nearestStation" = $1
      AND "hour" = $4
      AND "weekdayOrEnd" = $3
    LIMIT 2;`;

    const params = [
      nearestStation,
      validDepartureHour + "時", // Current hour
      arrivalTime.getDay() === 0 || arrivalTime.getDay() === 6 ? 'weekend' : 'weekday',
      nextHour + "時", // Next hour
    ];

    const res = await client.query(query, params);

    if (res.rows.length > 0) {
      const schedules = res.rows.map((row) => {
        // Split the minutes string into an array and filter it based on arrivalTime's minute
        const filteredMinutes = row.minutes
      .split(' ')  // Split the string into an array
      .filter(minute => parseInt(minute) >= arrivalMinute)  // Filter minutes >= arrivalMinute
      .slice(0, 3)  // Take only the first 3
      .join(' ');  // Join the filtered minutes back into a space-separated string

        return {
          direction: row.direction,
          hour: row.hour,
          minutes: filteredMinutes, // Return the filtered minutes
          nearestStation: row.nearestStation,
          weekdayOrEnd: row.weekdayOrEnd,
          searchInfo: {
            queryTime: jstNow.toLocaleTimeString("ja-JP"),
            arrivalTime: arrivalTime.toLocaleTimeString("ja-JP"),
            walkingMinutes: walkingMinutes,
          },
        };
      });

       // Check if we have less than 3 results, and fetch from the next hour if necessary
       if (schedules[0].minutes.split(' ').length < 3) {
        // Fetch missing minutes from the next hour's data
        const missingMinutesCount = 3 - schedules[0].minutes.split(' ').length;
        
        const nextHourSchedule = res.rows.find(row => row.hour === (validDepartureHour + 1) + "時");

        if (nextHourSchedule) {
          const additionalMinutes = nextHourSchedule.minutes
            .split(' ')
            .slice(0, missingMinutesCount)  // Take the missing number of minutes
            .join(' ');

          // Append the missing minutes to the current schedule
          schedules[0].minutes += ' ' + additionalMinutes;
        }
      }

      return {
        statusCode: 200,
        body: JSON.stringify(schedules),
      };
    } else {
      return {
        statusCode: 404,
        body: JSON.stringify({
          error: 'No schedules found',
          searchParams: {
            station: nearestStation,
            searchTime: jstNow.toLocaleTimeString('ja-JP'),
            arrivalTime: arrivalTime.toLocaleTimeString('ja-JP'),
            walkingMinutes: walkingMinutes,
            dayType:
              arrivalTime.getDay() === 0 || arrivalTime.getDay() === 6
                ? 'weekend'
                : 'weekday',
          },
        }),
      };
    }
  } catch (error) {
    console.error('Error processing request:', error);

    return {
      statusCode: 500,
      body: JSON.stringify({
        error: 'Internal server error',
        message: error.message,
        type: error.name,
      }),
    };
  } finally {
    if (client) {
      try {
        await client.end();
      } catch (error) {
        console.error('Error closing database connection:', error);
      }
    }
  }
};

明日はこのundefinedを修正し、少なくとも札幌の地下鉄データは全部入れたいと思います(できれば、リリースも着手したい)。

それではまたごきげんよう。

いいなと思ったら応援しよう!