サンプル・プログラムの実行例
目次
- サンプル・プログラムの実行例
- サンプル・プログラムのダウンロード
- 準備:pahooGeoCode クラス
- 準備:地図サービスの選択
- 準備:キャッシュ・システム
- 気象庁防災情報XMLフォーマット
- VZSA50の構造
- 準備:表示幅、表示列数など
- 解説:最新の地上実況図URLを取得
- 解説:地上実況図を読み込む
- 解説:前線記号
- 解説:前線を描く(Googleマップ)
- 解説:前線を描く(Leaflet)
- 解説:ラベルを描く(Googleマップ)
- 解説:ラベルを描く(Leaflet)
- 解説:天気図描画スクリプトを生成する
- 解説:ツイート機能
- 解説:html2canvasライブラリ
- 準備:pahooTwitterAPIクラス
- 解説:メディア付き投稿(RAWデータ)
- 解説:ツイート処理
- 活用例
- 参考サイト
サンプル・プログラムのダウンロード
weatherMap.php | サンプル・プログラム本体。 |
pahooGeoCode.php | 住所・緯度・経度に関わるクラス pahooGeoCode。 使い方は「PHPで住所・ランドマークから最寄り駅を求める」「PHPで住所・ランドマークから緯度・経度を求める」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooCache.php | キャッシュ処理に関わるクラス pahooCache。 キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
1.4.1 | 2023/09/20 | js_html2image()--Leaflet用html2image()発火プロセス見直し |
1.4 | 2022/03/10 | 気象庁防災情報XMLのhttps化に対応 |
1.3 | 2021/06/20 | ツイート機能を追加 |
1.2 | 2021/04/18 | 台風,熱帯低気圧に対応 |
1.1 | 2021/04/10 | キャッシュ・システム導入:pahooCacheクラス |
バージョン | 更新日 | 内容 |
---|---|---|
6.3.1 | 2023/07/09 | bug-fix |
6.3.0 | 2023/07/02 | getPointsGSI()追加 |
6.2.0 | 2023/07/02 | ip2address()追加 |
6.1.0 | 2022/12/30 | ip2address()追加 |
6.0.4 | 2022/12/13 | PHP8.2対応 |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.1 | 2023/02/11 | コメント追記 |
1.1 | 2021/04/08 | simplexml_load()メソッド追加 |
1.0 | 2021/04/02 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
5.2.0 | 2023/07/17 | oembed() v2対応 |
5.1.0 | 2023/07/16 | extractMediaURL() -- file:///形式に対応 |
5.0.0 | 2023/07/02 | メソッドをTwitter API v2へ移行;v1.1は別名or廃止 |
4.9.0 | 2023/04/15 | tweet3() 追加 |
4.8.0 | 2023/01/28 | tweet2(),twitter_strcut2(),extractMediaURL()追加 |
準備:pahooGeoCode クラス
37: class pahooGeoCode {
38: var $items; //検索結果格納用
39: var $error; //エラー・フラグ
40: var $errmsg; //エラー・メッセージ
41: var $hits; //検索ヒット件数
42: var $webapi; //直前に呼び出したWebAPI URL
43:
44: //Google Cloud Platform APIキー
45: //https://cloud.google.com/maps-platform/
46: //※Google Maps APIを利用しないのなら登録不要
47: var $GOOGLE_API_KEY_1 = '**************************'; //HTTPリファラ用
48: var $GOOGLE_API_KEY_2 = '**************************'; //IP制限用
49:
50: //Yahoo! JAPAN Webサービス アプリケーションID
51: //https://e.developer.yahoo.co.jp/register
52: //※Yahoo! JAPAN Webサービスを利用しないのなら登録不要
53: var $YAHOO_APPLICATION_ID = '*****************************';
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
地図や住所検索として Google を利用するのであれば、Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を参照されたい。
準備:地図サービスの選択
40: //地図描画サービスの選択
41: // 0:Google
42: // 2:地理院地図・OSM
43: define('MAPSERVICE', 2);
44:
45: //マップの表示サイズ(単位:ピクセル)
46: define('MAP_WIDTH', 600);
47: define('MAP_HEIGHT', 480);
48: //マップID
49: define('MAPID', 'map_id');
50: //初期値
51: define('DEF_LATITUDE', 35.0); //緯度
52: define('DEF_LONGITUDE', 137.0); //経度
53: define('DEF_TYPE', 'GSISTD'); //マップタイプ
54: define('DEF_ZOOM', 4); //ズーム
55:
56: define('SEMICIRCLE', 30); //半円を代替する多角形頂点数
住所検索サービスは、Google、Yahoo!JAPAN、HeartRails Geo API、OSM Nominatim Search API から選べる。あらかじめ、定数 GEOSERVICE に値を設定すること。
準備:キャッシュ・システム
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。
気象庁防災情報XMLフォーマット
今回は、高頻度 - 定時更新フィードにアクセスし、電文コード VZSA50 の地上実況図を取得する。
VZSA50の構造
注意すべきは、1つのXMLファイルの中に1つまたは複数の予報地点が含まれていること。さらに、天気予報(アイコン画像を含む)と降水確率は「地方」に紐付いているが、最低気温・最高気温は「予報地点」に紐付いているということである。地方と予報地点は1対N対応のため、後述する予報値地点データファイルに用意しておくことにした。
また、TimeDefine タグに示される7日分の情報が格納されているのだが、XML情報の取得時間帯によって、1つ目(refID=1)の情報が、今日になるか明日になるか分かれる。そこで、TimeDefine を取得して、現在日時(PCの内蔵時計)と比較して決定することにした。
降水確率や気温は、XML情報の取得タイミングによっては、1つ目(refID=1)~3つ目(refID=3)が「値なし」になっていることがある。そこで、後述する情報XML VPFW50 から、今日~明後日(または明日~明後日)の情報を取り出し、補完することにする。
準備:表示幅、表示列数など
45: //マップの表示サイズ(単位:ピクセル)
46: define('MAP_WIDTH', 600);
47: define('MAP_HEIGHT', 480);
48: //マップID
49: define('MAPID', 'map_id');
50: //初期値
51: define('DEF_LATITUDE', 35.0); //緯度
52: define('DEF_LONGITUDE', 137.0); //経度
53: define('DEF_TYPE', 'GSISTD'); //マップタイプ
54: define('DEF_ZOOM', 4); //ズーム
55:
56: define('SEMICIRCLE', 30); //半円を代替する多角形頂点数
解説:最新の地上実況図URLを取得
309: /**
310: * 気象庁防災情報XMLから最新の地上実況図URLを取得
311: * @param object $pcc pahooCacheオブジェクト
312: * @return string 地上実況図URL/FALSE:取得失敗
313: */
314: function jmaGetWeatherMapURL($pcc) {
315: //URLパターン
316: $vzsa50 = '/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VZSA50\_([0-9\_]+)\.xml/ui';
317:
318: $xml = $pcc->simplexml_load(FEED_REGULAR);
319: //レスポンス・チェック
320: if ($pcc->iserror() || !isset($xml->entry)) {
321: return FALSE;
322: }
323:
324: //フィード(XMLファイル)解析
325: $vzsa50_url = $vzsa50_dt = '';
326: $res = FALSE;
327: foreach ($xml->entry as $node) {
328: //日時がより新しいURLを採用
329: if (preg_match($vzsa50, $node->id, $arr) > 0) {
330: if ($arr[1] > $vzsa50_dt) {
331: $vzsa50_url = $arr[0];
332: $vzsa50_dt = $arr[1];
333: $res = TRUE;
334: }
335: }
336: }
337:
338: //エラー・チェック
339: if (! $res) {
340: return FALSE;
341: }
342:
343: return $vzsa50_url;
344: }
URLを正規表現で分解し、配信日時 yyyymmddhhmmss が最も大きく、VZSA50 を含むURLを返す。
解説:地上実況図を読み込む
346: /**
347: * 気象庁防災情報XMLから地上実況図を取得
348: * @param string $url 地上実況図URL
349: * @param string $dt 報告日時格納用
350: * @param array $items 情報を格納する配列
351: * @param object $pcc pahooCacheオブジェクト
352: * @return bool TRUE:取得成功/FALSE:失敗
353: */
354: function jmaGetIsobar($url, &$dt, &$items, $pcc) {
355: //名前空間
356: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
357:
358: $xml = $pcc->simplexml_load($url);
359: //レスポンス・チェック
360: if ($pcc->iserror() || !isset($xml->Body->MeteorologicalInfos)) {
361: return FALSE;
362: }
363:
364: //報告日時
365: $pat = '/([0-9]+)\-([0-9]+)\-([0-9]+)T([0-9]+)\:/ui';
366: $dt = (string)$xml->Body->MeteorologicalInfos->MeteorologicalInfo->DateTime;
367: if (preg_match($pat, $dt, $arr) > 0) {
368: $dt = sprintf('%d年%d月%d日 %d時', $arr[1], $arr[2], $arr[3], $arr[4]);
369: } else {
370: $dt = '';
371: }
372:
373: //等圧線情報
374: $res = FALSE;
375: $cnt = 0;
376: foreach ($xml->Body->MeteorologicalInfos->MeteorologicalInfo->Item as $item) {
377: $Property = $item->Kind->Property;
378: //等圧線
379: if ($Property->Type == '等圧線') {
380: $IsobarPart = $Property->IsobarPart->children(JMX_EB);
381: $items[$cnt]['type'] = (string)$Property->Type;
382: $items[$cnt]['pressure'] = (int)$IsobarPart->Pressure;
383: $bar = (string)$IsobarPart->Line;
384: $arr = preg_split('/\//ui', $bar);
385: //緯度・経度
386: $i = 0;
387: foreach ($arr as $ss) {
388: if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', $ss, $arr2) > 0) {
389: $items[$cnt]['point'][$i]['latitude'] = (float)$arr2[1];
390: $items[$cnt]['point'][$i]['longitude'] = (float)$arr2[2];
391: $i++;
392: }
393: }
394: $res = TRUE;
395: $cnt++;
396: //前線
397: } else if (preg_match('/前線/ui', $Property->Type) > 0) {
398: $CoordinatePart = $Property->CoordinatePart->children(JMX_EB);
399: $items[$cnt]['type'] = (string)$Property->Type;
400: $bar = (string)$CoordinatePart->Line;
401: $arr = preg_split('/\//ui', $bar);
402: //緯度・経度
403: $i = 0;
404: foreach ($arr as $ss) {
405: if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', $ss, $arr2) > 0) {
406: $items[$cnt]['point'][$i]['latitude'] = (float)$arr2[1];
407: $items[$cnt]['point'][$i]['longitude'] = (float)$arr2[2];
408: $i++;
409: }
410: }
411: $res = TRUE;
412: $cnt++;
413: //気圧
414: } else if (preg_match('/気圧|台風|熱帯/ui', $Property->Type) > 0) {
415: $items[$cnt]['type'] = (string)$Property->Type;
416: $CenterPart = $Property->CenterPart->children(JMX_EB);
417: if (preg_match('/([\+\-]+[0-9\.]+)([\+\-]+[0-9\.]+)/ui', (string)$CenterPart->Coordinate, $arr2) > 0) {
418: $items[$cnt]['latitude'] = (float)$arr2[1];
419: $items[$cnt]['longitude'] = (float)$arr2[2];
420: }
421: $items[$cnt]['pressure'] = (int)$CenterPart->Pressure;
422: $res = TRUE;
423: $cnt++;
424: }
425: }
426:
427: return TRUE;
428: }
XMLファイルを読み込んだら、まず、報告日時を $dt へ格納する。
次に、Type要素の値に応じて、等圧線、前線、気圧の位置情報を配列 $items へ格納していく。
解説:前線記号
寒冷前線系の三角形と、温暖前線系の半円を描く必要がある。いろいろ試行錯誤したのだが、結局、マップサービスの多角形描画機能を使って座標を計算して描かせることにした。
452: /**
453: * 二等辺三角形座標(寒冷前線用)
454: * @param float $lat0, $lng0 底辺の中心座標
455: * @param float $lat1, $lng2 底辺の一方の座標
456: * @param int $way 0:寒冷前線・停滞前線,1:閉塞前線
457: * @return array($lat, $lng) 頂点の座標
458: */
459: function isoTriangle($lat0, $lng0, $lat1, $lng1, $way=0) {
460: $angle = ($way == 0) ? 270 : 90;
461:
462: if ($lng1 - $lng0 == 0) {
463: $t = 0.0;
464: } else {
465: $t = rad2deg(atan(($lat1 - $lat0) / ($lng1 - $lng0)));
466: }
467: $r = sqrt(pow(($lng1 - $lng0), 2) + pow(($lat1 - $lat0), 2));
468: $lat = $r * sin(deg2rad($t + $angle)) + $lat0;
469: $lng = $r * cos(deg2rad($t + $angle)) + $lng0;
470:
471: return array($lat, $lng);
472: }
座標系は緯度、経度から成る球面座標系だが、マップサービスがメルカトル図法であることから、平面座標系であると近似して計算式を立てた。
ここで頂点 の座標から、∠BCOは である。
辺COの長さは であるから、頂点 の座標は
で求められる。
なお、底辺ABは前線であることから必ずしも直線ではなく、頂点A,B,Cを含む多角形としてマップに描画する。
474: /**
475: * 半円弧座標(温暖前線記号):多角形で近似する
476: * @param float $lat0, $lng0 中心座標
477: * @param float $lat1, $lng2 円弧の始点座標
478: * @param int $n 多角形の頂点数
479: * @param array points 円弧の座標を格納する
480: * @return なし
481: */
482: function semiCircle($lat0, $lng0, $lat1, $lng1, $n, &$points) {
483: if ($lng1 - $lng0 == 0) {
484: $t = 0.0;
485: } else {
486: $t = rad2deg(atan(($lat1 - $lat0) / ($lng1 - $lng0)));
487: }
488: $r = sqrt(pow(($lng1 - $lng0), 2) + pow(($lat1 - $lat0), 2));
489: for ($i = 0; $i < $n; $i++) {
490: $points[$i]['latitude'] = $r * sin(deg2rad($t + 180 / $n * $i)) + $lat0;
491: $points[$i]['longitude'] = $r * cos(deg2rad($t + 180 / $n * $i)) + $lng0;
492: }
493: }
多角形の頂点座標の求め方は、isoTriangle 関数と同様で、頂点数がN個になったとして計算する。
解説:前線を描く(Googleマップ)
551: /**
552: * 前線描画用スクリプト作成:Googleマップ
553: * @param array $item 前線の座標
554: * @param int $kind 前線の種類(0:温暖前線,1:寒冷前線,2:停滞前線,
555: * 3:閉塞前線)
556: * @return string 描画用スクリプト
557: */
558: function jsStationaryFront_gmap($item, $kind) {
559: static $table_color1 = array('red', 'blue', 'red', 'purple');
560: static $table_color2 = array('red', 'blue', 'blue', 'purple');
561: static $table_angle = array(0, 0, 0, 1);
562:
563: $i = 0;
564: $flag = TRUE;
565: $js = '';
566: while ($flag) {
567: //前線(温暖)
568: $ss = '';
569: $cnt = 0;
570: for ($j = $i; $j <= $i + 10; $j++) {
571: if (! isset($item['point'][$j])) {
572: $flag = FALSE;
573: break;
574: }
575: if ($cnt > 0) $ss .= ",\n";
576: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
577: $cnt++;
578: }
579: $js .=<<< EOT
580: new google.maps.Polyline({
581: map: map,
582: path: [
583: {$ss}
584: ],
585: strokeColor: '{$table_color1[$kind]}',
586: strokeOpacity: 1.0,
587: strokeWeight: 2,
588: });
589:
590: EOT;
591: $i += 10;
592:
593: if (isset($item['point'][$i + 20])) {
594: //温暖部
595: $ss = '';
596: if ($kind != 1) {
597: $points = array();
598: semiCircle($item['point'][$i + 5]['latitude'], $item['point'][$i + 5]['longitude'], $item['point'][$i + 10]['latitude'], $item['point'][$i + 10]['longitude'], SEMICIRCLE, $points);
599: $cnt = 0;
600: foreach ($points as $point) {
601: if ($cnt > 0) $ss .= ",\n";
602: $ss .= "\t\t{ lat: {$point['latitude']}, lng: {$point['longitude']} }";
603: $cnt++;
604: }
605: for ($j = $i; $j <= $i + 10; $j++) {
606: if ($cnt > 0) $ss .= ",\n";
607: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
608: $cnt++;
609: }
610: $js .=<<< EOT
611: new google.maps.Polygon({
612: map: map,
613: paths: [
614: {$ss}
615: ],
616: strokeColor: '{$table_color1[$kind]}',
617: strokeOpacity: 1.0,
618: strokeWeight: 2,
619: fillColor: '{$table_color1[$kind]}',
620: fillOpacity: 1.0,
621: });
622:
623: EOT;
624: } else {
625: $ss = '';
626: $cnt = 0;
627: for ($j = $i; $j <= $i + 10; $j++) {
628: if ($cnt > 0) $ss .= ",\n";
629: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
630: $cnt++;
631: }
632: $js .=<<< EOT
633: new google.maps.Polyline({
634: map: map,
635: path: [
636: {$ss}
637: ],
638: strokeColor: '{$table_color1[$kind]}',
639: strokeOpacity: 1.0,
640: strokeWeight: 2,
641: });
642:
643: EOT;
644: }
645:
646: //寒冷部
647: if ($kind != 0) {
648: 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]);
649: $ss = "\t\t{ lat: {$lat}, lng: {$lng} }";
650: $cnt = 1;
651: for ($j = $i + 10; $j <= $i + 20; $j++) {
652: if ($cnt > 0) $ss .= ",\n";
653: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
654: $cnt++;
655: }
656: $js .=<<< EOT
657: new google.maps.Polygon({
658: map: map,
659: paths: [
660: {$ss}
661: ],
662: strokeColor: '{$table_color2[$kind]}',
663: strokeOpacity: 1.0,
664: strokeWeight: 1,
665: fillColor: '{$table_color2[$kind]}',
666: fillOpacity: 1.0
667: });
668:
669: EOT;
670: } else {
671: $ss = '';
672: $cnt = 0;
673: for ($j = $i + 10; $j <= $i + 20; $j++) {
674: if ($cnt > 0) $ss .= ",\n";
675: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
676: $cnt++;
677: }
678: $js .=<<< EOT
679: new google.maps.Polyline({
680: map: map,
681: path: [
682: {$ss}
683: ],
684: strokeColor: '{$table_color1[$kind]}',
685: strokeOpacity: 1.0,
686: strokeWeight: 2,
687: });
688:
689: EOT;
690:
691: }
692: $i += 20;
693: }
694:
695: //前線(寒冷)
696: $cnt = 0;
697: $ss = '';
698: for ($j = $i; $j <= $i + 10; $j++) {
699: if (! isset($item['point'][$j])) {
700: $flag = FALSE;
701: break;
702: }
703: if ($cnt > 0) $ss .= ",\n";
704: $ss .= "\t\t{ lat: {$item['point'][$j]['latitude']}, lng: {$item['point'][$j]['longitude']} }";
705: $cnt++;
706: }
707: $js .=<<< EOT
708: new google.maps.Polyline({
709: map: map,
710: path: [
711: {$ss}
712: ],
713: strokeColor: '{$table_color2[$kind]}',
714: strokeOpacity: 1.0,
715: strokeWeight: 2,
716: });
717:
718: EOT;
719: $i += 10;
720: }
721: return $js;
722: }
温暖前線、寒冷前線、停滞前線、閉塞前線の識別を変数 $kind にもたせて、すべて1つの関数で描画を行う。
基本形は停滞前線で、他の3つの前線は次のようにして描く。
- 温暖前線‥‥寒冷前線記号は描かず、替わりに赤い直線(温暖前線)を引く。
- 寒冷前線‥‥温暖前線記号は描かず、替わりに青い直線(寒冷前線)を引く。
- 閉塞前線‥‥寒冷前線記号の向きを180度逆転し、紫色で描く。
解説:前線を描く(Leaflet)
725: /**
726: * 前線描画用スクリプト作成:Leaflet
727: * @param array $item 前線の座標
728: * @param int $kind 前線の種類(0:温暖前線,1:寒冷前線,2:停滞前線,
729: * 3:閉塞前線)
730: * @return string 描画用スクリプト
731: */
732: function jsStationaryFront_leaflet($item, $kind) {
733: static $table_color1 = array('red', 'blue', 'red', 'purple');
734: static $table_color2 = array('red', 'blue', 'blue', 'purple');
735: static $table_angle = array(0, 0, 0, 1);
736:
737: $i = 0;
738: $flag = TRUE;
739: $js = '';
740: while ($flag) {
741: //前線(温暖)
742: $ss = '';
743: for ($j = $i; $j <= $i + 10; $j++) {
744: if (! isset($item['point'][$j])) {
745: $flag = FALSE;
746: break;
747: }
748: $ss .=<<< EOT
749: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
750:
751: EOT;
752: }
753: $js .=<<< EOT
754: L.polyline([
755: {$ss}
756: ], {
757: color: '{$table_color1[$kind]}',
758: opacity: 1.0,
759: weight: 2
760: }
761: ).addTo(map);
762:
763: EOT;
764: $i += 10;
765:
766: if (isset($item['point'][$i + 20])) {
767: //温暖部
768: $ss = '';
769: if ($kind != 1) {
770: $points = array();
771: semiCircle($item['point'][$i + 5]['latitude'], $item['point'][$i + 5]['longitude'], $item['point'][$i + 10]['latitude'], $item['point'][$i + 10]['longitude'], SEMICIRCLE, $points);
772: foreach ($points as $point) {
773: $ss .=<<< EOT
774: [{$point['latitude']}, {$point['longitude']}],
775:
776: EOT;
777: }
778: for ($j = $i + 0; $j <= $i + 10; $j++) {
779: $ss .=<<< EOT
780: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
781:
782: EOT;
783: }
784: $js .=<<< EOT
785: L.polygon([
786: {$ss}
787: ], {
788: color: '{$table_color1[$kind]}',
789: fillColor: '{$table_color1[$kind]}',
790: fillOpacity: 1.0,
791: }
792: ).addTo(map);
793:
794: EOT;
795: } else {
796: for ($j = $i + 0; $j <= $i + 10; $j++) {
797: $ss .=<<< EOT
798: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
799:
800: EOT;
801: }
802: $js .=<<< EOT
803: L.polyline([
804: {$ss}
805: ], {
806: color: '{$table_color1[$kind]}',
807: opacity: 1.0,
808: weight: 2
809: }
810: ).addTo(map);
811:
812: EOT;
813: }
814:
815: //寒冷部
816: if ($kind != 0) {
817: 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]);
818: $ss =<<< EOT
819: [{$lat}, {$lng}],
820:
821: EOT;
822: for ($j = $i + 10; $j <= $i + 20; $j++) {
823: $ss .=<<< EOT
824: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
825:
826: EOT;
827: }
828: $js .=<<< EOT
829: L.polygon([
830: {$ss}
831: ], {
832: color: '{$table_color2[$kind]}',
833: fillColor: '{$table_color2[$kind]}',
834: fillOpacity: 1.0,
835: }
836: ).addTo(map);
837:
838: EOT;
839: } else {
840: $ss = '';
841: for ($j = $i + 10; $j <= $i + 20; $j++) {
842: $ss .=<<< EOT
843: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
844:
845: EOT;
846: }
847: $js .=<<< EOT
848: L.polyline([
849: {$ss}
850: ], {
851: color: '{$table_color1[$kind]}',
852: opacity: 1.0,
853: weight: 2
854: }
855: ).addTo(map);
856:
857: EOT;
858: }
859: $i += 20;
860: }
861: //前線(寒冷)
862: $ss = '';
863: for ($j = $i; $j <= $i + 10; $j++) {
864: if (! isset($item['point'][$j])) {
865: $flag = FALSE;
866: break;
867: }
868: $ss .=<<< EOT
869: [{$item['point'][$j]['latitude']}, {$item['point'][$j]['longitude']}],
870:
871: EOT;
872: }
873: $js .=<<< EOT
874: L.polyline([
875: {$ss}
876: ], {
877: color: '{$table_color2[$kind]}',
878: opacity: 1.0,
879: weight: 2
880: }
881: ).addTo(map);
882:
883: EOT;
884: $i += 10;
885: }
886:
887: return $js;
888: }
アルゴリズムは、Googleマップ用の jsStationaryFront_gmap とほぼ同じで、多角形や直線を描く命令部分を変えている。
解説:ラベルを描く(Googleマップ)
495: /**
496: * ラベル表示用スクリプト作成:Googleマップ
497: * @param float $latitude, $longitude ラベル表示座標
498: * @param string $label ラベル
499: * @param int $size フォントサイズ(pt)
500: * @param string $color フォントカラー
501: * @param string $weight 太さ
502: * @return string 描画用スクリプト
503: */
504: function jsLabel_gmap($latitude, $longitude, $label, $size=14, $color='#FF0000', $weight="normal") {
505: $js =<<< EOT
506: new google.maps.Marker({
507: map: map,
508: position: new google.maps.LatLng({$latitude}, {$longitude}),
509: icon: {
510: url: 'https://www.pahoo.org/common/space.gif'
511: },
512: label: {
513: text: '{$label}',
514: color: '{$color}',
515: fontSize: '{$size}px',
516: fontWeight: '{$weight}'
517: }
518: });
519:
520: EOT;
521: return $js;
522: }
アイコン表示命令を利用し、アイコン画像を表示させないようにしている。
解説:ラベルを描く(Leaflet)
524: /**
525: * ラベル表示用スクリプト作成:Leaflet
526: * @param float $latitude, $longitude ラベル表示座標
527: * @param string $label ラベル
528: * @param int $size フォントサイズ(pt)
529: * @param string $color フォントカラー
530: * @param string $weight 太さ
531: * @return string 描画用スクリプト
532: */
533: function jsLabel_leaflet($latitude, $longitude, $label, $size=14, $color='#FF0000', $weight="normal") {
534: $js =<<< EOT
535: new L.marker(
536: [{$latitude}, {$longitude}],
537: {
538: icon:
539: new L.divIcon({
540: html: '<span style="color:{$color}; font-size:{$size}px; font-weight:{$weight};">{$label}</span>',
541: iconSize: [0, 0],
542: iconAnchor: [{$size}, {$size}],
543: })
544: }
545: ).addTo(map);
546:
547: EOT;
548: return $js;
549: }
前述の jsLabel_gmap と同様、アイコン表示命令を利用し、アイコン画像を表示させないようにしている。
解説:天気図描画スクリプトを生成する
890: /**
891: * 天気図描画スクリプトを生成する
892: * @param string $url 地上実況図URL
893: * @param object $pgc pahooGeoCodeオブジェクト
894: * @param object $pcc pahooCacheオブジェクト
895: * @param string $dt 報告日時格納用
896: * @param string $errmsg エラーメッセージ格納用
897: * @return string スクリプト/FALSE:生成失敗
898: */
899: function jsWeatherMap($url, $pgc, $pcc, &$dt, &$errmsg) {
900: static $table = array('温暖前線', '寒冷前線', '停滞前線', '閉塞前線');
901: $errmsg = '';
902: $js = '';
903: $items = array();
904: $res = jmaGetIsobar($url, $dt, $items, $pcc);
905: if ($res == FALSE) {
906: $errmsg = '気象庁防災情報XMLから地上実況図を取得できません';
907: }
908:
909: //スクリプト生成
910: foreach ($items as $key=>$item) {
911: if ($item['type'] == '等圧線') {
912: $js .= $pgc->jsLine($item['point'], 'black', 0.5, 0.5, MAPSERVICE);
913: } else if (preg_match('/前線/ui', $item['type']) > 0) {
914: $kind = array_search($item['type'], $table);
915: if (MAPSERVICE == 0) {
916: $js .= jsStationaryFront_gmap($item, $kind);
917: } else {
918: $js .= jsStationaryFront_leaflet($item, $kind);
919: }
920: } else if (preg_match('/気圧|台風|熱帯/ui', $item['type']) > 0) {
921: switch ($item['type']) {
922: case '高気圧':
923: $label = '高';
924: $color = 'blue';
925: break;
926: case '低気圧':
927: $label = '低';
928: $color = 'red';
929: break;
930: case '台風':
931: $label = '台';
932: $color = 'magenta';
933: break;
934: case '熱帯低気圧':
935: $label = '熱';
936: $color = 'magenta';
937: break;
938: }
939: if (MAPSERVICE == 0) {
940: $js .= jsLabel_gmap($item['latitude'], $item['longitude'], $label, 18, $color, 'bold');
941: $js .= jsLabel_gmap($item['latitude'] - 1.3, $item['longitude'], $item['pressure'], 16, 'black', 'bold%');
942: } else {
943: $js .= jsLabel_leaflet($item['latitude'], $item['longitude'], $label, 18, $color, 'bold');
944: $js .= jsLabel_leaflet($item['latitude'] - 1.3, $item['longitude'], $item['pressure'], 16, 'black', 'bold');
945: }
946: }
947: }
948:
949: //HTMLの画像化
950: $js .= js_html2image();
951:
952: return $js;
953: }
解説:ツイート機能
FALSE なら、pahooTwitterAPI クラスを読み込まず、ツイート・ボタンも表示しない。ツイート・ボタンの作成については、「HTMLとCSSでさまざまなアイコンを表示する」を参照してほしい。
画像化したいオブジェクト(ID名)は定数 TARGET で指定する。
解説:html2canvasライブラリ
103: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
104:
画像化を実行するJavaScript関数は html2canvas である。
1027: <div id="{$target}" name="{$target}" style="width:{$width2}px;">
1028: <p>
1029: 🌞天気図 {$dt}現在
1030: {$tweet}
1031: </p>
1032: <div id="{$mapid}" style="width:{$width}px; height:{$height}px;"></div>
1033: </div>
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてマップ領域より、幅を20ピクセル大きくした範囲を画像化範囲としている。
244: /**
245: * HTMLオブジェクトの画像化
246: * @param なし
247: * @return string JavaScriptコード
248: */
249: function js_html2image() {
250: $target = TARGET;
251: $js = '';
252:
253: //Googleマップの場合
254: if (MAPSERVICE == 0) {
255: $js .=<<< EOT
256: google.maps.event.addListener(map, 'tilesloaded', function() {
257: var capture = document.querySelector('#{$target}');
258: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
259: var base64 = canvas.toDataURL('image/png'); //画像化
260: $('#base64').val(base64);
261: });
262: });
263:
264: EOT;
265:
266: //Leafletの場合(ブラウザによってはうまく動作しない)
267: } else {
268: $js .=<<< EOT
269: HTMLCanvasElement.prototype.getContext = function(origFn) {
270: return function(type, attribs) {
271: attribs = attribs || {};
272: attribs.preserveDrawingBuffer = true;
273: return origFn.call(this, type, attribs);
274: };
275: } (HTMLCanvasElement.prototype.getContext);
276:
277: //HTML画像化イベント登録
278: function html2image() {
279: var capture = document.querySelector('#{$target}');
280: html2canvas(capture, {useCORS: true, allowTaint:true}).then(canvas => {
281: var base64 = canvas.toDataURL('image/png'); //画像化
282: $('#base64').val(base64);
283: });
284: };
285:
286: //ズーム変更イベント
287: map.on('zoomend', function() {
288: html2image();
289: });
290:
291: //マップ移動イベント
292: map.on('moveend', function() {
293: html2image();
294: });
295:
296: //html2image()を発火させるために,ズームアウトして500msec後に元に戻す.
297: var zoom = map.getZoom();
298: map.setZoom(zoom - 1);
299: setTimeout(function() {
300: map.setZoom(zoom);
301: }, 500);
302:
303: EOT;
304: }
305:
306: return $js;
307: }
Leafletの場合、これに相当するイベントがないため、ズーム変更完了イベントにフックし、強制的にズーム変更イベントを発生させるタイミングで画像化する。しかし、この方法だとブラウザによって、マップ画像が無い状態で画像化されてしまうことがあるようだ。もし対策方法をご存じの方がいたらお知らせいただきたい。
これらをJavaScriptとして生成するユーザー関数が js_html2image である。
準備:pahooTwitterAPI クラス
27: //OAuth用パラメータ
28: // https://apps.twitter.com/
29: var $TWTR_CONSUMER_KEY = '***************'; //Cunsumer key
30: var $TWTR_CONSUMER_SECRET = '***************'; //Consumer secret
31: var $TWTR_ACCESS_KEY = '***************'; //Access Token (oauth_token)
32: var $TWTR_ACCESS_SECRET = '***************'; //Access Token Secret (oauth_token_secret)
33:
事前にプログラムを登録しておく必要があり、その方法は「Twitter API - WebAPIの登録方法」を参照されたい。入手したパラメータを、上述の変数に代入しておくこと。
解説:メディア付き投稿(RAWデータ)
584: /**
585: * バイナリデータを使ったメディア付きメッセージをツイートする.
586: * Tweetet API v2 を使用する.
587: * @param string $message 投稿メッセージ(UTF-8限定)
588: * @param array $items メディアデータ(バイナリデータ配列)
589: * @return bool TRUE:リクエスト成功/FALSE:失敗
590: */
591: function tweet_media_raw($message, $items) {
592: //メディアのアップロード
593: $media_ids = array();
594: $cnt = 0;
595: //Tweetet API v1.1 を使用する(v2にメディアアップロードが未実装のため)
596: $this->connection->setApiVersion('1.1');
597: foreach ($items as $data) {
598: $tmpname = $this->saveTempFile($data);
599: $media = $this->connection->upload('media/upload', ['media' => $tmpname]);
600: unlink($tmpname);
601: if (! isset($media->media_id_string)) break; //処理失敗
602: $media_ids[] = (string)$media->media_id_string;
603: $cnt++;
604: if ($cnt > 3) break; //最大4つまで
605: }
606:
607: //メディア付きツイート(Tweetet API v2 を使用する)
608: $this->connection->setApiVersion('2');
609: $option = [
610: 'text' => $message,
611: 'media' => [
612: 'media_ids' => $media_ids
613: ]
614: ];
615: $status = $this->connection->post('tweets', $option, TRUE);
616: $this->webapi = 'https://api.twitter.com/2/tweets';
617:
618: //処理に成功した.
619: if ($this->isSuccess()) {
620: $this->responses = $status->data;
621: $this->errcode = NULL;
622: $this->errmsg = '';
623: $this->error = FALSE;
624: $res = TRUE;
625: //処理に失敗した.
626: } else {
627: if ($this->isAuthError() == FALSE) {
628: $this->errmsg = $status->detail;
629: $this->error = TRUE;
630: }
631: $res = FALSE;
632: }
633: return $res;
634: }
解説:ツイート処理
219: /**
220: * ツイート処理
221: * @param string $message 投稿文
222: * @param string $res 応答メッセージ格納用
223: * @return bool TRUE:成功/FALSE:失敗または未処理
224: */
225: function mediaTweet($message, &$res) {
226: if (! TWITTER) return FALSE;
227:
228: $ret = TRUE;
229: if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
230: $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
231: $raws = array(base64_decode($base64));
232: $ptw = new pahooTwitterAPI();
233: $ptw->tweet_media_raw($message, $raws);
234: $errmsg = $ptw->errmsg;
235: $ret = ! $ptw->error;
236: $ptw = NULL;
237: if ($ret) {
238: $res = 'ツイートしました';
239: }
240: }
241: return $ret;
242: }
ブラウザからPOSTで受け取った画像データ $_POST['base64'&$x5D; はBASE64でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気にツイートする。
1080: //ツイート機能
1081: $message =<<< EOT
1082: 🌞天気図 {$dt}現在
1083:
1084: (ご参考)PHPで天気図を描く https://www.pahoo.org/e-soul/webtech/php06/php06-73-01.shtm
1085:
1086: EOT;
1087: mediaTweet($message, $res);
活用例
参考サイト
- 気象庁防災情報XMLフォーマット 情報提供ページ
- PHPで天気予報を求める:ぱふぅ家のホームページ
- PHPで住所・ランドマークから最寄り駅を求める:ぱふぅ家のホームページ
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- 日本周辺域の天気図:みんなの知識 ちょっと便利帳
そこで今回は、これらの情報をリアルタイムに取得し、Googleマップなどのマップに天気図を描くPHPプログラムを作ってみることにする。
(2023年9月20日)js_html2image()--Leaflet用html2image()発火プロセス見直し
(2022年3月10日)気象庁防災情報XMLのhttps化に対応。キャッシュディレクトリ(定数 DIR_CACHE で指定するもの)は、ディレクトリごと消去してから新しプログラムを起動すること。