17.アメダス気温の表示の仕方

今回は、アメダス気温表示プログラムの説明をします。


アメダス観測所のjsonデータのアドレスは、下記になります。

https://www.jma.go.jp/bosai/amedas/const/amedastable.json

と、下記URLからアメダス気象情報データの読み取りを行い、地図上に気温データを色分けし可視化表示します。

"https://www.jma.go.jp/bosai/amedas/data/map/" + queryDatetime + ".json"

では、早速コードを見ていきましょう。

  1. <script>
  2. <div id="mapid" style="position:absolute;height: 100%;width: 100vw;"></div>
  3. <script>
  4. (async function() {
  5. // 気温カラースケール定義
  6. const temparatureColorScale = [
  7. { temp: 35, color: '#B40068' },
  8. { temp: 30, color: '#FF2800' },
  9. { temp: 25, color: '#FF9900' },
  10. { temp: 20, color: '#FFF500' },
  11. { temp: 15, color: '#FFFF96' },
  12. { temp: 10, color: '#FFFFF0' },
  13. { temp: 5, color: '#B9EBFF' },
  14. { temp: 0, color: '#0096FF' },
  15. { temp: -5, color: '#0041FF' },
  16. { temp: -273.15, color: '#002080' }
  17. ]
  18. // 気温カラーの取得
  19. const getTemparatureColor = (temp) => {
  20. const colorScale = temparatureColorScale.find(item => {
  21. return temp > item.temp
  22. })
  23. if( !colorScale ) return {temp: 'N/A', color: '#000000'}
  24. return colorScale
  25. }
  26. // アメダスデータの読み取り
  27. const fetchAmedasThenGenerateVoronoi = async (option) => {
  28. // アメダス観測所データの読み取り
  29. const fetchAmedasObservatories = async () => {
  30. const url = "https://www.jma.go.jp/bosai/amedas/const/amedastable.json"
  31. const amedasObservatories = {}
  32. const res = await fetch(url)
  33. const json = await res.json()
  34. Object.keys(json).map( key => {
  35. const amedas = json[key]
  36. json[key].lat = amedas.lat[0] + (amedas.lat[1] / 60)
  37. json[key].lon = amedas.lon[0] + (amedas.lon[1] / 60)
  38. })
  39. return json
  40. }
  41. // アメダス気象情報データの読み取り
  42. const fetchAmedasWeatherInformations = async (queryDatetime) => {
  43. const url = "https://www.jma.go.jp/bosai/amedas/data/map/" + queryDatetime + ".json"
  44. const res = await fetch(url)
  45. const json = await res.json()
  46. return json
  47. }
  48. // アメダス観測所からボロノイポリゴンを作成
  49. const generateGeoJsonAmedasVoronoiPolygons = async (amedas, option) => {
  50. const points = []
  51. Object.keys(amedas).map( key => {
  52. const item = amedas[key]
  53. points.push(turf.point([item.lon, item.lat],{'key': key, 'kjName': item.kjName, 'knName': item.knName, 'alt': item.alt}))
  54. })
  55. const collection = turf.featureCollection(points)
  56. const voronoiPolygons = turf.voronoi(collection, {bbox: turf.bbox(turf.buffer(collection, 50, {}))})
  57. datetime = option.queryDatetime.match( /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/ )
  58. // アメダス観測所ポイントとボロノイポリゴンの内外判定を行い、観測所データをポリゴンへ移植
  59. points.map( point => {
  60. voronoiPolygons.features.some( polygon => {
  61. if( turf.booleanPointInPolygon(point, polygon) === true ){
  62. polygon.properties.key = point.properties.key
  63. polygon.properties.kjName = point.properties.kjName
  64. polygon.properties.knName = point.properties.knName
  65. polygon.properties.alt = point.properties.alt
  66. polygon.properties.lon = point.geometry.coordinates[0]
  67. polygon.properties.lat = point.geometry.coordinates[1]
  68. polygon.properties.queryDatetime = datetime[1] + '-' + datetime[2] + '-' + datetime[3] + ' ' + datetime[4] + ':' + datetime[5] + ':' + datetime[6]
  69. return true
  70. }
  71. return false
  72. })
  73. })
  74. // ボロノイ図を陸地でクリッピング
  75. if( option.isClipping === true) {
  76. const dissolvePolygons = turf.dissolve(turf.buffer(collection, 30, {steps: 2}))
  77. voronoiPolygons.features.map( voronoiPolygon => {
  78. dissolvePolygons.features.some( dissolvePolygon => {
  79. const intersectPolygon = turf.intersect(voronoiPolygon, dissolvePolygon)
  80. if (!intersectPolygon){
  81. return false
  82. }
  83. voronoiPolygon.geometry = intersectPolygon.geometry
  84. return true
  85. })
  86. })
  87. }
  88. return voronoiPolygons
  89. }
  90. // 現在日時からアメダス気象情報取得パラメータを生成
  91. const generateDateimeString = () => {
  92. const nowTime = new Date()
  93. nowTime.setHours((new Date()).getHours() -1)
  94. //nowTime.setMonth((new Date()).getMonth() -4)
  95. return nowTime.getFullYear().toString() + (nowTime.getMonth() + 1).toString().padStart(2, '0') + nowTime.getDate().toString().padStart(2, '0') + nowTime.getHours().toString().padStart(2, '0') + "0000"
  96. }
  97. const queryDatetime = generateDateimeString()
  98. const amedasObservatories = await fetchAmedasObservatories()
  99. const amedasWeatherInfos = await fetchAmedasWeatherInformations(queryDatetime)
  100. const voronoiPolygons = await generateGeoJsonAmedasVoronoiPolygons(amedasObservatories, {isClipping: option.isClipping, queryDatetime: queryDatetime})
  101. return {observatories: amedasObservatories, weatherInfos: amedasWeatherInfos, voronoiPolygons: voronoiPolygons, queryDatetime: queryDatetime }
  102. }
  103. // Leaflet地図初期化
  104. const initializeMainMap = () => {
  105. const map = L.map("mapid", L.extend({
  106. zoom: 18,
  107. minZoom: 3,
  108. worldCopyJump: "true"
  109. }))
  110. map.setView([35.6,139.8], 5)
  111. map.locate({setView: true, maxZoom: 9});
  112. function onLocationFound(e) {
  113. var radius = e.accuracy;
  114. L.circleMarker(e.latlng,{radius:10,color:'hsl(125, 93%, 38%)'}).addTo(map);
  115. }
  116. map.on('locationfound', onLocationFound);
  117. // スケールの追加
  118. L.control.scale({
  119. imperial: false,
  120. metric: true
  121. }).addTo(map)
  122. const controlLayers = L.control.layers({}, {},{collapsed: false}).addTo(map)
  123. return { map: map, controlLayers: controlLayers }
  124. }
  125. // ベースマップ
  126. const initializeBaseMaps = (mainMap) => {
  127. const map = mainMap.map
  128. const controlLayers = mainMap.controlLayers
  129. // ベース地図の追加
  130. const osmDarkLayer = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
  131. attribution: 'Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL.',
  132. maxZoom: 22,
  133. maxNativeZoom: 18
  134. }).addTo(map)
  135. const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  136. attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
  137. maxZoom: 22,
  138. maxNativeZoom: 18
  139. })
  140. const gsiStdLayer = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
  141. attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html" target="_blank">地理院タイル</a>',
  142. maxZoom: 22,
  143. maxNativeZoom: 18
  144. })
  145. const gsiOrtLayer = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg', {
  146. attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html" target="_blank">地理院タイル</a>, Images on 世界衛星モザイク画像 obtained from site https://lpdaac.usgs.gov/data_access maintained by the NASA Land Processes Distributed Active Archive Center (LP DAAC), USGS/Earth Resources Observation and Science (EROS) Center, Sioux Falls, South Dakota, (Year). Source of image data product.',
  147. maxZoom: 22,
  148. maxNativeZoom: 8
  149. })
  150. const gsiblankLayer = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png', {
  151. attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html" target="_blank">地理院タイル</a>',
  152. maxZoom: 22,
  153. maxNativeZoom: 14
  154. })
  155. const baseMaps = {
  156. 'OpenStreetMap(Dark)': osmDarkLayer,
  157. 'OpenStreetMap': osmLayer,
  158. '国土地理院(標準)': gsiStdLayer,
  159. '国土地理院(写真)': gsiOrtLayer,
  160. '国土地理院(白地図)': gsiblankLayer
  161. }
  162. // レイヤ切り替えコントール設定
  163. Object.keys(baseMaps).map( key => {
  164. controlLayers.addBaseLayer(baseMaps[key], key)
  165. })
  166. return baseMaps
  167. }
  168. // パネルウインドウ
  169. const initializePanels = (mainMap) => {
  170. const map = mainMap.map
  171. // 情報パネル
  172. const addInfoPanel = (lmap) => {
  173. const info = L.control()
  174. const div = L.DomUtil.create('div', 'info')
  175. info.onAdd = (map) => {
  176. return div
  177. }
  178. info.update = (props) => {
  179. div.innerHTML = '<h4>Temperature</h4>' +
  180. (props ?
  181. '<b>' + props.queryDatetime + '<br />' + props.kjName + '<br />' + props.knName + '</b><br />' + props.temp + ' ℃'
  182. : 'Hover over polygon'
  183. )
  184. }
  185. info.addTo(lmap)
  186. return info
  187. }
  188. // 凡例パネル
  189. const addLegendPanel = (lmap) => {
  190. const legend = L.control({position: 'bottomright'})
  191. legend.onAdd = (map) => {
  192. const div = L.DomUtil.create('div', 'info legend')
  193. temparatureColorScale.map( item => {
  194. div.innerHTML +=
  195. '<i style="background:' + item.color + '"></i> ' +
  196. '&gt; ' + item.temp + '<br>'
  197. })
  198. div.innerHTML += '<i style="background:' + getTemparatureColor('N/A').color + ' opacity:0.0"></i> ' + 'N/A'
  199. return div
  200. }
  201. legend.addTo(lmap)
  202. return legend
  203. }
  204. const infoPanel = addInfoPanel(map)
  205. const legendPanel = addLegendPanel(map)
  206. return {info : infoPanel, legend: legendPanel}
  207. }
  208. // オーバーレイレイヤ
  209. const initializeOverlayMaps = (mainMap, amedas, infoPanel) => {
  210. const map = mainMap.map
  211. const controlLayers = mainMap.controlLayers
  212. // アメダス観測所レイヤ
  213. const amedasObservatoryLayer = L.featureGroup([], {
  214. attribution: '<a href="https://www.jma.go.jp/bosai/map.html#6/41.27/133.308/&elem=temp&contents=amedas&interval=60">気象庁「アメダス」を加工して作成</a>'
  215. })
  216. .on('add', (event) => {
  217. Object.keys(amedas.observatories).map( key => {
  218. const amedasItem = amedas.observatories[key]
  219. L.circle([amedasItem.lat, amedasItem.lon], {radius: 200, weight: 2}).addTo(event.sourceTarget).bindPopup(amedasItem.kjName)
  220. })
  221. })
  222. .addTo(map)
  223. // アメダス気象情報(気温)レイヤ
  224. const amedasTemperatureLayer = L.geoJSON( amedas.voronoiPolygons, {
  225. attribution: '<a href="https://www.jma.go.jp/bosai/map.html#6/41.27/133.308/&elem=temp&contents=amedas&interval=60">気象庁「アメダス」を加工して作成</a>',
  226. onEachFeature: (feature, layer) => {
  227. layer.on({
  228. add: (event) => {
  229. const layer = event.target
  230. const style = {'color': '#000000', 'weight': 0.5, 'opacity': 1, 'fillOpacity': 0.8 }
  231. const weather = amedas.weatherInfos[layer.feature.properties.key]
  232. if( weather && weather.temp ) {
  233. style.fillColor = getTemparatureColor(weather.temp[0]).color
  234. layer.feature.properties.temp = weather.temp[0]
  235. } else {
  236. style.fillOpacity = 0
  237. style.fillColor = getTemparatureColor(30).color
  238. layer.feature.properties.temp = 'N/A'
  239. }
  240. layer.originalStyle = style
  241. layer.setStyle(style)
  242. },
  243. mouseover: (event) => {
  244. const layer = event.target
  245. layer.setStyle({
  246. weight: 5,
  247. color: '#666666'
  248. })
  249. infoPanel.update(layer.feature.properties)
  250. if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
  251. layer.bringToFront()
  252. }
  253. },
  254. mouseout: (event) => {
  255. const layer = event.target
  256. layer.setStyle(layer.originalStyle)
  257. infoPanel.update()
  258. }
  259. })
  260. }
  261. })
  262. .addTo(map)
  263. const overlayMaps = {
  264. "アメダス観測所": amedasObservatoryLayer,
  265. "気温": amedasTemperatureLayer
  266. }
  267. // レイヤ切り替えコントール設定
  268. Object.keys(overlayMaps).map( key => {
  269. controlLayers.addOverlay(overlayMaps[key], key)
  270. })
  271. return overlayMaps
  272. }
  273. // メイン
  274. const main = async () => {
  275. const amedas = await fetchAmedasThenGenerateVoronoi({isClipping: true})
  276. const mainMap = initializeMainMap()
  277. const panels = initializePanels(mainMap)
  278. const baseMaps = initializeBaseMaps(mainMap)
  279. const overlayMaps = initializeOverlayMaps(mainMap, amedas, panels.info)
  280. }
  281. main()
  282. }())
  283. </script>
  284. <style>
  285. .info {
  286. padding: 6px 8px;
  287. font: 14px/16px Arial, Helvetica, sans-serif;
  288. background: white;
  289. background: rgba(255,255,255,0.8);
  290. box-shadow: 0 0 15px rgba(0,0,0,0.2);
  291. border-radius: 5px;
  292. }
  293. .info h4 {
  294. margin: 0 0 5px;
  295. color: #777777;
  296. }
  297. .legend {
  298. line-height: 18px;
  299. color: #555555;
  300. }
  301. .legend i {
  302. width: 18px;
  303. height: 18px;
  304. float: left;
  305. margin-right: 8px;
  306. opacity: 0.7;
  307. }
  308. </style>
  309.  

このコードは、Leaflet.jsを用いて日本のアメダス(地域気象観測システム)の気象データ(特に気温)を地図上に可視化するものです。ボロノイ図を用いて観測所ごとのエリアを描画し、気温に基づいた色でエリアを塗り分けています。以下に、各部分の詳細な説明を示します。


HTML部分

<div id="mapid" style="position:absolute;height: 100%;width: 100vw;"></div>
  • 地図を表示するための要素。id="mapid"がLeaflet.jsで参照されます。
  • スタイルでは、地図が画面全体に広がるよう設定されています。

JavaScript部分

1. 気温カラースケール定義

const temparatureColorScale = [
{ temp: 35, color: '#B40068' },
{ temp: 30, color: '#FF2800' },
...
]
  • 気温ごとに異なる色を定義します。高温は赤系、低温は青系で、摂氏-273.15度まで対応。
const getTemparatureColor = (temp) => {
const colorScale = temparatureColorScale.find(item => temp > item.temp);
if (!colorScale) return {temp: 'N/A', color: '#000000'};
return colorScale;
}
  • 指定した気温に対応する色を取得する関数。気温がスケール外の場合、黒色を返します。

2. アメダスデータの読み取りとボロノイ図生成

(1) 観測所データの取得
const fetchAmedasObservatories = async () => {
const url = "https://www.jma.go.jp/bosai/amedas/const/amedastable.json";
...
Object.keys(json).map(key => {
json[key].lat = amedas.lat[0] + (amedas.lat[1] / 60);
json[key].lon = amedas.lon[0] + (amedas.lon[1] / 60);
});
return json;
}
  • アメダス観測所の位置データ(緯度・経度)を取得し、度分表記を度単位に変換。
(2) 気象データの取得
const fetchAmedasWeatherInformations = async (queryDatetime) => {
const url = "https://www.jma.go.jp/bosai/amedas/data/map/" + queryDatetime + ".json";
const res = await fetch(url);
const json = await res.json();
return json;
}
  • 指定した日時のアメダス気象データを取得。
(3) ボロノイ図の生成
const generateGeoJsonAmedasVoronoiPolygons = async (amedas, option) => {
...
const voronoiPolygons = turf.voronoi(collection, {bbox: turf.bbox(turf.buffer(collection, 50, {}))});
...
}
  • Turf.jsを使用してボロノイ図を生成。
  • 観測所のデータをエリア(ポリゴン)に関連付けし、必要に応じて陸地部分だけにクリップ(切り取り)します。
(4) データ取得日時の生成
const generateDateimeString = () => {
const nowTime = new Date();
nowTime.setHours((new Date()).getHours() - 1);
return nowTime.getFullYear().toString() + ... + "0000";
}
  • 現在日時をフォーマットし、APIで利用可能な形式に変換。

3. Leaflet地図の初期化

const initializeMainMap = () => {
const map = L.map("mapid", { zoom: 18, minZoom: 3, worldCopyJump: "true" });
map.setView([35.6,139.8], 5);
...
}
  • 地図をid="mapid"に表示。
  • 初期位置は緯度35.6度、経度139.8度(東京都周辺)。
  • ユーザーの現在位置に基づいてズームインする機能も追加。

4. ベースマップの設定

const initializeBaseMaps = (mainMap) => {
const osmDarkLayer = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', ...);
...
}
  • OpenStreetMapや地理院地図などの異なる背景地図を設定。
  • ユーザーが地図を切り替えられるようにコントロールを追加。

5. パネルウィンドウの追加

const initializePanels = (mainMap) => {
const addInfoPanel = (lmap) => {
...
}
const addLegendPanel = (lmap) => {
...
}
}
  • 気温情報を表示する情報パネルと、気温カラースケールを示す凡例パネルを地図に追加。

6. オーバーレイレイヤ

const initializeOverlayMaps = (mainMap, amedas, infoPanel) => {
...
const amedasTemperatureLayer = L.geoJSON(amedas.voronoiPolygons, { ... });
}
  • ボロノイ図のポリゴンとアメダス観測所の位置をオーバーレイレイヤとして地図に描画。
  • 気温に応じてポリゴンの色を変化。

CSS部分

.info {
padding: 6px 8px;
background: rgba(255,255,255,0.8);
...
}
.legend {
line-height: 18px;
color: #555555;
}
  • 情報パネルと凡例パネルのスタイルを設定。

全体の流れ

  1. アメダスデータを取得し、観測所と気象データを整理。
  2. ボロノイ図を生成し、観測所ごとのエリアを作成。
  3. 地図を初期化し、ボロノイ図や観測所をオーバーレイレイヤとして追加。
  4. ユーザーが地図を操作しやすいように、情報パネルや凡例を表示。
地域気象観測システム(アメダス)

アメダスの概要

アメダス(AMeDAS)とは「Automated Meteorological Data Acquisition System」の略で、「地域気象観測システム」といいます。

雨、風、雪などの気象状況を時間的、地域的に細かく監視するために、降水量、風向・風速、気温、湿度の観測を自動的におこない、気象災害の防止・軽減に重要な役割を果たしています。

アメダス観測所の例

四要素(降水量、風向・風速、気温、湿度)と積雪深を観測

アメダスは1974年11月1日に運用を開始して、現在、降水量を観測する観測所は全国に約1,300か所(約17km間隔)あります。

このうち、約840か所(約21km間隔)では降水量に加えて、風向・風速、気温、湿度を観測しているほか、雪の多い地方の約330か所では積雪の深さも観測しています。


アメダスで観測したデータを初期値として、数値予報モデルで計算し予測することで、天気予報を発表しています。

天気サイトを作ることで、気象学に興味を持ったあなたは、是非気象予報士試験に挑戦してみて下さいね‼️

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA