
NASA/JPLのデータを使って、これから24時間以内に地球へ接近する小惑星などの天体を表示するウィジェットです。
「地球から何km」と言われても距離感が分かりにくいため、地球と月の平均距離を基準にして、**「🌕 月までの距離の○倍」**でも表示します。
「こんなに近くを小惑星が通ってるの?」と、宇宙をちょっと身近に感じられるウィジェットです。
このウィジェットで分かること
- ☄️ 24時間以内に接近する天体
- 📏 天体のおおよその大きさ
- 🌕 月までの距離を基準にした接近距離
- 🌍 地球からの実際の距離
- 🕐 最接近する時刻
- 💨 地球との相対速度
- 🟠 PHA(潜在的に危険な小惑星)の判定
ウィジェットの表示内容
NASA/JPLの接近天体データから、対象の時間・距離の条件に当てはまる候補を選びます。
サイズごとの表示
- 小:対象範囲の接近天体を1件表示します。
- 中:接近時刻や距離、速度などを確認できます。
- 大:主な天体と候補の一覧を広いレイアウトで表示します。
設定手順
上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。
- Scriptableを用意する
App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。
- コードをコピーして保存する
下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、地球接近天体モニターなどの名前で保存してください。
保存したらこのページに戻り、次の手順へ進みます。
// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: purple; icon-glyph: meteor; // Version: 1.0.0 // ===================================================== // 地球接近天体モニター - Scriptable Widget // // NASA/JPL Small-Body Database // Close Approach Data API // // ・APIキー不要 // ・今から24時間以内に地球へ接近する天体を取得 // ・最接近順に表示 // ・距離を「月までの距離の○倍」で表示 // ・既知の直径がない場合はH値から概算 // ・PHA(潜在的に危険な小惑星)を判定 // // 小:最接近天体 1件 // 中:最接近天体の詳細 // 大:最接近天体を大きく+24時間以内の一覧 // ===================================================== // ===================================================== // 設定 // ===================================================== const CONFIG = { API: "https://ssd-api.jpl.nasa.gov/cad.api", // 1天文単位 km AU_KM: 149597870.7, // 地球〜月 平均距離 km MOON_KM: 384400, // 最大取得距離 // 0.05 AU ≒ 748万km DIST_MAX: "0.05", // キャッシュ時間 CACHE_MINUTES: 30, // 背景 BG: new Color("#080B14"), // テキスト WHITE: Color.white(), SUB: new Color("#AAB1C5"), MUTED: new Color("#70788C"), // アクセント BLUE: new Color("#78AFFF"), GREEN: new Color("#69D391"), ORANGE: new Color("#FFAA55"), // 区切り線 LINE: new Color("#252A38") }; // ===================================================== // メイン // ===================================================== const family = config.widgetFamily || "medium"; const result = await getApproaches(); let widget; if (family === "small") { widget = createSmall(result); } else if (family === "large") { widget = createLarge(result); } else { widget = createMedium(result); } if (config.runsInWidget) { Script.setWidget(widget); } else { if (family === "small") { await widget.presentSmall(); } else if (family === "large") { await widget.presentLarge(); } else { await widget.presentMedium(); } } Script.complete(); // ===================================================== // NASA/JPLデータ取得 // ===================================================== async function getApproaches() { const fm = FileManager.local(); const cacheDir = fm.joinPath( fm.documentsDirectory(), "neo-monitor" ); const cacheFile = fm.joinPath( cacheDir, "approaches.json" ); // --------------------------------------------------- // キャッシュ確認 // --------------------------------------------------- try { if (fm.fileExists(cacheFile)) { const modified = fm.modificationDate(cacheFile); const age = ( Date.now() - modified.getTime() ) / 1000 / 60; if (age < CONFIG.CACHE_MINUTES) { return JSON.parse( fm.readString(cacheFile) ); } } } catch (e) {} // --------------------------------------------------- // 現在〜24時間 // --------------------------------------------------- const nowJD = Date.now() / 86400000 + 2440587.5; const endJD = nowJD + 1; // API取得範囲は少し広めにする const dateMin = formatDateUTC( new Date( Date.now() - 86400000 ) ); const dateMax = formatDateUTC( new Date( Date.now() + 2 * 86400000 ) ); // --------------------------------------------------- // 通常データ // --------------------------------------------------- const url = CONFIG.API + "?date-min=" + dateMin + "&date-max=" + dateMax + "&dist-max=" + CONFIG.DIST_MAX + "&body=Earth" + "&diameter=true" + "&fullname=true" + "&sort=date"; // --------------------------------------------------- // PHAのみ // --------------------------------------------------- const phaURL = CONFIG.API + "?date-min=" + dateMin + "&date-max=" + dateMax + "&dist-max=" + CONFIG.DIST_MAX + "&body=Earth" + "&pha=true" + "&fullname=true"; try { const [ json, phaJson ] = await Promise.all([ new Request(url).loadJSON(), new Request(phaURL).loadJSON() ]); // ------------------------------------------------- // PHA一覧 // ------------------------------------------------- const phaSet = new Set(); if ( phaJson.data && phaJson.fields ) { const desIndex = phaJson.fields.indexOf("des"); if (desIndex >= 0) { for (const row of phaJson.data) { phaSet.add( String( row[desIndex] ) ); } } } // ------------------------------------------------- // データなし // ------------------------------------------------- if ( !json.data || !json.fields ) { return { items: [], updated: Date.now() }; } // ------------------------------------------------- // フィールド位置 // ------------------------------------------------- const index = {}; json.fields.forEach( (name, i) => { index[name] = i; } ); // ------------------------------------------------- // データ整形 // ------------------------------------------------- const items = []; for (const row of json.data) { const jd = Number( row[index.jd] ); // 今から24時間のみ if ( jd < nowJD || jd > endJD ) { continue; } const des = String( row[index.des] || "" ); const fullname = cleanName( row[index.fullname] || des ); const distAU = Number( row[index.dist] ); const distanceKM = distAU * CONFIG.AU_KM; const lunarDistance = distanceKM / CONFIG.MOON_KM; const velocity = Number( row[index.v_rel] ); const h = Number( row[index.h] ); const knownDiameter = row[index.diameter]; let diameterKM = null; let diameterEstimated = false; if ( knownDiameter !== null && knownDiameter !== undefined && knownDiameter !== "" ) { diameterKM = Number( knownDiameter ); } else { diameterKM = estimateDiameter(h); diameterEstimated = true; } items.push({ des, name: fullname, jd, distanceKM, lunarDistance, velocity, diameterKM, diameterEstimated, pha: phaSet.has(des) }); } // ------------------------------------------------- // 接近時刻順 // ------------------------------------------------- items.sort( (a, b) => a.jd - b.jd ); const output = { items, updated: Date.now() }; // ------------------------------------------------- // キャッシュ保存 // ------------------------------------------------- try { if ( !fm.fileExists(cacheDir) ) { fm.createDirectory( cacheDir, true ); } fm.writeString( cacheFile, JSON.stringify(output) ); } catch (e) {} return output; } catch (e) { // ------------------------------------------------- // APIエラー時 // 古いキャッシュがあれば使用 // ------------------------------------------------- try { if ( fm.fileExists(cacheFile) ) { return JSON.parse( fm.readString(cacheFile) ); } } catch (e) {} return { items: [], updated: Date.now(), error: true }; } } // ===================================================== // 小サイズ // ===================================================== function createSmall(result) { const w = baseWidget(); const items = result.items; // --------------------------------------------------- // タイトル // --------------------------------------------------- const title = w.addText( "☄️ 地球接近天体" ); title.font = Font.boldSystemFont(13); title.textColor = CONFIG.WHITE; // 固定余白 w.addSpacer(15); // --------------------------------------------------- // データなし // --------------------------------------------------- if (!items.length) { const none = w.addText( "24時間以内\n接近天体なし" ); none.font = Font.boldSystemFont(18); none.textColor = CONFIG.WHITE; none.centerAlignText(); w.addSpacer(); const sub = w.addText( "7.5百万km以内" ); sub.font = Font.systemFont(10); sub.textColor = CONFIG.SUB; return w; } const item = items[0]; // --------------------------------------------------- // 天体名 // --------------------------------------------------- const name = w.addText( item.name ); name.font = Font.boldSystemFont(17); name.textColor = CONFIG.WHITE; name.lineLimit = 1; name.minimumScaleFactor = 0.7; w.addSpacer(4); // --------------------------------------------------- // 大きさ // --------------------------------------------------- const diameter = w.addText( formatDiameter(item) ); diameter.font = Font.systemFont(12); diameter.textColor = CONFIG.SUB; // 月距離までの空白を固定 w.addSpacer(13); // --------------------------------------------------- // 月距離 // --------------------------------------------------- const moon = w.addText( "🌕 " + formatLD( item.lunarDistance ) + "倍" ); moon.font = Font.boldSystemFont(20); moon.textColor = CONFIG.BLUE; w.addSpacer(1); const distance = w.addText( formatKM( item.distanceKM ) + " km" ); distance.font = Font.systemFont(11); distance.textColor = CONFIG.SUB; w.addSpacer(); // --------------------------------------------------- // PHA // --------------------------------------------------- addRisk( w, item ); return w; } // ===================================================== // 中サイズ // ===================================================== function createMedium(result) { const w = baseWidget(); const items = result.items; // --------------------------------------------------- // ヘッダー // --------------------------------------------------- const head = w.addStack(); head.centerAlignContent(); const title = head.addText( "☄️ 地球接近天体" ); title.font = Font.boldSystemFont(15); title.textColor = CONFIG.WHITE; head.addSpacer(); const count = head.addText( "24時間 " + items.length + "天体" ); count.font = Font.systemFont(11); count.textColor = CONFIG.SUB; w.addSpacer(12); // --------------------------------------------------- // データなし // --------------------------------------------------- if (!items.length) { const none = w.addText( "24時間以内に接近する天体はありません" ); none.font = Font.boldSystemFont(16); none.textColor = CONFIG.WHITE; return w; } const item = items[0]; // --------------------------------------------------- // 最接近 // --------------------------------------------------- const label = w.addText( "最接近" ); label.font = Font.systemFont(10); label.textColor = CONFIG.SUB; const row = w.addStack(); row.centerAlignContent(); const name = row.addText( item.name ); name.font = Font.boldSystemFont(20); name.textColor = CONFIG.WHITE; name.lineLimit = 1; name.minimumScaleFactor = 0.7; row.addSpacer(); const diameter = row.addText( formatDiameter(item) ); diameter.font = Font.boldSystemFont(13); diameter.textColor = CONFIG.BLUE; w.addSpacer(10); // --------------------------------------------------- // 詳細 // --------------------------------------------------- const info = w.addStack(); info.layoutHorizontally(); const left = info.addStack(); left.layoutVertically(); addInfoLine( left, "距離", formatKM( item.distanceKM ) + " km" ); addInfoLine( left, "月まで", "🌕 " + formatLD( item.lunarDistance ) + "倍" ); info.addSpacer(25); const right = info.addStack(); right.layoutVertically(); addInfoLine( right, "速度", item.velocity.toFixed(1) + " km/s" ); addInfoLine( right, "接近", formatApproachTime( item.jd ) ); w.addSpacer(); addRisk( w, item ); return w; } // ===================================================== // 大サイズ // ===================================================== function createLarge(result) { const w = baseWidget(); const items = result.items; // --------------------------------------------------- // ヘッダー // --------------------------------------------------- const head = w.addStack(); head.centerAlignContent(); const title = head.addText( "☄️ 地球接近天体モニター" ); title.font = Font.boldSystemFont(16); title.textColor = CONFIG.WHITE; head.addSpacer(); const count = head.addText( "24時間 " + items.length + "天体" ); count.font = Font.systemFont(11); count.textColor = CONFIG.SUB; // --------------------------------------------------- // データなし // --------------------------------------------------- if (!items.length) { w.addSpacer(); const none = w.addText( "24時間以内に接近する天体はありません" ); none.font = Font.boldSystemFont(18); none.textColor = CONFIG.WHITE; w.addSpacer(); addFooter( w, result ); return w; } const closest = items[0]; // =================================================== // 最接近天体 // =================================================== w.addSpacer(15); const topLabel = w.addText( "最接近" ); topLabel.font = Font.systemFont(10); topLabel.textColor = CONFIG.SUB; w.addSpacer(2); // --------------------------------------------------- // 天体名 + 危険判定 // --------------------------------------------------- const nameRow = w.addStack(); nameRow.centerAlignContent(); const name = nameRow.addText( "☄️ " + closest.name ); name.font = Font.boldSystemFont(23); name.textColor = CONFIG.WHITE; name.lineLimit = 1; name.minimumScaleFactor = 0.7; nameRow.addSpacer(); const risk = nameRow.addText( closest.pha ? "🟠 PHA" : "🟢 通常" ); risk.font = Font.boldSystemFont(11); risk.textColor = closest.pha ? CONFIG.ORANGE : CONFIG.GREEN; w.addSpacer(2); // --------------------------------------------------- // 大きさ // --------------------------------------------------- const diameter = w.addText( formatDiameter( closest ) ); diameter.font = Font.systemFont(11); diameter.textColor = CONFIG.SUB; w.addSpacer(11); // --------------------------------------------------- // 月距離を主役に // --------------------------------------------------- const moonRow = w.addStack(); moonRow.centerAlignContent(); const moon = moonRow.addText( "🌕 " + formatLD( closest.lunarDistance ) + "倍" ); moon.font = Font.boldSystemFont(30); moon.textColor = CONFIG.BLUE; moonRow.addSpacer(); // --------------------------------------------------- // 接近・速度 // --------------------------------------------------- const rightInfo = moonRow.addStack(); rightInfo.layoutVertically(); const timeLabel = rightInfo.addText( "接近 " + formatApproachTime( closest.jd ) ); timeLabel.font = Font.boldSystemFont(12); timeLabel.textColor = CONFIG.WHITE; rightInfo.addSpacer(3); const velocity = rightInfo.addText( "速度 " + closest.velocity.toFixed(1) + " km/s" ); velocity.font = Font.systemFont(11); velocity.textColor = CONFIG.SUB; // --------------------------------------------------- // 実距離 // --------------------------------------------------- const distance = w.addText( formatKM( closest.distanceKM ) + " km" ); distance.font = Font.systemFont(11); distance.textColor = CONFIG.SUB; // =================================================== // 区切り // =================================================== w.addSpacer(14); addDivider(w); w.addSpacer(9); // =================================================== // 24時間以内一覧 // =================================================== const listTitleRow = w.addStack(); listTitleRow.centerAlignContent(); const listTitle = listTitleRow.addText( "24時間以内の接近" ); listTitle.font = Font.boldSystemFont(11); listTitle.textColor = CONFIG.WHITE; listTitleRow.addSpacer(); const listSub = listTitleRow.addText( "月距離" ); listSub.font = Font.systemFont(9); listSub.textColor = CONFIG.SUB; w.addSpacer(7); // --------------------------------------------------- // 最大5件 // --------------------------------------------------- const show = items.slice( 0, 5 ); for ( let i = 0; i < show.length; i++ ) { const item = show[i]; const row = w.addStack(); row.centerAlignContent(); // 天体名 addColumn( row, item.pha ? "🟠 " + shortName(item.name) : "☄️ " + shortName(item.name), 122, CONFIG.WHITE, 11, true ); // 大きさ addColumn( row, shortDiameter(item), 61, CONFIG.SUB, 10 ); // 月距離 addColumn( row, "🌕 " + formatLD( item.lunarDistance ) + "倍", 68, CONFIG.BLUE, 10, true ); // 時刻 const timeStack = row.addStack(); timeStack.size = new Size( 70, 0 ); const time = timeStack.addText( formatApproachTime( item.jd, true ) ); time.font = Font.systemFont(10); time.textColor = CONFIG.WHITE; time.lineLimit = 1; time.minimumScaleFactor = 0.7; if ( i < show.length - 1 ) { w.addSpacer(6); } } // =================================================== // フッター // =================================================== w.addSpacer(); addFooter( w, result ); return w; } // ===================================================== // 共通ウィジェット // ===================================================== function baseWidget() { const w = new ListWidget(); w.backgroundColor = CONFIG.BG; w.setPadding( 15, 15, 15, 15 ); return w; } // ===================================================== // 情報行 // ===================================================== function addInfoLine( stack, label, value ) { const row = stack.addStack(); row.centerAlignContent(); const l = row.addText( label ); l.font = Font.systemFont(10); l.textColor = CONFIG.SUB; row.addSpacer(8); const v = row.addText( value ); v.font = Font.boldSystemFont(12); v.textColor = CONFIG.WHITE; stack.addSpacer(5); } // ===================================================== // 危険表示 // ===================================================== function addRisk( stack, item ) { const risk = stack.addText( item.pha ? "🟠 PHA(潜在的に危険な小惑星)" : "🟢 通常" ); risk.font = Font.boldSystemFont(11); risk.textColor = item.pha ? CONFIG.ORANGE : CONFIG.GREEN; } // ===================================================== // 区切り線 // ===================================================== function addDivider(stack) { const line = stack.addStack(); line.size = new Size( 0, 1 ); line.backgroundColor = CONFIG.LINE; } // ===================================================== // フッター // ===================================================== function addFooter( stack, result ) { const footer = stack.addStack(); footer.centerAlignContent(); const source = footer.addText( "NASA/JPL" ); source.font = Font.systemFont(9); source.textColor = CONFIG.MUTED; footer.addSpacer(); const update = footer.addText( formatUpdated( result.updated ) + " 更新" ); update.font = Font.systemFont(9); update.textColor = CONFIG.MUTED; } // ===================================================== // カラム // ===================================================== function addColumn( stack, text, width, color, size, bold = false ) { const cell = stack.addStack(); cell.size = new Size( width, 0 ); const t = cell.addText( text ); t.font = bold ? Font.boldSystemFont(size) : Font.systemFont(size); t.textColor = color; t.lineLimit = 1; t.minimumScaleFactor = 0.65; return cell; } // ===================================================== // 直径 // ===================================================== function estimateDiameter(h) { if ( !Number.isFinite(h) ) { return null; } // 一般的な小惑星アルベド0.14を仮定 const albedo = 0.14; return ( 1329 / Math.sqrt(albedo) * Math.pow( 10, -h / 5 ) ); } function formatDiameter(item) { if ( !item.diameterKM ) { return "大きさ不明"; } const meters = item.diameterKM * 1000; const prefix = item.diameterEstimated ? "推定 約" : "約"; if ( meters < 1000 ) { return ( prefix + Math.round(meters) + "m" ); } return ( prefix + item.diameterKM.toFixed(1) + "km" ); } function shortDiameter(item) { if ( !item.diameterKM ) { return "不明"; } const meters = item.diameterKM * 1000; if ( meters < 1000 ) { return ( "約" + Math.round(meters) + "m" ); } return ( item.diameterKM.toFixed(1) + "km" ); } // ===================================================== // 日時 // ===================================================== function jdToDate(jd) { return new Date( ( jd - 2440587.5 ) * 86400000 ); } function formatApproachTime( jd, short = false ) { const d = jdToDate(jd); const formatter = new DateFormatter(); formatter.timeZone = "Asia/Tokyo"; const dayFormatter = new DateFormatter(); dayFormatter.timeZone = "Asia/Tokyo"; dayFormatter.dateFormat = "yyyy-MM-dd"; const now = new Date(); const targetDay = dayFormatter.string(d); const today = dayFormatter.string(now); const tomorrow = dayFormatter.string( new Date( now.getTime() + 86400000 ) ); formatter.dateFormat = "HH:mm"; const time = formatter.string(d); if (short) { if ( targetDay === today ) { return time; } if ( targetDay === tomorrow ) { return "翌 " + time; } } if ( targetDay === today ) { return "今日 " + time; } if ( targetDay === tomorrow ) { return "明日 " + time; } formatter.dateFormat = "M/d HH:mm"; return formatter.string(d); } // ===================================================== // 表示用 // ===================================================== function formatKM(km) { return Math.round(km) .toLocaleString( "ja-JP" ); } function formatLD(ld) { if ( ld < 10 ) { return ld.toFixed(1); } return Math.round(ld) .toString(); } function cleanName(name) { return String(name) .replace( /^\s+/, "" ) .replace( /^\((.+)\)$/, "$1" ); } function shortName(name) { const text = cleanName(name); if ( text.length <= 14 ) { return text; } return ( text.substring( 0, 13 ) + "…" ); } function formatUpdated(timestamp) { const d = new Date(timestamp); const formatter = new DateFormatter(); formatter.timeZone = "Asia/Tokyo"; formatter.dateFormat = "HH:mm"; return formatter.string(d); } function formatDateUTC(date) { const y = date.getUTCFullYear(); const m = String( date.getUTCMonth() + 1 ).padStart( 2, "0" ); const d = String( date.getUTCDate() ).padStart( 2, "0" ); return ( y + "-" + m + "-" + d ); } - ホーム画面に追加して設定する
iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。
追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。
「Parameter」は空欄で構いません。
編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。
「PHA」について
🟠 PHA は「Potentially Hazardous Asteroid(潜在的に危険な小惑星)」を意味します。
ただし、PHAと表示されているからといって、今回の接近で地球に衝突するという意味ではありません。
天体の大きさや軌道など一定の条件から分類されているものなので、「危険な天体が今まさに地球へ向かっている」という表示ではありません。
更新について
データは定期的にNASA/JPLから取得し、通信量やAPIへの負荷を抑えるため30分間キャッシュします。
そのため、常に秒単位で変化するリアルタイム表示ではありません。
動作確認・使用結果
| 確認した端末 | iPhone 17 ProMAX |
|---|---|
| iOS | iOS27 |
| Scriptable | 1.7.19 |
| 確認したサイズ | 小・中・大 |
| バージョン | 1.0.0 |
| 動作確認日 | 2026-10-01 |
表示・使用時のポイント
NASA/JPLの接近天体データから、対象の時間・距離の条件に当てはまる候補を選びます。該当する天体がない日は、その状態を表示します。
更新履歴
1.0.0
- 初回リリース
よくあるエラーと対処
- 確認すること
- 表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
- 天体が0件になる
- 対象期間と距離条件に該当する候補がない場合は正常です。
- 直径が推定表示になる
- 直径の実測値がない場合、明るさなどから概算します。
- 新しい候補がすぐに出ない
- キャッシュと更新間隔があります。取得日時を確認してください。
注意事項・制限
- 接近距離は衝突の予告を意味しません。PHAの分類も直ちに衝突するという意味ではありません。
- 天体の軌道・サイズなどは観測の追加で更新されることがあります。
- ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。
データ提供・参考資料
NASA/JPL Close-Approach Data API:地球接近天体の時刻・距離・速度など。
SHARE

