
国際宇宙ステーション(ISS)が、いま地球のどこを飛んでいるのかをホーム画面から確認できるウィジェットです。
ISSは地上から肉眼で見えることがありますが、「今日見えるの?」「何時ごろ、どっちを見ればいい?」を調べるには、その都度サイトやアプリを開く必要があります。
それなら、ISSの現在位置と、自分の場所から次に見えるタイミングをまとめてホーム画面に出したら面白いんじゃないかと思って作りました。
世界地図上にISSの現在位置を表示するだけでなく、自分のいる場所から「いまISSが見えるのか」「次にいつ見えるのか」も確認できます。
次回の観測時刻や最大仰角、見え始める方角なども表示するので、「今日はISS見えるかな?」と思ったときにホーム画面からパッと確認できます。
ISSの現在位置は外部データから自動取得しており、ウィジェットでは負荷を抑えるため約5分間キャッシュしています。
そのため、表示位置は完全なリアルタイムではなく数分程度の差が生じる場合があります。
ウィジェットの表示内容
ISSの位置と通過予測を取得します。
サイズごとの表示
- 小:ISSの位置・地図・座標と観測の目安を表示します。
- 中:自分の観測地点からの方向や角度なども表示します。
- 大:次の通過の開始・最大・終了時刻や継続時間を詳しく表示します。
設定手順
上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。
- Scriptableを用意する
App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。
- コードをコピーして保存する
下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、ISSは今どこ?などの名前で保存してください。
保存したらこのページに戻り、次の手順へ進みます。
// Version: 1.0.0 // ===================================================== // ISSはいまどこ? // Scriptable Widget // 小・中・大サイズ対応 // // Parameter: // 緯度,経度 // 例:35.004,134.550 // // 現在位置: // Where the ISS at? // // 観測予測: // Pollux Labs ISS Pass API // // ・ISS現在位置:5分キャッシュ // ・観測予測:6時間キャッシュ // ・最大仰角20°以上 // ・肉眼で見える通過のみ // ===================================================== // ===================================================== // 設定 // ===================================================== const POSITION_API = "https://api.wheretheiss.at/v1/satellites/25544"; const PASS_API = "https://iss-api.polluxlabs.io/iss-pass"; // ISS位置キャッシュ const POSITION_CACHE_SECONDS = 5 * 60; // 通過予測キャッシュ const PASS_CACHE_SECONDS = 6 * 60 * 60; // 観測対象とする最低最大仰角 const MIN_ELEVATION = 20; // 予測日数 const DAYS_AHEAD = 10; // 地図表示範囲 const MAP_LAT_MAX = 70; const MAP_LAT_MIN = -60; // ===================================================== // Parameter // ===================================================== function getObserver() { const raw = (args.widgetParameter || "").trim(); if (!raw) { return null; } const parts = raw.split(","); if (parts.length !== 2) { return null; } const lat = Number(parts[0].trim()); const lon = Number(parts[1].trim()); if ( !Number.isFinite(lat) || !Number.isFinite(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180 ) { return null; } return { lat, lon }; } // ===================================================== // ファイル // ===================================================== const fm = FileManager.local(); const POSITION_CACHE = fm.joinPath( fm.documentsDirectory(), "iss_position_cache_v3.json" ); // ===================================================== // 観測地点別キャッシュ名 // ===================================================== function getPassCachePath(observer) { const key = observer.lat.toFixed(3) .replace("-", "m") .replace(".", "_") + "_" + observer.lon.toFixed(3) .replace("-", "m") .replace(".", "_"); return fm.joinPath( fm.documentsDirectory(), "iss_pass_" + key + ".json" ); } // ===================================================== // JSONキャッシュ // ===================================================== function readCache(path) { try { if (!fm.fileExists(path)) { return null; } return JSON.parse( fm.readString(path) ); } catch (e) { return null; } } function writeCache(path, data) { try { fm.writeString( path, JSON.stringify({ savedAt: Date.now(), data }) ); } catch (e) { console.log( "Cache error: " + e ); } } function cacheFresh( cache, seconds ) { if ( !cache || !cache.savedAt ) { return false; } return ( Date.now() - cache.savedAt < seconds * 1000 ); } // ===================================================== // ISS現在位置 // ===================================================== async function fetchISSPosition() { const req = new Request(POSITION_API); req.timeoutInterval = 5; const d = await req.loadJSON(); if ( d.latitude == null || d.longitude == null ) { throw new Error( "ISS position invalid" ); } return { latitude: Number(d.latitude), longitude: Number(d.longitude), altitude: Number(d.altitude), velocity: Number(d.velocity), timestamp: Number(d.timestamp) || Math.floor(Date.now() / 1000) }; } async function getISSPosition() { const cache = readCache(POSITION_CACHE); if ( cacheFresh( cache, POSITION_CACHE_SECONDS ) ) { return cache.data; } try { const data = await fetchISSPosition(); writeCache( POSITION_CACHE, data ); return data; } catch (e) { console.log( "Position API: " + e ); if (cache) { return cache.data; } throw e; } } // ===================================================== // 観測可能な通過を取得 // ===================================================== async function fetchPasses(observer) { const url = PASS_API + "?lat=" + encodeURIComponent(observer.lat) + "&lon=" + encodeURIComponent(observer.lon) + "&visible_only=true" + "&min_elevation=" + MIN_ELEVATION + "&days_ahead=" + DAYS_AHEAD + "&n=10"; const req = new Request(url); req.timeoutInterval = 8; const data = await req.loadJSON(); if (!Array.isArray(data.passes)) { throw new Error( "Pass data invalid" ); } return data; } async function getPasses(observer) { const path = getPassCachePath(observer); const cache = readCache(path); if ( cacheFresh( cache, PASS_CACHE_SECONDS ) ) { return cache.data; } try { const data = await fetchPasses(observer); writeCache( path, data ); return data; } catch (e) { console.log( "Pass API: " + e ); if (cache) { return cache.data; } return { passes: [] }; } } // ===================================================== // 現在・次回の通過を判定 // ===================================================== function analyzePasses(data) { const now = Date.now(); const passes = data.passes || []; let current = null; let next = null; for (const pass of passes) { const start = new Date( pass.visible_start || pass.rise.time ).getTime(); const end = new Date( pass.visible_end || pass.set.time ).getTime(); // 現在見えている if ( now >= start && now <= end ) { current = pass; break; } // 次回 if ( start > now && next === null ) { next = pass; } } return { visibleNow: current !== null, current, next: current || next }; } // ===================================================== // 緯度経度 // ===================================================== function formatLat(value) { return ( Math.abs(value).toFixed(1) + "°" + (value >= 0 ? "N" : "S") ); } function formatLon(value) { return ( Math.abs(value).toFixed(1) + "°" + (value >= 0 ? "E" : "W") ); } // ===================================================== // ISSのおおまかな位置 // ===================================================== function getArea(lat, lon) { if ( lat >= 24 && lat <= 46 && lon >= 123 && lon <= 146 ) { return "日本付近"; } if ( lat >= 15 && lat <= 70 && lon >= -170 && lon <= -50 ) { return "北米付近"; } if ( lat >= -60 && lat <= 15 && lon >= -90 && lon <= -30 ) { return "南米付近"; } if ( lat >= 35 && lat <= 70 && lon >= -15 && lon <= 45 ) { return "ヨーロッパ付近"; } if ( lat >= -38 && lat <= 35 && lon >= -20 && lon <= 55 ) { return "アフリカ付近"; } if ( lat >= 5 && lat <= 70 && lon >= 45 && lon <= 150 ) { return "アジア付近"; } if ( lat >= -50 && lat <= -10 && lon >= 110 && lon <= 180 ) { return "オセアニア付近"; } if ( lat < 25 && lon >= 20 && lon <= 120 ) { return "インド洋上"; } if ( lon >= -70 && lon <= 20 ) { return "大西洋上"; } return "太平洋上"; } // ===================================================== // Natural Earthベース // ドット世界地図 // ===================================================== const DOT_MAP = [ "#...############..#.#####..#....#####............######.#.#####.##########################", ".....####################..##...###....##......###.#######################################", "...#####.############.....##..................####..################################.###..", "............###########...####.................##.#############################.....##....", ".............##################.............#.###################################...#.....", "..............###############.##............#####################################.........", "..............###############................###.###...##.#####################.#.........", "..............############.................##....##.#######################.#.............", "...............###########.................#.###....#######################....#..........", "................#########..................######.#...#####################..#............", "................#####...#.................###############.#################...............", "..................###....#...............#################....#############...............", "...................##.#..#...............#############.#####...####.#####.................", "....................###..................##############.##.....###...###...#..............", ".......................#.................###############........#.....##..................", "........................######............################......#...........#.............", "..........................######...........##.###########.............#...#...............", ".........................########..............#########..............#.###...............", ".........................##########............########...............#..#.#..##..........", ".........................###########............#######................#........##........", "..........................##########............#######..............................#....", "..........................#########.............#######..#..................###.#.........", "...........................########.............######..#..................#######........", "...........................########..............#####..#................##########.......", "...........................######................####....................##########.......", "...........................#####.................####.....................#########.......", "...........................###.#..........................................#....####.......", "...........................###..................................................##.......#", "..........................###....................................................#........", "..........................##...........................................................#..", "..........................##..............................................................", "...........................#..............................................................", ".........................................................................................." ]; // ===================================================== // 経緯度 → 地図座標 // ===================================================== function mapPosition( lat, lon, width, height ) { return { x: ((lon + 180) / 360) * width, y: ( (MAP_LAT_MAX - lat) / (MAP_LAT_MAX - MAP_LAT_MIN) ) * height }; } // ===================================================== // ドット地図 // ===================================================== function createDotMap( issLat, issLon, observer, width, height ) { const ctx = new DrawContext(); ctx.size = new Size( width, height ); ctx.opaque = true; ctx.respectScreenScale = false; // 海 ctx.setFillColor( new Color("#11253b") ); ctx.fillRect( new Rect( 0, 0, width, height ) ); const rows = DOT_MAP.length; const cols = Math.max( ...DOT_MAP.map(r => r.length) ); const cellW = width / cols; const cellH = height / rows; const dotW = Math.max( 0.65, cellW * 0.72 ); const dotH = Math.max( 0.65, cellH * 0.72 ); // 陸地 ctx.setFillColor( new Color("#668096") ); for ( let row = 0; row < rows; row++ ) { const line = DOT_MAP[row]; for ( let col = 0; col < line.length; col++ ) { if ( line[col] !== "#" ) { continue; } const x = col * cellW + (cellW - dotW) / 2; const y = row * cellH + (cellH - dotH) / 2; ctx.fillRect( new Rect( x, y, dotW, dotH ) ); } } // =================================================== // 観測地点 = 赤 // =================================================== const home = mapPosition( observer.lat, observer.lon, width, height ); ctx.setFillColor( new Color( "#ff453a", 0.25 ) ); ctx.fillEllipse( new Rect( home.x - 4, home.y - 4, 8, 8 ) ); ctx.setFillColor( new Color("#ff453a") ); ctx.fillEllipse( new Rect( home.x - 2, home.y - 2, 4, 4 ) ); // =================================================== // ISS = 水色 // =================================================== const iss = mapPosition( issLat, issLon, width, height ); ctx.setFillColor( new Color( "#48cfff", 0.12 ) ); ctx.fillEllipse( new Rect( iss.x - 8, iss.y - 8, 16, 16 ) ); ctx.setFillColor( new Color( "#48cfff", 0.35 ) ); ctx.fillEllipse( new Rect( iss.x - 5, iss.y - 5, 10, 10 ) ); ctx.setFillColor( new Color("#48cfff") ); ctx.fillEllipse( new Rect( iss.x - 3, iss.y - 3, 6, 6 ) ); ctx.setFillColor( Color.white() ); ctx.fillEllipse( new Rect( iss.x - 1.2, iss.y - 1.2, 2.4, 2.4 ) ); return ctx.getImage(); } // ===================================================== // 日付表示 // ===================================================== function dateTimeText(value) { if (!value) { return "--"; } const d = new Date(value); const df = new DateFormatter(); df.locale = "ja_JP"; df.dateFormat = "M/d HH:mm"; return df.string(d); } function timeText(value) { if (!value) { return "--"; } const d = new Date(value); const df = new DateFormatter(); df.locale = "ja_JP"; df.dateFormat = "HH:mm"; return df.string(d); } // ===================================================== // 方角を日本語化 // ===================================================== function directionJP(value) { const map = { N: "北", NNE: "北北東", NE: "北東", ENE: "東北東", E: "東", ESE: "東南東", SE: "南東", SSE: "南南東", S: "南", SSW: "南南西", SW: "南西", WSW: "西南西", W: "西", WNW: "西北西", NW: "北西", NNW: "北北西" }; return map[value] || value || "--"; } // ===================================================== // 通過情報 // ===================================================== function passInfo(pass) { if (!pass) { return { next: "予測なし", max: "--", direction: "--", rise: "--", peak: "--", set: "--", duration: "--" }; } const rise = pass.visible_start || pass.rise?.time; const set = pass.visible_end || pass.set?.time; const peak = pass.culmination?.time; const elevation = Number( pass.culmination?.elevation_deg ); // visible_duration_secを優先 const durationSec = Number( pass.visible_duration_sec ?? pass.duration_sec ?? 0 ); return { next: dateTimeText(rise), max: Number.isFinite(elevation) ? Math.round(elevation) + "°" : "--", direction: directionJP( pass.rise?.compass ) + " → " + directionJP( pass.set?.compass ), rise: timeText(rise), peak: timeText(peak), set: timeText(set), duration: durationSec > 0 ? "約" + Math.max( 1, Math.round( durationSec / 60 ) ) + "分" : "--" }; } // ===================================================== // テキスト // ===================================================== function addText( stack, text, size, opacity = 1, bold = false ) { const t = stack.addText(text); t.textColor = new Color( "#ffffff", opacity ); t.font = bold ? Font.boldSystemFont(size) : Font.systemFont(size); t.lineLimit = 1; return t; } // ===================================================== // ISS位置テキスト // ===================================================== function locationText(data) { return ( getArea( data.latitude, data.longitude ) + "(" + formatLat(data.latitude) + " " + formatLon(data.longitude) + ")" ); } // ===================================================== // 見える / 見えない // ===================================================== function addVisibility( stack, visible, size ) { const t = stack.addText( visible ? "🟢 いま見える!" : "🔴 現在は見えません" ); t.font = Font.boldSystemFont(size); t.textColor = Color.white(); t.lineLimit = 1; return t; } // ===================================================== // 小サイズ // ===================================================== function createSmall( widget, data, observer, analysis ) { widget.setPadding( 12, 12, 11, 12 ); addText( widget, "🛰️ ISSはいまどこ?", 12, 1, true ); widget.addSpacer(6); const map = createDotMap( data.latitude, data.longitude, observer, 150, 67 ); const img = widget.addImage(map); img.cornerRadius = 8; widget.addSpacer(6); addText( widget, locationText(data), 9, 0.8, true ); widget.addSpacer(3); addVisibility( widget, analysis.visibleNow, 10 ); widget.addSpacer(2); const info = passInfo(analysis.next); addText( widget, "次回 " + info.next, 9, 0.65 ); } // ===================================================== // 中サイズ // ===================================================== function createMedium( widget, data, observer, analysis ) { widget.setPadding( 13, 14, 11, 14 ); addText( widget, "🛰️ ISSはいまどこ?", 13, 1, true ); widget.addSpacer(6); const body = widget.addStack(); body.layoutHorizontally(); // --------------------------------- // 左:地図 // --------------------------------- const left = body.addStack(); left.layoutVertically(); const map = createDotMap( data.latitude, data.longitude, observer, 175, 82 ); const img = left.addImage(map); img.cornerRadius = 9; left.addSpacer(5); addText( left, locationText(data), 9, 0.75, true ); body.addSpacer(11); // --------------------------------- // 右:観測情報 // --------------------------------- const right = body.addStack(); right.layoutVertically(); const info = passInfo(analysis.next); addVisibility( right, analysis.visibleNow, 10 ); right.addSpacer(5); addText( right, "次回", 8, 0.45 ); addText( right, info.next, 11, 1, true ); right.addSpacer(5); addText( right, "最大仰角 " + info.max, 9, 0.8 ); right.addSpacer(2); addText( right, info.direction, 9, 0.7 ); } // ===================================================== // 大サイズ // ===================================================== function createLarge( widget, data, observer, analysis ) { widget.setPadding( 17, 17, 15, 17 ); addText( widget, "🛰️ ISSはいまどこ?", 17, 1, true ); widget.addSpacer(10); // --------------------------------- // 地図 // --------------------------------- const map = createDotMap( data.latitude, data.longitude, observer, 320, 150 ); const img = widget.addImage(map); img.cornerRadius = 12; widget.addSpacer(9); addText( widget, locationText(data), 11, 0.8, true ); widget.addSpacer(8); const info = passInfo(analysis.next); addVisibility( widget, analysis.visibleNow, 13 ); widget.addSpacer(4); addText( widget, "次回 " + info.next, 11, 0.85, true ); widget.addSpacer(7); // --------------------------------- // 最大仰角・方角 // --------------------------------- const summary = widget.addStack(); summary.layoutHorizontally(); addText( summary, "最大仰角 " + info.max, 11, 0.85, true ); summary.addSpacer(); addText( summary, info.direction, 11, 0.75 ); widget.addSpacer(12); // --------------------------------- // タイムライン // --------------------------------- const timeline = widget.addStack(); timeline.layoutHorizontally(); // 見え始め const riseBox = timeline.addStack(); riseBox.layoutVertically(); addText( riseBox, info.rise, 14, 1, true ); addText( riseBox, "見え始め", 9, 0.5 ); timeline.addSpacer(); // 最大 const peakBox = timeline.addStack(); peakBox.layoutVertically(); addText( peakBox, info.peak, 14, 1, true ); addText( peakBox, "最大 " + info.max, 9, 0.5 ); timeline.addSpacer(); // 終了 const setBox = timeline.addStack(); setBox.layoutVertically(); addText( setBox, info.set, 14, 1, true ); addText( setBox, "見え終わり", 9, 0.5 ); widget.addSpacer(10); addText( widget, "見える時間 " + info.duration, 10, 0.7 ); } // ===================================================== // Parameter未設定 // ===================================================== function createSetupWidget() { const widget = new ListWidget(); widget.backgroundColor = new Color("#0b1422"); widget.setPadding( 15, 15, 15, 15 ); addText( widget, "🛰️ ISSはいまどこ?", 14, 1, true ); widget.addSpacer(10); addText( widget, "観測地点を設定してください", 11, 0.8, true ); widget.addSpacer(5); addText( widget, "Parameter:緯度,経度", 9, 0.55 ); addText( widget, "例 35.004,134.550", 9, 0.55 ); return widget; } // ===================================================== // エラー // ===================================================== function createErrorWidget() { const widget = new ListWidget(); widget.backgroundColor = new Color("#0b1422"); widget.setPadding( 15, 15, 15, 15 ); addText( widget, "🛰️ ISSはいまどこ?", 14, 1, true ); widget.addSpacer(10); addText( widget, "データを取得できません", 11, 0.7 ); return widget; } // ===================================================== // メイン // ===================================================== let widget; const observer = getObserver(); if (!observer) { widget = createSetupWidget(); } else { try { // 並列取得で高速化 const [ position, passData ] = await Promise.all([ getISSPosition(), getPasses(observer) ]); const analysis = analyzePasses(passData); widget = new ListWidget(); widget.backgroundColor = new Color("#0b1422"); // 5分後に更新希望 widget.refreshAfterDate = new Date( Date.now() + POSITION_CACHE_SECONDS * 1000 ); const family = config.widgetFamily || "medium"; if ( family === "small" ) { createSmall( widget, position, observer, analysis ); } else if ( family === "large" ) { createLarge( widget, position, observer, analysis ); } else { createMedium( widget, position, observer, analysis ); } } catch (error) { console.log(error); widget = createErrorWidget(); } } // ===================================================== // 表示 // ===================================================== if (config.runsInWidget) { Script.setWidget(widget); } else { await widget.presentMedium(); } Script.complete(); - ウィジェットの設定を作る
下の入力欄で自分用の設定を作り、生成されたパラメーターをコピーします。詳しい設定仕様は、設定手順の下でも確認できます。
ISSの観測地点(緯度・経度)
ISSを観測したい地点を指定します。緯度は−90〜90、経度は−180〜180です。
コピーできたら、次の手順でホーム画面のウィジェットへ貼り付けます。
- ホーム画面に追加して設定する
iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。
追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。
「Parameter」に直前の手順でコピーした値を貼り付けます。設定を変えるときも、同じ入力欄で作り直して貼り替えてください。
編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。
パラメータ・カスタマイズ
観測地点を設定する
「自分のいる場所からISSが見えるか」を判定するため、観測地点の緯度・経度をParameter(パラメータ)に設定します。
入力形式は、
緯度,経度
です。
例えば、
35.004,134.550
のように、緯度と経度を半角カンマで区切って入力します。
位置情報へのアクセス許可は必要ありません。一度Parameterを設定すれば、その場所を観測地点として使用します。
緯度・経度が分からない場合
緯度・経度はGoogleマップで簡単に調べられます。
Googleマップで緯度・経度を調べる
Googleマップを開いて、ISSを観測したい場所を長押ししてください。地図上にピンが表示され、35.004123, 134.550123 のような緯度・経度を確認できます。
表示された「緯度,経度」をコピーして、そのままScriptableウィジェットのParameterへ貼り付ければ設定完了です。
位置情報へのアクセス許可は必要ありません。一度Parameterを設定すれば、その場所を観測地点として使用します。
「いま見える!」とは?
ISSが自分の近くを通過していても、必ず肉眼で見えるわけではありません。
このウィジェットでは単純なISSの位置だけではなく、観測地点から実際に見える条件を満たしているかをもとに判定します。
条件を満たしている時間帯には、
🟢 いま見える!
それ以外は、
🔴 現在は見えません
と表示されます。
「現在は見えません」と表示されていても、次に観測できる予定があれば「次回 9/19 19:42」のように表示されます。
地図の見方
世界地図上には2種類のポイントを表示します。
赤い点がParameterで設定した自分の観測地点、水色に光っている点がISSの現在位置です。
ISSは非常に速い速度で地球を周回しているため、ウィジェットが更新されるたびに水色のポイントが移動していきます。
更新について
ISSの現在位置は定期的に自動取得します。
ただし、ホーム画面のウィジェットを実際に更新するタイミングはiOSによって管理されているため、常にリアルタイムで動き続ける地図ではありません。
ウィジェットが更新された時点でのISSの最新位置が表示されます。
動作確認・使用結果
| 確認した端末 | iPhone 17 ProMAX |
|---|---|
| iOS | iOS27 |
| Scriptable | 1.7.19 |
| 確認したサイズ | 小・中・大 |
| バージョン | 1.0.0 |
| 動作確認日 | 2026-09-19 |
表示・使用時のポイント
ISSの位置と通過予測を取得します。観測地点を入れると、自分の地点から見える条件や次の通過時刻を確認できます。位置は常時追跡ではなく取得時点の情報です。
更新履歴
1.0.0
- 初回リリース
よくあるエラーと対処
- 確認すること
- 表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
- 観測地点の予測が出ない
- 緯度・経度の順序と符号を確認し、設定欄で作り直してください。
- 次の通過が出ない
- 取得期間に適した通過がない場合や予測APIに応答がない場合があります。時間を置いて再実行してください。
- 位置がすぐ動かない
- 保存データやウィジェットの更新間隔によります。取得時刻を確認してください。
注意事項・制限
- 「見える」は計算上の目安です。雲や地形、建物による見え方までは保証しません。
- 観測地点を指定しない場合、自分の場所からの観測条件は判断できません。
- ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。
データ提供・参考資料
Where the ISS at?:ISSの現在位置。
Pollux Labs ISS API:観測地点からの通過予測。
SHARE

