PHPで星空マップを表示する

(1/1)
日時と場所を指定すると、そのとき地平線の上にある星を調べられる「星空マップ」を作る。PHPで天体の位置を計算し、ブラウザーの Canvas に恒星・星座・天の川・太陽・月・惑星を描く。
地図から観測地点を選ぶ機能と、表示した星図を画像として保存する機能も用意した。さらに、恒星表などの原本を取得・加工する skyMapDataBuilder.php も紹介する。

本記事は skyMap.php と skyMapDataBuilder.php のバージョン1.0.0を対象とする。星空案内や学習のためのサンプルであり、計算の条件と限界は「計算条件と注意点」にまとめた。

サンプル・プログラムの実行例

恒星・星座線・天の川・太陽・惑星を描いた全天星空マップ
全天表示は中心が天頂、外周が地平線である。北が上、東が左、南が下、西が右となり、空を見上げたときの向きに対応する。
図は2026年(令和8年)10月22日10時13分(日本時間)の例で、太陽が地平線の上にある。昼間でも星の位置を描くため、実際の空でこのように恒星が見えるわけではない。

目次

サンプル・プログラムのダウンロード

導入時の補足:惑星係数は配布ZIPの vsop87 フォルダーに同梱している。pahooAstronomy.php と同じフォルダー内に配置すること。地図・標高取得に使う pahooGeoCode.php と .pahooEnv は確認時点のZIPには含まれていないため、別途用意し、API情報を設定する。外部データを再生成する場合は「外部データ生成ツール」を参照する。
同梱の案内ファイル「!最初に読んでください.txt」も現状のZIPにはないため、ファイル配置は次節を参照していただきたい。
圧縮ファイルの内容
skyMap.php星空マップ本体。PHPの計算処理とJavaScriptの画面処理
skyMapDataBuilder.php外部データの取得・変換・保存ツール
SkyMap.md仕様書。設定値などは現行ソースも確認すること
skyMap-stars.jsonHYG v4.1から抽出した6.5等までの8,920恒星
skyMap-constellations.json88星座・695線分、主要恒星名109個、補助恒星
skyMap-milkyway.json天の川の概略濃度データ
vsop87/金星・地球・木星・土星・天王星・海王星のVSOP87D係数(JSON)と出典・ライセンス文書。pahooAstronomy.phpと同じフォルダー内に、vsop87フォルダーごと配置する。
skyMap-HYG.txt
skyMap-license.txt
skyMap-constellations-source.txt
skyMap-milkyway-source.txt
skyMap-milkyway-license.txt
各データの出典・加工内容・ライセンス
.pahooEnvクラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。
使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。
pahooAstronomy.php天文計算クラス pahooAstronomy。
使い方は「PHPでホロスコープを描く」などを参照。include_path が通ったディレクトリに配置すること。
pahooCalendar.php暦・潮位計算クラス pahooCalendar。
暦・潮位計算クラスの使い方は「PHPで二十四節気・七十二候一覧を作成」「PHPで月齢を計算」「PHPで日出没・月出没・月齢・潮を計算」「PHPで潮位を計算する」などを参照。include_path が通ったディレクトリに配置すること。
pahooGeoCode.php住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「PHPで住所・ランドマークから最寄り駅を求める」「PHPで住所・ランドマークから緯度・経度を求める」などを参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
skyMap.php 更新履歴
バージョン 更新日 内容
1.0.0 2026/10/22 配布版
0.8.0 2026/10/21 関数・制御文の目的コメント,地図設定定数,上部入力パネルと行配置,国内10都市,座標3桁表示,画像右下の著作権表記
0.7.0 2026/10/21 別ウィンドウの地図・ランドマーク検索,既存pahooGeoCodeによる標高取得,国内5都市の座標・標高プリセット
0.6.0 2026/10/12 全天・東西南北90度の半円星図切替,BMP/JPEG/PNG/WebP画像保存
0.5.0 2026/10/10 等級による恒星の大きさ・濃淡,B−V色指数の連続的な淡い色分け,凡例
skyMapDataBuilder.php 更新履歴
バージョン 更新日 内容
1.0.0 2026/10/22 共通初期化に対応.引数・戻り値コメントを項目ごとに改行し,タグの区切りと行頭インデントをタブへ統一,バージョンアップ履歴を追加.
0.1.0 2026/10/22 初版.9種類の外部データを原本ダウンロードから生成,選択・一括選択/解除,フォルダー/ZIP保存,出典・ライセンス・生成記録を同梱.
pahooAstronomy.php 更新履歴
バージョン 更新日 内容
2.3.0 2026/10/21 天象を視黄経差・視赤経変化・真角距離から判定し,年月日を正規化
2.2.0 2026/10/21 euatorialCoordinate()を追加
2.1.1 2025/11/29 PHP8.5対応:double→float 対象コメント変更
2.1.0 2025/06/01 euatorialCoordinate() を追加
2.0.0 2025/05/25 全面改訂, 天象を求められるようにした
pahooCalendar.php 更新履歴
バージョン 更新日 内容
4.7.0 2026/10/21 __construct():時差0を正常値として扱うよう失敗判定を厳密比較へ変更
4.6.0 2025/10/07 getMidAutumnMoon() 追加
4.5.1 2025/05/31 deg2ddmm(), deg2hhmm() 不具合修正
4.5.0 2024/03/17 ヒジュラ暦メソッドを追加
4.4.1 2024/03/17 getCabinetOfficeHolidayTable() -- bug-fix
pahooGeoCode.php 更新履歴
バージョン 更新日 内容
7.0.0 2026/10/18 汎用GeoJSON描画メソッドを追加(既存メソッドは変更なし)
6.10.4 2026/09/26 外部APIに接続できない・応答がXMLでないときにWarningで停止しないようにした,searchPoint3 -- 前回のエラーを持ち越さないようにした
6.10.3 2026/09/26 静岡県の表記,jsPolygon_Gmap/Leaflet の引数取り違え,searchPoint,getPointsGSI,makeYOLP_GeoSelectCategory,getLocalGovernmentCodeList,HeartRails系URLエンコード bug-fix
6.10.2 2026/09/26 unknown_certificate() -- サーバ証明書を検証するようにした
6.10.1 2026/09/26 drawGMap, drawLeaflet -- アイコン画像を取得できなくても停止しないようにした(getIconSize追加)
pahooInputData.php 更新履歴
バージョン 更新日 内容
2.0.3 2026/09/26 validEmail(), validNumber(), getValidNumber(), roundFloat(), pahooLoadEnv() bug-fix
2.0.2 2026/09/26 pahooLoadEnv() -- str_starts_with()を使わないようにした(PHP7対応)
2.0.1 2025/08/11 getParam() bug-fix
2.0.0 2025/08/11 pahooLoadEnv() 追加
1.9.0 2025/07/26 getParam() 引数に$trim追加

使い方:日時と観測地点の入力

緯度・経度・標高・限界等級・観測日時とタイムゾーンの入力画面
パラメータの入力画面
緯度・経度・標高、観測日時とタイムゾーンを指定し、星空を表示 を押す。札幌から那覇までの国内10都市はボタンで代表地点を選べる。
限界等級 は表示する恒星の暗さの上限で、数値を大きくすると暗い星まで描く。初期値は6.0等である。-1時間・+1時間 は日時の入力値を変更するボタンである。
入力日時は選択した地域の時刻として扱う。海外の地点を地図から選んでもタイムゾーンは自動変更されないため、自分で選び直す必要がある。夏時間の終了時に同じ時計表示が2回現れる時刻を区別したい場合は、UTCで指定する。
標高は地表からの高さではなく海面を基準とした値である。建物の上で観測する場合は、地表の標高に建物上の観測位置の高さを加える。
入力パラメータ(GETパラメータ名)
項目範囲・形式初期値
緯度(latitude)−90~90度。北緯が正、南緯が負35.4658度
経度(longitude)−180~180度。東経が正、西経が負139.6223度
標高(height)−500~9000m0m
限界等級(magnitude)0~6.5等。画面では0.1刻み6.0等
観測日時(datetime)1900~2100年。YYYY-MM-DDTHH:MMアクセス時の日本の日時
タイムゾーン(timezone)Asia/Tokyo、UTCなど、PHPが認識する識別子Asia/Tokyo

使い方:地図検索と標高取得

東京駅を検索し、地図中央の赤い十字で観測地点を選ぶ画面
別ウィンドウで開く地図検索画面
地図検索 を押すと、別ウィンドウで住所やランドマークを検索できる。地図をクリック・移動して赤い十字を観測地点に合わせ、この地点を星図に反映 を押すと、緯度・経度・標高を入力画面へ戻す。
メイン画面の 標高取得 でも標高を取得できる。取得できない場合は手入力する。地図から戻ったあと、日時などを確認して 星空を表示 を押す。

使い方:表示切替と画像保存

全天表示のほか、北・西・南・東を向いた半円形の星図へ切り替えられる。方角別表示の横幅は中心方向から左右45度ずつ、合計90度で、下辺が地平線、上端が天頂となる。表示方向はブラウザーに保存される。
星座線、星座名、主要恒星名、太陽・月・惑星、天の川は個別に表示を切り替えられる。恒星を選ぶと名前・方位・高度・等級などを確認できる。星座名は通常最大20個とし、「すべて表示」でも名称が重なる場合は省略する。

太陽系の位置一覧は地平線の下にある天体も含み、月相の拡大図では月の満ち欠けと照らされる割合を確認できる。太陽・月・惑星の表示は恒星の限界等級とは独立している。
星図は BMP・JPEG・PNG・WebP で保存できる。全天画像は1600×2128ピクセル、方角別画像は1600×1488ピクセルで、日時・地点・凡例・出典も入る。ブラウザーが対応していない形式ではエラーを表示する。

準備:PHP の https対応

クラウド連携や相手先サイトのデータを読み込むのに https通信を使うため、PHPに OpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOKだ。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

また、クラウド連携や相手先サイトとの https通信では、なりすましを防ぐため、相手サーバのSSL/TLS証明書を検証している。
Windows版のPHPには、証明書の検証に使うCA証明書(ルート証明書の一覧)が同梱されていないため、そのままでは https によるデータ取得に失敗する。CA証明書ファイル(cacert.pem)を入手し、"php.ini" に次の2行を追加してから、PHP(Webサーバ)を再起動してほしい。パスは cacert.pem を置いた場所に合わせること。
curl.cainfo = "C:\php\extras\ssl\cacert.pem"
openssl.cafile = "C:\php\extras\ssl\cacert.pem"
Linuxでは、通常はOSのCA証明書が使われるので、この設定は要らない。

これで準備は完了だ。

準備:pahooGeoCode クラス

pahooGeoCode.php

  41: class pahooGeoCode {
  42:     public $items;      // 検索結果格納用
  43:     public $error;      // エラー・フラグ
  44:     public $errmsg;     // エラー・メッセージ
  45:     public $hits;       // 検索ヒット件数
  46:     public $webapi; // 直前に呼び出したWebAPI URL
  47: 
  48:     // 都道府県境界線データ
  49:     // SimpleMaps.com is a product of Pareto Software, LLC. © 2010-2025
  50:     // https://simplemaps.com/gis/country/jp
  51:     // ※各自の環境に合わせて設定すること
  52:     public $GeoJsonJP = __DIR__ . '/jp.json';
  53: 
  54:     // -- 以下のデータは .env ファイルに記述可能
  55:     // Google Cloud Platform APIキー
  56:     // https://cloud.google.com/maps-platform/
  57:     // ※Google Maps APIを利用しないのなら登録不要
  58:     public $GOOGLE_API_KEY_1 = '';      // HTTPリファラ用
  59:     public $GOOGLE_API_KEY_2 = '';      // IP制限用
  60:     public $GOOGLE_MAP_ID    = '';      // GoogleMaps ID
  61: 
  62:     // Yahoo! JAPAN Webサービス アプリケーションID
  63:     // https://e.developer.yahoo.co.jp/register
  64:     // ※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
  65:     public $YAHOO_APPLICATION_ID = '';
  66: 
  67:     // OSM Nominatim Search API利用時に知らせるメールアドレス
  68:     // https://wiki.openstreetmap.org/wiki/JA:Nominatim#.E6.A4.9C.E7.B4.A2
  69:     // ※OSM Nominatim Search APIを利用しないのなら登録不要
  70:     public $NOMINATIM_EMAIL = '';
  71: 
  72:     // IP2Location.io APIキー
  73:     // https://www.ip2location.io/
  74:     // ※IP2Location.ioを利用しないのなら登録不要
  75:     public $IP2LOCATION_API_KEY = '';

GoogleマップやLeafletなどによる地図描画や住所検索を行うためのクラスが pahooGeoCode である。同梱のクラス・ファイル "pahooGeoCode.php" は include_path が通ったディレクトリに配置してほしい。他のプログラムでも pahooGeoCodeクラス を利用するが、常に最新のクラス・ファイルを1つ配置すればよい。

地図や住所検索として Google を利用するのであれば Google Cloud Platform APIキー とマップID が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を、Yahoo!JAPAN を利用するのであれば Yahoo! JAPAN Webサービス アプリケーションIDが必要で、その入手方法は「Yahoo!JAPAN デベロッパーネットワーク - WebAPIの登録方法」を、IP2Location.ioを利用するのであれば「PHPでIPアドレスやホスト名から住所を求める」を、それぞれ参照されたい。

住所検索などで外部のWebAPIに接続できないときや、応答がXMLでないときは、警告(Warning)を出さずに、エラーとして戻る。エラーの有無はメソッド iserror、内容はメソッド geterror で調べられる。メソッド searchPoint3 は、検索のたびにエラーの状態をリセットする。

PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
.pahooEnv には「名前=値」を1行に1つずつ書く。値は "..." や '...' で囲んでもよい(囲んだ引用符は取り除かれる)。# ではじまる行はコメントで、空行や「=」のない行は読み飛ばす。

準備:動作環境とファイル配置

PHP 7.4以降とCanvas対応ブラウザーを使用する。データ生成には cURL、天の川の生成には GD が必要である。XMLを使う地図検索や標高取得には SimpleXML も用意する。外部サービスへの接続にはHTTPS通信ができるPHP環境が必要となる。
skyMap.php と skyMapDataBuilder.php、3種類の skyMap-*.json、付属する出典・ライセンス文書を同じフォルダーへ配置する。共通PHPファイルは include_path が通る場所へ置く。

惑星係数は pahooAstronomy.php が自分自身のあるフォルダーの vsop87/ から読む。たとえば共通クラスを common/ に置くなら、生成した common/vsop87/ をそこへ配置する。JSONだけを本体の横へ移しても読み込めないので注意していただきたい。
地図機能は「PHPで地図で指定した場所の天気予報を求める」と同じ pahooGeoCode を利用する。.pahooEnv へのAPI情報の設定は「各種クラウド連携サービス(WebAPI)の登録方法」を参照すること。

skyMap.php

  53: // 各種定数(START) ===========================================================
  54: 
  55: // 恒星表ファイル
  56: define('STAR_CATALOG', __DIR__ . '/skyMap-stars.json');
  57: // 恒星表のリファレンス
  58: define('HYG_REFERENCE', 'https://codeberg.org/astronexus/hyg');
  59: // 88星座の星座線・星座名・主要恒星名・補助端点
  60: define('CONSTELLATION_CATALOG', __DIR__ . '/skyMap-constellations.json');
  61: // 天の川ファイル
  62: define('MILKYWAY_CATALOG', __DIR__ . '/skyMap-milkyway.json');
  63: // 入力可能な最小年・最大年
  64: define('MIN_YEAR', 1900);
  65: define('MAX_YEAR', 2100);
  66: 
  67: // 地図描画サービスの選択
  68: //    0:Google
  69: //    2:地理院地図・OSM
  70: define('MAPSERVICE', 0);
  71: 
  72: // 住所検索サービスの選択
  73: //    0:Google
  74: //    1:Yahoo!JAPAN
  75: //   11:HeartRails Geo API
  76: //   12:OSM Nominatim Search API
  77: //   13:国土地理院ジオコーディングAPI
  78: define('GEOSERVICE', 2);
  79: 
  80: // 逆ジオコーディングサービスの選択
  81: //    0:Google
  82: //    1:Yahoo!JAPAN
  83: //   11:HeartRails Geo API
  84: //   21:簡易ジオコーディングサービス
  85: define('REVGEOSERVICE', 11);
  86: 
  87: // マップの表示サイズ(単位:ピクセル)
  88: define('MAP_WIDTH',  600);
  89: define('MAP_HEIGHT', 400);
  90: // マップID
  91: define('MAPID', 'map_id');
  92: // 初期値
  93: define('DEF_LATITUDE',  35.4658);       // 緯度
  94: define('DEF_LONGITUDE', 139.6223);      // 経度
  95: define('DEF_TYPE',      'roadmap');     // マップタイプ
  96: define('DEF_ZOOM',      13);            // ズーム
  97: define('DEF_CATEGORY', 'address');      // カテゴリ
  98: define('DEF_DISTANCE',  250);           // 距離
  99: 
 100: // 検索キーの最小・最大文字長
 101: define('QUERY_MIN_LEN', 3);
 102: define('QUERY_MAX_LEN', 99);
 103: 
 104: // 各種定数(END) =============================================================

準備:地図サービス(WebAPI)の選択

searchOutlet.php

  62: // 地図描画サービスの選択
  63: //    0:Google
  64: //    2:地理院地図・OSM
  65: define('MAPSERVICE', 2);
  66: 
  67: // 住所検索サービスの選択
  68: //    0:Google
  69: //    1:Yahoo!JAPAN
  70: //   11:HeartRails Geo API
  71: //   12:OSM Nominatim Search API
  72: //   13:国土地理院ジオコーディングAPI
  73: define('GEOSERVICE', 11);
  74: 
  75: // 逆ジオコーディングサービスの選択
  76: //    0:Google
  77: //    1:Yahoo!JAPAN
  78: //   11:HeartRails Geo API
  79: //   21:簡易ジオコーディングサービス
  80: define('REVGEOSERVICE', 0);

表示する地図は、Googleマップ、地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVICE に値を設定すること。
住所検索サービスは、Google、Yahoo!JAPAN、HeartRails Geo API、OSM Nominatim Search API、国土地理院ジオコーディングAPI から選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
逆ジオコーディングサービスは、Google、Yahoo!JAPAN、HeartRails Geo API、簡易ジオコーディングサービスから選べる。あらかじめ、定数 REVGEOSERVICE に値を設定すること。

サンプル・プログラムの流れ

サンプル・プログラムの流れ
処理はサーバーとブラウザーに分かれる。PHPが入力を検証し、恒星や太陽系天体の方位・高度を求め、JSONをHTMLへ埋め込む。JavaScriptはそのデータを星図へ投影して描画する。
地図検索・標高取得は action で別処理へ振り分け、天体計算を実行せず応答する。表示レイヤーや方向の切替、画像保存はブラウザー側で行う。

解説:入力の検証と日時の変換

readParameters() はGET入力を受け取り、数値の範囲、日時、タイムゾーンを検証する。配列など文字列でない入力や無限大の数値も拒否する。
日時には DateTimeImmutable::createFromFormat() を使う。PHPは範囲を超えた日付を繰り上げることがあるため、変換後の文字列と元の入力を比較し、2月30日などの不正な日付や夏時間開始時の存在しない時刻を検出する。
検証後、選択した地域の日時 local とUTCへ変換した utc を保持する。以後の計算はUTCを基準に行い、画面には利用者が指定した地域の日時を表示する。

skyMap.php

 473: /**
 474:  * 星図のGET入力を検証し,選択したタイムゾーンの日時とUTC日時を用意する.
 475:  * @param   array $input latitude・longitude・height・datetime・timezone・magnitudeの入力
 476:  * @return  array 数値化した座標・標高・限界等級とlocal・utcのDateTimeImmutableを含む配列
 477:  * @throws  InvalidArgumentException 入力形式・範囲・日時が不正な場合
 478: */
 479: function readParameters($input) {
 480:     $defaults = array('latitude'=>(string)DEF_LATITUDE, 'longitude'=>(string)DEF_LONGITUDE,
 481:         'datetime'=>(new DateTimeImmutable('now', new DateTimeZone('Asia/Tokyo')))->format('Y-m-d\TH:i'),
 482:         'timezone'=>'Asia/Tokyo', 'magnitude'=>'6.0', 'height'=>'0');
 483:     $p = array();
 484: 
 485:     // 各入力項目に既定値または利用者が指定した値を設定する.
 486:     foreach ($defaults as $key=>$value) {
 487:         // 配列など文字列以外の入力を受け付けない.
 488:         if (isset($input[$key]) && ! is_string($input[$key])) throw new InvalidArgumentException('入力形式が不正です.');
 489:         $p[$key] = isset($input[$key]) ? $input[$key] : $value;
 490:     }
 491: 
 492:     // 座標・限界等級・標高をそれぞれの数値範囲で確認する.
 493:     foreach (array('latitude'=>array(-90,90), 'longitude'=>array(-180,180), 'magnitude'=>array(0,6.5), 'height'=>array(-500,9000)) as $key=>$range) {
 494:         // 数値に変換できない値や許容範囲外の値を拒否する.
 495:         if (! is_numeric($p[$key]) || ! is_finite((float)$p[$key]) || $p[$key] < $range[0] || $p[$key] > $range[1]) {
 496:             throw new InvalidArgumentException('緯度は−90~90,経度は−180~180,限界等級は0~6.5,標高は−500~9000mで指定してください.');
 497:         }
 498:         $p[$key] = (float)$p[$key];
 499:     }
 500: 
 501:     // PHPが認識できないタイムゾーンを拒否する.
 502:     if (! in_array($p['timezone'], DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), TRUE)) {
 503:         throw new InvalidArgumentException('タイムゾーンが不正です.');
 504:     }
 505:     $tz = new DateTimeZone($p['timezone']);
 506:     $local = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i', $p['datetime'], $tz);
 507: 
 508:     // 無効な日付・夏時間の欠落時刻・対象年代外の日付を拒否する.
 509:     if ($local === FALSE || $local->format('Y-m-d\TH:i') !== $p['datetime']
 510:         || (int)$local->format('Y') < MIN_YEAR || (int)$local->format('Y') > MAX_YEAR) {
 511:         throw new InvalidArgumentException('日時は1900~2100年の有効な年月日時分で指定してください(夏時間の欠落時刻も指定できません).');
 512:     }
 513:     $p['local'] = $local;
 514:     $p['utc'] = $local->setTimezone(new DateTimeZone('UTC'));
 515: 
 516:     return $p;
 517: }

解説:恒星の位置を求める

恒星表の座標は J2000.0 を基準とした 赤経・赤緯 である。これは地球の緯度・経度のように、空の上での位置を表す座標と考えればよい。そのままでは観測する年による差を反映できない。
starEquatorialOfDate() は恒星自身の移動である 固有運動 と、地球の自転軸の向きがゆっくり変わる 歳差 を反映する。固有運動は3次元のベクトルへ加える形にし、天の極付近で赤緯の余弦による割り算が不安定になるのを避けている。

dateToJulianDay() はUTCの日時を日数の連続値である ユリウス日 へ直し、localSiderealTime() は日時と観測地点の経度から 地方平均恒星時 を求める。恒星時は、地球の自転に対して星がどちらに見えるかを表す基準となる。
equatorialToHorizontal() では恒星時と赤経の差を使い、観測地点の緯度も加えて、東・北・天頂の3方向の成分を計算する。そこから北0度・東90度の 方位角 と地平線0度・天頂90度の 高度 を求める。
makeSkyData() は限界等級より暗い恒星と地平線の下の恒星を除き、描画する恒星の一覧を作る。

skyMap.php

 427: /**
 428:  * 恒星の固有運動とIAU 1976歳差を反映し,J2000.0座標を観測日の平均分点へ変換.
 429:  * @param   array $star 恒星表の行:[id,hip,raDeg,decDeg,mag,bv,pmRaCosDecMasYr,pmDecMasYr]
 430:  * @param   float $jd 観測日時のユリウス日(対象1900~2100年)
 431:  * @return  array [赤経,赤緯](度)
 432: */
 433: function starEquatorialOfDate($star, $jd) {
 434:     $years = ($jd - 2451545.0) / 365.25;
 435:     $a = deg2rad($star[2]);
 436:     $d = deg2rad($star[3]);
 437:     // pmRAはμ_α*cos(δ).極付近の除算を避け,接平面ベクトルで加える.
 438:     $k = deg2rad(1.0 / 3600000.0) * $years;
 439:     $x = cos($d) * cos($a) + $k * (-$star[6] * sin($a) - $star[7] * sin($d) * cos($a));
 440:     $y = cos($d) * sin($a) + $k * ( $star[6] * cos($a) - $star[7] * sin($d) * sin($a));
 441:     $z = sin($d) + $k * $star[7] * cos($d);
 442:     $a = atan2($y, $x);
 443:     $d = atan2($z, sqrt($x * $x + $y * $y));
 444:     $t = $years / 100.0;
 445:     $zeta = deg2rad((2306.2181 * $t + 0.30188 * $t*$t + 0.017998 * $t*$t*$t) / 3600.0);
 446:     $zz = deg2rad((2306.2181 * $t + 1.09468 * $t*$t + 0.018203 * $t*$t*$t) / 3600.0);
 447:     $theta = deg2rad((2004.3109 * $t - 0.42665 * $t*$t - 0.041833 * $t*$t*$t) / 3600.0);
 448:     $aa = cos($d) * sin($a + $zeta);
 449:     $bb = cos($theta) * cos($d) * cos($a + $zeta) - sin($theta) * sin($d);
 450:     $cc = sin($theta) * cos($d) * cos($a + $zeta) + cos($theta) * sin($d);
 451:     return array(normalizeAngle(rad2deg(atan2($aa, $bb) + $zz)), rad2deg(asin(max(-1.0, min(1.0, $cc)))));
 452: }

skyMap.php

 454: /**
 455:  * 赤道座標を大気差なしの地平座標へ変換する.
 456:  * @param   float $ra 赤経(度)
 457:  * @param   float $dec 赤緯(度)
 458:  * @param   float $lst 地方平均恒星時(度)
 459:  * @param   float $latitude 観測地の緯度(北緯+,度)
 460:  * @return  array [方位角,高度](度).方位角は北0・東90
 461: */
 462: function equatorialToHorizontal($ra, $dec, $lst, $latitude) {
 463:     $h = deg2rad(normalizeAngle($lst - $ra));
 464:     $d = deg2rad($dec);
 465:     $p = deg2rad($latitude);
 466:     $east = -cos($d) * sin($h);
 467:     $north = cos($p) * sin($d) - sin($p) * cos($d) * cos($h);
 468:     $up = sin($p) * sin($d) + cos($p) * cos($d) * cos($h);
 469:     $azimuth = hypot($east, $north) < 1.0e-12 ? 0.0 : normalizeAngle(rad2deg(atan2($east, $north)));
 470:     return array($azimuth, rad2deg(atan2($up, hypot($east, $north))));
 471: }

解説:星座線と名称のデータ

星座データは HIP番号 で星座線の端点を指定する。恒星の表示限界を明るい側へ変えても線の形が欠けないよう、端点の計算を恒星の表示対象の抽出から独立させている。限界等級に収まらない端点には補助恒星データも利用する。
星座名の配置位置も観測日時・地点に合わせて変換する。星座線は恒星を結ぶ案内用の図形で、国際天文学連合(IAU)が定める星座の境界線そのものではない。へび座は頭と尾に名称を分けている。

skyMap.php

 622: /**
 623:  * 恒星・星座・天体・天の川の観測日時と観測地点に対応した描画データを生成する.
 624:  * @param   array $p readParametersで検証した観測パラメータ
 625:  * @return  array 星図のメタデータ,恒星・星座・太陽系・月相・天の川を含むJSON応答用データ
 626: */
 627: function makeSkyData($p) {
 628:     // WindowsのACLではis_readable()の判定と実読込が異なる場合がある.実読込で確認する.
 629:     $source = @file_get_contents(STAR_CATALOG);
 630:     // 恒星表を読み込めない場合は計算を中止する.
 631:     if ($source === FALSE) throw new RuntimeException('Star catalog is not readable');
 632:     $catalog = json_decode($source, TRUE, 512, JSON_THROW_ON_ERROR);
 633:     $jd = dateToJulianDay($p['utc']);
 634:     $lst = localSiderealTime($jd, $p['longitude']);
 635:     $milkywaySource=@file_get_contents(MILKYWAY_CATALOG);
 636: 
 637:     // 天の川データを読み込めない場合は計算を中止する.
 638:     if($milkywaySource===FALSE)throw new RuntimeException('Milky Way data not readable');
 639:     $milkyway=json_decode($milkywaySource,TRUE,512,JSON_THROW_ON_ERROR);
 640: 
 641:     // J2000基底を観測日の赤道座標へ変換.逆写像には転置行列を使う.
 642:     $basis=array();
 643:     // J2000の三つの基底軸を観測日の平均分点へ変換する.
 644:     foreach(array(array(0,0),array(90,0),array(0,90)) as $axis){
 645:         list($a,$d)=starEquatorialOfDate(array(0,0,$axis[0],$axis[1],0,0,0,0),$jd);
 646:         $basis[]=array(cos(deg2rad($d))*cos(deg2rad($a)),cos(deg2rad($d))*sin(deg2rad($a)),sin(deg2rad($d)));
 647:     }
 648:     $milkyway['precessionBasis']=$basis;
 649:     $source = @file_get_contents(CONSTELLATION_CATALOG);
 650: 
 651:     // 星座データを読み込めない場合は計算を中止する.
 652:     if ($source === FALSE) throw new RuntimeException('Constellation catalog is not readable');
 653:     $figures = json_decode($source, TRUE, 512, JSON_THROW_ON_ERROR);
 654:     $names = $figures['starNames'];
 655:     // 星座線の端点は限界等級・地平線による恒星の絞り込みとは独立に計算する.
 656:     $needed = array();
 657:     // 各星座から星座線に必要な恒星番号を集める.
 658:     foreach ($figures['constellations'] as $c)
 659:     // 各線分の両端の番号を必要な恒星の集合へ追加する.
 660:     foreach ($c['lines'] as $line) {
 661:         $needed[$line[0]] = TRUE; $needed[$line[1]] = TRUE;
 662:     }
 663:     $vertices = array();
 664: 
 665:     // 恒星表と補助恒星を合わせて星座線の端点を計算する.
 666:     foreach (array_merge($catalog['stars'], $figures['supportStars']) as $s) {
 667:         // 星座線に使わない恒星は端点計算から除く.
 668:         if (! isset($needed[$s[1]])) continue;
 669:         list($ra, $dec) = starEquatorialOfDate($s, $jd);
 670:         list($az, $alt) = equatorialToHorizontal($ra, $dec, $lst, $p['latitude']);
 671:         $vertices[$s[1]] = array($az, $alt);
 672:     }
 673:     $constellations = array();
 674: 
 675:     // 各星座の線と名称の描画用データを作る.
 676:     foreach ($figures['constellations'] as $c) {
 677:         $labels = array();
 678:         // 複数の名称位置がある星座も位置ごとに変換する.
 679:         foreach ($c['labels'] as $label) {
 680:             list($ra, $dec) = starEquatorialOfDate(array(0,0,$label['raDeg'],$label['decDeg'],0,0,0,0), $jd);
 681:             list($az, $alt) = equatorialToHorizontal($ra, $dec, $lst, $p['latitude']);
 682:             $labels[] = array('name'=>$label['name'], 'azimuthDeg'=>$az, 'altitudeDeg'=>$alt);
 683:         }
 684:         $constellations[] = array('id'=>$c['id'], 'name'=>$c['name'], 'lines'=>$c['lines'], 'labels'=>$labels);
 685:     }
 686:     $stars = array();
 687: 
 688:     // 恒星表の各恒星を観測日の地平座標へ変換する.
 689:     foreach ($catalog['stars'] as $s) {
 690:         // 指定した限界等級より暗い恒星を表示対象から除く.
 691:         if ($s[4] > $p['magnitude']) continue;
 692:         list($ra, $dec) = starEquatorialOfDate($s, $jd);
 693:         list($az, $alt) = equatorialToHorizontal($ra, $dec, $lst, $p['latitude']);
 694:         // 地平線の下にある恒星を表示対象から除く.
 695:         if ($alt < 0.0) continue;
 696:         $stars[] = array('id'=>$s[0], 'hip'=>$s[1], 'azimuthDeg'=>round($az, 6),
 697:             'altitudeDeg'=>round($alt, 6), 'magnitude'=>$s[4], 'bv'=>$s[5],
 698:             'name'=>isset($names[$s[1]]) ? $names[$s[1]] : '');
 699:     }
 700:     $bodies=makeSolarSystemData($p,$lst);
 701: 
 702:     return array('version'=>VERSION, 'utc'=>$p['utc']->format('Y-m-d\TH:i:s\Z'),
 703:         'local'=>$p['local']->format('Y-m-d H:i P'), 'timezone'=>$p['timezone'],
 704:         'latitude'=>$p['latitude'], 'longitude'=>$p['longitude'], 'magnitude'=>$p['magnitude'],
 705:         'julianDay'=>$jd, 'siderealDeg'=>$lst, 'catalogCount'=>count($catalog['stars']), 'stars'=>$stars,
 706:         'constellations'=>$constellations, 'lineVertices'=>$vertices, 'height'=>$p['height'],
 707:         'bodies'=>$bodies,'moonPhase'=>makeMoonPhase($bodies),'milkyWay'=>$milkyway);
 708: }

解説:太陽・月・惑星と月相

makeSolarSystemData() は pahooAstronomy を呼び、太陽・月と、水星・金星・火星・木星・土星・天王星・海王星の計9天体を計算する。
太陽は longitude_sun() と distance_sun()、月は longitude_moon()・latitude_moon()・distance_moon()、惑星は zodiacEarth() を利用する。黄道座標を eclip2equat() で赤道座標へ変換し、観測地点から見た方位・高度を求める。

地球の中心から見た座標から、緯度・経度・標高に応じた観測点のベクトルを引き、視差 を補正する。これは特に地球に近い月で大切になる。
現行の共通クラスでは金星・木星・土星・天王星・海王星に VSOP87D の全係数を使用し、水星・火星には平均軌道要素による計算を使う。地球の係数も生成できるが、星図の通常処理で常に6天体分すべてを読むわけではない。画面の計算条件には金星について旧説明が残るため、現行クラスの処理に合わせて理解していただきたい。

makeMoonPhase() は観測点からの太陽と月の位置・距離を使い、月から太陽へ向かう方向と、月から観測者へ向かう方向の角度を求める。この角度の余弦から、月面の照らされて見える割合を計算する。
新月付近・上弦付近などの名称は太陽と月の黄経の差から決める。一方、明るい側の向きは3次元の照明方向として渡し、ブラウザーの makeMoonLayer() で月の球面を明暗に塗り分ける。星図上の月と拡大図でこの画像を使う。

skyMap.php

 519: /**
 520:  * 既存クラスの計算結果を使い,太陽・月・7惑星の地平座標に観測地点の視差を補正.
 521:  * @param   array $p 検証済み観測パラメータ(utc・latitude・longitude・height等)
 522:  * @param   float $lst 地方平均恒星時(度)
 523:  * @return  array 地平線下を含む9天体の位置・距離・名称等.大気差なし
 524: */
 525: function makeSolarSystemData($p, $lst) {
 526:     // 天文計算クラス:include_pathが通ったディレクトリに配置
 527:     require_once('pahooAstronomy.php');
 528: 
 529:     $astronomy = new pahooAstronomy('jp', 0.0);
 530:     // 天文計算クラスを初期化できなければ計算を中止する.
 531:     if ($astronomy->error) throw new RuntimeException('UTC astronomy initialization failed');
 532:     $args = array_map('intval', explode(' ', $p['utc']->format('Y n j G i s')));
 533:     $names = array('Sun'=>'太陽','Moon'=>'月','Mercury'=>'水星','Venus'=>'金星','Mars'=>'火星',
 534:         'Jupiter'=>'木星','Saturn'=>'土星','Uranus'=>'天王星','Neptune'=>'海王星');
 535:     $bodies = array();
 536: 
 537:     // 太陽・月・各惑星を共通の観測条件で計算する.
 538:     foreach ($names as $id=>$name) {
 539:         // 太陽には既存の太陽位置と距離の計算を使用する.
 540:         if ($id === 'Sun') {
 541:             $longitude = $astronomy->longitude_sun(...$args); $latitude = 0.0;
 542:             $distance = $astronomy->distance_sun(...$args) * 149597870.7;
 543:         }
 544:         // 月には既存の月位置と地心距離の計算を使用する.
 545:         else if ($id === 'Moon') {
 546:             $longitude = $astronomy->longitude_moon(...$args);
 547:             $latitude = $astronomy->latitude_moon(...$args);
 548:             $distance = $astronomy->distance_moon(...$args) * 384400.0;
 549:         }
 550:         // 惑星には既存の惑星黄道座標と距離の計算を使用する.
 551:         else {
 552:             list($longitude, $latitude, $distance) = $astronomy->zodiacEarth($id, ...$args);
 553:             $distance *= 149597870.7;
 554:         }
 555:         list($ra, $dec) = $astronomy->eclip2equat($longitude, $latitude, ...$args);
 556: 
 557:         // 月では特に大きい地心視差を,観測地点の3次元ベクトルで補正する.
 558:         $u = atan(0.9966471893 * tan(deg2rad($p['latitude'])));
 559:         $rhoCos = 6378.137 * cos($u) + $p['height']/1000.0 * cos(deg2rad($p['latitude']));
 560:         $rhoSin = 6356.7523142 * sin($u) + $p['height']/1000.0 * sin(deg2rad($p['latitude']));
 561:         $x = $distance*cos(deg2rad($dec))*cos(deg2rad($ra)) - $rhoCos*cos(deg2rad($lst));
 562:         $y = $distance*cos(deg2rad($dec))*sin(deg2rad($ra)) - $rhoCos*sin(deg2rad($lst));
 563:         $z = $distance*sin(deg2rad($dec)) - $rhoSin;
 564:         $topRa = normalizeAngle(rad2deg(atan2($y,$x)));
 565:         $topDec = rad2deg(atan2($z,hypot($x,$y)));
 566:         list($az,$alt) = equatorialToHorizontal($topRa,$topDec,$lst,$p['latitude']);
 567:         $bodies[] = array('id'=>$id,'name'=>$name,'azimuthDeg'=>$az,'altitudeDeg'=>$alt,
 568:             'aboveHorizon'=>$alt>=0.0,'geocentricRaDeg'=>$ra,'geocentricDecDeg'=>$dec,
 569:             'topocentricRaDeg'=>$topRa,'topocentricDecDeg'=>$topDec,'topocentricDistanceKm'=>sqrt($x*$x+$y*$y+$z*$z),
 570:             'distanceKm'=>$distance,'eclipticLongitudeDeg'=>$longitude,'eclipticLatitudeDeg'=>$latitude);
 571:     }
 572:     return $bodies;
 573: }

skyMap.php

 575: /**
 576:  * 観測地点からの太陽・月の位置と距離から,月の位相角・照明率・照明方向を求める.
 577:  * @param   array $bodies makeSolarSystemDataの9天体の配列
 578:  * @return  array name・phaseAngleDeg・illuminatedFraction(0~1)・elongationDeg・lightDirection(東,北,天頂)・aboveHorizon
 579: */
 580: function makeMoonPhase($bodies) {
 581:     $byId = array_column($bodies,NULL,'id');
 582:     $sun = $byId['Sun']; $moon = $byId['Moon'];
 583:     $vectors = array();
 584: 
 585:     // 太陽と月の地平座標を観測地点からの位置ベクトルへ変換する.
 586:     foreach (array('Sun'=>$sun,'Moon'=>$moon) as $id=>$b) {
 587:         $a=deg2rad($b['azimuthDeg']); $h=deg2rad($b['altitudeDeg']); $r=$b['topocentricDistanceKm'];
 588:         $vectors[$id]=array($r*cos($h)*sin($a),$r*cos($h)*cos($a),$r*sin($h));
 589:     }
 590:     $light = array(); $length=0.0;
 591: 
 592:     // 三つの軸ごとに月から太陽へのベクトルと長さを求める.
 593:     for($i=0;$i<3;$i++){ $light[$i]=$vectors['Sun'][$i]-$vectors['Moon'][$i]; $length+=$light[$i]*$light[$i]; }
 594:     $length=sqrt($length); $cosPhase=0.0;
 595: 
 596:     // 照明方向を正規化し,観測者方向との位相角の余弦を求める.
 597:     for($i=0;$i<3;$i++){ $light[$i]/=$length; $cosPhase-=$light[$i]*$vectors['Moon'][$i]/$moon['topocentricDistanceKm']; }
 598:     $cosPhase=max(-1.0,min(1.0,$cosPhase));
 599:     $angle=normalizeAngle($moon['eclipticLongitudeDeg']-$sun['eclipticLongitudeDeg']);
 600: 
 601:     // 黄経差が0度付近なら新月付近と案内する.
 602:     if($angle<10||$angle>=350)$name='新月付近';
 603:     // 上弦より前の黄経差なら満ちていく月と案内する.
 604:     else if($angle<80)$name='満ちていく月';
 605:     // 黄経差が90度付近なら上弦付近と案内する.
 606:     else if($angle<100)$name='上弦付近';
 607:     // 上弦から満月の間なら満月へ向かう月と案内する.
 608:     else if($angle<170)$name='満月へ向かう月';
 609:     // 黄経差が180度付近なら満月付近と案内する.
 610:     else if($angle<190)$name='満月付近';
 611:     // 満月から下弦の間なら欠けていく月と案内する.
 612:     else if($angle<260)$name='欠けていく月';
 613:     // 黄経差が270度付近なら下弦付近と案内する.
 614:     else if($angle<280)$name='下弦付近';
 615:     // 残りの黄経差では新月へ向かう月と案内する.
 616:     else $name='新月へ向かう月';
 617:     return array('name'=>$name,'phaseAngleDeg'=>rad2deg(acos($cosPhase)),
 618:         'illuminatedFraction'=>(1.0+$cosPhase)/2.0,'elongationDeg'=>$angle,
 619:         'lightDirection'=>$light,'aboveHorizon'=>$moon['aboveHorizon']);
 620: }

解説:Canvasへの投影と描画

projectStar() は方位と高度をCanvas上の座標へ変換するJavaScript関数である。全天表示は天頂中心の 正距方位図法 で、中心からの距離を「90度−高度」に比例させる。東を左へ描くため、横座標は方位角の正弦を引く形になる。
方角別表示では中心方位からの差を使い、左右45度の範囲を半円へ投影する。図外の天体を除き、星座線は地平線や表示範囲との交点を処理して、見える部分だけを描く。

drawSky() は背景、天の川、補助線、星座線、恒星、太陽系天体、ラベルなどを重ねる。恒星は明るいほど大きく濃くし、B−V色指数 に応じて青白色から淡い黄・橙色へ色分けする。色指数がない恒星には白を使う。
名称は文字の領域が重ならないように配置する。主要恒星名は明るさを優先して表示候補の約半数までに抑え、省略した名称は恒星の選択で確認できる。星座名は代表的なものを優先し、画面全体へ分散させる。

skyMap.php

 987: function projectStar(azimuth, altitude) {
 988:     // 方角別表示では指定方向の左右45度を半円へ投影する.
 989:     if(viewMode!=='all'){
 990:         const h=altitude*Math.PI/180,delta=azimuthOffset(azimuth)*Math.PI/180;
 991:         return {x:center+radius*Math.sin(2*delta)*Math.cos(h),y:mapBase()-radius*Math.sin(h)};
 992:     }
 993:     const r = radius * (90 - altitude) / 90, a = azimuth * Math.PI / 180;
 994:     return {x:center-r*Math.sin(a), y:center-r*Math.cos(a)};
 995: }

解説:天の川の描画

天の川は一つ一つの恒星を追加するのではなく、D3 Celestial の輪郭データから作った720×360画素の濃度画像を使う。makeSkyData() はJ2000.0の3本の基準軸を観測日の座標へ変換し、その結果もJSONへ入れる。
JavaScriptの makeMilkyWayLayer() は星図の各画素を inverseProject() で方位・高度へ戻し、観測日の赤道座標からJ2000.0へ逆変換する。その場所の天の川の濃度を補間して、透過画像として重ねる。これにより、日時・地点・表示方向に合わせて天の川の位置も変わる。
この濃淡は星空案内用の概略表現で、光害や月明かりによる見え方を再現するものではない。

解説:地図画面との連携

メイン処理は action=location を makeLocationPicker() へ、action=altitude を標高取得へ振り分ける。前者は pahooGeoCode の searchPoint3() と drawJSmap()、後者は getAltitude() を利用する。
地図画面からは postMessage() で座標と標高を親画面へ渡す。受信側は送信元、開いたウィンドウ、要求ごとのトークンを確認する。これにより、関係のないページから送られた値をそのまま入力欄へ採用するのを避ける。
標高を取得できなければ NULL を返し、画面では標高を未入力にして手入力を案内する。以前の場所の標高や0mを成功値として流用しない。

skyMap.php

 164: /**
 165:  * 標高を取得する.0mと取得失敗を区別する.
 166:  * @param   float $latitude 緯度(世界測地系,度)
 167:  * @param   float $longitude 経度(世界測地系,東経+,度)
 168:  * @return  float|null 標高(m,−500~9000).取得できない場合はNULL
 169: */
 170: function getLocationAltitude($latitude, $longitude) {
 171:     // 住所・緯度・経度に関わるクラス:include_pathが通ったディレクトリに配置
 172:     require_once('pahooGeoCode.php');
 173: 
 174:     $pgc = new pahooGeoCode();
 175:     $previous = ini_set('default_socket_timeout', '8');
 176:     try {
 177:         // XMLを読めない環境では標高取得不能として返す.
 178:         if (! function_exists('simplexml_load_file')) return NULL;
 179:         $value = $pgc->getAltitude($latitude, $longitude);
 180:         return $value !== FALSE && is_numeric($value) && is_finite((float)$value) && $value >= -500 && $value <= 9000 ? (float)$value : NULL;
 181:     } finally {
 182:         // 一時的に変更した通信タイムアウトを元へ戻す.
 183:         if ($previous !== FALSE) ini_set('default_socket_timeout', $previous);
 184:     }
 185: }

解説:画像ファイルへの保存

getExportBlob() は保存用Canvasを用意し、現在の表示方向・レイヤーを高解像度で描き直す。タイトル、日時、観測地点、限界等級、凡例、出典、著作権表示を加え、生成した Blob をダウンロード用リンクへ渡す。画像の生成はブラウザー内で完結する。
JPEG・PNG・WebPはCanvasの toBlob() を利用する。要求した形式と実際のMIME型を比較し、対応していない形式を別形式の拡張子で保存しないようにする。
BMPは encodeBmp() で24bit形式を組み立てる。CanvasのRGBAデータからBGRの順へ並べ替え、下の行から保存する。各行の長さを4バイトの倍数へそろえる余白も加える。

skyMap.php

1500: function encodeBmp(source){
1501:     const w=source.width,h=source.height,stride=(w*3+3)&~3,buffer=new ArrayBuffer(54+stride*h),v=new DataView(buffer),bytes=new Uint8Array(buffer);
1502:     v.setUint16(0,0x4d42,true);v.setUint32(2,buffer.byteLength,true);v.setUint32(10,54,true);v.setUint32(14,40,true);
1503:     v.setInt32(18,w,true);v.setInt32(22,h,true);v.setUint16(26,1,true);v.setUint16(28,24,true);v.setUint32(34,stride*h,true);
1504:     const rgba=source.getContext('2d').getImageData(0,0,w,h).data;
1505: 
1506:     // BMPの各走査行を画像の下側から格納する.
1507:     for(let y=0;y<h;y++)
1508: 
1509:     // 各画素のRGBをBMP用のBGR順へ並べ替える.
1510:     for(let x=0;x<w;x++){
1511:         const s=(y*w+x)*4,d=54+(h-1-y)*stride+x*3;bytes[d]=rgba[s+2];bytes[d+1]=rgba[s+1];bytes[d+2]=rgba[s];
1512:     }
1513: 
1514:     return new Blob([buffer],{type:'image/bmp'});
1515: }

解説:JSON出力とエラー処理

URLに format=json を指定すると、画面の代わりに描画用データをJSONで返す。たとえば、東京付近の2026年(令和8年)10月22日21時を指定するURLは次のようになる。
skyMap.php?latitude=35.681&longitude=139.767&height=3.4&datetime=2026-10-22T21%3A00&timezone=Asia%2FTokyo&magnitude=6.0&format=json
JSONには stars、constellations、lineVertices、bodies、moonPhase、milkyWay などが入る。bodies は地平線下の天体も保持し、地平線上かどうかを aboveHorizon で表す。
不正な入力はHTTP 400、星図の計算やファイル読込に失敗した場合はHTTP 500で通知する。内部の例外内容はサーバーログへ記録し、画面には計算できない旨を表示する。標高取得失敗はHTTP 503と height=null などで通知する。

外部データ生成ツールの使い方

skyMapDataBuilder.php をブラウザーで開くと、恒星表・星座・天の川と、金星・地球・木星・土星・天王星・海王星の係数、計9対象が表示される。初期状態は全選択で、すべて と クリア で一括変更できる。
1つ以上の対象を選び、フォルダー保存またはZIP保存を指定して生成する。Chrome・EdgeのlocalhostまたはHTTPS環境では、最初に保存先を選び、その後サーバーが原本を取得・加工する。生成には数分かかることがある。

フォルダー保存は、選択した端末のフォルダーへ相対的な階層を保って書き出す。同名ファイルは置換されるため、既存データは事前にバックアップしていただきたい。対応する保存APIがない場合は、ZIPのダウンロードリンクから保存する。
このツールは利用者の端末へ生成物を保存するものであり、入力した任意のサーバーパスへ書き込む仕組みではない。生成データを星図サーバーへ置く作業は別途必要となる。

解説:原本の取得とデータ変換

builderDownload() が固定コミットの原本を取得し、builderStars() がHYG v4.1のCSVを変換する。赤経は時から度へ15倍し、必要な8列へ絞る。6.5等までの恒星を抽出し、8,920個であることも検証する。
builderConstellations() はStellarium Modernの線分をHIP番号で恒星表と対応付け、日本語の星座名・恒星名、名称の配置位置、補助恒星を付ける。星座だけを選択した場合も対応付けにHYGを取得するが、恒星表JSONは出力しない。

builderMilkyway() は天の川の輪郭をGDで塗り、領域の重なりを濃度として加算する。赤経の継ぎ目や輪郭内の穴を処理し、ガウス平滑化で境界をなめらかにして、8bit濃度を行順に並べたバイナリをbase64化する。
VSOP87Dは共通の原本を1回取得し、選択した天体の経度・緯度・距離の全係数を天体別JSONへ分ける。恒星表・星座で共用するHYGも1要求内では共通化する。毎回原本を取得するため、以前保存した原本ファイルには依存しない。

skyMapDataBuilder.php

 116: /**
 117:     * HYG原本から描画用恒星表とHIP索引を作る.
 118:     * @param    string $csv HYG v4.1 CSV本文
 119:     * @return   array catalog(6.5等まで)・byHip(全恒星)の配列
 120:     * @throws RuntimeException CSV構造・件数が不正
 121:     */
 122: function builderStars($csv) {
 123:     $fp = fopen('php://temp/maxmemory:5242880', 'w+'); fwrite($fp, $csv); rewind($fp);
 124:     $header = fgetcsv($fp, 0, ',', '"', ''); $stars = array(); $byHip = array();
 125:     // 変換に必要な列がそろっていることを確認する.
 126:     foreach (array('id','hip','ra','dec','mag','ci','pmra','pmdec') as $key) {
 127:         // CSVの仕様変更は黙って読み飛ばさず中断する.
 128:         if (!is_array($header) || !in_array($key, $header, true)) throw new RuntimeException('HYG列不足: '.$key);
 129:     }
 130:     // 恒星ごとに単位を変換し,HIP索引と明るい恒星の表を作る.
 131:     while (($row = fgetcsv($fp, 0, ',', '"', '')) !== false) {
 132:         // 欠落行は不正な原本として扱う.
 133:         if (count($row) !== count($header)) throw new RuntimeException('HYG CSV行の列数が不正です.');
 134:         $s = array_combine($header, $row);
 135:         // 太陽と等級未収録の天体を除く.
 136:         if ((int)$s['id'] === 0 || $s['mag'] === '') continue;
 137:         $v = array((int)$s['id'], (int)$s['hip'], (float)$s['ra']*15, (float)$s['dec'],
 138:             (float)$s['mag'], $s['ci']==='' ? null : (float)$s['ci'], (float)$s['pmra'], (float)$s['pmdec']);
 139:         // HIP番号を持つ恒星を星座線の端点参照に使う.
 140:         if ($v[1]) $byHip[$v[1]] = $v;
 141:         // 星空マップの最大限界等級までを出力する.
 142:         if ($v[4] <= 6.5) $stars[] = $v;
 143:     }
 144:     fclose($fp);
 145:     // 想定外の原本を検出するため,既知のv4.1件数を確認する.
 146:     if (count($stars) !== 8920) throw new RuntimeException('HYG v4.1の恒星数が想定と異なります: '.count($stars));
 147:     return array('catalog'=>array('source'=>'HYG v4.1 / David Nash (Astronexus)', 'license'=>'CC BY-SA 4.0',
 148:         'epoch'=>2000.0, 'columns'=>array('id','hip','raDeg','decDeg','mag','bv','pmRaCosDecMasYr','pmDecMasYr'),
 149:         'stars'=>$stars), 'byHip'=>$byHip);
 150: }

解説:生成記録とフォルダー・ZIP保存

builderBuild() は選択したデータと必要な権利資料をまとめ、skyMap-data-manifest.json にツールのバージョン、選択対象、原本URL、取得日時、原本と出力ファイルの SHA-256 を記録する。生成ツール専用の案内文「!最初に読んでください.txt」も加える。
builderZip() はZIPのローカルヘッダー、各ファイル、中央ディレクトリー、終端レコードを pack() で作る。ファイル内容は無圧縮で、CRC-32とUTF-8のファイル名フラグを付ける。このため ZipArchive 拡張を必要としない。

ブラウザーはPOSTで生成を要求し、ファイル内容とZIPをbase64で受信する。フォルダー保存では受信した相対パスを確認してから必要な階層を作り、ZIP保存では showSaveFilePicker() で得た保存先へ書き込む。処理が途中で失敗した場合にも取り出せるよう、生成済みZIPへのリンクを残す。
コマンドラインから全対象を生成する場合は、次のように実行できる。星図本体や共通PHPクラスを生成するものではない。
php skyMapDataBuilder.php all skyMap-data.zip

skyMapDataBuilder.php

 351: /**
 352:     * UTF-8ファイル名対応の無圧縮ZIPを作る(ZipArchive拡張不要).
 353:     * @param    array $files 相対パスをキー,ファイル本文を値とする配列
 354:     * @return   string ZIPバイナリ
 355:     */
 356: function builderZip($files) {
 357:     $zip = ''; $central = ''; $count = 0; $date = (46<<9) | (1<<5) | 1;
 358:     // ファイル本体と中央ディレクトリーを順に構築する.
 359:     foreach ($files as $name=>$body) {
 360:         $offset = strlen($zip); $size = strlen($body); $crc = crc32($body); $n = strlen($name);
 361:         $zip .= pack('VvvvvvVVVvv', 0x04034b50, 20, 0x800, 0, 0, $date, $crc, $size, $size, $n, 0).$name.$body;
 362:         $central .= pack('VvvvvvvVVVvvvvvVV', 0x02014b50, 20, 20, 0x800, 0, 0, $date, $crc, $size, $size, $n, 0, 0, 0, 0, 0, $offset).$name;
 363:         $count++;
 364:     }
 365:     return $zip.$central.pack('VvvvvVVv', 0x06054b50, 0, 0, $count, $count, strlen($central), strlen($zip), 0);
 366: }

外部データの出典とライセンス

自作コードと外部データの利用条件は分けて扱う。HYG・Stellarium由来の加工データでは作者・出典・ライセンスへのリンクと加工の明示を保持し、CC BY-SA 4.0の継承条件に従う。天の川データではBSD 3-Clauseの著作権表示・条件・免責を保持する。
VSOP87Dの配布元は、自身の作業をPublic Domainとする宣言と、元理論は自身の作業ではない旨を記している。この宣言を第三者の作業まで広げて解釈せず、付属する権利資料を確認する。生成データ一式をMIT Licenseへ変更する扱いにはしない。

保存画像にもデータの出典とライセンス表記を残す。地図サービスの帰属表示もそれぞれの条件に従う。
仕様書にある確認事項として、現行画面にはVSOP87Dの common/vsop87/LICENSE.txt への直接リンクと宣言の適用範囲の説明が不足している。本記事では出典を明示したが、配布物・画面側の権利表記の補完は別途必要である。
使用する外部データ
データ出典・加工ライセンス等
恒星表HYG v4.1/David Nash(Astronexus)。等級抽出、列削減、単位変換CC BY-SA 4.0
星座線・名称Stellarium Modern/Stellarium team。HIP番号の対応、日本語名・配置位置・補助恒星を追加CC BY-SA 4.0。星座イラストは使用しない
天の川D3 Celestial/Olaf Frohn、原輪郭Jose R. Vieira。濃度画像化・平滑化BSD 3-Clause
惑星係数VSOP87D/Bureau des Longitudes、JSON配布Greg A. Miller。天体別に全項を抽出配布元のLICENSE.txtとsource.txtを保持

計算条件と注意点

恒星の基準座標はJ2000.0、歳差はIAU 1976で、UTCをUT1の近似として用いる。恒星の計算には章動・年周光行差・大気差を含まない。太陽系天体も使用クラスと計算経路に依存し、精密な天文暦と同じ精度を保証するものではない。
標高を含む観測点からの視差は補正するが、標高による地平線の低下は加算しない。山・建物による遮蔽も考慮しないため、高度が正の天体でも実際には見えない場合がある。

天体の記号は実際の見かけの直径や惑星の明るさを再現しない。月の表示には月面模様、秤動、月食の影を含まない。
また、光害・薄明・月明かり・雲の影響は再現しない。表示された恒星や天の川が、その場所・時刻に肉眼で見えるという意味ではない。星の方角や時刻による変化を学ぶ用途に活用していただきたい。

参考サイト

(この項おわり)
header