
16.台風情報の作り方
今回は、最難関の台風情報描画プログラムの説明をします。
今回は気象庁jsonデータではなく、気象庁xmlデータを利用します。
気象庁xmlデータのアドレスは、下記になります。
https://www.data.jma.go.jp/developer/xml/feed/extra.xml
気象庁xmlデータの中から、タイトル「台風解析・予報情報(5日予報)(H30)」の台風情報を取得します。
取得コードは、下記になります。
<?php
// XMLデータを取得するURL
$url = "https://www.data.jma.go.jp/developer/xml/feed/extra.xml"; // 実際のXML URLを指定
// URLからXMLデータを取得
$xmlData = file_get_contents($url);
if ($xmlData !== false) {
// XMLデータをSimpleXMLオブジェクトに変換
$xml = simplexml_load_string($xmlData);
// ターゲットのタイトルテキスト
$targetTitle = "台風解析・予報情報(5日予報)(H30)";
// 対象とする時刻のリスト(日本時間に+9時間)
$targetTimes1 = ['T00', 'T03', 'T06', 'T09', 'T12', 'T15', 'T18', 'T21'];
$targetTimesJP1 = [];
foreach ($targetTimes1 as $timeStr) {
$hour = (int)substr($timeStr, 1);
$hourJP = ($hour + 9) % 24;
$timeStrJP = 'T' . str_pad($hourJP, 2, '0', STR_PAD_LEFT);
$targetTimesJP1[] = $timeStrJP;
}
$id1 = '';
foreach ($xml->entry as $entry) {
if ((string)$entry->title === $targetTitle) {
$children = $entry->children();
for ($i = 0; $i < count($children); $i++) {
if ($children[$i]->getName() === 'updated') {
$updatedTime = (string)$children[$i];
$updatedDateTime = new DateTime($updatedTime, new DateTimeZone('UTC'));
$updatedDateTime->setTimezone(new DateTimeZone('Asia/Tokyo'));
$currentDateTime = new DateTime('now', new DateTimeZone('Asia/Tokyo'));
$diffInSeconds = $currentDateTime->getTimestamp() - $updatedDateTime->getTimestamp();
$diffInHours = $diffInSeconds / 3600;
$timePart = 'T' . $updatedDateTime->format('H');
if (in_array($timePart, $targetTimesJP1) && $diffInHours >= 0 && $diffInHours <= 4) {
if ($i > 0 && $children[$i - 1]->getName() === 'id') {
$id1 = (string)$children[$i - 1];
break 2;
}
} elseif ($diffInHours > 4) {
continue;
}
}
}
}
}
?>
気象庁xmlデータの中から、タイトル「台風解析・予報情報(5日予報)(H30)」を抽出し、<updated>の時刻が、4時間以内のものだけidを取得します。
気象庁xmlデータは降順に更新されていくので、4時間以内という制限をかけないと、台風が熱帯低気圧になった時に、4時間前以降の台風情報を取得し続けるからです。
そのidアドレスを取得し、更に台風情報の中身の解析を行います。
それでは、上記コードの説明をします。
<?php
// 対象とする時刻のリスト(日本時間に+9時間)
$targetTimes1 = ['T00', 'T03', 'T06', 'T09', 'T12', 'T15', 'T18', 'T21'];
$targetTimesJP1 = [];
foreach ($targetTimes1 as $timeStr) {
$hour = (int)substr($timeStr, 1);
$hourJP = ($hour + 9) % 24;
$timeStrJP = 'T' . str_pad($hourJP, 2, '0', STR_PAD_LEFT);
$targetTimesJP1[] = $timeStrJP;
}
?>
台風情報の5日予報は、3時間ごとに情報が更新されます。
更新時刻は、$targetTimes1 = [‘T00’, ‘T03’, ‘T06’, ‘T09’, ‘T12’, ‘T15’, ‘T18’, ‘T21’];になります。
処理内容:
$timeStr
の時刻部分を抽出:
<?php
$hour = (int)substr($timeStr, 1);
?>
substr($timeStr, 1)
:
T00
,T03
などの文字列から、最初のT
を除去し、時刻部分を抽出(例:T03
→03
)。
(int)
:
- 抽出した文字列を整数型に変換。
2.日本標準時に変換:
<?php
$hourJP = ($hour + 9) % 24;
?>
- UTCの時刻(
$hour
)に日本標準時の9時間を加算。 % 24
により24時間を超えた場合に日付を跨いで時刻を正規化(例: 25時 → 1時)。
3.新しい時刻文字列を作成:
<?php
$timeStrJP = 'T' . str_pad($hourJP, 2, '0', STR_PAD_LEFT);
?>
4.変換結果を格納:
<?php
$targetTimesJP1[] = $timeStrJP;
?>
JSTに変換された時刻文字列を $targetTimesJP1
配列に追加。
変換結果の例
元の UTC
時刻リスト($targetTimes1
)が以下の場合:
['T00', 'T03', 'T06', 'T09', 'T12', 'T15', 'T18', 'T21']
対応する日本標準時(JST)時刻リスト($targetTimesJP1
)は:
['T09', 'T12', 'T15', 'T18', 'T21', 'T00', 'T03', 'T06']
- 例:
T00
(UTC午前0時) →T09
(JST午前9時)。T21
(UTC午後9時) →T06
(JST翌日の午前6時)。
$targetTimes1と$targetTimesJP1同じかどうか?
内容(値)
- 値としては、どちらのリストも 同じ時刻(T00~T21) を持っています。
- 順序を無視して比較すると、両方とも同じ集合を表します。
順序
- 順序が異なるため、配列としては 別物 です。
- プログラムで比較するとき、順序の違いが重要になる場面では結果が変わります。
順序が違うことの影響
プログラム内では、in_array
関数を使用してリスト内に特定の値が含まれるかをチェックしています。この場合、in_array
は順序を無視して一致を確認するため、順序の違いによる問題は発生しません。
ただし、もしリストを使って他の処理を行う場合(例えば、リストの要素を順序通りに処理する場合)、順序が異なると正しく動作しない可能性があります。
取得した$id1(詳細な台風情報のアドレス)を次の処理に繋げます。
<?php
if ($id1 !== '') {
echo "<script>var xmlUrl = '{$id1}';</script>";
} else {
echo "<script>document.getElementById('no-typhoon').style.display = 'block';</script>";
}
?>
$id1が存在する時、xmlUrlに代入します。
$id1が存在しない場合、画面にno-typhoonと表示します。
$id1で取得した台風解析・予報情報(5日予報)(H30)の内容は下記になります。
<Report xmlns="http://xml.kishou.go.jp/jmaxml1/" xmlns:jmx="http://xml.kishou.go.jp/jmaxml1/" xmlns:jmx_add="http://xml.kishou.go.jp/jmaxml1/addition1/">
<Control>
<Title>台風解析・予報情報(5日予報)(H30)</Title>
<DateTime>2024-08-25T06:41:19Z</DateTime>
<Status>通常</Status>
<EditorialOffice>気象庁本庁</EditorialOffice>
<PublishingOffice>気象庁</PublishingOffice>
</Control>
<Head xmlns="http://xml.kishou.go.jp/jmaxml1/informationBasis1/">
<Title>台風解析・予報情報</Title>
<ReportDateTime>2024-08-25T15:45:00+09:00</ReportDateTime>
<TargetDateTime>2024-08-25T15:00:00+09:00</TargetDateTime>
<TargetDuration>PT120H</TargetDuration>
<EventID>TC2412</EventID>
<InfoType>発表</InfoType>
<Serial>35</Serial>
<InfoKind>台風解析・予報情報(5日予報)</InfoKind>
<InfoKindVersion>1.0_2</InfoKindVersion>
<Headline>
<Text/>
</Headline>
</Head>
<Body xmlns="http://xml.kishou.go.jp/jmaxml1/body/meteorology1/" xmlns:jmx_eb="http://xml.kishou.go.jp/jmaxml1/elementBasis1/">
<MeteorologicalInfos type="台風情報">
<MeteorologicalInfo>
<DateTime type="実況">2024-08-25T15:00:00+09:00</DateTime>
<Item>
<Kind>
<Property>
<Type>呼称</Type>
<TyphoonNamePart>
<Name>SHANSHAN</Name>
<NameKana>サンサン</NameKana>
<Number>2410</Number>
<Remark/>
</TyphoonNamePart>
</Property>
</Kind>
<Kind>
<Property>
<Type>階級</Type>
<ClassPart>
<jmx_eb:TyphoonClass type="熱帯擾乱種類">台風(TY)</jmx_eb:TyphoonClass>
<jmx_eb:AreaClass type="大きさ階級"/>
<jmx_eb:IntensityClass type="強さ階級">強い</jmx_eb:IntensityClass>
</ClassPart>
</Property>
</Kind>
<Kind>
<Property>
<Type>中心</Type>
<CenterPart>
<jmx_eb:Coordinate description="北緯25.7度東経137.9度" condition="ほぼ正確" type="中心位置(度)">+25.7+137.9/</jmx_eb:Coordinate>
<jmx_eb:Coordinate description="北緯25度40分東経137度55分" condition="ほぼ正確" type="中心位置(度分)">+2540+13755/</jmx_eb:Coordinate>
<Location>日本の南</Location>
<jmx_eb:Direction unit="16方位漢字" type="移動方向">北西</jmx_eb:Direction>
<jmx_eb:Speed description="15ノット" unit="ノット" type="移動速度">15</jmx_eb:Speed>
<jmx_eb:Speed description="毎時30キロ" unit="km/h" type="移動速度">30</jmx_eb:Speed>
<jmx_eb:Pressure description="中心気圧980ヘクトパスカル" unit="hPa" type="中心気圧">980</jmx_eb:Pressure>
</CenterPart>
</Property>
</Kind>
<Kind>
<Property>
<Type>風</Type>
<WindPart>
<jmx_eb:WindSpeed description="中心付近の最大風速65ノット" condition="中心付近" unit="ノット" type="最大風速">65</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="中心付近の最大風速35メートル" condition="中心付近" unit="m/s" type="最大風速">35</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="最大瞬間風速95ノット" unit="ノット" type="最大瞬間風速">95</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="最大瞬間風速50メートル" unit="m/s" type="最大瞬間風速">50</jmx_eb:WindSpeed>
</WindPart>
<WarningAreaPart type="暴風域">
<jmx_eb:WindSpeed description="風速50ノット以上" condition="以上" unit="ノット" type="風速">50</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="風速25メートル以上" condition="以上" unit="m/s" type="風速">25</jmx_eb:WindSpeed>
<jmx_eb:Circle>
<jmx_eb:Axes>
<jmx_eb:Axis>
<jmx_eb:Direction description="全域" condition="全域" unit="8方位漢字" type="方向"/>
<jmx_eb:Radius description="30海里" unit="海里" type="半径">30</jmx_eb:Radius>
<jmx_eb:Radius description="55キロ" unit="km" type="半径">55</jmx_eb:Radius>
</jmx_eb:Axis>
</jmx_eb:Axes>
</jmx_eb:Circle>
</WarningAreaPart>
<WarningAreaPart type="強風域">
<jmx_eb:WindSpeed description="風速30ノット以上" condition="以上" unit="ノット" type="風速">30</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="風速15メートル以上" condition="以上" unit="m/s" type="風速">15</jmx_eb:WindSpeed>
<jmx_eb:Circle>
<jmx_eb:Axes>
<jmx_eb:Axis>
<jmx_eb:Direction description="全域" condition="全域" unit="8方位漢字" type="方向"/>
<jmx_eb:Radius description="150海里" unit="海里" type="半径">150</jmx_eb:Radius>
<jmx_eb:Radius description="280キロ" unit="km" type="半径">280</jmx_eb:Radius>
</jmx_eb:Axis>
</jmx_eb:Axes>
</jmx_eb:Circle>
</WarningAreaPart>
</Property>
</Kind>
<Area>
<Name>熱帯低気圧</Name>
<jmx_eb:Circle type="強風域">
<jmx_eb:BasePoint description="北緯25.7度東経137.9度" type="中心位置(度)">+25.7+137.9/</jmx_eb:BasePoint>
<jmx_eb:BasePoint description="北緯25度40分東経137度55分" type="中心位置(度分)">+2540+13755/</jmx_eb:BasePoint>
<jmx_eb:Axes>
<jmx_eb:Axis>
<jmx_eb:Direction description="全域" condition="全域" unit="8方位漢字" type="方向"/>
<jmx_eb:Radius description="150海里" unit="海里" type="半径">150</jmx_eb:Radius>
<jmx_eb:Radius description="280キロ" unit="km" type="半径">280</jmx_eb:Radius>
</jmx_eb:Axis>
</jmx_eb:Axes>
</jmx_eb:Circle>
</Area>
</Item>
</MeteorologicalInfo>
</MeteorologicalInfos>
</Body>
</Report>
<DateTime type="予報 24時間後">2024-08-26T15:00:00+09:00</DateTime>
<Item>
<Kind>
<Property>
<Type>階級</Type>
<ClassPart>
<jmx_eb:TyphoonClass type="熱帯擾乱種類">台風(TY)</jmx_eb:TyphoonClass>
<jmx_eb:IntensityClass type="強さ階級">強い</jmx_eb:IntensityClass>
</ClassPart>
</Property>
</Kind>
<Kind>
<Property>
<Type>中心</Type>
<CenterPart>
<ProbabilityCircle type="予報円">
<jmx_eb:BasePoint description="北緯28.0度東経133.5度" type="中心位置(度)">+28.0+133.5/</jmx_eb:BasePoint>
<jmx_eb:BasePoint description="北緯28度00分東経133度30分" type="中心位置(度分)">+2800+13330/</jmx_eb:BasePoint>
<jmx_eb:Axes>
<jmx_eb:Axis>
<jmx_eb:Direction description="全域" condition="全域" unit="8方位漢字" type="方向"/>
<jmx_eb:Radius description="中心が70パーセントの確率で入る予報円の半径50海里" unit="海里" type="70パーセント確率半径">50</jmx_eb:Radius>
<jmx_eb:Radius description="中心が70パーセントの確率で入る予報円の半径95キロ" unit="km" type="70パーセント確率半径">95</jmx_eb:Radius>
</jmx_eb:Axis>
</jmx_eb:Axes>
</ProbabilityCircle>
<Location>日本の南</Location>
<jmx_eb:Direction unit="16方位漢字" type="移動方向">西北西</jmx_eb:Direction>
<jmx_eb:Speed description="11ノット" unit="ノット" type="移動速度">11</jmx_eb:Speed>
<jmx_eb:Speed description="毎時20キロ" unit="km/h" type="移動速度">20</jmx_eb:Speed>
<jmx_eb:Pressure description="中心気圧965ヘクトパスカル" unit="hPa" type="中心気圧">965</jmx_eb:Pressure>
</CenterPart>
</Property>
</Kind>
<Kind>
<Property>
<Type>風</Type>
<WindPart>
<jmx_eb:WindSpeed description="中心付近の最大風速75ノット" condition="中心付近" unit="ノット" type="最大風速">75</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="中心付近の最大風速40メートル" condition="中心付近" unit="m/s" type="最大風速">40</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="最大瞬間風速105ノット" unit="ノット" type="最大瞬間風速">105</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="最大瞬間風速55メートル" unit="m/s" type="最大瞬間風速">55</jmx_eb:WindSpeed>
</WindPart>
<WarningAreaPart type="暴風警戒域">
<jmx_eb:WindSpeed description="風速50ノット以上" condition="以上" unit="ノット" type="風速">50</jmx_eb:WindSpeed>
<jmx_eb:WindSpeed description="風速25メートル以上" condition="以上" unit="m/s" type="風速">25</jmx_eb:WindSpeed>
<jmx_eb:Circle>
<jmx_eb:Axes>
<jmx_eb:Axis>
<jmx_eb:Direction description="全域" condition="全域" unit="8方位漢字" type="方向"/>
<jmx_eb:Radius description="100海里" unit="海里" type="半径">100</jmx_eb:Radius>
<jmx_eb:Radius description="185キロ" unit="km" type="半径">185</jmx_eb:Radius>
</jmx_eb:Axis>
</jmx_eb:Axes>
</jmx_eb:Circle>
</WarningAreaPart>
</Property>
</Kind>
<Area>
<Name>熱帯低気圧</Name>
<jmx_eb:Circle type="予報円">
<jmx_eb:BasePoint description="北緯28.0度東経133.5度" type="中心位置(度)">+28.0+133.5/</jmx_eb:BasePoint>
<jmx_eb:BasePoint description="北緯28度00分東経133度30分" type="中心位置(度分)">+2800+13330/</jmx_eb:BasePoint>
<jmx_eb:Axes>
<jmx_eb:Axis>
<jmx_eb:Direction description="全域" condition="全域" unit="8方位漢字" type="方向"/>
<jmx_eb:Radius description="中心が70パーセントの確率で入る予報円の半径50海里" unit="海里" type="70パーセント確率半径">50</jmx_eb:Radius>
<jmx_eb:Radius description="中心が70パーセントの確率で入る予報円の半径95キロ" unit="km" type="70パーセント確率半径">95</jmx_eb:Radius>
</jmx_eb:Axis>
</jmx_eb:Axes>
</jmx_eb:Circle>
</Area>
</Item>
上記が台風情報xmlデータの一部になります。
このxmlデータを解析して、台風情報描画プログラムの作成を行います。
xmlデータ取得後のプログラムを一気に最後まで掲載します。
<script>
if (typeof xmlUrl !== 'undefined') {
var proxyUrl = 'proxy.php?url=' + encodeURIComponent(xmlUrl);
var xhr = new XMLHttpRequest();
xhr.open("GET", proxyUrl, true);
xhr.responseType = "document"; // XMLとして取得
xhr.onload = function () {
if (xhr.status === 200) {
var xmlDoc = xhr.responseXML;
console.log("取得したXMLデータ:", xmlDoc);
// Leafletマップを初期化
const map = L.map('map').setView([30, 130], 5);
// OSMレイヤーを追加
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
function getElementsByTagNameWithoutNamespace(tagName, parentElement) {
var elements = (parentElement || xmlDoc).getElementsByTagName('*');
var result = [];
for (var i = 0; i < elements.length; i++) {
if (elements[i].localName === tagName) {
result.push(elements[i]);
}
}
return result;
}
function getElementsByTagNameAndAttribute(element, tagName, attrName, attrValue) {
var elements = [];
var allElements = element.getElementsByTagName('*');
for (var i = 0; i < allElements.length; i++) {
if (allElements[i].localName === tagName && (attrValue === null || allElements[i].getAttribute(attrName) === attrValue)) {
elements.push(allElements[i]);
}
}
return elements;
}
const targetTypes = ['実況', '予報 6時間後', '予報 12時間後', '予報 24時間後'];
const forecastMapping = [
{ primary: '予報 48時間後', fallback: '予報 45時間後' },
{ primary: '予報 72時間後', fallback: '予報 69時間後' },
{ primary: '予報 96時間後', fallback: '予報 93時間後' },
{ primary: '予報 120時間後', fallback: '予報 117時間後' }
];
const pressureElement = getElementsByTagNameWithoutNamespace('Pressure')[0];
const LocationElement = getElementsByTagNameWithoutNamespace('Location')[0];
const DirectionElement = getElementsByTagNameWithoutNamespace('Direction')[0];
const windSpeedElements = getElementsByTagNameWithoutNamespace('WindSpeed');
let maxWindSpeed = "N/A";
for (let i = 0; i < windSpeedElements.length; i++) {
if (windSpeedElements[i].getAttribute('unit') === 'm/s' && windSpeedElements[i].getAttribute('type') === '最大風速') {
maxWindSpeed = windSpeedElements[i].textContent.trim();
break;
}
}
const maximumgustSpeedElements = getElementsByTagNameWithoutNamespace('WindSpeed');
let maximumgustSpeed = "N/A";
for (let i = 0; i < maximumgustSpeedElements.length; i++) {
if (maximumgustSpeedElements[i].getAttribute('unit') === 'm/s' && maximumgustSpeedElements[i].getAttribute('type') === '最大瞬間風速') {
maximumgustSpeed = maximumgustSpeedElements[i].textContent.trim();
break;
}
}
const speedElements = getElementsByTagNameWithoutNamespace('Speed');
let movingSpeed = "不明";
for (let i = 0; i < speedElements.length; i++) {
const unit = speedElements[i].getAttribute('unit');
const description = speedElements[i].getAttribute('description');
if (unit === 'km/h') {
movingSpeed = description;
break;
}
}
const Pressure = pressureElement ? pressureElement.textContent.trim() : "N/A";
const Location = LocationElement ? LocationElement.textContent.trim() : "N/A";
const Direction = DirectionElement ? DirectionElement.textContent.trim() : "N/A";
const dateTimeMap = {};
const dateTimeEls = getElementsByTagNameWithoutNamespace('DateTime');
dateTimeEls.forEach(function(element) {
const type = element.getAttribute('type');
const dateTime = element.textContent.trim();
dateTimeMap[type] = formatDateTime(dateTime);
});
const jikkyoDateTime = dateTimeMap['実況'] || 'N/A';
const centerCoordinateElements = getElementsByTagNameWithoutNamespace('Coordinate');
if (centerCoordinateElements.length > 0) {
const firstCoordinate = centerCoordinateElements[0].textContent.trim();
const coordinateParts = firstCoordinate.split('+');
if (coordinateParts.length < 3) {
console.error("座標データの形式が正しくありません。");
document.getElementById('no-typhoon').style.display = 'block';
return;
}
const firstLat = parseFloat(coordinateParts[1]);
const firstLon = parseFloat(coordinateParts[2]);
if (isNaN(firstLat) || isNaN(firstLon)) {
console.error("座標データの解析に失敗しました。");
document.getElementById('no-typhoon').style.display = 'block';
return;
}
L.marker([firstLat, firstLon]).addTo(map).bindPopup(`
<b>現在の台風の中心位置:</b> ${firstLat}°, ${firstLon}°<br>
<b>存在地域:</b> ${Location} <br>
<b>中心気圧:</b> ${Pressure} hPa<br>
<b>最大風速:</b> ${maxWindSpeed} m/s<br>
<b>最大瞬間風速:</b> ${maximumgustSpeed} m/s<br>
<b>移動方向:</b> ${Direction} <br>
<b>速さ:</b> ${movingSpeed} <br>
<b>情報:</b> ${'実況: ' + jikkyoDateTime}
`).openPopup();
L.marker([firstLat, firstLon], {
icon: L.divIcon({
className: 'forecast-label',
html: '実況
' + jikkyoDateTime,
iconSize: [110, 35],
iconAnchor: [25, -10]
}),
interactive: false
}).addTo(map);
// ★変更箇所: 予報時間を取得する関数
function getForecastHour(typeString) {
if (typeString === "実況") {
return 0;
}
const match = typeString.match(/予報\s*(\d+)時間後/);
if (match && match[1]) {
return parseInt(match[1], 10);
}
return null;
}
const circleLayers = [{
lat: firstLat,
lon: firstLon,
radius: 0,
circle: null,
hour: 0
}];
var meteorologicalInfos = getElementsByTagNameWithoutNamespace('MeteorologicalInfo');
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
var isTargetType = targetTypes.includes(dateTimeType);
var isForecastType = forecastMapping.some(mapping => mapping.primary === dateTimeType || mapping.fallback === dateTimeType);
if (!isTargetType && !isForecastType) {
return;
}
var forecastHour = getForecastHour(dateTimeType);
var dateTimeText = dateTimeElement.textContent.trim();
var formattedDateTime = formatDateTime(dateTimeText);
var circleElements = getElementsByTagNameWithoutNamespace('Circle', meteorologicalInfo);
circleElements.forEach(function(circle) {
var typeAttr = circle.getAttribute('type');
if (typeAttr !== "予報円") return;
var basePointElements = circle.getElementsByTagName('*');
var basePointElement = null;
for (var i = 0; i < basePointElements.length; i++) {
if (basePointElements[i].localName === 'BasePoint') {
basePointElement = basePointElements[i];
break;
}
}
var radiusElements = circle.getElementsByTagName('*');
var radiusElement = null;
for (var i = 0; i < radiusElements.length; i++) {
if (radiusElements[i].localName === 'Radius' && radiusElements[i].getAttribute('unit') === 'km') {
radiusElement = radiusElements[i];
break;
}
}
if (basePointElement && radiusElement) {
const latLngMatches = basePointElement.textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (!latLngMatches || latLngMatches.length < 2) {
console.warn("予報円の座標解析に失敗しました。");
return;
}
const lat = parseFloat(latLngMatches[0]);
const lng = parseFloat(latLngMatches[1]);
const radiusKm = parseFloat(radiusElement.textContent);
if (isNaN(lat) || isNaN(lng) || isNaN(radiusKm)) {
console.warn("予報円のデータ解析に失敗しました。");
return;
}
const circlePoints = getCirclePoints(lat, lng, radiusKm * 1000, 64);
const circleLayer = L.polygon(circlePoints, {
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.3,
dashArray: '5,5'
}).addTo(map);
circleLayer.bringToFront();
const labelText = dateTimeType + '
' + formattedDateTime;
circleLayer.bindTooltip(labelText, {
permanent: false,
direction: 'top',
className: 'forecast-label',
offset: [0, -20]
});
circleLayer.tooltipVisible = true;
circleLayer.openTooltip();
circleLayer.on('click', function(e) {
if (this.tooltipVisible) {
this.closeTooltip();
this.tooltipVisible = false;
} else {
this.openTooltip();
this.tooltipVisible = true;
}
});
const typhoonDot = L.divIcon({ className: 'typhoon-dot' });
L.marker([lat, lng], { icon: typhoonDot }).addTo(map);
circleLayers.push({
lat: lat,
lon: lng,
radius: radiusKm * 1000,
circle: circleLayer,
hour: (forecastHour !== null ? forecastHour : 0)
});
}
});
});
// ★変更箇所: 予報円を予報時間順にソート、隣接のみ共通外接線
circleLayers.sort((a, b) => a.hour - b.hour);
if (circleLayers.length > 1) {
for (let i = 0; i < circleLayers.length - 1; i++) {
const circle1 = circleLayers[i];
const circle2 = circleLayers[i + 1];
const centerLatLng1 = L.latLng(circle1.lat, circle1.lon);
const centerLatLng2 = L.latLng(circle2.lat, circle2.lon);
const p1 = map.latLngToLayerPoint(centerLatLng1);
const p2 = map.latLngToLayerPoint(centerLatLng2);
const radiusLatLng1 = destinationPoint(circle1.lat, circle1.lon, 0, circle1.radius);
const radiusLatLng2 = destinationPoint(circle2.lat, circle2.lon, 0, circle2.radius);
const radiusPoint1 = map.latLngToLayerPoint([radiusLatLng1.lat, radiusLatLng1.lng]);
const radiusPoint2 = map.latLngToLayerPoint([radiusLatLng2.lat, radiusLatLng2.lng]);
const r1 = p1.distanceTo(radiusPoint1);
const r2 = p2.distanceTo(radiusPoint2);
const tangents = calculateOuterTangents(p1, r1, p2, r2);
if (tangents) {
tangents.forEach(line => {
L.polyline([
map.layerPointToLatLng(line[0]),
map.layerPointToLatLng(line[1])
], { color: 'purple', weight: 2 }).addTo(map);
});
}
const centerLine = [[circle1.lat, circle1.lon], [circle2.lat, circle2.lon]];
L.polyline(centerLine, { color: 'red', weight: 1, dashArray: '5, 5' }).addTo(map);
}
}
// 暴風域(実況)赤円表示
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (dateTimeType !== '実況') return;
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
if (basePoints.length === 0) return;
var basePointElement = basePoints[0];
const latLngMatches = basePointElement.textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (!latLngMatches || latLngMatches.length < 2) return;
const lat = parseFloat(latLngMatches[0]);
const lng = parseFloat(latLngMatches[1]);
var stormAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '暴風域');
if (stormAreaParts.length === 0) return;
var stormAreaPart = stormAreaParts[0];
var radiusElements = stormAreaPart.getElementsByTagName('*');
var radiusKm = null;
for (var k = 0; k < radiusElements.length; k++) {
if (radiusElements[k].localName === 'Radius' && radiusElements[k].getAttribute('unit') === 'km') {
radiusKm = parseFloat(radiusElements[k].textContent.trim());
break;
}
}
if (!radiusKm) return;
L.circle([lat, lng], {
color: 'red',
fillColor: 'red',
fillOpacity: 0.3,
radius: radiusKm * 1000
}).addTo(map).bindPopup('暴風域');
});
// ★変更箇所: 暴風警戒域も隣接するもののみ共通外接線
var stormAreas = [];
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
var isTargetType = targetTypes.includes(dateTimeType);
var isForecastType = forecastMapping.some(mapping => mapping.primary === dateTimeType || mapping.fallback === dateTimeType);
if (!isTargetType && !isForecastType) {
return;
}
var forecastHour = getForecastHour(dateTimeType);
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
var lat = null;
var lng = null;
if (basePoints.length > 0) {
const latLngMatches = basePoints[0].textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (latLngMatches && latLngMatches.length >= 2) {
lat = parseFloat(latLngMatches[0]);
lng = parseFloat(latLngMatches[1]);
}
}
var warningAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '暴風警戒域');
if (warningAreaParts.length > 0 && lat !== null && lng !== null) {
var warningAreaPart = warningAreaParts[0];
var radiusElements = warningAreaPart.getElementsByTagName('*');
var radiusKm = null;
for (var k = 0; k < radiusElements.length; k++) {
if (radiusElements[k].localName === 'Radius' && radiusElements[k].getAttribute('unit') === 'km') {
radiusKm = parseFloat(radiusElements[k].textContent.trim());
break;
}
}
if (radiusKm) {
stormAreas.push({
lat: lat,
lng: lng,
radius: radiusKm * 1000,
hour: (forecastHour !== null ? forecastHour : 0)
});
}
}
});
// 暴風警戒域を時間順にソート
stormAreas.sort((a, b) => a.hour - b.hour);
// 暴風警戒域を個別描画、隣接ペアで共通外接線
for (let i = 0; i < stormAreas.length; i++) {
const area = stormAreas[i];
const circlePoints = getCirclePoints(area.lat, area.lng, area.radius, 64);
L.polygon(circlePoints, {
color: 'red',
fillColor: 'red',
fillOpacity: 0.3,
interactive: false,
}).addTo(map);
const stormDot = L.divIcon({ className: 'typhoon-dot' });
L.marker([area.lat, area.lng], { icon: stormDot }).addTo(map);
}
if (stormAreas.length > 1) {
for (let i = 0; i < stormAreas.length - 1; i++) {
const area1 = stormAreas[i];
const area2 = stormAreas[i + 1];
const centerLatLng1 = L.latLng(area1.lat, area1.lng);
const centerLatLng2 = L.latLng(area2.lat, area2.lng);
const p1 = map.latLngToLayerPoint(centerLatLng1);
const p2 = map.latLngToLayerPoint(centerLatLng2);
const radiusLatLng1 = destinationPoint(area1.lat, area1.lng, 0, area1.radius);
const radiusLatLng2 = destinationPoint(area2.lat, area2.lng, 0, area2.radius);
const radiusPoint1 = map.latLngToLayerPoint([radiusLatLng1.lat, radiusLatLng1.lng]);
const radiusPoint2 = map.latLngToLayerPoint([radiusLatLng2.lat, radiusLatLng2.lng]);
const r1 = p1.distanceTo(radiusPoint1);
const r2 = p2.distanceTo(radiusPoint2);
const tangents = calculateOuterTangents(p1, r1, p2, r2);
if (tangents) {
tangents.forEach(line => {
L.polyline([
map.layerPointToLatLng(line[0]),
map.layerPointToLatLng(line[1])
], { color: 'purple', weight: 2 }).addTo(map);
});
}
const centerLine = [[area1.lat, area1.lng], [area2.lat, area2.lng]];
L.polyline(centerLine, { color: 'red', weight: 1, dashArray: '5, 5' }).addTo(map);
}
}
// 強風域(従来通りの表示、変更なし)
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
if (dateTimeType !== '実況') return;
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
var lat = null;
var lng = null;
if (basePoints.length > 0) {
const latLngMatches = basePoints[0].textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (latLngMatches && latLngMatches.length >= 2) {
lat = parseFloat(latLngMatches[0]);
lng = parseFloat(latLngMatches[1]);
}
}
if (lat !== null && lng !== null) {
var strongWindAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '強風域');
if (strongWindAreaParts.length > 0) {
var strongWindAreaPart = strongWindAreaParts[0];
var axesElements = strongWindAreaPart.getElementsByTagName('*');
var radii = [];
for (var m = 0; m < axesElements.length; m++) {
if (axesElements[m].localName === 'Axis') {
var radiusKm = null;
var direction = null;
var axisChildren = axesElements[m].getElementsByTagName('*');
for (var n = 0; n < axisChildren.length; n++) {
if (axisChildren[n].localName === 'Direction') {
direction = axisChildren[n].textContent.trim();
}
if (axisChildren[n].localName === 'Radius' && axisChildren[n].getAttribute('unit') === 'km') {
radiusKm = parseFloat(axisChildren[n].textContent.trim());
}
}
if (radiusKm && direction) {
radii.push({ direction: direction, radiusKm: radiusKm });
}
}
}
if (radii.length > 0) {
var maxRadius = Math.max(...radii.map(r => r.radiusKm));
var circlePoints = getCirclePoints(lat, lng, maxRadius * 1000, 64);
L.polygon(circlePoints, {
color: 'yellow',
fillColor: 'yellow',
fillOpacity: 0.3,
interactive: false,
}).addTo(map);
}
}
}
});
} else {
document.getElementById('no-typhoon').style.display = 'block';
console.error("中心位置が見つかりませんでした。");
}
} else {
document.getElementById('no-typhoon').style.display = 'block';
console.error("XMLデータの取得に失敗しました。ステータスコード:", xhr.status);
}
};
xhr.onerror = function () {
document.getElementById('no-typhoon').style.display = 'block';
console.error("XMLデータの取得に失敗しました。");
};
xhr.send();
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) return 'N/A';
const date = new Date(dateTimeString);
if (isNaN(date)) return '無効な日付';
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);
const hours = ('0' + date.getHours()).slice(-2);
const minutes = ('0' + date.getMinutes()).slice(-2);
return `${month}月${day}日 ${hours}時${minutes}分`;
}
function calculateOuterTangents(p1, r1, p2, r2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const d = Math.hypot(dx, dy);
if (d <= Math.abs(r1 - r2)) {
return null;
}
if (d === 0 && r1 === r2) {
return null;
}
const vx = (p2.x - p1.x) / d;
const vy = (p2.y - p1.y) / d;
const c = (r1 - r2) / d;
const h = Math.sqrt(1 - c * c);
const nx1 = vx * c - h * vy;
const ny1 = vy * c + h * vx;
const nx2 = vx * c + h * vy;
const ny2 = vy * c - h * vx;
const startX1 = p1.x + r1 * nx1;
const startY1 = p1.y + r1 * ny1;
const endX1 = p2.x + r2 * nx1;
const endY1 = p2.y + r2 * ny1;
const startX2 = p1.x + r1 * nx2;
const startY2 = p1.y + r1 * ny2;
const endX2 = p2.x + r2 * nx2;
const endY2 = p2.y + r2 * ny2;
return [
[L.point(startX1, startY1), L.point(endX1, endY1)],
[L.point(startX2, startY2), L.point(endX2, endY2)]
];
}
function getCirclePoints(lat, lng, radius, segments) {
var points = [];
for (var i = 0; i < segments; i++) {
var angle = (i / segments) * 360;
var point = destinationPoint(lat, lng, angle, radius);
points.push([point.lat, point.lng]);
}
return points;
}
function destinationPoint(lat, lng, bearing, distance) {
var R = 6378137;
var δ = distance / R;
var θ = bearing * Math.PI / 180;
var φ1 = lat * Math.PI / 180;
var λ1 = lng * Math.PI / 180;
var φ2 = Math.asin(Math.sin(φ1) * Math.cos(δ) +
Math.cos(φ1) * Math.sin(δ) * Math.cos(θ));
var λ2 = λ1 + Math.atan2(Math.sin(θ) * Math.sin(δ) * Math.cos(φ1),
Math.cos(δ) - Math.sin(φ1) * Math.sin(φ2));
return {
lat: φ2 * 180 / Math.PI,
lng: λ2 * 180 / Math.PI
};
}
function convexHull(points) {
if (points.length < 3) return points;
var start = points.reduce((prev, curr) => {
return (prev[1] < curr[1] || (prev[1] === curr[1] && prev[0] < curr[0])) ? prev : curr;
});
points.sort((a, b) => {
var angleA = Math.atan2(a[1] - start[1], a[0] - start[0]);
var angleB = Math.atan2(b[1] - start[1], b[0] - start[0]);
return angleA - angleB;
});
var hull = [];
for (var i = 0; i < points.length; i++) {
while (hull.length >= 2 && crossProduct(hull[hull.length - 2], hull[hull.length - 1], points[i]) <= 0) {
hull.pop();
}
hull.push(points[i]);
}
return hull;
}
function crossProduct(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
</script>
長いコードになりましたが、一つずつ解説したいと思います。
1. XMLデータの取得
<script>
if (typeof xmlUrl !== 'undefined') {
var proxyUrl = 'proxy.php?url=' + encodeURIComponent(xmlUrl);
var xhr = new XMLHttpRequest();
xhr.open("GET", proxyUrl, true);
xhr.responseType = "document"; // XMLとして取得
</script>
- 目的:
xmlUrl
が定義されていれば、指定されたURLからXMLデータを取得します。 - proxyUrl: クロスドメイン制約を回避するために
proxy.php
を介してリクエストを送信。 XMLHttpRequest
: 非同期リクエストを送信するためのオブジェクト。responseType
: 応答をXML形式で取得。
クロスドメイン制約 (Cross-Origin Resource Sharing, CORS) とは?
概要
Webブラウザは、セキュリティ上の理由で、あるドメイン(例:https://example.com
)上のウェブページが、別のドメイン(例:https://api.another.com
)に直接リクエストを送ることを制限しています。この仕組みをクロスドメイン制約または**CORS(Cross-Origin Resource Sharing)**と言います。
🛑 なぜ制約が必要なのか?
クロスドメイン制約は、**「CSRF(クロスサイトリクエストフォージェリ)」**などの攻撃を防ぐためのセキュリティ対策です。
💥 CSRF攻撃の例
- あなたが銀行サイトにログインしている状態で、悪意のあるサイトを開く。
- そのサイトが、あなたの認証情報を使って銀行のAPIに不正なリクエストを送信。
- 結果、あなたの知らない間に銀行口座からお金が送金される。
👉 このような状況を防ぐために、CORS制約が導入されています。
🌐 クロスドメイン制約の基本ルール
- 同一オリジンポリシーに従うリクエストは許可される。
- 同一オリジンとは、以下の3つの要素がすべて同じ場合を指します。
- プロトコル(
http://
とhttps://
) - ドメイン(
example.com
) - ポート番号(
80
,443
)
- プロトコル(
- 同一オリジンとは、以下の3つの要素がすべて同じ場合を指します。
✅ 例:同一オリジン
- リクエスト元:
https://example.com
- API:
https://example.com/data
❌ 例:クロスオリジン
- リクエスト元:
https://example.com
- API:
https://api.another.com/data
🔗 CORSエラーの回避方法
1️⃣ サーバー側でCORSヘッダーを設定
サーバーがリクエスト元を許可するために、HTTPレスポンスヘッダーに以下を追加する必要があります。
httpコードをコピーするAccess-Control-Allow-Origin: https://example.com
2️⃣ プロキシサーバーを使う方法(あなたのコードの例)
もしサーバー側の設定を変更できない場合は、プロキシサーバーを使ってクロスドメイン制約を回避します。
🧩 プロキシサーバーとは?
プロキシサーバーは、クライアント(ブラウザ)と外部APIの間に立つ中継サーバーです。
🔧 あなたのコードのプロキシURL
var proxyUrl = 'proxy.php?url=' + encodeURIComponent(xmlUrl);
💡 動作の仕組み
- クライアントは、**自分のサーバー内の
proxy.php
**にリクエストを送る。 proxy.php
が、外部APIにリクエストを送信。- 取得したデータを、クライアントに返す。
👉 結果的に、クライアントからのリクエストは「同一オリジン」として扱われるため、CORS制約を回避できます。
🖥️ proxy.php の内容は、下記になります。
<?php
// リクエストされたURLを取得
$url = $_GET['url'];
// cURLを使って指定されたURLからデータを取得
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 必要に応じてSSL検証を無効にする
$response = curl_exec($ch);
curl_close($ch);
// レスポンスをそのままクライアントに返す
header('Content-Type: application/xml');
echo $response;
?>
📝 まとめ
項目 | 説明 |
---|---|
クロスドメイン制約 | 異なるドメイン間でのリクエストを制限するブラウザのセキュリティ機能 |
CORSエラー | サーバーがリクエスト元を許可していない場合に発生 |
プロキシサーバー | クライアントと外部APIの間に立つ中継サーバー |
回避方法 | サーバー側でCORSヘッダー設定、またはプロキシ使用 |
2. XMLデータの処理
<script>
xhr.onload = function () {
if (xhr.status === 200) {
var xmlDoc = xhr.responseXML;
console.log("取得したXMLデータ:", xmlDoc);
</script>
🧩 1. xhr.onload の役割
xhr.onload = function () {
✅ 解説
onload
は、リクエストが正常に完了したときに実行されるイベントハンドラです。- この関数は、サーバーからのレスポンスを受け取った後に呼び出されます。
🚦 主なポイント
onload
は、非同期リクエストが完了した時点で動作する。- サーバーが応答を返した場合に、この関数内でレスポンスの処理を行います。
🧩 2. if (xhr.status === 200) の役割
if (xhr.status === 200) {
✅ 解説
xhr.status
は、HTTPリクエストのステータスコードを取得します。- ステータスコード 200 は、リクエスト成功を意味します。
🚦 ステータスコードの例
ステータスコード | 説明 |
---|---|
200 | リクエスト成功 |
404 | リソースが見つからない |
500 | サーバーエラー |
💡 チェックの理由
- サーバーがリクエストに正常に応答した場合のみ、レスポンスを処理します。
- ステータスコードが 200以外 の場合は、エラーとして扱われます。
🧩 3. var xmlDoc = xhr.responseXML の役割
var xmlDoc = xhr.responseXML;
✅ 解説
xhr.responseXML
は、サーバーから取得したXML形式のデータを**Document
オブジェクト**として返します。xmlDoc
には、XMLデータがパース(解析)された状態で格納されます。
🚦 responseXML
と responseText
の違い
プロパティ | 説明 | データ形式 |
---|---|---|
responseText | レスポンスをテキスト形式で取得 | 文字列 |
responseXML | レスポンスをXML形式で取得 | Document オブジェクト |
📄 例: XMLデータ
サーバーから取得したXMLが以下のようなデータだとします。
<Weather>
<Location>Tokyo</Location>
<Temperature unit="Celsius">15</Temperature>
</Weather>
xhr.responseXML
を使うと、これをDOMツリーとして扱うことができます。
🛠️ XMLの操作例
var locationElement = xmlDoc.getElementsByTagName('Location')[0];
console.log(locationElement.textContent); // "Tokyo"
🧩 4. console.log("取得したXMLデータ:", xmlDoc) の役割
console.log("取得したXMLデータ:", xmlDoc);
✅ 解説
console.log
は、デバッグのためにデータをブラウザの開発者ツールのコンソールに出力します。xmlDoc
の内容を確認することで、XMLデータが正しく取得できたかをチェックします。
🚦 出力例
取得したXMLデータ: [object XMLDocument]
もし、XMLの内容を詳しく確認したい場合は、次のように出力することもできます。
console.log(new XMLSerializer().serializeToString(xmlDoc));
🔎 コードの流れのまとめ
処理の順序 | 説明 |
---|---|
xhr.onload | リクエスト完了後に実行されるイベントハンドラ |
if (xhr.status === 200) | ステータスコードが 200 の場合のみ処理を進める |
var xmlDoc = xhr.responseXML | レスポンスデータを XML形式 で取得 |
console.log | デバッグ用に、取得したXMLデータをコンソールに出力 |
🎯 全体の動作イメージ
- XMLHttpRequest でサーバーにリクエストを送る。
- サーバーが応答し、リクエストが完了すると
onload
が実行される。 - 応答のステータスコードが 200 の場合、XMLデータをパースし、
Document
オブジェクトとして取得。 - デバッグ用に
console.log
で取得したXMLデータを確認。
3. Leafletマップの初期化
<script>
const map = L.map('map').setView([30, 130], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }).addTo(map);
</script>
L.map
: Leafletで地図を初期化。setView
: 中心座標とズームレベルを設定。tileLayer
: OpenStreetMapのタイルを地図に追加。
4. XML解析関数①
<script>
function getElementsByTagNameWithoutNamespace(tagName, parentElement) {
var elements = (parentElement || xmlDoc).getElementsByTagName('*');
var result = [];
for (var i = 0; i < elements.length; i++) {
if (elements[i].localName === tagName) {
result.push(elements[i]);
}
}
return result;
}
</script>
このコードは、XMLデータの中から、名前空間(Namespace)を無視して特定のタグ名の要素を取得するためのカスタム関数です。
通常の getElementsByTagName()
では、名前空間付きのXMLを扱うと正確な要素の取得が難しくなるため、この関数は名前空間に依存しない柔軟な検索を可能にします。
📘 コードの詳細解説
💻 コードの目的
getElementsByTagNameWithoutNamespace(tagName, parentElement)
は、XMLドキュメント内の特定のタグ名を持つ要素を取得する。- 名前空間を無視してタグ名で検索するため、
localName
を使って比較する。
📂 1. 関数の定義
function getElementsByTagNameWithoutNamespace(tagName, parentElement) {
✅ 解説
- 関数名:
getElementsByTagNameWithoutNamespace
- 名前空間を無視してタグを取得するという意味。
- 引数
tagName
:検索したいタグ名(例:Location
、Pressure
)。parentElement
(オプション):検索の開始地点となる要素。- 指定しない場合、デフォルトで
xmlDoc
(XMLドキュメント全体)が使われます。
- 指定しない場合、デフォルトで
🧩 2. 要素を取得する処理
var elements = (parentElement || xmlDoc).getElementsByTagName('*');
✅ 解説
parentElement || xmlDoc
parentElement
が渡されていれば、それを使用。parentElement
がundefined
の場合は、xmlDoc
を使用。
getElementsByTagName('*')
'*'
を指定することで、すべての要素を取得します。
🛠️ この時点での例
取得した要素のリスト:
<Weather>
<Location>Tokyo</Location>
<Pressure unit="hPa">1015</Pressure>
</Weather>
この段階で取得される要素は、Weather
、Location
、Pressure
のすべてのタグです。
🔄 3. 結果のフィルタリング
var result = [];
for (var i = 0; i < elements.length; i++) {
if (elements[i].localName === tagName) {
result.push(elements[i]);
}
}
✅ 解説
elements[i].localName
localName
は、名前空間を除いたタグ名を返します。- 例えば、次のようなXMLにおいても、
localName
でタグ名を取得できます。
<ns:Location xmlns:ns="http://example.com">Tokyo</ns:Location>
- 通常の
getElementsByTagName('Location')
だと、<ns:Location>
という名前空間付きのタグを取得できませんが、localName
を使うと、Location
として認識できます。 result.push(elements[i])
- 一致する要素を
result
配列に追加します。
- 一致する要素を
📦 4. 結果を返す
return result;
✅ 解説
- 最終的に、タグ名が一致した要素の配列を返します。
🎨 動作の具体例
✅ XMLデータの例
<Weather xmlns="http://example.com">
<Location>Tokyo</Location>
<Pressure unit="hPa">1015</Pressure>
</Weather>
✅ 関数の呼び出し
var locationElements = getElementsByTagNameWithoutNamespace('Location');
console.log(locationElements[0].textContent); // "Tokyo"
🔎 getElementsByTagName vs localName の違い
方法 | 説明 | 名前空間付きタグ |
---|---|---|
getElementsByTagName('Location') | 名前空間を考慮して検索する | 取得できない |
localName === 'Location' | 名前空間を無視してタグ名だけで検索する | 取得できる |
📋 まとめ
コードの部分 | 説明 |
---|---|
`parentElement | |
getElementsByTagName('*') | すべての要素を取得する |
localName === tagName | 名前空間を無視してタグ名を比較する |
result.push(elements[i]) | 一致した要素を結果配列に追加する |
return result | 一致した要素の配列を返す |
この関数は、気象データXMLを扱う場面で便利です。名前空間を考慮しなくて済むため、異なる形式のXMLデータにも柔軟に対応できます!
4-1. XML解析関数②
<script>
function getElementsByTagNameAndAttribute(element, tagName, attrName, attrValue) {
var elements = [];
var allElements = element.getElementsByTagName('*');
for (var i = 0; i < allElements.length; i++) {
if (allElements[i].localName === tagName && (attrValue === null || allElements[i].getAttribute(attrName) === attrValue)) {
elements.push(allElements[i]);
}
}
return elements;
}
</script>
この関数 getElementsByTagNameAndAttribute
は、特定のタグ名と属性の条件に一致する要素を取得するカスタム関数です。
具体的には、XMLドキュメント内のすべての要素を検索し、以下の条件に一致する要素を配列として返します:
- タグ名(
tagName
)が一致 - 属性名(
attrName
)が一致 - 属性値(
attrValue
)が一致(属性値がnull
の場合は属性名があるだけでOK)
💻 コードの詳細な解説
📂 1. 関数の定義
function getElementsByTagNameAndAttribute(element, tagName, attrName, attrValue) {
✅ 解説
element
:検索の開始地点となるXML要素(ドキュメント全体でも特定の要素でも良い)。tagName
:検索対象のタグ名(例:WindSpeed
、Pressure
)。attrName
:検索対象の属性名(例:unit
、type
)。attrValue
:検索対象の属性値(例:m/s
、最大風速
)。null
の場合、属性名があるだけで一致とみなす。
🧩 2. 空の配列を用意
var elements = [];
✅ 解説
- 一致した要素を保存するための空の配列を用意します。
🔄 3. すべての要素を取得
var allElements = element.getElementsByTagName('*');
✅ 解説
getElementsByTagName('*')
で、指定したelement
の中にあるすべての子要素を取得します。
🧮 4. ループで各要素をチェック
for (var i = 0; i < allElements.length; i++) {
✅ 解説
- ループを使って、すべての要素を1つずつチェックします。
🔍 5. 条件の確認
if (
allElements[i].localName === tagName &&
(attrValue === null || allElements[i].getAttribute(attrName) === attrValue)
) {
✅ 条件の解説
allElements[i].localName === tagName
- 名前空間を無視して、タグ名が一致するかを確認します。
allElements[i].getAttribute(attrName) === attrValue
- 属性名(
attrName
) に対応する属性値がattrValue
と一致するかを確認します。
- 属性名(
attrValue === null
の場合は、属性名が存在するだけでOK です。
📦 6. 一致した要素を配列に追加
elements.push(allElements[i]);
✅ 解説
- 条件を満たした要素を、
elements
配列に追加します。
📤 7. 配列を返す
return elements;
✅ 解説
- 最終的に、条件に一致した要素の配列を関数の戻り値として返します。
🖼️ 動作の具体例
✅ XMLデータの例
<Weather>
<WindSpeed unit="m/s" type="最大風速">30</WindSpeed>
<WindSpeed unit="km/h" type="最大瞬間風速">108</WindSpeed>
<Pressure unit="hPa">1015</Pressure>
</Weather>
✅ 関数の呼び出し例
var elements = getElementsByTagNameAndAttribute(xmlDoc, 'WindSpeed', 'unit', 'm/s');
✅ 関数の流れ
- タグ名が
WindSpeed
- 属性名が
unit
- 属性値が
m/s
インデックス | 要素 | localName | unit 属性値 | 判定 |
---|---|---|---|---|
0 | <WindSpeed unit="m/s" type="最大風速">30</WindSpeed> | WindSpeed | m/s | ✅ 一致 |
1 | <WindSpeed unit="km/h" type="最大瞬間風速">108</WindSpeed> | WindSpeed | km/h | ❌ 不一致 |
2 | <Pressure unit="hPa">1015</Pressure> | Pressure | hPa | ❌ 不一致 |
✅ 取得結果
[
<WindSpeed unit="m/s" type="最大風速">30</WindSpeed>
]
📝 ポイント
コード部分 | 説明 |
---|---|
allElements[i].localName === tagName | 名前空間を無視してタグ名を比較する |
allElements[i].getAttribute(attrName) | 属性値を取得する |
`(attrValue === null | |
elements.push(allElements[i]) | 条件を満たした要素を結果配列に追加する |
🎯 関数の用途
- XMLデータの中で、特定のタグ名と属性条件に一致する要素を効率的に取得する。
- 特に気象データの解析で、**風速(最大風速、最大瞬間風速)**などの属性条件を使って検索したいときに便利。
5.予報時間のマッピング処理
<script>
const targetTypes = ['実況', '予報 6時間後', '予報 12時間後', '予報 24時間後'];
const forecastMapping = [
{ primary: '予報 48時間後', fallback: '予報 45時間後' },
{ primary: '予報 72時間後', fallback: '予報 69時間後' },
{ primary: '予報 96時間後', fallback: '予報 93時間後' },
{ primary: '予報 120時間後', fallback: '予報 117時間後' }
];
</script>
このコードは 気象データの特定の予報時間(タイムステップ) を対象にした配列を定義しており、予報データの取得や解析時に 主要な予報時間 と、代替の予報時間 を扱うためのマッピングを作成しています。
主に 気象予報モデルの時間解像度 に対応するデータを効率的に取得するためのものです。
📚 コードの解説
1️⃣ targetTypes の解説
const targetTypes = ['実況', '予報 6時間後', '予報 12時間後', '予報 24時間後'];
✅ 説明
targetTypes
は、取得したい気象データの 主な時間ポイント をリストで指定しています。
要素 | 意味 |
---|---|
'実況' | 現在の観測データ(実況データ) |
'予報 6時間後' | 6時間後の予報データ |
'予報 12時間後' | 12時間後の予報データ |
'予報 24時間後' | 24時間後の予報データ |
2️⃣ forecastMapping の解説
const forecastMapping = [
{ primary: '予報 48時間後', fallback: '予報 45時間後' },
{ primary: '予報 72時間後', fallback: '予報 69時間後' },
{ primary: '予報 96時間後', fallback: '予報 93時間後' },
{ primary: '予報 120時間後', fallback: '予報 117時間後' }
];
✅ 説明
forecastMapping
は、特定の 予報時間のマッピング を表しています。- 各オブジェクトは、2つのプロパティを持っています:
primary
:通常取得したい予報時間。fallback
:primary
の予報が見つからない場合の代替時間。
プロパティ | 意味 |
---|---|
primary | 主に取得したい予報時間。 |
fallback | primary が見つからないときの代替予報時間。 |
🔄 代替処理が必要な理由
気象データは必ずしも正確に提供されるとは限りません。
たとえば、通常は 48時間後の予報 が欲しいけれど、その時間帯のデータがない場合、45時間後の予報 を使いたい、という状況があります。
このマッピングは、そうした データ欠損時のリカバリ処理 をサポートします。
🖼 具体例
✅ XMLデータの例
<DateTime type="予報 45時間後">2025-01-10T15:00:00Z</DateTime>
✅ コードの流れ
- 48時間後の予報を探します(
primary
)。 - 48時間後の予報が見つからなければ、45時間後の予報(
fallback
)を探します。
🧩 動作イメージ
予報時間 | 取得すべき予報時間 |
---|---|
予報 48時間後 | 使用する(primary ) |
予報 45時間後 | 代替として使用(fallback ) |
📌 ポイント
targetTypes
は短期的な予報データを対象。forecastMapping
は中長期的な予報データを対象で、欠損時の代替処理が組み込まれています。
🧠 このコードの用途
- 気象データ解析アプリで、時間ごとの予報データを取得し、データ欠損を回避する仕組みを実装するためのものです。
例えば、台風や天気予報のアプリで、指定した時間のデータが存在しない場合に、少しずれた時間のデータを代わりに使うことで、アプリの安定性を向上させます。
5. XMLデータから気象情報を抽出
<script>
const Pressure = pressureElement ? pressureElement.textContent.trim() : "N/A";
const Location = LocationElement ? LocationElement.textContent.trim() : "N/A";
const Direction = DirectionElement ? DirectionElement.textContent.trim() : "N/A";
const dateTimeMap = {};
const dateTimeEls = getElementsByTagNameWithoutNamespace('DateTime');
dateTimeEls.forEach(function(element) {
const type = element.getAttribute('type');
const dateTime = element.textContent.trim();
dateTimeMap[type] = formatDateTime(dateTime);
});
const jikkyoDateTime = dateTimeMap['実況'] || 'N/A';
const centerCoordinateElements = getElementsByTagNameWithoutNamespace('Coordinate');
if (centerCoordinateElements.length > 0) {
const firstCoordinate = centerCoordinateElements[0].textContent.trim();
const coordinateParts = firstCoordinate.split('+');
if (coordinateParts.length < 3) {
console.error("座標データの形式が正しくありません。");
document.getElementById('no-typhoon').style.display = 'block';
return;
}
const firstLat = parseFloat(coordinateParts[1]);
const firstLon = parseFloat(coordinateParts[2]);
if (isNaN(firstLat) || isNaN(firstLon)) {
console.error("座標データの解析に失敗しました。");
document.getElementById('no-typhoon').style.display = 'block';
return;
}
L.marker([firstLat, firstLon]).addTo(map).bindPopup(`
<b>現在の台風の中心位置:</b> ${firstLat}°, ${firstLon}°<br>
<b>存在地域:</b> ${Location} <br>
<b>中心気圧:</b> ${Pressure} hPa<br>
<b>最大風速:</b> ${maxWindSpeed} m/s<br>
<b>最大瞬間風速:</b> ${maximumgustSpeed} m/s<br>
<b>移動方向:</b> ${Direction} <br>
<b>速さ:</b> ${movingSpeed} <br>
<b>情報:</b> ${'実況: ' + jikkyoDateTime}
`).openPopup();
</script>
1️⃣ 気象データの抽出と整形
📌 コード
const Pressure = pressureElement ? pressureElement.textContent.trim() : "N/A";
const Location = LocationElement ? LocationElement.textContent.trim() : "N/A";
const Direction = DirectionElement ? DirectionElement.textContent.trim() : "N/A";
✅ 解説
pressureElement
、LocationElement
、DirectionElement
は事前に抽出されたXMLの要素。- 三項演算子を使って値を取得し、要素が存在しない場合は "N/A"(該当なし) を設定しています。
変数 | 内容 |
---|---|
Pressure | 台風の中心気圧(hPa単位) |
Location | 台風の存在地域 |
Direction | 台風の移動方向 |
✅ .trim() メソッドの詳細解説
📌 コード
const Pressure = pressureElement ? pressureElement.textContent.trim() : "N/A";
この中にある .trim()
は、文字列の前後の空白や改行を取り除くための JavaScriptの文字列メソッドです。
🔍 .trim()の機能
trim()
は、文字列の先頭と末尾の以下の不要な文字を削除します:- 空白(スペース)
- 改行(\n)
- タブ文字(\t)
- 復帰文字(\r)
🌟 なぜ使うのか?
- XMLデータやAPIレスポンスで取得した文字列には、意図しない空白や改行が含まれることがよくあります。
.textContent
で取得したテキストには、こういった不要な空白が含まれる場合があるため、.trim()
を使ってデータを整形します。
🧪 例:.trim()の動作確認
🎯 空白がある場合
const text = " 1013 hPa ";
console.log(text); // " 1013 hPa "
console.log(text.trim()); // "1013 hPa"
🎯 改行やタブがある場合
const text = "\n\t1013 hPa\n";
console.log(text); // "\n\t1013 hPa\n"
console.log(text.trim()); // "1013 hPa"
❓ .trim() を使わないと何が起きる?
- 不要な空白や改行が含まれたままのデータを扱うことになります。
- これにより、比較処理やデータの表示に不具合が発生する場合があります。
⚠️ 問題の例
const pressureValue = pressureElement.textContent; // " 1013 hPa "
if (pressureValue === "1013 hPa") {
console.log("一致しました!");
} else {
console.log("一致しません!"); // ❌ 一致しない
}
✅ .trim() を使った正しい比較
const pressureValue = pressureElement.textContent.trim(); // "1013 hPa"
if (pressureValue === "1013 hPa") {
console.log("一致しました!"); // ✅ 一致する
}
🧩 まとめ
メソッド | 役割 |
---|---|
.textContent | 要素のテキストを取得 |
.trim() | 文字列の前後の空白・改行・タブ文字を削除 |
.trim()
を使うことで、データの前後の不要な空白を取り除き、正確な値を取得できます。
XMLデータのように整形が必要なデータを扱う際にとても重要なメソッドです。
✅ ?(三項演算子)の詳細解説
📌 コード
const Pressure = pressureElement ? pressureElement.textContent.trim() : "N/A";
この中にある ?
は、三項演算子(Ternary Operator) と呼ばれる、簡略化した条件分岐の記法です。
🌟 三項演算子の基本構文
条件式 ? 真の場合の値 : 偽の場合の値
意味:
条件式
がtrue
の場合 →?
の後ろの値が返される条件式
がfalse
の場合 →:
の後ろの値が返される
🧩 このコードの意味
const Pressure = pressureElement ? pressureElement.textContent.trim() : "N/A";
✅ 解釈
pressureElement
が存在する場合(true)- →
pressureElement.textContent.trim()
の結果をPressure
に代入する
- →
pressureElement
が存在しない場合(false)- →
"N/A"
をPressure
に代入する
- →
💻 三項演算子を使った具体例
🎯 例1:天気の温度を表示する
const temperature = 25;
const weatherMessage = temperature > 30 ? "暑いです!" : "快適です!";
console.log(weatherMessage); // "快適です!"
🎯 例2:ユーザー名の表示
const userName = null;
const displayName = userName ? userName : "ゲスト";
console.log(displayName); // "ゲスト"
⚡ 三項演算子のメリット
if-else
文を短く簡潔に書ける- コードの可読性が向上する(短い条件分岐で特に効果的)
❓ if-else文と比較
🖋️ if-else文で書く場合
let Pressure;
if (pressureElement) {
Pressure = pressureElement.textContent.trim();
} else {
Pressure = "N/A";
}
✂️ 三項演算子で書く場合
const Pressure = pressureElement ? pressureElement.textContent.trim() : "N/A";
🔎 まとめ
構文 | 意味 |
---|---|
条件式 ? 真の場合の値 : 偽の場合の値 | 条件式が true なら左側、false なら右側を返す |
このコードは pressureElement
の有無に応じて適切な値を設定するための便利な方法です。
2️⃣ 日付データのマッピング
📌 コード
const dateTimeMap = {};
const dateTimeEls = getElementsByTagNameWithoutNamespace('DateTime');
dateTimeEls.forEach(function(element) {
const type = element.getAttribute('type');
const dateTime = element.textContent.trim();
dateTimeMap[type] = formatDateTime(dateTime);
});
const jikkyoDateTime = dateTimeMap['実況'] || 'N/A';
function formatDateTime(dateTimeString) {
if (!dateTimeString) return 'N/A';
const date = new Date(dateTimeString);
if (isNaN(date)) return '無効な日付';
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);
const hours = ('0' + date.getHours()).slice(-2);
const minutes = ('0' + date.getMinutes()).slice(-2);
return `${month}月${day}日 ${hours}時${minutes}分`;
}
このコードは、XMLデータに含まれる日時情報を整理し、**「月日 時間」**の形式にフォーマットする処理を行っています。詳細に解説していきます。
🔍 コード全体の流れ
- XMLから
<DateTime>
要素を取得し、その中のtype
属性と 日時データ をdateTimeMap
というオブジェクトに格納します。 formatDateTime()
関数で、日時データを 「MM月DD日 HH時MM分」 の形式に変換します。実況
の日時情報を取得し、なければN/A
を代入します。
📚 コード詳細解説
🧩 1. dateTimeMap の作成
const dateTimeMap = {};
const dateTimeEls = getElementsByTagNameWithoutNamespace('DateTime');
dateTimeEls.forEach(function(element) {
const type = element.getAttribute('type');
const dateTime = element.textContent.trim();
dateTimeMap[type] = formatDateTime(dateTime);
});
🔎 解説
getElementsByTagNameWithoutNamespace('DateTime')
- XMLのすべての
<DateTime>
要素を取得します。
- XMLのすべての
forEach()
ループで各要素を処理- 取得した
<DateTime>
要素ごとに以下を行います:type
属性を取得(例:実況
、予報 6時間後
など)- 日時データを取得し、空白を削除(
textContent.trim()
)
- 取得した
formatDateTime()
を呼び出し、日付を整形- 整形された日付を、
type
属性をキーとしてdateTimeMap
に格納します。
- 整形された日付を、
🧩 2. formatDateTime() 関数の詳細
function formatDateTime(dateTimeString) {
if (!dateTimeString) return 'N/A';
const date = new Date(dateTimeString);
if (isNaN(date)) return '無効な日付';
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);
const hours = ('0' + date.getHours()).slice(-2);
const minutes = ('0' + date.getMinutes()).slice(-2);
return `${month}月${day}日 ${hours}時${minutes}分`;
}
🔎 解説
この関数は、ISO 8601形式の日時文字列を受け取り、**「MM月DD日 HH時MM分」**の日本語形式に変換します。
🧪 処理のステップ
if (!dateTimeString) return 'N/A';
- 空の日時文字列が渡された場合、
'N/A'
を返します。
- 空の日時文字列が渡された場合、
const date = new Date(dateTimeString);
- 日時文字列を
Date
オブジェクトに変換します。
- 日時文字列を
if (isNaN(date)) return '無効な日付';
- 無効な日付であれば、
'無効な日付'
を返します。
- 無効な日付であれば、
🕒 日時のフォーマット処理
getMonth()
- 月を取得します。JavaScriptでは 月は0から始まるため、
+1
をします。 - 例:
0
→ 1月、1
→ 2月
- 月を取得します。JavaScriptでは 月は0から始まるため、
('0' + (date.getMonth() + 1)).slice(-2)
- 月を2桁にフォーマットします。
- 例:
3
→03
getDate()
、getHours()
、getMinutes()
- 日、時、分を取得し、2桁にフォーマットします。
🧩 3. jikkyoDateTime の取得
const jikkyoDateTime = dateTimeMap['実況'] || 'N/A';
🔎 解説
dateTimeMap['実況']
から、実況の日時を取得します。- 実況の日時が存在しない場合は、
'N/A'
を代入します。
✅ 具体例
📄 XMLの例
<DateTime type="実況">2025-01-03T12:00:00+09:00</DateTime>
<DateTime type="予報 6時間後">2025-01-03T18:00:00+09:00</DateTime>
📊 処理結果
dateTimeMap = {
"実況": "01月03日 12時00分",
"予報 6時間後": "01月03日 18時00分"
};
const jikkyoDateTime = dateTimeMap['実況']; // "01月03日 12時00分"
🧩 コードの全体の目的
- XMLデータから
<DateTime>
要素を取得 - 各日時を 整形して
dateTimeMap
に格納 - 実況の日時情報を取得して、「01月03日 12時00分」の形式で表示する
🔔 ポイントまとめ
コード部分 | 説明 |
---|---|
getElementsByTagNameWithoutNamespace('DateTime') | XMLの DateTime 要素を取得 |
formatDateTime(dateTime) | 日時を「MM月DD日 HH時MM分」の形式に変換 |
dateTimeMap[type] = formatDateTime(dateTime) | 日時を dateTimeMap に保存 |
`jikkyoDateTime = dateTimeMap['実況'] |
3️⃣ 座標データの解析
📌 コード
const centerCoordinateElements = getElementsByTagNameWithoutNamespace('Coordinate');
if (centerCoordinateElements.length > 0) {
const firstCoordinate = centerCoordinateElements[0].textContent.trim();
const coordinateParts = firstCoordinate.split('+');
if (coordinateParts.length < 3) {
console.error("座標データの形式が正しくありません。");
document.getElementById('no-typhoon').style.display = 'block';
return;
}
const firstLat = parseFloat(coordinateParts[1]);
const firstLon = parseFloat(coordinateParts[2]);
if (isNaN(firstLat) || isNaN(firstLon)) {
console.error("座標データの解析に失敗しました。");
document.getElementById('no-typhoon').style.display = 'block';
return;
}
このコードは、XMLデータから台風の中心座標情報を取得し、緯度・経度を解析する処理を行っています。座標データの形式チェックやエラー処理も含まれており、正しい座標情報を取得できるかどうかを確認しています。
🔍 コードの流れ
<Coordinate>
要素を取得- 取得した座標データを 文字列としてトリミング
- 座標データを
+
記号で分割し、緯度・経度の部分を抽出 - 形式チェックを行い、誤ったデータがあればエラーを表示
- 緯度・経度を数値に変換し、解析結果を確認
📚 詳細解説
🧩 1. <Coordinate> 要素の取得
const centerCoordinateElements = getElementsByTagNameWithoutNamespace('Coordinate');
🔎 解説
- XMLデータの
<Coordinate>
要素を取得します。 getElementsByTagNameWithoutNamespace('Coordinate')
は、名前空間なしでタグ名がCoordinate
のすべての要素を返します。
🧩 2. 座標データの最初の要素を取得
if (centerCoordinateElements.length > 0) {
const firstCoordinate = centerCoordinateElements[0].textContent.trim();
🔎 解説
if (centerCoordinateElements.length > 0)
- 取得した要素が 1つ以上ある場合に処理を続行します。
- 要素がない場合は、処理をスキップします。
centerCoordinateElements[0].textContent.trim()
- 最初の
<Coordinate>
要素のテキスト内容を取得し、前後の空白を削除します。
- 最初の
🧩 3. 座標データの分割
const coordinateParts = firstCoordinate.split('+');
🔎 解説
firstCoordinate.split('+')
- 座標データを
+
記号で分割します。 - 分割後は、配列
coordinateParts
に格納されます。
- 座標データを
🗺️ 座標データの例
N123.45+E123.45+S123.45
📊 分割結果の例
coordinateParts = ["N123.45", "E123.45", "S123.45"];
🧩 4. 座標データの形式チェック
if (coordinateParts.length < 3) {
console.error("座標データの形式が正しくありません。");
document.getElementById('no-typhoon').style.display = 'block';
return;
}
🔎 解説
- 座標データは 少なくとも3つの部分に分割されている必要があります。
- 例:
N123.45+E123.45+S123.45
- 例:
if (coordinateParts.length < 3)
- 分割した配列の要素数が3未満の場合、形式エラーとして処理を中断します。
console.error()
- エラーをコンソールに表示します。
document.getElementById('no-typhoon').style.display = 'block';
- エラーがあった場合は、画面上の 「台風情報なし」のメッセージを表示します。
return;
- エラー時に関数の実行を中断します。
🧩 5. 緯度・経度の抽出と変換
const firstLat = parseFloat(coordinateParts[1]);
const firstLon = parseFloat(coordinateParts[2]);
🔎 解説
parseFloat()
- 文字列を 浮動小数点数(小数) に変換します。
- 例:
"123.45"
→123.45
coordinateParts[1]
- 緯度(Latitude) の値を取得します。
coordinateParts[2]
- 経度(Longitude) の値を取得します。
🧩 6. 緯度・経度のチェック
if (isNaN(firstLat) || isNaN(firstLon)) {
console.error("座標データの解析に失敗しました。");
document.getElementById('no-typhoon').style.display = 'block';
return;
}
🔎 解説
isNaN()
- 値が 数値ではない場合、
true
を返します。
- 値が 数値ではない場合、
if (isNaN(firstLat) || isNaN(firstLon))
- 緯度・経度のいずれかが無効な数値の場合、エラー処理を行います。
- エラー処理の流れ
- エラーメッセージをコンソールに出力
- 「台風情報なし」のメッセージを画面に表示
- 処理を 中断(
return
)
✅ 全体の動作まとめ
処理ステップ | 内容 |
---|---|
<Coordinate> 要素を取得 | XMLから座標データを取得する |
テキストをトリム | 前後の空白を削除する |
+ で分割 | 座標データを + 記号で分割 |
形式チェック | 3つ以上の要素があることを確認 |
緯度・経度を抽出 | 緯度・経度の数値を取得 |
数値チェック | 緯度・経度が 有効な数値であるか確認 |
エラー処理 | 無効なデータがある場合は エラーメッセージを表示し、処理を中断 |
🧪 実際の例
📄 XMLの例
<Coordinate>N123.45+E123.45+S123.45</Coordinate>
📊 処理結果
項目 | 値 |
---|---|
緯度(Latitude) | 123.45 |
経度(Longitude) | 123.45 |
🛠️ エラー処理の例
座標データ | エラー内容 |
---|---|
N123.45+E123.45 | 形式エラー:「座標データの形式が正しくありません」 |
N123.45+XYZ | 解析エラー:「座標データの解析に失敗しました」 |
🎯 ポイントまとめ
- 座標データの解析
- 緯度・経度を取得し、数値として扱う。
- エラー処理
- 座標データが正しい形式か確認し、エラー時には画面表示を変更する。
このコードは、台風の中心位置を地図上に表示するための準備を行っている重要な処理です。
4️⃣ Leafletマップにマーカーを追加
📌 コード
L.marker([firstLat, firstLon]).addTo(map).bindPopup(`
<b>現在の台風の中心位置:</b> ${firstLat}°, ${firstLon}°<br>
<b>存在地域:</b> ${Location} <br>
<b>中心気圧:</b> ${Pressure} hPa<br>
<b>最大風速:</b> ${maxWindSpeed} m/s<br>
<b>最大瞬間風速:</b> ${maximumgustSpeed} m/s<br>
<b>移動方向:</b> ${Direction} <br>
<b>速さ:</b> ${movingSpeed} <br>
<b>情報:</b> ${'実況: ' + jikkyoDateTime}
`).openPopup();
L.marker([firstLat, firstLon], {
icon: L.divIcon({
className: 'forecast-label',
html: '実況<br>' + jikkyoDateTime,
iconSize: [110, 35],
iconAnchor: [25, -10]
}),
interactive: false
}).addTo(map);
このコードは、台風の 緯度・経度データ を地図上に マーカーとして表示し、情報をポップアップ表示 する処理です。また、マーカーには 実況日時ラベル も表示されています。
🧩 ライブラリについて:Leaflet.js
- このコードは Leaflet.js を使って地図を操作しています。
L.marker()
:地図上に マーカー(ピン) を表示するための関数。addTo(map)
:指定したmap
オブジェクト にマーカーを追加します。bindPopup()
:マーカーをクリックした際に 情報をポップアップ で表示します。
📚 コードの詳細解説
🧩 1. 台風の中心位置を地図にマーカーとして表示
L.marker([firstLat, firstLon])
.addTo(map)
.bindPopup(`
<b>現在の台風の中心位置:</b> ${firstLat}°, ${firstLon}°<br>
<b>存在地域:</b> ${Location} <br>
<b>中心気圧:</b> ${Pressure} hPa<br>
<b>最大風速:</b> ${maxWindSpeed} m/s<br>
<b>最大瞬間風速:</b> ${maximumgustSpeed} m/s<br>
<b>移動方向:</b> ${Direction} <br>
<b>速さ:</b> ${movingSpeed} <br>
<b>情報:</b> ${'実況: ' + jikkyoDateTime}
`).openPopup();
🔎 解説
L.marker([firstLat, firstLon])
- 緯度
firstLat
、経度firstLon
の位置にマーカーを作成。 - 例:
L.marker([35.6895, 139.6917])
→ 東京都の位置にマーカーを配置。
- 緯度
addTo(map)
- 作成したマーカーを指定した
map
オブジェクト に追加。 - ここで
map
は、Leaflet.js で初期化された地図。
- 作成したマーカーを指定した
bindPopup()
- マーカーをクリックすると 情報がポップアップ で表示される。
- ポップアップ内には、台風の情報(中心位置、存在地域、中心気圧、最大風速など)が表示される。
📝 テンプレートリテラル
- バッククォート(``) を使って文字列内に変数を埋め込んでいます。
- 例:
${firstLat}
→ 変数firstLat
の値を埋め込む。
- 例:
📋 ポップアップで表示する情報
項目 | 値の例 |
---|---|
現在の台風の中心位置 | 35.6895°, 139.6917° |
存在地域 | 日本近海 |
中心気圧 | 950 hPa |
最大風速 | 40 m/s |
最大瞬間風速 | 60 m/s |
移動方向 | 北北東 |
速さ | 20 km/h |
情報 | 実況:2025年1月3日15時00分 |
openPopup()
- 地図が表示された際に ポップアップを自動で開く。
🧩 2. マーカーに「実況」ラベルを表示
L.marker([firstLat, firstLon], {
icon: L.divIcon({
className: 'forecast-label',
html: '実況<br>' + jikkyoDateTime,
iconSize: [110, 35],
iconAnchor: [25, -10]
}),
interactive: false
}).addTo(map);
🔎 解説
この部分では、マーカーに 「実況」ラベル を表示しています。
L.divIcon()
- HTML要素を使ったカスタムアイコンを作成します。
className: 'forecast-label'
forecast-label
というクラス名をアイコンに適用します。- これにより、CSSを使って見た目をカスタマイズ可能です。
html: '実況<br>' + jikkyoDateTime
- マーカーの中に 「実況」ラベル と、実況日時 を表示します。
- 例:
実況<br>2025年1月3日15時00分
iconSize: [110, 35]
- アイコンの サイズ を設定。
- 横幅 110px、高さ 35px に設定。
iconAnchor: [25, -10]
- アイコンの位置を調整します。
- アンカーポイントを [25px, -10px] に設定。
interactive: false
- インタラクティブ性を無効化します。
- ユーザーがマウスでラベルをクリックしても反応しない設定。
addTo(map)
- 作成したマーカーを地図に追加します。
💻 HTMLとCSSの役割
HTML内の要素
<div id="map"></div>
:地図を表示するための要素。
CSSスタイル
forecast-label
クラスを使って、実況ラベルの見た目をカスタマイズ。- 例:
.forecast-label {
font-size: 12px;
font-weight: bold;
background: rgba(255,255,255,0.8);
padding: 2px 4px;
border-radius: 3px;
border: 1px solid #ccc;
}
🧪 実際の表示例
マーカーの種類 | 表示内容 |
---|---|
通常マーカー | 台風の中心位置、中心気圧、最大風速などの詳細情報をポップアップで表示 |
実況ラベル | 実況日時(例:実況 2025年1月3日15時00分 ) |
🎯 ポイントまとめ
L.marker()
:地図上に マーカー を表示する。bindPopup()
:マーカーをクリックした際に ポップアップ で情報を表示。L.divIcon()
:HTMLを使って カスタムラベル を表示。- エラー処理が行われている場合は、地図にラベルやマーカーが表示されないことも考慮。
🧩 まとめ
このコードは、次のような処理を行っています:
- XMLデータから台風情報を抽出。
- 欠損データに対処し、適切な値を設定。
- 台風の中心位置にマーカーを配置し、情報をポップアップで表示。
- エラー処理も行い、座標データが不正な場合は適切なエラーメッセージを出力。
5️⃣台風情報の予報時間を数値(時間単位) として返す関数
<script>
function getForecastHour(typeString) {
if (typeString === "実況") {
return 0;
}
const match = typeString.match(/予報\s*(\d+)時間後/);
if (match && match[1]) {
return parseInt(match[1], 10);
}
return null;
}
</script>
このコードは、台風情報の予報時間を特定の文字列から抽出し、その時間を 数値(時間単位) として返す関数です。
例えば、"予報 12時間後" の文字列から 12 を取り出して数値として返します。また、"実況" という文字列の場合は、 0 を返します。
🧩 コード全体の説明
💡 関数名:getForecastHour(typeString)
この関数は、予報の種類を表す文字列(typeString) を受け取り、その中に含まれる時間情報を抽出します。
📋 コード詳細
function getForecastHour(typeString) {
getForecastHour
は予報の種類を受け取り、時間数を返す関数。typeString
は予報の種類を表す文字列(例:"実況"、"予報 6時間後"、"予報 24時間後")。
🔹 1. "実況" の場合は 0 を返す
if (typeString === "実況") {
return 0;
}
- 予報の種類が "実況" なら、0 を返します。
- 理由:「実況」とは現在の観測データを意味し、時間の経過を表さないため、予報時間は 0時間後 となります。
🔹 2. 正規表現で "予報 ◯時間後" の時間を抽出
const match = typeString.match(/予報\s*(\d+)時間後/);
🔍 正規表現解説:/予報\s*(\d+)時間後/
予報
:文字列が 「予報」 から始まることを指定。\s*
:空白文字(スペースやタブ) が0個以上ある場合に一致。(\d+)
:1つ以上の数字 に一致し、この数字をキャプチャ(抽出)します。時間後
:文字列が 「時間後」 で終わることを指定。
✅ マッチする文字列の例
文字列 | マッチする部分 | 抽出される数字 |
---|---|---|
"予報 6時間後" | 予報 6時間後 | 6 |
"予報12時間後" | 予報12時間後 | 12 |
"予報 48時間後" | 予報 48時間後 | 48 |
🔹 3. 時間が抽出できた場合、その数値を返す
if (match && match[1]) {
return parseInt(match[1], 10);
}
🔍 処理の流れ
match
は正規表現の結果を格納。- マッチした場合は配列が返り、マッチしなかった場合は
null
が返る。
- マッチした場合は配列が返り、マッチしなかった場合は
match[1]
に抽出された 時間の数字 が格納されている。parseInt()
を使って、文字列として抽出された数字を 整数に変換 します。parseInt(match[1], 10)
は、抽出された文字列を 10進数 の数値に変換。
🔹 4. 抽出できなかった場合は null
を返す
return null;
- 正規表現でマッチしなかった 場合、予報時間が不明なので
null
を返します。
🧪 処理の動作例
入力文字列 | 戻り値 |
---|---|
"実況" | 0 |
"予報 6時間後" | 6 |
"予報12時間後" | 12 |
"予報 48時間後" | 48 |
"その他の文字列" | null |
💻 実際の利用例
console.log(getForecastHour("実況")); // 0
console.log(getForecastHour("予報 6時間後")); // 6
console.log(getForecastHour("予報12時間後")); // 12
console.log(getForecastHour("予報 48時間後")); // 48
console.log(getForecastHour("不明な文字列")); // null
📌 ポイントまとめ
処理内容 | 説明 |
---|---|
"実況" の場合 | 0 を返す。実況データは「現在時刻」を意味するため。 |
"予報 ◯時間後" の場合 | 正規表現で時間を抽出し、数値に変換して返す。 |
その他の文字列 | null を返す。予報時間が特定できない場合。 |
🎯 この関数の役割
- 台風予報の時間情報を解析し、数値の時間 として扱えるようにする。
- 正規表現を使って柔軟に時間を抽出。
- "実況" といった特殊ケースも適切に処理。
6. 台風情報(予報円や座標データ)を取得して地図上に可視化
<script>
var meteorologicalInfos = getElementsByTagNameWithoutNamespace('MeteorologicalInfo');
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
var isTargetType = targetTypes.includes(dateTimeType);
var isForecastType = forecastMapping.some(mapping => mapping.primary === dateTimeType || mapping.fallback === dateTimeType);
if (!isTargetType && !isForecastType) {
return;
}
var forecastHour = getForecastHour(dateTimeType);
var dateTimeText = dateTimeElement.textContent.trim();
var formattedDateTime = formatDateTime(dateTimeText);
var circleElements = getElementsByTagNameWithoutNamespace('Circle', meteorologicalInfo);
circleElements.forEach(function(circle) {
var typeAttr = circle.getAttribute('type');
if (typeAttr !== "予報円") return;
var basePointElements = circle.getElementsByTagName('*');
var basePointElement = null;
for (var i = 0; i < basePointElements.length; i++) {
if (basePointElements[i].localName === 'BasePoint') {
basePointElement = basePointElements[i];
break;
}
}
var radiusElements = circle.getElementsByTagName('*');
var radiusElement = null;
for (var i = 0; i < radiusElements.length; i++) {
if (radiusElements[i].localName === 'Radius' && radiusElements[i].getAttribute('unit') === 'km') {
radiusElement = radiusElements[i];
break;
}
}
if (basePointElement && radiusElement) {
const latLngMatches = basePointElement.textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (!latLngMatches || latLngMatches.length < 2) {
console.warn("予報円の座標解析に失敗しました。");
return;
}
const lat = parseFloat(latLngMatches[0]);
const lng = parseFloat(latLngMatches[1]);
const radiusKm = parseFloat(radiusElement.textContent);
if (isNaN(lat) || isNaN(lng) || isNaN(radiusKm)) {
console.warn("予報円のデータ解析に失敗しました。");
return;
}
const circlePoints = getCirclePoints(lat, lng, radiusKm * 1000, 64);
const circleLayer = L.polygon(circlePoints, {
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.3,
dashArray: '5,5'
}).addTo(map);
circleLayer.bringToFront();
const labelText = dateTimeType + '
' + formattedDateTime;
circleLayer.bindTooltip(labelText, {
permanent: false,
direction: 'top',
className: 'forecast-label',
offset: [0, -20]
});
circleLayer.tooltipVisible = true;
circleLayer.openTooltip();
circleLayer.on('click', function(e) {
if (this.tooltipVisible) {
this.closeTooltip();
this.tooltipVisible = false;
} else {
this.openTooltip();
this.tooltipVisible = true;
}
});
const typhoonDot = L.divIcon({ className: 'typhoon-dot' });
L.marker([lat, lng], { icon: typhoonDot }).addTo(map);
circleLayers.push({
lat: lat,
lon: lng,
radius: radiusKm * 1000,
circle: circleLayer,
hour: (forecastHour !== null ? forecastHour : 0)
});
}
});
});
</script>
このコードは、XMLデータから台風情報(予報円や座標データ)を取得して地図上に可視化する処理を行っています。以下では、コードをセクションごとに詳細に解説します。
1. 全体の流れ
- XMLデータ内の
<MeteorologicalInfo>
要素を取得。 - その中から、指定した条件に一致するデータを抽出。
- 予報円の中心座標・半径を計算し、地図上に描画。
- 各予報円にはラベルやインタラクション(クリックでツールチップを開閉)が追加される。
2. 主要な変数と準備
var meteorologicalInfos = getElementsByTagNameWithoutNamespace('MeteorologicalInfo');
meteorologicalInfos
:- XMLからすべての
<MeteorologicalInfo>
要素を取得。 - 台風の予報や実況データ が格納されている要素。
- XMLからすべての
3. <DateTime>
要素の抽出と処理
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
dateTimeElements
:<DateTime>
要素を取得。- 台風情報の「時刻」を含む要素。
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
dateTimeType
:<DateTime>
要素の属性type
(例:"実況"
,"予報 12時間後"
)。- 台風データの種類を判別。
4. 予報や実況の種類をチェック
var isTargetType = targetTypes.includes(dateTimeType);
var isForecastType = forecastMapping.some(mapping => mapping.primary === dateTimeType || mapping.fallback === dateTimeType);
if (!isTargetType && !isForecastType) {
return;
}
isTargetType
:- 現在のデータが 実況 または指定した予報(例: "6時間後", "12時間後")かどうかを確認。
isForecastType
:- 他の予報データ(
forecastMapping
に基づく)を確認。
- 他の予報データ(
- 条件に合致しない場合:
- 現在のデータをスキップ。
5. 予報時間の取得と時刻フォーマット
var forecastHour = getForecastHour(dateTimeType);
var dateTimeText = dateTimeElement.textContent.trim();
var formattedDateTime = formatDateTime(dateTimeText);
getForecastHour
:- 予報時間(例:
"予報 12時間後"
→12
)を取得する関数。
- 予報時間(例:
formattedDateTime
:- 元の時刻データを読みやすい形式(例:
"01月01日 12時30分"
)に変換。
- 元の時刻データを読みやすい形式(例:
6. 予報円の抽出
var circleElements = getElementsByTagNameWithoutNamespace('Circle', meteorologicalInfo);
<Circle>
要素には、台風の予報円情報(中心座標や半径)が含まれる。
7. 中心座標と半径の解析
中心座標の取得
var basePointElements = circle.getElementsByTagName('*');
for (var i = 0; i < basePointElements.length; i++) {
if (basePointElements[i].localName === 'BasePoint') {
basePointElement = basePointElements[i];
break;
}
}
<BasePoint>
要素を探し、中心座標を取得。basePointElement.textContent
:- 座標データ(例:
"35.0+135.0"
)。
- 座標データ(例:
半径の取得
var radiusElements = circle.getElementsByTagName('*');
for (var i = 0; i < radiusElements.length; i++) {
if (radiusElements[i].localName === 'Radius' && radiusElements[i].getAttribute('unit') === 'km') {
radiusElement = radiusElements[i];
break;
}
}
<Radius>
要素:- 台風予報円の半径(単位: キロメートル)。
8. 地図への描画
予報円を描画
const circlePoints = getCirclePoints(lat, lng, radiusKm * 1000, 64);
const circleLayer = L.polygon(circlePoints, {
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.3,
dashArray: '5,5'
}).addTo(map);
circleLayer.bringToFront();
getCirclePoints
:- 半径に基づいて予報円を描画する座標リストを生成。
L.polygon
:- Leaflet.js の関数で、円を地図上に描画。
ラベルとツールチップの追加
const labelText = dateTimeType + '<br>' + formattedDateTime;
circleLayer.bindTooltip(labelText, {
permanent: false,
direction: 'top',
className: 'forecast-label',
offset: [0, -20]
});
bindTooltip
:- 円に対するラベル(ツールチップ)を設定。
- ラベルには予報種別と時刻情報を表示。
インタラクションの設定
circleLayer.on('click', function(e) {
if (this.tooltipVisible) {
this.closeTooltip();
this.tooltipVisible = false;
} else {
this.openTooltip();
this.tooltipVisible = true;
}
});
- クリック時の挙動:
- ツールチップを開閉。
予報円の中心にマーカーを追加
const typhoonDot = L.divIcon({ className: 'typhoon-dot' });
L.marker([lat, lng], { icon: typhoonDot }).addTo(map);
- 台風の中心位置を小さなアイコン(点)で地図に描画。
9. circleLayers に情報を保存
circleLayers.push({
lat: lat,
lon: lng,
radius: radiusKm * 1000,
circle: circleLayer,
hour: (forecastHour !== null ? forecastHour : 0)
});
- 予報円の詳細情報をオブジェクトとして
circleLayers
に格納。 - 後続の処理で利用するため、地図の状態を保存。
まとめ
このコードは、台風予報データを解析して地図上に 予報円を描画 し、視覚的に台風の予測範囲や進路を表示 します。ユーザーに分かりやすい形式で情報を提供し、インタラクティブな操作も可能にする仕組みです。
7.予報円の描画
<script>
var meteorologicalInfos = getElementsByTagNameWithoutNamespace('MeteorologicalInfo');
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
var isTargetType = targetTypes.includes(dateTimeType);
var isForecastType = forecastMapping.some(mapping => mapping.primary === dateTimeType || mapping.fallback === dateTimeType);
if (!isTargetType && !isForecastType) {
return;
}
var forecastHour = getForecastHour(dateTimeType);
var dateTimeText = dateTimeElement.textContent.trim();
var formattedDateTime = formatDateTime(dateTimeText);
var circleElements = getElementsByTagNameWithoutNamespace('Circle', meteorologicalInfo);
circleElements.forEach(function(circle) {
var typeAttr = circle.getAttribute('type');
if (typeAttr !== "予報円") return;
var basePointElements = circle.getElementsByTagName('*');
var basePointElement = null;
for (var i = 0; i < basePointElements.length; i++) {
if (basePointElements[i].localName === 'BasePoint') {
basePointElement = basePointElements[i];
break;
}
}
var radiusElements = circle.getElementsByTagName('*');
var radiusElement = null;
for (var i = 0; i < radiusElements.length; i++) {
if (radiusElements[i].localName === 'Radius' && radiusElements[i].getAttribute('unit') === 'km') {
radiusElement = radiusElements[i];
break;
}
}
if (basePointElement && radiusElement) {
const latLngMatches = basePointElement.textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (!latLngMatches || latLngMatches.length < 2) {
console.warn("予報円の座標解析に失敗しました。");
return;
}
const lat = parseFloat(latLngMatches[0]);
const lng = parseFloat(latLngMatches[1]);
const radiusKm = parseFloat(radiusElement.textContent);
if (isNaN(lat) || isNaN(lng) || isNaN(radiusKm)) {
console.warn("予報円のデータ解析に失敗しました。");
return;
}
const circlePoints = getCirclePoints(lat, lng, radiusKm * 1000, 64);
const circleLayer = L.polygon(circlePoints, {
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.3,
dashArray: '5,5'
}).addTo(map);
circleLayer.bringToFront();
const labelText = dateTimeType + '
' + formattedDateTime;
circleLayer.bindTooltip(labelText, {
permanent: false,
direction: 'top',
className: 'forecast-label',
offset: [0, -20]
});
circleLayer.tooltipVisible = true;
circleLayer.openTooltip();
circleLayer.on('click', function(e) {
if (this.tooltipVisible) {
this.closeTooltip();
this.tooltipVisible = false;
} else {
this.openTooltip();
this.tooltipVisible = true;
}
});
const typhoonDot = L.divIcon({ className: 'typhoon-dot' });
L.marker([lat, lng], { icon: typhoonDot }).addTo(map);
circleLayers.push({
lat: lat,
lon: lng,
radius: radiusKm * 1000,
circle: circleLayer,
hour: (forecastHour !== null ? forecastHour : 0)
});
}
});
});
</script>
このコードは、台風予報データを解析して地図上に 予報円を描画 し、視覚的に台風の予測範囲や進路を表示 します。ユーザーに分かりやすい形式で情報を提供し、インタラクティブな操作も可能にする仕組みです。
このコードは、台風に関する予報や実況データ(例えば予報円)を XML データから取得し、地図上に描画する処理を行っています。以下にコードの詳細な解説を示します。
全体の処理概要
- 台風情報データ(
MeteorologicalInfo
)を取得し、それぞれの要素を解析する。 - 日時情報(
DateTime
)を取得してデータが対象となる条件に合うか判定。 - 予報円データ(
Circle
)の中心座標と半径を解析し、地図に描画。 - 描画した円にラベルとツールチップを設定し、クリックイベントで表示/非表示を切り替え。
- 予報円データをリストに格納して後続の処理に備える。
1. 台風情報の取得と解析
var meteorologicalInfos = getElementsByTagNameWithoutNamespace('MeteorologicalInfo');
- XML データ内の
<MeteorologicalInfo>
要素をすべて取得。 - 台風の気象データ が格納されている要素。
meteorologicalInfos.forEach(function(meteorologicalInfo) {
- 各
<MeteorologicalInfo>
要素を順に処理。
2. 日時情報の取得と判定
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
<DateTime>
要素を取得し、その中の最初の要素を処理。dateTimeElement
:- 台風情報の日時を表す要素。
対象データのフィルタリング
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
var isTargetType = targetTypes.includes(dateTimeType);
var isForecastType = forecastMapping.some(mapping => mapping.primary === dateTimeType || mapping.fallback === dateTimeType);
if (!isTargetType && !isForecastType) {
return;
}
dateTimeType
:<DateTime>
の属性type
の値(例:"実況"
,"予報 6時間後"
)。
isTargetType
:- この日時データが対象(実況や予報データ)かを判定。
isForecastType
:- 予報マッピング情報に一致するかを判定。
- 条件に一致しない場合、この要素をスキップ。
日時情報のフォーマット
var forecastHour = getForecastHour(dateTimeType);
var dateTimeText = dateTimeElement.textContent.trim();
var formattedDateTime = formatDateTime(dateTimeText);
getForecastHour
:- 予報の時間数(例:
"予報 12時間後"
→12
)を計算。
- 予報の時間数(例:
formatDateTime
:- 元の日時データを読みやすい形式に変換(例:
"2025-01-15T12:00:00Z"
→"2025年1月15日 12:00"
)。
- 元の日時データを読みやすい形式に変換(例:
3. 予報円データの解析
予報円の取得
var circleElements = getElementsByTagNameWithoutNamespace('Circle', meteorologicalInfo);
<Circle>
要素をすべて取得。- 予報円のデータ(中心座標や半径)が含まれる。
中心座標と半径の抽出
var basePointElements = circle.getElementsByTagName('*');
var basePointElement = null;
for (var i = 0; i < basePointElements.length; i++) {
if (basePointElements[i].localName === 'BasePoint') {
basePointElement = basePointElements[i];
break;
}
}
var radiusElements = circle.getElementsByTagName('*');
var radiusElement = null;
for (var i = 0; i < radiusElements.length; i++) {
if (radiusElements[i].localName === 'Radius' && radiusElements[i].getAttribute('unit') === 'km') {
radiusElement = radiusElements[i];
break;
}
}
BasePoint
:- 予報円の中心座標を表す要素。
Radius
:- 予報円の半径を表す要素(単位: キロメートル)。
中心座標と半径の解析
const latLngMatches = basePointElement.textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (!latLngMatches || latLngMatches.length < 2) {
console.warn("予報円の座標解析に失敗しました。");
return;
}
const lat = parseFloat(latLngMatches[0]);
const lng = parseFloat(latLngMatches[1]);
const radiusKm = parseFloat(radiusElement.textContent);
if (isNaN(lat) || isNaN(lng) || isNaN(radiusKm)) {
console.warn("予報円のデータ解析に失敗しました。");
return;
}
- 座標データを正規表現で抽出し、数値に変換。
- 半径も数値に変換し、データが有効か検証。
4. 地図上への描画
予報円の描画
const circlePoints = getCirclePoints(lat, lng, radiusKm * 1000, 64);
const circleLayer = L.polygon(circlePoints, {
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.3,
dashArray: '5,5'
}).addTo(map);
circleLayer.bringToFront();
getCirclePoints
:- 半径と中心座標から、予報円を構成する座標リストを生成。
L.polygon
:- Leaflet.js の関数で、円を地図上に描画。
ツールチップとラベル
const labelText = dateTimeType + '<br>' + formattedDateTime;
circleLayer.bindTooltip(labelText, {
permanent: false,
direction: 'top',
className: 'forecast-label',
offset: [0, -20]
});
circleLayer.tooltipVisible = true;
circleLayer.openTooltip();
- ツールチップに予報種別と日時を表示。
- 初期状態でツールチップを開いた状態にする。
クリックイベントの設定
circleLayer.on('click', function(e) {
if (this.tooltipVisible) {
this.closeTooltip();
this.tooltipVisible = false;
} else {
this.openTooltip();
this.tooltipVisible = true;
}
});
- 円をクリックするとツールチップの表示/非表示を切り替える。
中心位置のマーカー
const typhoonDot = L.divIcon({ className: 'typhoon-dot' });
L.marker([lat, lng], { icon: typhoonDot }).addTo(map);
- 台風の中心座標に小さなアイコンを追加。
5. 結果をリストに格納
circleLayers.push({
lat: lat,
lon: lng,
radius: radiusKm * 1000,
circle: circleLayer,
hour: (forecastHour !== null ? forecastHour : 0)
});
- 描画した予報円とその情報をリスト
circleLayers
に保存。 - 後で別の処理に利用可能。
結論
このコードは、台風予報円データを解析して地図上に視覚化する高度な処理を行っています。台風の予報範囲を視覚的に表示することで、利用者が直感的に状況を把握できるよう設計されています。
8. 2つの予報円の共通外接線を計算する関数
<script>
function calculateOuterTangents(p1, r1, p2, r2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const d = Math.hypot(dx, dy);
if (d <= Math.abs(r1 - r2)) {
return null;
}
if (d === 0 && r1 === r2) {
return null;
}
const vx = (p2.x - p1.x) / d;
const vy = (p2.y - p1.y) / d;
const c = (r1 - r2) / d;
const h = Math.sqrt(1 - c * c);
const nx1 = vx * c - h * vy;
const ny1 = vy * c + h * vx;
const nx2 = vx * c + h * vy;
const ny2 = vy * c - h * vx;
const startX1 = p1.x + r1 * nx1;
const startY1 = p1.y + r1 * ny1;
const endX1 = p2.x + r2 * nx1;
const endY1 = p2.y + r2 * ny1;
const startX2 = p1.x + r1 * nx2;
const startY2 = p1.y + r1 * ny2;
const endX2 = p2.x + r2 * nx2;
const endY2 = p2.y + r2 * ny2;
return [
[L.point(startX1, startY1), L.point(endX1, endY1)],
[L.point(startX2, startY2), L.point(endX2, endY2)]
];
}
</script>
この関数 calculateOuterTangents
は、2つの円の共通外接線(外部接線)を計算し、その始点と終点を返します。関数の詳細な動作を以下に解説します。
関数の目的
- 2つの円(中心点
p1
,p2
と半径r1
,r2
)が与えられた場合、これらの円の共通外接線を計算します。 - 外接線は2本あるため、それぞれの線の始点・終点を配列として返します。
1. 引数の説明
p1
: 円1の中心座標(Leafletのピクセル座標)。r1
: 円1の半径(ピクセル単位)。p2
: 円2の中心座標(Leafletのピクセル座標)。r2
: 円2の半径(ピクセル単位)。
2. 円の相対位置を計算
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const d = Math.hypot(dx, dy);
dx
,dy
: 円1と円2の中心間の距離(X軸とY軸の差)。d
: 2つの中心間のユークリッド距離(斜辺の長さ)。
3. 不正な状態をチェック
if (d <= Math.abs(r1 - r2)) {
return null;
}
if (d === 0 && r1 === r2) {
return null;
}
- 条件1: 円が内包または重なっている場合(
d <= |r1 - r2|
):- 内接線が存在しないため、
null
を返します。
- 内接線が存在しないため、
- 条件2: 円が完全に重なっている場合(
d === 0
かつr1 === r2
):- 外接線が無限に存在するため、
null
を返します。
- 外接線が無限に存在するため、
4. 円の方向ベクトルと単位ベクトル
const vx = (p2.x - p1.x) / d;
const vy = (p2.y - p1.y) / d;
vx
,vy
: 中心間の単位ベクトル(dx
,dy
をd
で割って正規化)。- このベクトルは、円1から円2への方向を表します。
5. 接線の角度計算
const c = (r1 - r2) / d;
const h = Math.sqrt(1 - c * c);
c
: 接線がどれだけ中心間ベクトルに沿って偏っているかを表すスカラー値。h
: 接線が中心間ベクトルからどれだけ横方向に離れているかを表すスカラー値。h
は三平方の定理を使って計算。
6. 接線の方向ベクトル計算
const nx1 = vx * c - h * vy;
const ny1 = vy * c + h * vx;
const nx2 = vx * c + h * vy;
const ny2 = vy * c - h * vx;
(nx1, ny1)
と(nx2, ny2)
:- それぞれの外接線の方向ベクトルを表します。
- ベクトルの符号を変えることで2本の外接線を計算。
7. 接線の始点・終点を計算
const startX1 = p1.x + r1 * nx1;
const startY1 = p1.y + r1 * ny1;
const endX1 = p2.x + r2 * nx1;
const endY1 = p2.y + r2 * ny1;
const startX2 = p1.x + r1 * nx2;
const startY2 = p1.y + r1 * ny2;
const endX2 = p2.x + r2 * nx2;
const endY2 = p2.y + r2 * ny2;
- 各接線の始点・終点を計算:
- 始点(円1上の接点):
(startX, startY)
は円1の中心から方向ベクトルに沿ってr1
の距離に位置。 - 終点(円2上の接点):
(endX, endY)
は円2の中心から方向ベクトルに沿ってr2
の距離に位置。
- 始点(円1上の接点):
8. 結果を返す
return [
[L.point(startX1, startY1), L.point(endX1, endY1)],
[L.point(startX2, startY2), L.point(endX2, endY2)]
];
- 結果:
- 2本の外接線の始点と終点を
L.point
オブジェクト(Leafletのピクセル座標形式)として返します。 - 結果は以下の形式:javascriptコピーする編集する
[ [接線1の始点, 接線1の終点], [接線2の始点, 接線2の終点] ]
- 2本の外接線の始点と終点を
関数の全体的な流れ
- 円の相対位置を計算して、外接線が存在するかをチェック。
- 円の方向ベクトルを基に、接線の方向ベクトルを計算。
- 接線の始点と終点を計算し、2本の外接線を返す。
この関数は、円同士の位置関係と大きさを考慮して、外接線を正確に計算するロジックを実装しています。結果は、地図上で視覚的に表示するために利用されます。
9. 隣接する予報円間に共通外接線を描画
<script>
// ★変更箇所: 予報円を予報時間順にソート、隣接のみ共通外接線
circleLayers.sort((a, b) => a.hour - b.hour);
if (circleLayers.length > 1) {
for (let i = 0; i < circleLayers.length - 1; i++) {
const circle1 = circleLayers[i];
const circle2 = circleLayers[i + 1];
const centerLatLng1 = L.latLng(circle1.lat, circle1.lon);
const centerLatLng2 = L.latLng(circle2.lat, circle2.lon);
const p1 = map.latLngToLayerPoint(centerLatLng1);
const p2 = map.latLngToLayerPoint(centerLatLng2);
const radiusLatLng1 = destinationPoint(circle1.lat, circle1.lon, 0, circle1.radius);
const radiusLatLng2 = destinationPoint(circle2.lat, circle2.lon, 0, circle2.radius);
const radiusPoint1 = map.latLngToLayerPoint([radiusLatLng1.lat, radiusLatLng1.lng]);
const radiusPoint2 = map.latLngToLayerPoint([radiusLatLng2.lat, radiusLatLng2.lng]);
const r1 = p1.distanceTo(radiusPoint1);
const r2 = p2.distanceTo(radiusPoint2);
const tangents = calculateOuterTangents(p1, r1, p2, r2);
if (tangents) {
tangents.forEach(line => {
L.polyline([
map.layerPointToLatLng(line[0]),
map.layerPointToLatLng(line[1])
], { color: 'purple', weight: 2 }).addTo(map);
});
}
const centerLine = [[circle1.lat, circle1.lon], [circle2.lat, circle2.lon]];
L.polyline(centerLine, { color: 'red', weight: 1, dashArray: '5, 5' }).addTo(map);
}
}
</script>
このコードは、台風予報円を予報時間順にソートし、隣接する予報円間に共通外接線を描画する処理を実行しています。また、予報円の中心間に赤い破線を描画して、その関係を視覚的に示します。
以下に詳細な解説を示します。
コードの概要
- 予報円を予報時間順にソートする。
- 隣接する予報円ペアの情報を取得し、それぞれの中心座標と半径を計算。
- 共通外接線(外接する2つの予報円の接線)を計算。
- 共通外接線を紫色の線として地図上に描画。
- 予報円の中心間に赤い破線を描画して円の連続性を示す。
1. 予報円を予報時間順にソート
circleLayers.sort((a, b) => a.hour - b.hour);
circleLayers
:- 各予報円のデータ(中心座標、半径、予報時間など)を格納するリスト。
- ソート条件:
- 各予報円の予報時間(
hour
)を昇順で並べ替え。
- 各予報円の予報時間(
2. 隣接する予報円ペアの取得と処理
if (circleLayers.length > 1) {
for (let i = 0; i < circleLayers.length - 1; i++) {
const circle1 = circleLayers[i];
const circle2 = circleLayers[i + 1];
- 条件チェック:
- 予報円が2つ以上存在する場合のみ処理を実行。
- 隣接ペアの取得:
circle1
とcircle2
は隣接する2つの予報円。
3. 各予報円の中心座標と半径を計算
const centerLatLng1 = L.latLng(circle1.lat, circle1.lon);
const centerLatLng2 = L.latLng(circle2.lat, circle2.lon);
const p1 = map.latLngToLayerPoint(centerLatLng1);
const p2 = map.latLngToLayerPoint(centerLatLng2);
const radiusLatLng1 = destinationPoint(circle1.lat, circle1.lon, 0, circle1.radius);
const radiusLatLng2 = destinationPoint(circle2.lat, circle2.lon, 0, circle2.radius);
const radiusPoint1 = map.latLngToLayerPoint([radiusLatLng1.lat, radiusLatLng1.lng]);
const radiusPoint2 = map.latLngToLayerPoint([radiusLatLng2.lat, radiusLatLng2.lng]);
const r1 = p1.distanceTo(radiusPoint1);
const r2 = p2.distanceTo(radiusPoint2);
- 中心座標の変換:
L.latLng
は座標データを Leaflet.js の緯度経度オブジェクトに変換。map.latLngToLayerPoint
は地図上のピクセル座標に変換。
- 半径の座標計算:
destinationPoint
:- 指定された半径(距離)を中心から描いた点を計算。
- 例: 半径
r
を持つ円の「右端の座標」。
- これをピクセル座標に変換して、半径
r1
,r2
を計算。
4. 共通外接線の計算
const tangents = calculateOuterTangents(p1, r1, p2, r2);
if (tangents) {
tangents.forEach(line => {
L.polyline([
map.layerPointToLatLng(line[0]),
map.layerPointToLatLng(line[1])
], { color: 'purple', weight: 2 }).addTo(map);
});
}
calculateOuterTangents
:- 2つの円の共通外接線を計算する関数。
- 結果として、円同士の外接線の始点・終点がピクセル座標で返される。
- 外接線を地図上に描画:
- 各外接線の座標を地図の緯度経度に変換。
L.polyline
を使用して、紫色の線を地図上に描画。
5. 中心間の赤い破線を描画
const centerLine = [[circle1.lat, circle1.lon], [circle2.lat, circle2.lon]];
L.polyline(centerLine, { color: 'red', weight: 1, dashArray: '5, 5' }).addTo(map);
- 中心線の座標リスト:
- 2つの予報円の中心座標を線で結ぶためのリスト。
- 破線を描画:
L.polyline
で赤い破線を地図上に描画。dashArray: '5, 5'
は線のスタイルを破線に設定。
コードの動作まとめ
- 予報時間順に予報円をソートして、隣接する円を比較。
- 隣接する円の 中心座標と半径を基に外接線を計算。
- 外接線を紫色の線として地図に描画。
- 各予報円の中心間に赤い破線を描画し、時間的なつながりを示す。
これにより、複数の予報円が時間順にどのように移動するか、またそれぞれの関係性が視覚的に把握できるようになります。
10.暴風域(実況)を地図上に赤い円で表示
<script>
// 暴風域(実況)赤円表示
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (dateTimeType !== '実況') return;
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
if (basePoints.length === 0) return;
var basePointElement = basePoints[0];
const latLngMatches = basePointElement.textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (!latLngMatches || latLngMatches.length < 2) return;
const lat = parseFloat(latLngMatches[0]);
const lng = parseFloat(latLngMatches[1]);
var stormAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '暴風域');
if (stormAreaParts.length === 0) return;
var stormAreaPart = stormAreaParts[0];
var radiusElements = stormAreaPart.getElementsByTagName('*');
var radiusKm = null;
for (var k = 0; k < radiusElements.length; k++) {
if (radiusElements[k].localName === 'Radius' && radiusElements[k].getAttribute('unit') === 'km') {
radiusKm = parseFloat(radiusElements[k].textContent.trim());
break;
}
}
if (!radiusKm) return;
L.circle([lat, lng], {
color: 'red',
fillColor: 'red',
fillOpacity: 0.3,
radius: radiusKm * 1000
}).addTo(map).bindPopup('暴風域');
});
</script>
このコードは「暴風域(実況)」を地図上に赤い円で表示するためのものです。以下に詳細な解説を行います。 🌪️📍
コードの全体的な流れ
- データ解析:
meteorologicalInfos
の各気象情報を解析。 - 条件判定: 必要な条件(実況データか、暴風域データが存在するか)を満たすかチェック。
- 座標と半径の取得: 暴風域の中心座標(緯度・経度)と半径を抽出。
- 赤い円を地図に描画: 暴風域を Leaflet の円として地図上に追加。
各部分の詳細解説
1. 実況データをフィルタリング 🕵️♂️
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (dateTimeType !== '実況') return;
- 目的:
meteorologicalInfos
内の各情報を調べ、type
属性が「実況」のデータだけを対象にする。
- 処理の流れ:
DateTime
要素を取得。type
属性が実況
でない場合はスキップ。
2. 中心座標を取得 📍
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
if (basePoints.length === 0) return;
var basePointElement = basePoints[0];
const latLngMatches = basePointElement.textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (!latLngMatches || latLngMatches.length < 2) return;
const lat = parseFloat(latLngMatches[0]);
const lng = parseFloat(latLngMatches[1]);
- 目的:
- 暴風域の中心位置(緯度・経度)を取得。
- 処理の流れ:
BasePoint
要素を検索し、そのtype
が中心位置(度)
であることを確認。textContent
から緯度・経度を正規表現で抽出。- 緯度・経度を
parseFloat
で数値に変換。
3. 暴風域の半径を取得 📏
var stormAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '暴風域');
if (stormAreaParts.length === 0) return;
var stormAreaPart = stormAreaParts[0];
var radiusElements = stormAreaPart.getElementsByTagName('*');
var radiusKm = null;
for (var k = 0; k < radiusElements.length; k++) {
if (radiusElements[k].localName === 'Radius' && radiusElements[k].getAttribute('unit') === 'km') {
radiusKm = parseFloat(radiusElements[k].textContent.trim());
break;
}
}
if (!radiusKm) return;
- 目的:
- 暴風域の半径を取得し、描画に利用。
- 処理の流れ:
WarningAreaPart
要素を検索し、そのtype
が暴風域
であることを確認。- 子要素の中から、
localName
がRadius
でunit
がkm
の要素を探す。 - 半径の値を取得して
radiusKm
に格納。
4. 赤い円を地図に描画 🌐
L.circle([lat, lng], {
color: 'red',
fillColor: 'red',
fillOpacity: 0.3,
radius: radiusKm * 1000
}).addTo(map).bindPopup('暴風域');
});
- 目的:
- 暴風域を赤い円として地図に表示。
- 処理の流れ:
L.circle
:- 座標:
[lat, lng]
を中心。 - スタイル:
- 円の色 (
color
): 赤 ('red'
)。 - 塗りつぶし色 (
fillColor
): 赤 ('red'
)。 - 塗りつぶしの透明度 (
fillOpacity
): 0.3(30% 透過)。 - 半径 (
radius
):radiusKm * 1000
(メートル単位に変換)。
- 円の色 (
- 座標:
- 地図への追加:
.addTo(map)
: Leaflet の地図オブジェクトmap
に円を追加。
- ポップアップを設定:
.bindPopup('暴風域')
: 円をクリックしたときに「暴風域」というメッセージを表示。
コード全体の流れ
- 🎯 実況データだけを対象に処理。
- 📍 中心座標を解析して取得。
- 📏 半径データを抽出。
- 🌐 地図上に赤い円を描画。
- 描画された円をクリックすると「暴風域」というポップアップが表示される。
視覚的な結果
地図上に 赤い半透明の円 が表示され、暴風域の位置と範囲を視覚的に把握できます。ユーザーは、クリックで詳細を確認できます! 🌪️📊
11.隣接する警戒域同士を結ぶ共通外接線を描画
<script>
// ★変更箇所: 暴風警戒域も隣接するもののみ共通外接線
var stormAreas = [];
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
var isTargetType = targetTypes.includes(dateTimeType);
var isForecastType = forecastMapping.some(mapping => mapping.primary === dateTimeType || mapping.fallback === dateTimeType);
if (!isTargetType && !isForecastType) {
return;
}
var forecastHour = getForecastHour(dateTimeType);
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
var lat = null;
var lng = null;
if (basePoints.length > 0) {
const latLngMatches = basePoints[0].textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (latLngMatches && latLngMatches.length >= 2) {
lat = parseFloat(latLngMatches[0]);
lng = parseFloat(latLngMatches[1]);
}
}
var warningAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '暴風警戒域');
if (warningAreaParts.length > 0 && lat !== null && lng !== null) {
var warningAreaPart = warningAreaParts[0];
var radiusElements = warningAreaPart.getElementsByTagName('*');
var radiusKm = null;
for (var k = 0; k < radiusElements.length; k++) {
if (radiusElements[k].localName === 'Radius' && radiusElements[k].getAttribute('unit') === 'km') {
radiusKm = parseFloat(radiusElements[k].textContent.trim());
break;
}
}
if (radiusKm) {
stormAreas.push({
lat: lat,
lng: lng,
radius: radiusKm * 1000,
hour: (forecastHour !== null ? forecastHour : 0)
});
}
}
});
// 暴風警戒域を時間順にソート
stormAreas.sort((a, b) => a.hour - b.hour);
// 暴風警戒域を個別描画、隣接ペアで共通外接線
for (let i = 0; i < stormAreas.length; i++) {
const area = stormAreas[i];
const circlePoints = getCirclePoints(area.lat, area.lng, area.radius, 64);
L.polygon(circlePoints, {
color: 'red',
fillColor: 'red',
fillOpacity: 0.3,
interactive: false,
}).addTo(map);
const stormDot = L.divIcon({ className: 'typhoon-dot' });
L.marker([area.lat, area.lng], { icon: stormDot }).addTo(map);
}
if (stormAreas.length > 1) {
for (let i = 0; i < stormAreas.length - 1; i++) {
const area1 = stormAreas[i];
const area2 = stormAreas[i + 1];
const centerLatLng1 = L.latLng(area1.lat, area1.lng);
const centerLatLng2 = L.latLng(area2.lat, area2.lng);
const p1 = map.latLngToLayerPoint(centerLatLng1);
const p2 = map.latLngToLayerPoint(centerLatLng2);
const radiusLatLng1 = destinationPoint(area1.lat, area1.lng, 0, area1.radius);
const radiusLatLng2 = destinationPoint(area2.lat, area2.lng, 0, area2.radius);
const radiusPoint1 = map.latLngToLayerPoint([radiusLatLng1.lat, radiusLatLng1.lng]);
const radiusPoint2 = map.latLngToLayerPoint([radiusLatLng2.lat, radiusLatLng2.lng]);
const r1 = p1.distanceTo(radiusPoint1);
const r2 = p2.distanceTo(radiusPoint2);
const tangents = calculateOuterTangents(p1, r1, p2, r2);
if (tangents) {
tangents.forEach(line => {
L.polyline([
map.layerPointToLatLng(line[0]),
map.layerPointToLatLng(line[1])
], { color: 'purple', weight: 2 }).addTo(map);
});
}
const centerLine = [[area1.lat, area1.lng], [area2.lat, area2.lng]];
L.polyline(centerLine, { color: 'red', weight: 1, dashArray: '5, 5' }).addTo(map);
}
}
</script>
このコードは「暴風警戒域」を地図上に描画し、隣接する警戒域同士を結ぶ共通外接線を描画するものです。以下に詳細に解説します。 🌪️🗺️
全体的な流れ
- 暴風警戒域のデータ収集:
- 気象情報 (
meteorologicalInfos
) から暴風警戒域を抽出。 - 中心座標 (緯度・経度)、半径 (メートル単位)、予報時間を取得して格納。
- 気象情報 (
- 暴風警戒域の描画:
- 抽出したデータを元に、地図上に赤い円で警戒域を表示。
- 隣接警戒域の共通外接線描画:
- 暴風警戒域が隣接する場合、外接線を紫色で表示。
- 各警戒域を結ぶ中心線も描画。
コード詳細解説
1. 暴風警戒域のデータ収集
1.1 データ取得と条件判定
var stormAreas = [];
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
var isTargetType = targetTypes.includes(dateTimeType);
var isForecastType = forecastMapping.some(mapping => mapping.primary === dateTimeType || mapping.fallback === dateTimeType);
if (!isTargetType && !isForecastType) {
return;
}
- 目的:
- 各
meteorologicalInfo
を解析し、対象のデータ(暴風警戒域に関連する予報/実況データ)のみを選別。
- 各
- 処理:
DateTime
要素を取得。type
属性がtargetTypes
またはforecastMapping
に一致するか判定。- 一致しない場合はスキップ。
1.2 中心座標と半径を取得
var forecastHour = getForecastHour(dateTimeType);
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
var lat = null;
var lng = null;
if (basePoints.length > 0) {
const latLngMatches = basePoints[0].textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (latLngMatches && latLngMatches.length >= 2) {
lat = parseFloat(latLngMatches[0]);
lng = parseFloat(latLngMatches[1]);
}
}
var warningAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '暴風警戒域');
if (warningAreaParts.length > 0 && lat !== null && lng !== null) {
var warningAreaPart = warningAreaParts[0];
var radiusElements = warningAreaPart.getElementsByTagName('*');
var radiusKm = null;
for (var k = 0; k < radiusElements.length; k++) {
if (radiusElements[k].localName === 'Radius' && radiusElements[k].getAttribute('unit') === 'km') {
radiusKm = parseFloat(radiusElements[k].textContent.trim());
break;
}
}
if (radiusKm) {
stormAreas.push({
lat: lat,
lng: lng,
radius: radiusKm * 1000,
hour: (forecastHour !== null ? forecastHour : 0)
});
}
}
});
- 目的:
- 中心座標と警戒域の半径を取得し、
stormAreas
配列に格納。
- 中心座標と警戒域の半径を取得し、
- 処理の流れ:
BasePoint
要素から緯度・経度を取得。WarningAreaPart
要素から半径データ (Radius
) を取得。- 単位をメートルに変換し、時間情報と共に保存。
2. 暴風警戒域の描画
2.1 暴風警戒域を赤い円で表示
stormAreas.sort((a, b) => a.hour - b.hour);
for (let i = 0; i < stormAreas.length; i++) {
const area = stormAreas[i];
const circlePoints = getCirclePoints(area.lat, area.lng, area.radius, 64);
L.polygon(circlePoints, {
color: 'red',
fillColor: 'red',
fillOpacity: 0.3,
interactive: false,
}).addTo(map);
const stormDot = L.divIcon({ className: 'typhoon-dot' });
L.marker([area.lat, area.lng], { icon: stormDot }).addTo(map);
}
- 目的:
- 暴風警戒域を赤い円で地図に描画。
- 処理の流れ:
getCirclePoints
で警戒域の円周座標を計算(64分割)。L.polygon
を使って赤い円を描画。- 中心にマーカーを配置(デザイン指定用に
typhoon-dot
クラスを使用)。
3. 隣接警戒域の共通外接線描画
3.1 外接線を描画
if (stormAreas.length > 1) {
for (let i = 0; i < stormAreas.length - 1; i++) {
const area1 = stormAreas[i];
const area2 = stormAreas[i + 1];
const centerLatLng1 = L.latLng(area1.lat, area1.lng);
const centerLatLng2 = L.latLng(area2.lat, area2.lng);
const p1 = map.latLngToLayerPoint(centerLatLng1);
const p2 = map.latLngToLayerPoint(centerLatLng2);
const radiusLatLng1 = destinationPoint(area1.lat, area1.lng, 0, area1.radius);
const radiusLatLng2 = destinationPoint(area2.lat, area2.lng, 0, area2.radius);
const radiusPoint1 = map.latLngToLayerPoint([radiusLatLng1.lat, radiusLatLng1.lng]);
const radiusPoint2 = map.latLngToLayerPoint([radiusLatLng2.lat, radiusLatLng2.lng]);
const r1 = p1.distanceTo(radiusPoint1);
const r2 = p2.distanceTo(radiusPoint2);
const tangents = calculateOuterTangents(p1, r1, p2, r2);
if (tangents) {
tangents.forEach(line => {
L.polyline([
map.layerPointToLatLng(line[0]),
map.layerPointToLatLng(line[1])
], { color: 'purple', weight: 2 }).addTo(map);
});
}
const centerLine = [[area1.lat, area1.lng], [area2.lat, area2.lng]];
L.polyline(centerLine, { color: 'red', weight: 1, dashArray: '5, 5' }).addTo(map);
}
}
- 目的:
- 隣接する警戒域間の外接線を紫色で描画。
- 処理の流れ:
- 各警戒域の中心と半径を取得。
- 隣接する2つの円の外接線を計算 (
calculateOuterTangents
)。 - 外接線を紫色 (
purple
) で描画。 - 各警戒域の中心を結ぶ破線 (
dashArray: '5, 5'
) も表示。
視覚的結果
- 地図上に赤い円で暴風警戒域が表示されます。
- 隣接する警戒域同士は紫色の外接線で結ばれ、中心同士を赤い破線で接続。
- 警戒域の動きや関係性を視覚的に把握可能です。 🌪️📊
12.強風域を地図上に描画
<script>
// 強風域(従来通りの表示、変更なし)
meteorologicalInfos.forEach(function(meteorologicalInfo) {
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
if (dateTimeType !== '実況') return;
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
var lat = null;
var lng = null;
if (basePoints.length > 0) {
const latLngMatches = basePoints[0].textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (latLngMatches && latLngMatches.length >= 2) {
lat = parseFloat(latLngMatches[0]);
lng = parseFloat(latLngMatches[1]);
}
}
if (lat !== null && lng !== null) {
var strongWindAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '強風域');
if (strongWindAreaParts.length > 0) {
var strongWindAreaPart = strongWindAreaParts[0];
var axesElements = strongWindAreaPart.getElementsByTagName('*');
var radii = [];
for (var m = 0; m < axesElements.length; m++) {
if (axesElements[m].localName === 'Axis') {
var radiusKm = null;
var direction = null;
var axisChildren = axesElements[m].getElementsByTagName('*');
for (var n = 0; n < axisChildren.length; n++) {
if (axisChildren[n].localName === 'Direction') {
direction = axisChildren[n].textContent.trim();
}
if (axisChildren[n].localName === 'Radius' && axisChildren[n].getAttribute('unit') === 'km') {
radiusKm = parseFloat(axisChildren[n].textContent.trim());
}
}
if (radiusKm && direction) {
radii.push({ direction: direction, radiusKm: radiusKm });
}
}
}
if (radii.length > 0) {
var maxRadius = Math.max(...radii.map(r => r.radiusKm));
var circlePoints = getCirclePoints(lat, lng, maxRadius * 1000, 64);
L.polygon(circlePoints, {
color: 'yellow',
fillColor: 'yellow',
fillOpacity: 0.3,
interactive: false,
}).addTo(map);
}
}
}
});
</script>
このコードは、気象情報から強風域を取得し、その情報を地図上に描画する処理を行っています。従来通りの表示方法で実装されており、以下の流れでデータを取得・解析・描画しています。
コードの詳細解説
1. 気象情報(meteorologicalInfos)のループ処理
meteorologicalInfos.forEach(function(meteorologicalInfo) {
- 配列
meteorologicalInfos
をループで処理します。 - 各
meteorologicalInfo
は、気象データを保持するオブジェクト。
2. 実況データの判定
var dateTimeElements = getElementsByTagNameWithoutNamespace('DateTime', meteorologicalInfo);
var dateTimeElement = dateTimeElements[0];
if (!dateTimeElement) return;
var dateTimeType = dateTimeElement.getAttribute('type');
if (!dateTimeType) return;
if (dateTimeType !== '実況') return;
- 気象情報から日時データ(
DateTime
)を取得。 type
属性をチェックし、「実況」タイプのデータのみ処理対象にします。- 実況データは現在の観測情報を示します。
- 実況データでない場合は
return
して次の情報へ。
3. 中心位置(緯度・経度)の取得
var basePoints = getElementsByTagNameAndAttribute(meteorologicalInfo, 'BasePoint', 'type', '中心位置(度)');
var lat = null;
var lng = null;
if (basePoints.length > 0) {
const latLngMatches = basePoints[0].textContent.match(/([\+\-]?[0-9]+(?:\.[0-9]+)?)/g);
if (latLngMatches && latLngMatches.length >= 2) {
lat = parseFloat(latLngMatches[0]);
lng = parseFloat(latLngMatches[1]);
}
}
BasePoint
要素から中心位置(緯度・経度)を取得。- 取得した緯度・経度は
lat
とlng
に格納。 - 緯度・経度が取得できない場合は、このデータをスキップします。
4. 強風域データの取得
var strongWindAreaParts = getElementsByTagNameAndAttribute(meteorologicalInfo, 'WarningAreaPart', 'type', '強風域');
if (strongWindAreaParts.length > 0) {
var strongWindAreaPart = strongWindAreaParts[0];
var axesElements = strongWindAreaPart.getElementsByTagName('*');
var radii = [];
for (var m = 0; m < axesElements.length; m++) {
if (axesElements[m].localName === 'Axis') {
var radiusKm = null;
var direction = null;
var axisChildren = axesElements[m].getElementsByTagName('*');
for (var n = 0; n < axisChildren.length; n++) {
if (axisChildren[n].localName === 'Direction') {
direction = axisChildren[n].textContent.trim();
}
if (axisChildren[n].localName === 'Radius' && axisChildren[n].getAttribute('unit') === 'km') {
radiusKm = parseFloat(axisChildren[n].textContent.trim());
}
}
if (radiusKm && direction) {
radii.push({ direction: direction, radiusKm: radiusKm });
}
}
}
}
- 強風域情報(
WarningAreaPart
)のうち、type
が「強風域」のものを取得。 - 強風域のデータから以下を抽出:
- 方向(
Direction
): 強風がどの方向に影響しているか。 - 半径(
Radius
): 強風域の半径(単位:km)。
- 方向(
- 有効なデータが見つかると、
radii
配列に{ direction, radiusKm }
の形で保存。
5. 強風域の最大半径を計算
if (radii.length > 0) {
var maxRadius = Math.max(...radii.map(r => r.radiusKm));
var circlePoints = getCirclePoints(lat, lng, maxRadius * 1000, 64);
L.polygon(circlePoints, {
color: 'yellow',
fillColor: 'yellow',
fillOpacity: 0.3,
interactive: false,
}).addTo(map);
}
- 強風域データの中から、最大半径(
maxRadius
)を計算。 - 地図描画用に、中心位置(
lat
,lng
)を基準に最大半径の円を描画。 - 円の外観は以下の通り:
- 色: 黄色 (
color: 'yellow'
,fillColor: 'yellow'
)。 - 不透明度: 0.3 (
fillOpacity: 0.3
)。 - インタラクション: なし (
interactive: false
)。
- 色: 黄色 (
全体の流れ
- 気象情報をループ処理。
- 実況データ(現在観測情報)を対象に絞る。
- 中心位置(緯度・経度)を取得。
- 強風域データを解析し、最大半径を計算。
- 最大半径を基に黄色の円を地図に描画。
コードの役割
このコードは、気象データから「強風域」を抽出して地図に可視化するものです。具体的には、最大強風域の範囲を黄色の円として地図に表示することで、視覚的に影響範囲を確認できるようにしています。
12.その他の関数
<script>
function getCirclePoints(lat, lng, radius, segments) {
var points = [];
for (var i = 0; i < segments; i++) {
var angle = (i / segments) * 360;
var point = destinationPoint(lat, lng, angle, radius);
points.push([point.lat, point.lng]);
}
return points;
}
function destinationPoint(lat, lng, bearing, distance) {
var R = 6378137;
var δ = distance / R;
var θ = bearing * Math.PI / 180;
var φ1 = lat * Math.PI / 180;
var λ1 = lng * Math.PI / 180;
var φ2 = Math.asin(Math.sin(φ1) * Math.cos(δ) +
Math.cos(φ1) * Math.sin(δ) * Math.cos(θ));
var λ2 = λ1 + Math.atan2(Math.sin(θ) * Math.sin(δ) * Math.cos(φ1),
Math.cos(δ) - Math.sin(φ1) * Math.sin(φ2));
return {
lat: φ2 * 180 / Math.PI,
lng: λ2 * 180 / Math.PI
};
}
function convexHull(points) {
if (points.length < 3) return points;
var start = points.reduce((prev, curr) => {
return (prev[1] < curr[1] || (prev[1] === curr[1] && prev[0] < curr[0])) ? prev : curr;
});
points.sort((a, b) => {
var angleA = Math.atan2(a[1] - start[1], a[0] - start[0]);
var angleB = Math.atan2(b[1] - start[1], b[0] - start[0]);
return angleA - angleB;
});
var hull = [];
for (var i = 0; i < points.length; i++) {
while (hull.length >= 2 && crossProduct(hull[hull.length - 2], hull[hull.length - 1], points[i]) <= 0) {
hull.pop();
}
hull.push(points[i]);
}
return hull;
}
function crossProduct(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
</script>
このコードは、地球上の緯度・経度に基づいて円周上の点を計算し、その点を使って凸包(Convex Hull)を求めるための一連の関数です。それぞれの関数の詳細を以下で解説します。
1. getCirclePoints 関数
概要
この関数は、指定した中心位置(緯度・経度)から、指定した半径で円周上の点を計算し、指定した数のセグメントに分けてその座標を返します。
詳細
function getCirclePoints(lat, lng, radius, segments) {
var points = [];
for (var i = 0; i < segments; i++) {
var angle = (i / segments) * 360; // 角度(0度〜360度)を計算
var point = destinationPoint(lat, lng, angle, radius); // 指定した角度で円周上の座標を計算
points.push([point.lat, point.lng]); // 計算した座標を配列に追加
}
return points; // 全ての点を返す
}
lat
,lng
: 円の中心の緯度・経度。radius
: 円の半径(メートル単位)。segments
: 円を何分割するか。分割数が多いほど円が滑らかになります。
処理の流れ
segments
数だけループを回して、円周上の各点を計算します。- 各点は
destinationPoint
関数を使って、指定した角度(angle
)と半径(radius
)で計算されます。 - 計算した点は
points
配列に格納され、最終的にその配列を返します。
2. destinationPoint 関数
概要
この関数は、地球上の指定した出発点(緯度・経度)から指定した方向(方位角)と距離に基づいて、到達点の緯度・経度を計算します。これは球面三角法を使用しており、地球の曲率を考慮しています。
詳細
function destinationPoint(lat, lng, bearing, distance) {
var R = 6378137; // 地球の半径(メートル単位)
var δ = distance / R; // 距離を地球半径で割って角度に変換
var θ = bearing * Math.PI / 180; // 角度(度)をラジアンに変換
var φ1 = lat * Math.PI / 180; // 出発点の緯度をラジアンに変換
var λ1 = lng * Math.PI / 180; // 出発点の経度をラジアンに変換
// 新しい緯度(φ2)の計算
var φ2 = Math.asin(Math.sin(φ1) * Math.cos(δ) +
Math.cos(φ1) * Math.sin(δ) * Math.cos(θ));
// 新しい経度(λ2)の計算
var λ2 = λ1 + Math.atan2(Math.sin(θ) * Math.sin(δ) * Math.cos(φ1),
Math.cos(δ) - Math.sin(φ1) * Math.sin(φ2));
// 計算した緯度・経度を度に戻して返す
return {
lat: φ2 * 180 / Math.PI,
lng: λ2 * 180 / Math.PI
};
}
lat
,lng
: 出発点の緯度・経度。bearing
: 出発点からの進行方向(方位角)。distance
: 出発点からの距離(メートル単位)。
処理の流れ
- 地球の半径(
R
)を基に、距離(distance
)を角度に変換します。 - 出発点の緯度・経度をラジアンに変換。
- 球面三角法を使って新しい緯度(
φ2
)と経度(λ2
)を計算します。 - 結果を度(°)に戻して返します。
3. convexHull 関数
概要
この関数は、与えられた複数の座標点から、最小の凸包(Convex Hull)を計算します。凸包とは、与えられた点群を囲む最小の凸多角形です。
詳細
function convexHull(points) {
if (points.length < 3) return points; // 点が3つ未満ならそのまま返す
var start = points.reduce((prev, curr) => {
return (prev[1] < curr[1] || (prev[1] === curr[1] && prev[0] < curr[0])) ? prev : curr;
});
points.sort((a, b) => {
var angleA = Math.atan2(a[1] - start[1], a[0] - start[0]);
var angleB = Math.atan2(b[1] - start[1], b[0] - start[0]);
return angleA - angleB;
});
var hull = [];
for (var i = 0; i < points.length; i++) {
while (hull.length >= 2 && crossProduct(hull[hull.length - 2], hull[hull.length - 1], points[i]) <= 0) {
hull.pop();
}
hull.push(points[i]);
}
return hull;
}
points
: 凸包を求めたい点の配列。
処理の流れ
- 点の最小値を探す:
reduce
関数で、最も左下の点(最小の緯度と経度)をstart
として選びます。 - 角度順にソート: 全ての点を、
start
点からの角度でソートします。 - 凸包の計算:
crossProduct
を使って、点群が凸包を形成するように点を選別し、hull
配列に追加します。
4. crossProduct 関数
概要
この関数は、2つのベクトルが形成する外積(クロスプロダクト)を計算します。外積が負の値であれば反時計回り、正の値であれば時計回りであることを示します。
詳細
function crossProduct(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
o
,a
,b
: 3つの点(o: 原点、a: 出発点、b: 次の点)。
処理の流れ
o
,a
,b
の順に2つのベクトルを作り、その外積を計算します。この外積が0より小さい場合は反時計回り、0より大きい場合は時計回りです。
全体の流れ
getCirclePoints
で円周上の点を計算し、その点群を取得。convexHull
でその点群から最小の凸包を計算。- 各点間で方向や半径を考慮して円の描画や、最小の凸包に基づく計算を行う。
このコードは、地図上で円を描くための点を計算し、その周囲を囲む最小の凸包を計算するために利用されると考えられます。
13. 最後に
この台風情報描画プログラムは、ChatGPT-o1previewに100回以上聞いて、作成する事が出来ました。
この解説記事も、ChatGPTにしてもらっています。
ChatGPTが無かったら、出来なかったものです。
台風情報描画プログラムの作成の記事が、一人でも多くの命を助ける台風情報の一助になれば幸いです。
ここまで読んだあたなは、本当の勇者です‼️
共に自作の天気サイト作成同盟として、歩んできましょう‼️
またこの台風情報では、
時刻リスト($targetTimes1
)が以下の場合のみの対応していますが、この時刻に二個目の台風情報が追加されることもあります。
['T00', 'T03', 'T06', 'T09', 'T12', 'T15', 'T18', 'T21']
また、下記時刻リストにも台風情報が発生するので、取得プログラムを対応させるようにしましょう。
['T01', 'T04', 'T07', 'T10', 'T13', 'T16', 'T19', 'T22']
最後までお読みくださり、本当にありがとうございます。
あなたが台風情報を完成させることを願っております。
分からないことは、ChatGPTに聞いて下さいね‼️😊🤖