UNPKG

butter-lib

Version:

BuTTER Library は、ストレージ上に細分化した状態で保存されているGTFSを基にした時刻表情報を集め、ブラウザ内で必要な情報に加工するライブラリです。DBを使わずにデータ処理をブラウザ内とする

748 lines (683 loc) 26.9 kB
// src/index.js // Top level file is just a mixin of submodules & constants import { ButterInternal } from './internal.js' const protobuf = require('./gtfs-rt_pb.js'); const h3 = require('h3-js/legacy') const haversine = require('haversine') // const protobuf = require('protobufjs') // const protoStr = require('./gtfs-realtime.proto.js') const helperLib = require('./helper.js') const helper = helperLib.helper const defaultButterRootV0 = 'https://butter.takoyaki3.com/v0.0.0/root.json' const defaultButterRootV1 = 'https://butter.takoyaki3.com/v1.0.0/root.json' /** * 2つの数値を加算します。 * @param {number} a - 最初の数値。 * @param {number} b - 2番目の数値。 * @returns {number} - 加算された結果。 */ function addNumbers (a, b) { return a + b } let internal = null // ButterInternalオブジェクトを格納する変数 const Butter = { addNumbers, async getHostUpdated () { await internal.waitCAReady() const hostList = [] for (let i = 0; i < internal.RUNTIME.CA.hosts.length; i++) { const host = internal.RUNTIME.CA.hosts[i] let updated try { // ホストからのデータ取得を試みる const data = await helper.fetchJSON(`${host}/datalist.json`, internal.RUNTIME.pub_key) // updatedをISO 8601形式に変換します if (data.updated === null) continue const updatedISO = data.updated.replace(/_/g, ':') updated = new Date(updatedISO) // Dateオブジェクトを生成します } catch (e) { console.log(`Failed to fetch data from ${host}:`, e.message) // エラーメッセージをログに出力 updated = 'Failed to access' // updatedにアクセス失敗の旨のメッセージをセット } hostList.push({ host, updated }) } return hostList }, getComsumedOp () { return helper.getConsumedOp() }, async getHostDataList () { console.log('getHostDataList') await internal.waitCAReady() console.log('waitCAReady') const data = await helper.fetchJSON(`${internal.RUNTIME.host}/datalist.json`, internal.RUNTIME.pub_key) return data.data_list }, async getAgencyInfo (gtfsID) { await internal.waitCAReady() const data = await helper.fetchJSON(`${internal.RUNTIME.host}/${gtfsID}/info.json`, internal.RUNTIME.pub_key) return data }, async getVersionId (gtfsID, versionID) { await internal.waitCAReady() if (!versionID) { const versionInfo = await Butter.getAgencyInfo(gtfsID) return versionInfo.slice(-1)[0].version_id } return versionID }, /** * GTFSのバージョン情報を取得します。 * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - バージョン情報。 */ async getVersionInfo (gtfsID, versionID) { await internal.waitCAReady() if (!versionID) { const versionInfo = await Butter.getAgencyInfo(gtfsID) versionID = versionInfo.slice(-1)[0].version_id } const data = await helper.fetchJSON(`${internal.RUNTIME.host}/${gtfsID}/${versionID}/info.json`, internal.RUNTIME.pub_key) return data }, /** * GTFSのバス停の情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - バス停の情報。 */ async getBusStops (gtfsID, versionID) {}, /** * GTFSの事業者情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - 事業者情報。 * */ async getAgency (gtfsID, versionID) {}, /** * GTFSのカレンダー情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - カレンダー情報。 */ async getCalendar (gtfsID, versionID) {}, /** * GTFSのカレンダー日付情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - カレンダー日付情報。 */ async getCalendarDates (gtfsID, versionID) {}, /** * GTFSの運賃属性情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - 運賃属性情報。 */ async getFareAttributes (gtfsID, versionID) {}, /** * GTFSの運賃ルール情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - 運賃ルール情報。 */ async getFareRules (gtfsID, versionID) {}, /** * GTFSのフィード情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - フィード情報。 */ async getFeedInfo (gtfsID, versionID) {}, /** * GTFSの事業者情報(日本語)を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - 事業者情報(日本語)。 */ async getOfficeJp (gtfsID, versionID) {}, /** * GTFSの路線情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - 路線情報。 */ async getRoutes (gtfsID, versionID) {}, /** * GTFSのシェイプ情報を取得します. * @param {string} gtfsID - GTFSのID * @param {string} [versionID='optional'] - バージョンID * @returns {Promise<Object>} - シェイプ情報 */ async getShapes (gtfsID, versionID) {}, /** * GTFSの停留所時刻情報を取得します. * @param {string} gtfsID - GTFSのID * @param {string} [versionID='optional'] - バージョンID * @returns {Promise<Object>} - 停留所時刻情報 */ async getStopTimes (gtfsID, versionID) {}, /** * GTFSの乗り換え情報を取得します. * @param {string} gtfsID - GTFSのID * @param {string} [versionID='optional'] - バージョンID * @returns {Promise<Object>} - 乗り換え情報 */ async getTransfers (gtfsID, versionID) {}, /** * GTFSの翻訳情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - 翻訳情報。 */ async getTranslations (gtfsID, versionID) {}, /** * GTFSのトリップ情報を取得します. * @param {string} gtfsID - GTFSのID。 * @param {string} [versionID='optional'] - バージョンID。 * @returns {Promise<Object>} - トリップ情報。 */ async getTrips (gtfsID, versionID) {}, /** * ストップハッシュによる時刻表を取得します。 * @async * @param {string} gtfsID - GTFSのID。 * @param {string} versionID - バージョンID。 * @param {string} stopHash - ストップハッシュ。 * @returns {Promise<Object>} フェッチしたデータ。 */ async getTimeTableByStopHash (gtfsID, versionID, stopHash) { await internal.waitCAReady() versionID = await Butter.getVersionId(gtfsID, versionID) const data = await helper.fetchTarCSV(`${internal.RUNTIME.host}/${gtfsID}/${versionID}/byStops/${stopHash}.tar.gz`, internal.RUNTIME.pub_key) return data }, /** * トリップハッシュによる時刻表を取得します。 * @async * @param {string} gtfsID - GTFSのID。 * @param {string} versionID - バージョンID。 * @param {string} tripHash - トリップハッシュ。 * @returns {Promise<Object>} フェッチしたデータ。 */ async getTimeTableByTripHash (gtfsID, versionID, tripHash) { await internal.waitCAReady() versionID = await Butter.getVersionId(gtfsID, versionID) const data = await helper.fetchTarCSV(`${internal.RUNTIME.host}/${gtfsID}/${versionID}/byTrips/${tripHash}.tar.gz`, internal.RUNTIME.pub_key) return data }, /** * ストップIDによる時刻表を取得します。 * @async * @param {string} gtfsID - GTFSのID。 * @param {string} versionID - バージョンID。 * @param {string} stopID - ストップID。 * @returns {Promise<Object>} 指定したストップIDの時刻表。 */ async getTimeTableByStopID (gtfsID, versionID, stopID) { await internal.waitCAReady() const versionInfo = await Butter.getVersionInfo(gtfsID, versionID) // console.log({ versionInfo }) const stopHash = await helper.hash.v2(stopID, versionInfo.by_stop_hash_value_size) // console.log({ stopHash }) const timeTables = await Butter.getTimeTableByStopHash(gtfsID, versionID, stopHash) // console.log({ timeTables }) return timeTables[stopID] }, /** * トリップIDによる時刻表を取得します。 * @async * @param {string} gtfsID - GTFSのID。 * @param {string} versionID - バージョンID。 * @param {string} tripID - トリップID。 * @returns {Promise<Object>} 指定したトリップIDの時刻表。 */ async getTimeTableByTripID (gtfsID, versionID, tripID) { await internal.waitCAReady() const versionInfo = await Butter.getVersionInfo(gtfsID, versionID) const tripHash = await helper.hash.v2(tripID, versionInfo.by_trip_hash_value_size) const timeTables = await Butter.getTimeTableByTripHash(gtfsID, versionID, tripHash) return timeTables[tripID] }, async getServiceIDs (gtfsID, versionID, dateStr) { await internal.waitCAReady() const data = await Promise.all([ Butter.getCalendar(gtfsID, versionID), Butter.getCalendarDates(gtfsID, versionID) ]) const service = data[0] const calendar = data[1] const special = calendar.filter(e => { return e.date === dateStr }) const addedServiceIds = special .filter(e => e.exception_type === '1') .map(e => e.service_id) const removedServiceIds = special .filter(e => e.exception_type === '2') .map(e => e.service_id) const date = helper.parseDate(dateStr) const weekOfDay = helper.getDayOfWeek(dateStr) const enabled = service.filter(e => { const startDate = helper.parseDate(e.start_date) const endDate = helper.parseDate(e.end_date) const flags = [ e.monday === '1' && weekOfDay === '月', e.tuesday === '1' && weekOfDay === '火', e.wednesday === '1' && weekOfDay === '水', e.thursday === '1' && weekOfDay === '木', e.friday === '1' && weekOfDay === '金', e.saturday === '1' && weekOfDay === '土', e.sunday === '1' && weekOfDay === '日' ] return flags.some(f => f) && date.getTime() >= startDate.getTime() && date.getTime() <= endDate.getTime() }).map(e => e.service_id) const validServiceIds = enabled.filter(id => !removedServiceIds.includes(id)) const finalServiceIds = [...validServiceIds, ...addedServiceIds] return finalServiceIds }, /** * 指定したストップIDのトリップを検索します。 * @async * @param {string} gtfsID - GTFSのID。 * @param {string} versionID - バージョンID。 * @param {Array<string>} stopIDs - ストップIDの配列。 * @returns {Promise<Array<string>>} 交差するトリップIDの配列。 */ async findTrips (gtfsID, versionID, stopIDs) { const data = [] for (const stopID of stopIDs) { data.push(await Butter.getTimeTableByStopID(gtfsID, versionID, stopID)) } const trips = data.map(x => new Set(x.map(y => y.trip_id))) const interSection = trips.reduce((prev, cur) => helper.setIntersection(prev, cur), trips.length > 0 ? trips[0] : new Set()) // TODO コーナーケースの検討 return [...interSection] }, /** * 指定した日付のストップIDによる時刻表を検索します。 * @async * @param {string} gtfsID - GTFSのID。 * @param {string} versionID - バージョンID。 * @param {string} stopID - ストップID。 * @param {string} date - 日付。 * @returns {Promise<Array<Object>>} 指定したストップIDと日付の時刻表。 */ async findTimeTableByStopID (gtfsID, versionID, stopID, date) { await internal.waitCAReady() const data = await Promise.all([ Butter.getTimeTableByStopID(gtfsID, versionID, stopID), Butter.getServiceIDs(gtfsID, versionID, date) ]) const timetable = data[0] const services = data[1] let ret = [] for (const serviceID of services) { ret = ret.concat( timetable.filter(e => e.service_id === serviceID) ) } return ret }, /** * トリップIDによる時刻表を検索します。 * @async * @param {string} gtfsID - GTFSのID。 * @param {string} versionID - バージョンID。 * @param {Array<string>} TripIDs - トリップIDの配列。 * @returns {Promise<Array<Object>>} 指定したトリップIDの時刻表。 */ async findTimeTableByTripIDs (gtfsID, versionID, TripIDs) { const data = await Promise.all(TripIDs.map(e => Butter.getTimeTableByTripID(gtfsID, versionID, e))) let ret = [] for (const e of data) { ret = ret.concat(e) } return ret }, async fetchTimeTableV1 (gtfsID, options, version) { await internal.waitCAReady() // const optionSample = { // // INDEX1 // stop_ids: ['出発するバス停のID', '目的地のバス停ID', '経由地のバス停ID'], // // INDEX1-REQUIRED OPTION // date: '20230101', // // INDEX2 // trip_ids: [ // '取得したいバスのID' // ], // // FILTER1 // start_time: 'いつのバスからリストアップするか', // // FILTER2 // end_time: 'いつまでのバスをリストアップするか', // // TODO 距離・位置に関する処理 // positions: [ // { lat: '', lon: '', r: '半径' } // ]// 優先度低い // } if (!version) { const versionInfo = await Butter.getAgencyInfo(gtfsID) version = versionInfo.slice(-1)[0].version_id } let stop_times = [] if (options.stop_ids) { if (!options.date) { throw new Error('date is required when stop_ids are selected') } const stopID = options.stop_ids[0] const constraints = options.stop_ids const data = await Promise.all([ Butter.findTimeTableByStopID(gtfsID, version, stopID, options.date), Butter.findTrips(gtfsID, version, constraints) ]) const tripSet = new Set(data[1].concat(options.trip_ids || [])) const filtered = data[0].filter(e => tripSet.has(e.trip_id)) for (const e of filtered) { let headsign = e.stop_headsign if (!headsign) { headsign = e.trip_headsign } stop_times.push({ trip_id: e.trip_id, stop_id: e.stop_id, arrival_time: e.arrival_time, departure_time: e.departure_time, headsign, stop_headsign: e.stop_headsign, trip_headsign: e.trip_headsign, stop_name: e.stop_name, predict_time: 'NOT IMPLEMENTED' }) } } else if (options.trip_ids) { const data = await Butter.findTimeTableByTripIDs(gtfsID, version, options.trip_ids) for (const e of data) { let headsign = e.stop_headsign if (!headsign) { headsign = e.trip_headsign } stop_times.push({ trip_id: e.trip_id, stop_id: e.stop_id, arrival_time: e.arrival_time, departure_time: e.departure_time, headsign, stop_headsign: e.stop_headsign, trip_headsign: e.trip_headsign, stop_name: e.stop_name, predict_time: 'NOT IMPLEMENTED' }) } } else { throw new Error('stop_ids or trip_ids are required') } if (options.start_time) { stop_times = stop_times.filter(e => e.arrival_time >= options.start_time) } if (options.end_time) { stop_times = stop_times.filter(e => e.arrival_time <= options.end_time) } stop_times = stop_times.sort((a, b) => { if (a.arrival_time < b.arrival_time) return -1 if (a.arrival_time > b.arrival_time) return 1 if (a.trip_id < b.trip_id) return -1 if (a.trip_id > b.trip_id) return 1 return 0 }) return { stop_times: Object.values(stop_times), properties: 'NOT IMPLEMENTED' } }, async getStopsWithinRadius (lat, lon, radius) { try { await internal.waitCAReady() const h3Index = h3.geoToH3(lat, lon, 7) console.log('h3Index', h3Index) console.log('lat', lat, 'lon', lon, 'radius', radius) const url = `${internal.RUNTIME.host}/byH3index/${h3Index}_stops.csv` console.log({url}) const stopData = await helper.fetchCSV(url, internal.RUNTIME.pub_key) const stops = stopData const stopsWithinRadius = stops.filter(stop => { const start = { latitude: lat, longitude: lon } const end = { latitude: stop.stop_lat, longitude: stop.stop_lon } const distance = haversine(start, end, { unit: 'meter' }) return distance <= radius }) return stopsWithinRadius } catch (error) { console.log(`no stops data: ${error.message}`) return [] } }, async getStopsBySubstring (substring) { await internal.waitCAReady() try { const url = `${internal.RUNTIME.host}/n-gram/${encodeURIComponent(substring[0])}.csv` const csvData = await helper.fetchCSV(url) const stops = [] for (const record of csvData) { // stop_name に substring が含まれる場合のみ、結果に追加 if (record.stop_name && record.stop_name.includes(substring)) { stops.push(record) } } return stops } catch (error) { console.error(`Error fetching stops data: ${error.message}`) return [] } }, async getVehiclePositionFromURL (url) { await internal.waitCAReady(); try { // GTFS-RTデータの取得 url = 'https://cros-proxy.butter.takoyaki3.com/' + url console.log('url', url) const data = await helper.fetchAsArrayBuffer(url) // Protobufメッセージのデコード const message = protobuf.FeedMessage.deserializeBinary(new Uint8Array(data)); const entity = message.toObject(); // バスの位置情報の取得 return entity.entityList; } catch (error) { console.error('Error fetching vehicle position data:', error); throw error; } }, async getVehiclePositionUrls () { await internal.waitCAReady() const url = `${internal.RUNTIME.host}/datalist.json` // JSONデータが存在するURLを指定 try { const data = await helper.fetchJSON(url) const result = {} for (const item of data.data_list) { if (item.VehiclePosition_url) { result[item.gtfs_id] = item.VehiclePosition_url } } return result } catch (error) { console.error('Error fetching vehicle position URLs:', error) throw error } }, async getBusInfo (latitude, longitude) { await internal.waitCAReady() const vehiclePositionUrls = await this.getVehiclePositionUrls() // Convert latitude and longitude to H3 index const h3Index = h3.geoToH3(latitude, longitude, 7) try { const url = `${internal.RUNTIME.host}/byH3index/${h3Index}_stops.csv` const data = await helper.fetchCSV(url) console.log(data.length) if(!data) return [] const gtfsIds = [] data.forEach(stop => { if (!gtfsIds.includes(stop.gtfs_id)) { gtfsIds.push(stop.gtfs_id) } }) const busInfos = [] for (const gtfs_id of gtfsIds) { if (!(gtfs_id in vehiclePositionUrls)) continue const url = vehiclePositionUrls[gtfs_id] if (url === null) continue const busInfo = await this.getVehiclePositionFromURL(url) busInfos.push(busInfo) } return busInfos } catch (error) { console.error(`Error fetching bus info data: ${error.message}`) return [] } }, async getDataInfo (gtfs_id) { const dataList = await this.getHostDataList() for (const i in dataList) { if (dataList[i].gtfs_id === gtfs_id) return dataList[i] } }, /** * 初期化処理を行います。 */ async init (butterRoot = defaultButterRootV0, config = { useFetch: false, version: 'v0.0.0'}) { helper.setUseFetch(config.useFetch) if (config.version === 'v0.0.0' || butterRoot !== defaultButterRootV0) { // Load ButterRoot v0.0.0 internal = new ButterInternal(butterRoot) } else { // Load ButterRoot v1.0.0 internal = new ButterInternal(defaultButterRootV1) } /** * getXXX系の関数は同一の処理なので,ディクショナリを用いて一括で定義する */ const urls = { getBusStops: 'GTFS/stops.txt', getAgency: 'GTFS/agency.txt', getCalendar: 'GTFS/calendar.txt', getCalendarDates: 'GTFS/calendar_dates.txt', getFareAttributes: 'GTFS/fare_attributes.txt', getFareRules: 'GTFS/fare_rules.txt', getFeedInfo: 'GTFS/feed_info.txt', getOfficeJp: 'GTFS/office_jp.txt', getRoutes: 'GTFS/routes.txt', getShapes: 'GTFS/shapes.txt', getStopTimes: 'GTFS/stop_times.txt', getTransfers: 'GTFS/transfers.txt', getTranslations: 'GTFS/translations.txt', getTrips: 'GTFS/trips.txt' } // https://butter.oozora283.com/ToeiBus/2023-12-20T11_34_38Z_00/info.json /** * getXXX系の関数を生成するための関数 * @param {string} target - 生成する関数の名前 * @returns {function} - 生成された関数 */ const getFactory = (target) => { return async (gtfsID, versionID) => { await internal.waitCAReady() versionID = await Butter.getVersionId(gtfsID, versionID) const data = await helper.fetchCSV(`${internal.RUNTIME.host}/${gtfsID}/${versionID}/${urls[target]}`, internal.RUNTIME.pub_key) return data } } for (const key in urls) { Butter[key] = getFactory(key) } }, async getBusRealTimeInfo (obj) { const vehiclePositionUrls = await this.getVehiclePositionUrls() if (!(obj.gtfs_id in vehiclePositionUrls)) return [] const url = vehiclePositionUrls[obj.gtfs_id] if (url === null) return [] const busInfo = await this.getVehiclePositionFromURL(url) return busInfo }, async getStopsForBusPassingThrough (gtfsId, stopId, versionId) { await internal.waitCAReady() if (!versionId) { const versionInfo = await Butter.getAgencyInfo(gtfsId) versionId = versionInfo.slice(-1)[0].version_id } // 指定されたバス停を通るバスの時刻表を取得 const stopTimes = await this.getTimeTableByStopID(gtfsId, versionId, stopId) // 各バスのトリップIDを取得 const tripIDs = stopTimes.map(stopTime => stopTime.trip_id) // 重複を除去 const uniqueTripIds = [...new Set(tripIDs)] // 各トリップIDに対して、バス停一覧を取得 const stopsForTrips = await Promise.all( uniqueTripIds.map(tripId => this.getTimeTableByTripID(gtfsId, versionId, tripId)) ) const stops = [] stopsForTrips.forEach(stopTimes => stops.push(...stopTimes.map(stopTime => stopTime.stop_id))) return [...new Set(stops)] }, async getRealTimePositionsByGtfsId (gtfsId) { await internal.waitCAReady() try { const vehiclePositionUrls = await this.getVehiclePositionUrls() if (!(gtfsId in vehiclePositionUrls)) return [] const url = vehiclePositionUrls[gtfsId] if (!url) return [] console.log('url', url) const busInfo = await this.getVehiclePositionFromURL(url) const busInfo2 = busInfo.map(e => { return { gtfs_id: gtfsId, ...e } }) const busInfo3 = busInfo2.map(e => helper.convertKeysToSnakeCase(e)) return busInfo3 } catch (error) { console.error(`Error fetching bus info data: ${gtfsId} ${error.message}`) return [] } }, async getRealTimePositionsByLatLon (latitude, longitude) { await internal.waitCAReady() // Convert latitude and longitude to H3 index const h3Index = h3.geoToH3(latitude, longitude, 7) try { const url = `${internal.RUNTIME.host}/byH3index/${h3Index}_stops.csv` const data = await helper.fetchJSON(url) const lines = data.split('\n') const headers = lines[0].split(',') const gtfsIds = [] for (let i = 1; i < lines.length; i++) { if (lines[i].trim() === '') continue const values = lines[i].split(',') const stop = {} for (let j = 0; j < headers.length; j++) { stop[headers[j]] = values[j] } // Include the gtfs_id in the result if it's not already there if (!gtfsIds.includes(stop.gtfs_id)) { gtfsIds.push(stop.gtfs_id) } } const busInfos = [] for (const gtfsId of gtfsIds) { const vehiclePositionUrls = await this.getVehiclePositionUrls() if (!(gtfsId in vehiclePositionUrls)) continue const url = vehiclePositionUrls[gtfsId] if (url === null) continue const busInfo = await this.getVehiclePositionFromURL(url) busInfos.push(...busInfo) } return busInfos } catch (error) { console.error(`Error fetching bus info data: ${error.message}`) return [] } }, } // module.exports = Butter; export default Butter