PHPで天気図を描く

(1/1)
気象庁防災情報 XMLには、地上実況図として、気圧配置の座標や等圧線・前線の座標が配信されている。
そこで今回は、これらの情報をリアルタイムに取得し、Google マップなどのマップに天気図を描く PHP プログラムを作ってみることにする。

(2021 年 4 月 18 日)台風,熱帯低気圧に対応.
(2021 年 4 月 10 日)キャッシュ・システム導入:pahooCache クラス.
(2021 年 3 月 23 日)require_once('pahooWeather.php') は不要なので除いた。

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

PHPで天気図を描く

目次

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

圧縮ファイルの内容
weatherMap.phpサンプル・プログラム本体。
pahooGeoCode.php住所・緯度・経度に関わるクラス pahooGeoCode。
使い方は「PHPで住所・ランドマークから最寄り駅を求める」「PHPで住所・ランドマークから緯度・経度を求める」などを参照。include_path が通ったディレクトリに配置すること。
pahooCache.phpキャッシュ処理に関わるクラス pahooCache。
キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。

準備:pahooGeoCode クラス

0037: class pahooGeoCode {
0038:     var $items;      //検索結果格納用
0039:     var $error;      //エラーフラグ
0040:     var $hits;       //検索ヒット件数
0041:     var $webapi; //直前に呼び出したWebAPI URL
0042: 
0043:     //Google Cloud Platform APIキー
0044:     //https://cloud.google.com/maps-platform/
0045:     //※Google Maps APIを利用しないのなら登録不要
0046:     var $GOOGLE_API_KEY_1 = '**************************';   //HTTPリファラ用
0047:     var $GOOGLE_API_KEY_2 = '**************************';   //IP制限用
0048: 
0049:     //Yahoo! JAPAN Webサービス アプリケーションID
0050:     //https://e.developer.yahoo.co.jp/register
0051:     //※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
0052:     var $YAHOO_APPLICATION_ID = '*****************************';

地図サービスを利用するために、クラスファイル "pahooGeoCode.php" を使用する。組み込み関数  require_once  を使って読めるディレクトリに配置する。ディレクトリは、設定ファイル php.ini に記述されているオプション設定 include_path に設定しておく。
クラスについては「PHP でクラスを使ってテキストの読みやすさを調べる」を参照されたい。

地図や住所検索として Google を利用するのであれば、Google Cloud Platform API キー が必要で、その入手方法は「Google Cloud Platform - WebAPI の登録方法」を参照されたい。

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

0034: //地図描画サービスの選択
0035: //    0:Google
0036: //    2:地理院地図・OSM
0037: define('MAPSERVICE', 2);
0038: 
0039: //マップの表示サイズ(単位:ピクセル)
0040: define('MAP_WIDTH',  600);
0041: define('MAP_HEIGHT', 480);
0042: //マップID
0043: define('MAPID', 'map_id');
0044: //初期値
0045: define('DEF_LATITUDE',  35.0);           //緯度
0046: define('DEF_LONGITUDE', 137.0);           //経度
0047: define('DEF_TYPE',     'GSISTD');        //マップタイプ
0048: define('DEF_ZOOM',     4);              //ズーム
0049: 
0050: define('SEMICIRCLE', 30);         //半円を代替する多角形頂点数

表示する地図は、Google マップ地理院地図・オープンストリートマップ(OSM)から選べる。あらかじめ、定数 MAPSERVIC に値を設定すること。
住所検索サービスは、GoogleYahoo!JAPANHeartRails Geo APIOSM Nominatim Search API から選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
PHPで天気図を描く
地理院地図表示
PHPで天気図を描く
オープンストリートマップ表示

準備:キャッシュ・システム

0083: //キャッシュ保持時間(分) 0:キャッシュしない
0084: //気象庁へのアクセス負荷軽減のため,60分以上のキャッシュ保持をお勧めします.
0085: define('LIFE_CACHE', 120);
0086: 
0087: //キャッシュ・ディレクトリ
0088: //書き込み可能で,外部からアクセスされないディレクトリを指定してください.
0089: //最大150Mバイトを消費します.天気予報系プログラムは同じディレクトリで構わない.
0090: define('DIR_CACHE', './pcache/');

気象庁サイトへ負荷をかけないよう、キャッシュ・クラス pahooCache を導入した。使用方法については、「PHP で天気予報を求める - キャッシュ・システム」を参照いただきたい。
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。

気象庁防災情報XMLフォーマット

気象庁防災情報 XML フォーマットについては、「PHP で天気予報を求める - 気象庁防災情報 XML フォーマット」をご覧いただきたい。
今回は、高頻度 - 定時更新フィードにアクセスし、電文コード VZSA50 の地上実況図を取得する。

VZSA50の構造

地上実況図 XML VZSA50 には、ある地域の 1週間分の天気予報が収められており、構造は下記の通りである。ここから必要な情報を取り出す。
注意すべきは、1 つの XML ファイルの中に 1 つまたは複数の予報地点が含まれていること。さらに、天気予報(アイコン画像を含む)と降水確率は「地方」に紐付いているが、最低気温・最高気温は「予報地点」に紐付いているということである。地方と予報地点は 1 対 N 対応のため、後述する予報値地点データファイルに用意しておくことにした。
また、TimeDefine タグに示される 7 日分の情報が格納されているのだが、XML 情報の取得時間帯によって、1 つ目(refID=1)の情報が、今日になるか明日になるか分かれる。そこで、TimeDefine を取得して、現在日時(PC の内蔵時計)と比較して決定することにした。
降水確率や気温は、XML 情報の取得タイミングによっては、1 つ目(refID=1)~3 つ目(refID=3)が「値なし」になっていることがある。そこで、後述する情報 XML VPFW50 から、今日~明後日(または明日~明後日)の情報を取り出し、補完することにする。
VZSA50の構造(xml) Report Control Title 地上実況図 DateTime 配信日時 Status ステータス EditorialOffice 編集者 PublishingOffice 配信者 Head Title 地上実況図 ReportDateTime 配信日時 TargetDateTime 対象日時 InfoKind 情報の種類 InfoKindVersion 情報バージョン Headline Text Body MeteorologicalInfos MeteorologicalInfo DateTime 実況日時 Item Kind Property Type 等圧線 IsobarPart Pressure 気圧(hPa) Line 座標列 Item Kind Property Type 低気圧|高気圧 CenterPart Coordinate 中心座標 Direction 移動方向 Speed 移動速度(km/h) Speed 移動速度(ノット) Pressure 中心気圧(hPa) Item Kind Property Type 温暖|寒冷|停滞|閉塞前線 CoordinatePart Line 座標列

準備:表示幅、表示列数など

0039: //マップの表示サイズ(単位:ピクセル)
0040: define('MAP_WIDTH',  600);
0041: define('MAP_HEIGHT', 480);
0042: //マップID
0043: define('MAPID', 'map_id');
0044: //初期値
0045: define('DEF_LATITUDE',  35.0);           //緯度
0046: define('DEF_LONGITUDE', 137.0);           //経度
0047: define('DEF_TYPE',     'GSISTD');        //マップタイプ
0048: define('DEF_ZOOM',     4);              //ズーム
0049: 
0050: define('SEMICIRCLE', 30);         //半円を代替する多角形頂点数

マップの表示サイズなどの初期値は、とくに断りがないかぎりは自由に変更できる。

解説:最新の地上実況図URLを取得

0193: /**
0194:  * 気象庁防災情報XMLから最新の地上実況図URLを取得
0195:  * @param object $pcc pahooCacheオブジェクト
0196:  * @return string 地上実況図URL/FALSE:取得失敗
0197: */
0198: function jmaGetWeatherMapURL($pcc) {
0199:     //URLパターン
0200:     $vzsa50 = '/http\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VZSA50\_([0-9\_]+)\.xml/ui';
0201: 
0202:     $xml = $pcc->simplexml_load(FEED_REGULAR);
0203:     //レスポンス・チェック
0204:     if ($pcc->iserror() || !isset($xml->entry)) {
0205:         return FALSE;
0206:     }
0207: 
0208:     //フィード(XMLファイル)解析
0209:     $vzsa50_url = $vzsa50_dt = '';
0210:     $res = FALSE;
0211:     foreach ($xml->entry as $node) {
0212:         //日時がより新しいURLを採用
0213:         if (preg_match($vzsa50$node->id$arr) > 0) {
0214:             if ($arr[1] > $vzsa50_dt) {
0215:                 $vzsa50_url = $arr[0];
0216:                 $vzsa50_dt  = $arr[1];
0217:                 $res = TRUE;
0218:             }
0219:         }
0220:     }
0221: 
0222:     //エラー・チェック
0223:     if (! $res) {
0224:         return FALSE;
0225:     }
0226: 
0227:     return $vzsa50_url;
0228: }

フィードから最新の地上実況図 URL を取得するユーザー関数は jmaGetWeatherMapURL である。
URL を正規表現で分解し、配信日時 yyyymmddhhmmss が最も大きく、VZSA50 を含む URL を返す。

解説:地上実況図を読み込む

0230: /**
0231:  * 気象庁防災情報XMLから地上実況図を取得
0232:  * @param string $url   地上実況図URL
0233:  * @param string $dt    報告日時格納用
0234:  * @param array  $items 情報を格納する配列
0235:  * @param object $pcc   pahooCacheオブジェクト
0236:  * @return bool TRUE:取得成功/FALSE:失敗
0237: */
0238: function jmaGetIsobar($url, &$dt, &$items$pcc) {
0239:     //名前空間
0240:     define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
0241: 
0242:     $xml = $pcc->simplexml_load($url);
0243:     //レスポンス・チェック
0244:     if ($pcc->iserror() || !isset($xml->Body->MeteorologicalInfos)) {
0245:         return FALSE;
0246:     }
0247: 
0248:     //報告日時
0249:     $pat = '/([0-9]+)\-([0-9]+)\-([0-9]+)T([0-9]+)\:/ui';
0250:     $dt = (string)$xml->Body->MeteorologicalInfos->MeteorologicalInfo->DateTime;
0251:     if (preg_match($pat$dt$arr) > 0) {
0252:         $dt = sprintf('%d年%d月%d日 %d時', $arr[1]$arr[2]$arr[3]$arr[4]);
0253:     } else {
0254:         $dt = '';
0255:     }
0256: 
0257:     //等圧線情報
0258:     $res = FALSE;
0259:     $cnt = 0;
0260:     foreach ($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item as $item) {
0261:         $Property = $item->Kind->Property;
0262:         //等圧線
0263:         if ($Property->Type == '等圧線') {
0264:             $IsobarPart = $Property->IsobarPart->children(JMX_EB);
0265:             $items[$cnt]['type'] = (string)$Property->Type;
0266:             $items[$cnt]['pressure'] = (int)$IsobarPart->Pressure;
0267:             $bar = (string)$IsobarPart->Line;
0268:             $arr = preg_split('/\//ui', $bar);
0269:             //緯度・経度
0270:             $i = 0;
0271:             foreach ($arr as $ss) {
0272:                 if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', $ss$arr2) > 0) {
0273:                     $items[$cnt]['point'][$i]['latitude']  = (float)$arr2[1];
0274:                     $items[$cnt]['point'][$i]['longitude'] = (float)$arr2[2];
0275:                     $i++;
0276:                 }
0277:             }
0278:             $res = TRUE;
0279:             $cnt++;
0280:         //前線
0281:         } else if (preg_match('/前線/ui', $Property->Type) > 0) {
0282:             $CoordinatePart = $Property->CoordinatePart->children(JMX_EB);
0283:             $items[$cnt]['type'] = (string)$Property->Type;
0284:             $bar = (string)$CoordinatePart->Line;
0285:             $arr = preg_split('/\//ui', $bar);
0286:             //緯度・経度
0287:             $i = 0;
0288:             foreach ($arr as $ss) {
0289:                 if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', $ss$arr2) > 0) {
0290:                     $items[$cnt]['point'][$i]['latitude']  = (float)$arr2[1];
0291:                     $items[$cnt]['point'][$i]['longitude'] = (float)$arr2[2];
0292:                     $i++;
0293:                 }
0294:             }
0295:             $res = TRUE;
0296:             $cnt++;
0297:         //気圧
0298:         } else if (preg_match('/気圧|台風|熱帯/ui', $Property->Type) > 0) {
0299:             $items[$cnt]['type']  = (string)$Property->Type;
0300:             $CenterPart = $Property->CenterPart->children(JMX_EB);
0301:             if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', (string)$CenterPart->Coordinate$arr2) > 0) {
0302:                 $items[$cnt]['latitude']  = (float)$arr2[1];
0303:                 $items[$cnt]['longitude'] = (float)$arr2[2];
0304:             }
0305:             $items[$cnt]['pressure'] = (int)$CenterPart->Pressure;
0306:             $res = TRUE;
0307:             $cnt++;
0308:         }
0309:     }
0310: 
0311:     return TRUE;
0312: }

VZSA50 から地上実況図を配列 $items へ格納するユーザー関数が jmaGetIsobar である。

XML ファイルを読み込んだら、まず、報告日時を $dt へ格納する。
次に、Type要素の値に応じて、等圧線、前線、気圧の位置情報を配列 $items へ格納していく。

解説:前線記号

PHPで天気図を描く
等圧線や気圧配置は、取得した座標をそのままマッピングすればいいのだが、苦労したのが左図の前線記号である。
寒冷前線系の三角形と、温暖前線系の半円を描く必要がある。いろいろ試行錯誤したのだが、結局、マップサービスの多角形描画機能を使って座標を計算して描かせることにした。

0336: /**
0337:  * 二等辺三角形座標(寒冷前線用)
0338:  * @param float $lat0, $lng0 底辺の中心座標
0339:  * @param float $lat1, $lng2 底辺の一方の座標
0340:  * @param int   $way         0:寒冷前線・停滞前線,1:閉塞前線
0341:  * @return  array($lat, $lng)  頂点の座標
0342: */
0343: function isoTriangle($lat0$lng0$lat1$lng1$way=0) {
0344:     $angle = ($way == 0) ? 270 : 90;
0345: 
0346:     if ($lng1 - $lng0 == 0) {
0347:         $t = 0.0;
0348:     } else {
0349:         $t = rad2deg(atan(($lat1 - $lat0) / ($lng1 - $lng0)));
0350:     }
0351:     $r = sqrt(pow(($lng1 - $lng0), 2) + pow(($lat1 - $lat0), 2));
0352:     $lat = $r * sin(deg2rad($t + $angle)) + $lat0;
0353:     $lng = $r * cos(deg2rad($t + $angle)) + $lng0;
0354: 
0355:     return array($lat$lng);
0356: }

PHPで天気図を描く
関連前線記号の三角形は、当初、正三角形を考えていたが、温暖前線記号の半円とのバランスを考えて、AC=BC となる二等辺三角形とした。
座標系は緯度、経度から成る球面座標系だが、マップサービスがメルカトル図法であることから、平面座標系であると近似して計算式を立てた。
底辺AB の中点  mimetex  を置くと、三角形 BCO は直角三角形になる。
ここで頂点  mimetex  の座標から、∠BCO は  mimetex  である。
辺CO の長さは  mimetex  であるから、頂点  mimetex  の座標は
 mimetex 
 mimetex 
で求められる。

なお、底辺AB は前線であることから必ずしも直線ではなく、頂点 A,B,C を含む多角形としてマップに描画する。

0358: /**
0359:  * 半円弧座標(温暖前線記号):多角形で近似する
0360:  * @param float $lat0, $lng0 中心座標
0361:  * @param float $lat1, $lng2 円弧の始点座標
0362:  * @param int   $n           多角形の頂点数
0363:  * @param array points 円弧の座標を格納する
0364:  * @return なし
0365: */
0366: function semiCircle($lat0$lng0$lat1$lng1$n, &$points) {
0367:     if ($lng1 - $lng0 == 0) {
0368:         $t = 0.0;
0369:     } else {
0370:         $t = rad2deg(atan(($lat1 - $lat0) / ($lng1 - $lng0)));
0371:     }
0372:     $r = sqrt(pow(($lng1 - $lng0), 2) + pow(($lat1 - $lat0), 2));
0373:     for ($i = 0; $i < $n$i++) {
0374:         $points[$i]['latitude']  = $r * sin(deg2rad($t + 180 / $n * $i)) + $lat0;
0375:         $points[$i]['longitude'] = $r * cos(deg2rad($t + 180 / $n * $i)) + $lng0;
0376:     }
0377: }

次に半円形だが、マップサービスに円弧を描く機能がない。そこで、頂点数 N が十分に大きい多角形として描くことにした。

多角形の頂点座標の求め方は、isoTriangle 関数と同様で、頂点数が N 個になったとして計算する。

解説:前線を描く(Googleマップ)

0435: /**
0436:  * 前線描画用スクリプト作成:Googleマップ
0437:  * @param array $item 前線の座標
0438:  * @param int   $kind 前線の種類(0:温暖前線,1:寒冷前線,2:停滞前線,
0439:  *                                  3:閉塞前線)
0440:  * @return string 描画用スクリプト
0441: */
0442: function jsStationaryFront_gmap($item$kind) {
0443:     static $table_color1 = array('red', 'blue', 'red', 'purple');
0444:     static $table_color2 = array('red', 'blue', 'blue', 'purple');
0445:     static $table_angle  = array(0, 0, 0, 1);
0446: 
0447:     $i = 0;
0448:     $flag = TRUE;
0449:     $js = '';
0450:     while ($flag) {
0451:         //前線(温暖)
0452:         $ss = '';
0453:         $cnt = 0;
0454:         for ($j = $i$j <= $i + 10; $j++) {
0455:             if (! isset($item['point'][$j])) {
0456:                 $flag = FALSE;
0457:                 break;
0458:             }
0459:             if ($cnt > 0)   $ss .= ",\n";
0460:             $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
0461:             $cnt++;
0462:         }
0463: $js .=<<< EOT
0464: new google.maps.Polyline({
0465:     map: map,
0466:     path: [
0467: {$ss}
0468:     ],
0469:     strokeColor: '{$table_color1[$kind]}',
0470:     strokeOpacity: 1.0,
0471:     strokeWeight: 2,
0472: });
0473: 
0474: EOT;
0475:         $i += 10;
0476: 
0477:         if (isset($item['point'][$i + 20])) {
0478:             //温暖部
0479:             $ss = '';
0480:             if ($kind != 1) {
0481:                 $points = array();
0482:                 semiCircle($item['point'][$i + 5]['latitude'], $item['point'][$i + 5]['longitude'], $item['point'][$i + 10]['latitude'], $item['point'][$i + 10]['longitude'], SEMICIRCLE$points);
0483:                 $cnt = 0;
0484:                 foreach ($points as $point) {
0485:                     if ($cnt > 0)   $ss .= ",\n";
0486:                     $ss .= "\t\t{ lat: {$point['latitude']}, lng: {$point['longitude']} }";
0487:                     $cnt++;
0488:                 }
0489:                 for ($j = $i$j <= $i + 10; $j++) {
0490:                     if ($cnt > 0)   $ss .= ",\n";
0491:                     $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
0492:                     $cnt++;
0493:                 }
0494: $js .=<<< EOT
0495: new google.maps.Polygon({
0496:     map: map,
0497:     paths: [
0498:     {$ss}
0499:     ],
0500:     strokeColor: '{$table_color1[$kind]}',
0501:     strokeOpacity: 1.0,
0502:     strokeWeight: 2,
0503:     fillColor: '{$table_color1[$kind]}',
0504:     fillOpacity: 1.0,
0505: });
0506: 
0507: EOT;
0508:             } else {
0509:                 $ss = '';
0510:                 $cnt = 0;
0511:                 for ($j = $i$j <= $i + 10; $j++) {
0512:                     if ($cnt > 0)   $ss .= ",\n";
0513:                     $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
0514:                     $cnt++;
0515:                 }
0516: $js .=<<< EOT
0517: new google.maps.Polyline({
0518:     map: map,
0519:     path: [
0520: {$ss}
0521:     ],
0522:     strokeColor: '{$table_color1[$kind]}',
0523:     strokeOpacity: 1.0,
0524:     strokeWeight: 2,
0525: });
0526: 
0527: EOT;
0528:             }
0529: 
0530:             //寒冷部
0531:             if ($kind != 0) {
0532:                 list($lat$lng) = isoTriangle($item['point'][$i + 15]['latitude'], $item['point'][$i + 15]['longitude'], $item['point'][$i + 19]['latitude'], $item['point'][$i + 19]['longitude'], $table_angle[$kind]);
0533:                 $ss = "\t\t{ lat: {$lat}, lng: {$lng} }";
0534:                 $cnt = 1;
0535:                 for ($j = $i + 10; $j <= $i + 20; $j++) {
0536:                     if ($cnt > 0)   $ss .= ",\n";
0537:                     $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
0538:                     $cnt++;
0539:                 }
0540: $js .=<<< EOT
0541: new google.maps.Polygon({
0542:     map: map,
0543:     paths: [
0544:     {$ss}
0545:     ],
0546:     strokeColor: '{$table_color2[$kind]}',
0547:     strokeOpacity: 1.0,
0548:     strokeWeight: 1,
0549:     fillColor: '{$table_color2[$kind]}',
0550:     fillOpacity: 1.0
0551: });
0552: 
0553: EOT;
0554:             } else {
0555:                 $ss = '';
0556:                 $cnt = 0;
0557:                 for ($j = $i + 10; $j <= $i + 20; $j++) {
0558:                     if ($cnt > 0)   $ss .= ",\n";
0559:                     $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
0560:                     $cnt++;
0561:                 }
0562: $js .=<<< EOT
0563: new google.maps.Polyline({
0564:     map: map,
0565:     path: [
0566: {$ss}
0567:     ],
0568:     strokeColor: '{$table_color1[$kind]}',
0569:     strokeOpacity: 1.0,
0570:     strokeWeight: 2,
0571: });
0572: 
0573: EOT;
0574: 
0575:             }
0576:             $i += 20;
0577:         }
0578: 
0579:         //前線(寒冷)
0580:         $cnt = 0;
0581:         $ss = '';
0582:         for ($j = $i$j <= $i + 10; $j++) {
0583:             if (! isset($item['point'][$j])) {
0584:                 $flag = FALSE;
0585:                 break;
0586:             }
0587:             if ($cnt > 0)   $ss .= ",\n";
0588:             $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
0589:             $cnt++;
0590:         }
0591: $js .=<<< EOT
0592: new google.maps.Polyline({
0593:     map: map,
0594:     path: [
0595: {$ss}
0596:     ],
0597:     strokeColor: '{$table_color2[$kind]}',
0598:     strokeOpacity: 1.0,
0599:     strokeWeight: 2,
0600: });
0601: 
0602: EOT;
0603:         $i += 10;
0604:     }
0605:     return $js;
0606: }

前述の関数 isoTrianglesemiCircle を利用し、Google マップ上に前線を描くユーザー関数が jsStationaryFront_gmap である。
温暖前線、寒冷前線、停滞前線、閉塞前線の識別を変数 $kind にもたせて、すべて 1 つの関数で描画を行う。
基本形は停滞前線で、他の 3 つの前線は次のようにして描く。
  • 温暖前線‥‥寒冷前線記号は描かず、替わりに赤い直線(温暖前線)を引く。
  • 寒冷前線‥‥温暖前線記号は描かず、替わりに青い直線(寒冷前線)を引く。
  • 閉塞前線‥‥寒冷前線記号の向きを 180 度逆転し、紫色で描く。

解説:前線を描く(Leaflet)

0609: /**
0610:  * 前線描画用スクリプト作成:Leaflet
0611:  * @param array $item 前線の座標
0612:  * @param int   $kind 前線の種類(0:温暖前線,1:寒冷前線,2:停滞前線,
0613:  *                                  3:閉塞前線)
0614:  * @return string 描画用スクリプト
0615: */
0616: function jsStationaryFront_leaflet($item$kind) {
0617:     static $table_color1 = array('red', 'blue', 'red', 'purple');
0618:     static $table_color2 = array('red', 'blue', 'blue', 'purple');
0619:     static $table_angle  = array(0, 0, 0, 1);
0620: 
0621:     $i = 0;
0622:     $flag = TRUE;
0623:     $js = '';
0624:     while ($flag) {
0625:         //前線(温暖)
0626:         $ss = '';
0627:         for ($j = $i$j <= $i + 10; $j++) {
0628:             if (! isset($item['point'][$j])) {
0629:                 $flag = FALSE;
0630:                 break;
0631:             }
0632: $ss .=<<< EOT
0633:             [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
0634: 
0635: EOT;
0636:         }
0637: $js .=<<< EOT
0638: L.polyline([
0639: {$ss}
0640:         ], {
0641:             color: '{$table_color1[$kind]}',
0642:             opacity: 1.0,
0643:             weight: 2
0644:         }
0645:     ).addTo(map);
0646: 
0647: EOT;
0648:         $i += 10;
0649: 
0650:         if (isset($item['point'][$i + 20])) {
0651:             //温暖部
0652:             $ss = '';
0653:             if ($kind != 1) {
0654:                 $points = array();
0655:                 semiCircle($item['point'][$i + 5]['latitude'], $item['point'][$i + 5]['longitude'], $item['point'][$i + 10]['latitude'], $item['point'][$i + 10]['longitude'], SEMICIRCLE$points);
0656:                 foreach ($points as $point) {
0657: $ss .=<<< EOT
0658:             [{$point['latitude']}, {$point['longitude']}],
0659: 
0660: EOT;
0661:                 }
0662:                 for ($j = $i + 0; $j <= $i + 10; $j++) {
0663: $ss .=<<< EOT
0664:             [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
0665: 
0666: EOT;
0667:                 }
0668: $js .=<<< EOT
0669:     L.polygon([
0670: {$ss}
0671:         ], {
0672:             color: '{$table_color1[$kind]}',
0673:             fillColor: '{$table_color1[$kind]}',
0674:             fillOpacity: 1.0,
0675:         }
0676:     ).addTo(map);
0677: 
0678: EOT;
0679:             } else {
0680:                 for ($j = $i + 0; $j <= $i + 10; $j++) {
0681: $ss .=<<< EOT
0682:             [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
0683: 
0684: EOT;
0685:                 }
0686: $js .=<<< EOT
0687:     L.polyline([
0688: {$ss}
0689:         ], {
0690:             color: '{$table_color1[$kind]}',
0691:             opacity: 1.0,
0692:             weight: 2
0693:         }
0694:     ).addTo(map);
0695: 
0696: EOT;
0697:             }
0698: 
0699:             //寒冷部
0700:             if ($kind != 0) {
0701:                 list($lat$lng) = isoTriangle($item['point'][$i + 15]['latitude'], $item['point'][$i + 15]['longitude'], $item['point'][$i + 19]['latitude'], $item['point'][$i + 19]['longitude'], $table_angle[$kind]);
0702: $ss =<<< EOT
0703:             [{$lat}, {$lng}],
0704: 
0705: EOT;
0706:                 for ($j = $i + 10; $j <= $i + 20; $j++) {
0707: $ss .=<<< EOT
0708:             [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
0709: 
0710: EOT;
0711:                 }
0712: $js .=<<< EOT
0713: L.polygon([
0714: {$ss}
0715:         ], {
0716:             color: '{$table_color2[$kind]}',
0717:             fillColor: '{$table_color2[$kind]}',
0718:             fillOpacity: 1.0,
0719:         }
0720:     ).addTo(map);
0721: 
0722: EOT;
0723:             } else {
0724:                 $ss = '';
0725:                 for ($j = $i + 10; $j <= $i + 20; $j++) {
0726: $ss .=<<< EOT
0727:             [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
0728: 
0729: EOT;
0730:                 }
0731: $js .=<<< EOT
0732:     L.polyline([
0733: {$ss}
0734:         ], {
0735:             color: '{$table_color1[$kind]}',
0736:             opacity: 1.0,
0737:             weight: 2
0738:         }
0739:     ).addTo(map);
0740: 
0741: EOT;
0742:             }
0743:             $i += 20;
0744:         }
0745:         //前線(寒冷)
0746:         $ss = '';
0747:         for ($j = $i$j <= $i + 10; $j++) {
0748:             if (! isset($item['point'][$j])) {
0749:                 $flag = FALSE;
0750:                 break;
0751:             }
0752: $ss .=<<< EOT
0753:             [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
0754: 
0755: EOT;
0756:         }
0757: $js .=<<< EOT
0758: L.polyline([
0759: {$ss}
0760:         ], {
0761:             color: '{$table_color2[$kind]}',
0762:             opacity: 1.0,
0763:             weight: 2
0764:         }
0765:     ).addTo(map);
0766: 
0767: EOT;
0768:         $i += 10;
0769:     }
0770: 
0771:     return $js;
0772: }

Leaflet 上に前線を描くユーザー関数が jsStationaryFront_leaflet である。
アルゴリズムは、Google マップ用の jsStationaryFront_gmap とほぼ同じで、多角形や直線を描く命令部分を変えている。

解説:ラベルを描く(Googleマップ)

0379: /**
0380:  * ラベル表示用スクリプト作成:Googleマップ
0381:  * @param float  $latitude, $longitude ラベル表示座標
0382:  * @param string $label ラベル
0383:  * @param int    $size  フォントサイズ(pt)
0384:  * @param string $color  フォントカラー
0385:  * @param string $weight 太さ
0386:  * @return string 描画用スクリプト
0387: */
0388: function jsLabel_gmap($latitude$longitude$label$size=14, $color='#FF0000', $weight="normal") {
0389: $js =<<< EOT
0390: new google.maps.Marker({
0391:     map: map,
0392:     position: new google.maps.LatLng({$latitude}, {$longitude}),
0393:     icon: {
0394:         url: 'https://www.pahoo.org/common/space.gif'
0395:     },
0396:     label: {
0397:         text: '{$label}',
0398:         color: '{$color}',
0399:         fontSize: '{$size}px',
0400:         fontWeight: '{$weight}'
0401:     }
0402: });
0403: 
0404: EOT;
0405:     return $js;
0406: }

Google マップ上に高気圧や低気圧といったラベルや、気圧(数値)を描くユーザー関数が jsLabel_gmap である。
アイコン表示命令を利用し、アイコン画像を表示させないようにしている。

解説:ラベルを描く(Leaflet)

0408: /**
0409:  * ラベル表示用スクリプト作成:Leaflet
0410:  * @param float  $latitude, $longitude ラベル表示座標
0411:  * @param string $label  ラベル
0412:  * @param int    $size   フォントサイズ(pt)
0413:  * @param string $color  フォントカラー
0414:  * @param string $weight 太さ
0415:  * @return string 描画用スクリプト
0416: */
0417: function jsLabel_leaflet($latitude$longitude$label$size=14, $color='#FF0000', $weight="normal") {
0418: $js =<<< EOT
0419: new L.marker(
0420:     [{$latitude}, {$longitude}],
0421:     {
0422:     icon:
0423:         new L.divIcon({
0424:             html: '<span style="color:{$color}; font-size:{$size}px; font-weight:{$weight};">{$label}</span>',
0425:             iconSize: [0, 0],
0426:             iconAnchor: [{$size}, {$size}],
0427:         })
0428:     }
0429: ).addTo(map);
0430: 
0431: EOT;
0432:     return $js;
0433: }

Leaflet 上に高気圧や低気圧といったラベルや、気圧(数値)を描くユーザー関数が Leaflet である。
前述の jsLabel_gmap と同様、アイコン表示命令を利用し、アイコン画像を表示させないようにしている。

解説:天気図描画スクリプトを生成する

0774: /**
0775:  * 天気図描画スクリプトを生成する
0776:  * @param string $url    地上実況図URL
0777:  * @param object $pgc   pahooGeoCodeオブジェクト
0778:  * @param object $pcc    pahooCacheオブジェクト
0779:  * @param string $dt     報告日時格納用
0780:  * @param string $errmsg エラーメッセージ格納用
0781:  * @return string スクリプト/FALSE:生成失敗
0782: */
0783: function jsWeatherMap($url$pgc$pcc, &$dt, &$errmsg) {
0784:     static $table = array('温暖前線', '寒冷前線', '停滞前線', '閉塞前線');
0785:     $errmsg = '';
0786:     $js = '';
0787:     $items = array();
0788:     $res = jmaGetIsobar($url$dt$items$pcc);
0789:     if ($res == FALSE) {
0790:         $errmsg = '気象庁防災情報XMLから地上実況図を取得できません';
0791:     }
0792: 
0793:     //スクリプト生成
0794:     foreach ($items as $key=>$item) {
0795:         if ($item['type'] == '等圧線') {
0796:             $js .= $pgc->jsLine($item['point'], 'black', 0.5, 0.5, MAPSERVICE);
0797:         } else if (preg_match('/前線/ui', $item['type']) > 0) {
0798:             $kind = array_search($item['type'], $table);
0799:             if (MAPSERVICE == 0) {
0800:                 $js .= jsStationaryFront_gmap($item$kind);
0801:             } else {
0802:                 $js .= jsStationaryFront_leaflet($item$kind);
0803:             }
0804:         } else if (preg_match('/気圧|台風|熱帯/ui', $item['type']) > 0) {
0805:             switch ($item['type']) {
0806:                 case '高気圧':
0807:                     $label = '';
0808:                     $color = 'blue';
0809:                     break;
0810:                 case '低気圧':
0811:                     $label = '';
0812:                     $color = 'red';
0813:                     break;
0814:                 case '台風':
0815:                     $label = '';
0816:                     $color = 'magenta';
0817:                     break;
0818:                 case '熱帯低気圧':
0819:                     $label = '';
0820:                     $color = 'magenta';
0821:                     break;
0822:             }
0823:             if (MAPSERVICE == 0) {
0824:                 $js .= jsLabel_gmap($item['latitude'], $item['longitude'], $label, 18, $color, 'bold');
0825:                 $js .= jsLabel_gmap($item['latitude'] - 1.3, $item['longitude'], $item['pressure'], 16, 'black', 'bold%');
0826:             } else {
0827:                 $js .= jsLabel_leaflet($item['latitude'], $item['longitude'], $label, 18, $color, 'bold');
0828:                 $js .= jsLabel_leaflet($item['latitude'] - 1.3, $item['longitude'], $item['pressure'], 16, 'black', 'bold');
0829:             }
0830:         }
0831:     }
0832: 
0833:     return $js;
0834: }

jmaGetIsobar 関数で取り込んだデータを利用し、いままで紹介してきたユーザー関数、および pahooGeoCode::jsLine メソッドを使い、前線、気圧、等圧線を描くための JavaScript を生成するユーザー関数が jsWeatherMap である。

活用例

みんなの知識 ちょっと便利帳」では、「日本周辺域の天気図」で本プログラムを利用し、見やすいページを提供している。ありがとうございます。

参考サイト

(この項おわり)
header