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

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


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

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

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

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

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

<script>    
        <div id="mapid" style="position:absolute;height: 100%;width: 100vw;"></div>
<script>

(async function() { 

// 気温カラースケール定義
const temparatureColorScale = [
  { temp: 35, color: '#B40068' },
  { temp: 30, color: '#FF2800' },
  { temp: 25, color: '#FF9900' },
  { temp: 20, color: '#FFF500' },
  { temp: 15, color: '#FFFF96' },
  { temp: 10, color: '#FFFFF0' },
  { temp: 5, color: '#B9EBFF' },
  { temp: 0, color: '#0096FF' },
  { temp: -5, color: '#0041FF' },
  { temp: -273.15, color: '#002080' }
]

// 気温カラーの取得
const getTemparatureColor = (temp) => {
  const colorScale = temparatureColorScale.find(item => {
    return temp > item.temp 
  })

  if( !colorScale ) return {temp: 'N/A', color: '#000000'}

  return colorScale
}


// アメダスデータの読み取り
const fetchAmedasThenGenerateVoronoi = async (option) => {

  // アメダス観測所データの読み取り
  const fetchAmedasObservatories = async () => {
    const url = "https://www.jma.go.jp/bosai/amedas/const/amedastable.json"
    const amedasObservatories = {}
    
    const res = await fetch(url)
    const json = await res.json()

    Object.keys(json).map( key => {
      const amedas = json[key]
      json[key].lat = amedas.lat[0] + (amedas.lat[1] / 60)
      json[key].lon = amedas.lon[0] + (amedas.lon[1] / 60)
    })

    return json
  }

  // アメダス気象情報データの読み取り
  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
  }


  // アメダス観測所からボロノイポリゴンを作成
  const generateGeoJsonAmedasVoronoiPolygons = async (amedas, option) => {
    const points = []
    Object.keys(amedas).map( key => {
      const item = amedas[key]
      points.push(turf.point([item.lon, item.lat],{'key': key, 'kjName': item.kjName, 'knName': item.knName, 'alt': item.alt}))
    })
    const collection = turf.featureCollection(points)
    const voronoiPolygons = turf.voronoi(collection, {bbox: turf.bbox(turf.buffer(collection, 50, {}))})

    datetime = option.queryDatetime.match( /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/ )
    // アメダス観測所ポイントとボロノイポリゴンの内外判定を行い、観測所データをポリゴンへ移植
    points.map( point => {
      voronoiPolygons.features.some( polygon => {
        if( turf.booleanPointInPolygon(point, polygon) === true ){
          polygon.properties.key = point.properties.key
          polygon.properties.kjName = point.properties.kjName
          polygon.properties.knName = point.properties.knName
          polygon.properties.alt = point.properties.alt
          polygon.properties.lon = point.geometry.coordinates[0]
          polygon.properties.lat = point.geometry.coordinates[1]
          polygon.properties.queryDatetime = datetime[1] + '-' + datetime[2] + '-' + datetime[3] + ' ' + datetime[4] + ':' + datetime[5] + ':' + datetime[6]
          return true
        }
        return false
      })
    })

    // ボロノイ図を陸地でクリッピング
    if( option.isClipping === true) {
      const dissolvePolygons = turf.dissolve(turf.buffer(collection, 30, {steps: 2}))
      voronoiPolygons.features.map( voronoiPolygon => {
        dissolvePolygons.features.some( dissolvePolygon => {
          const intersectPolygon = turf.intersect(voronoiPolygon, dissolvePolygon)
          if (!intersectPolygon){
            return false
          } 
          voronoiPolygon.geometry = intersectPolygon.geometry
          return true
        })
      })
    }

    return voronoiPolygons
  }

      // 現在日時からアメダス気象情報取得パラメータを生成
  const generateDateimeString = () => {
    const nowTime = new Date()
    nowTime.setHours((new Date()).getHours() -1)
    //nowTime.setMonth((new Date()).getMonth() -4)
    return nowTime.getFullYear().toString() + (nowTime.getMonth() + 1).toString().padStart(2, '0')  + nowTime.getDate().toString().padStart(2, '0')  + nowTime.getHours().toString().padStart(2, '0')  + "0000"
  }

  const queryDatetime = generateDateimeString()

  const amedasObservatories = await fetchAmedasObservatories()
  const amedasWeatherInfos =  await fetchAmedasWeatherInformations(queryDatetime)
  const voronoiPolygons = await generateGeoJsonAmedasVoronoiPolygons(amedasObservatories, {isClipping: option.isClipping, queryDatetime: queryDatetime})

  return {observatories: amedasObservatories, weatherInfos: amedasWeatherInfos, voronoiPolygons: voronoiPolygons, queryDatetime: queryDatetime }
}

// Leaflet地図初期化
const initializeMainMap = () => {

  const map = L.map("mapid", L.extend({
    zoom: 18,
    minZoom: 3,
    worldCopyJump: "true"
  }))

  map.setView([35.6,139.8], 5)

  map.locate({setView: true, maxZoom: 9});
    function onLocationFound(e) {
    var radius = e.accuracy;
    L.circleMarker(e.latlng,{radius:10,color:'hsl(125, 93%, 38%)'}).addTo(map);
  }
  map.on('locationfound', onLocationFound);


  // スケールの追加
  L.control.scale({
    imperial: false,
    metric: true
  }).addTo(map)

  const controlLayers = L.control.layers({}, {},{collapsed: false}).addTo(map)

  return { map: map, controlLayers: controlLayers }
}

// ベースマップ
const initializeBaseMaps = (mainMap) => {
  const map = mainMap.map
  const controlLayers = mainMap.controlLayers

  // ベース地図の追加
  const osmDarkLayer = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
    attribution: 'Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL.',
    maxZoom: 22,
    maxNativeZoom: 18
  }).addTo(map)

  const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
    maxZoom: 22,
    maxNativeZoom: 18
  })

  const gsiStdLayer = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
    attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html" target="_blank">地理院タイル</a>',
    maxZoom: 22,
    maxNativeZoom: 18
  })

  const gsiOrtLayer = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg', {
    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.',
    maxZoom: 22,
    maxNativeZoom: 8
  })

  const gsiblankLayer = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png', {
    attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html" target="_blank">地理院タイル</a>',
    maxZoom: 22,
    maxNativeZoom: 14
  })

  const baseMaps = {
    'OpenStreetMap(Dark)': osmDarkLayer,
    'OpenStreetMap': osmLayer,
    '国土地理院(標準)': gsiStdLayer,
    '国土地理院(写真)': gsiOrtLayer,
    '国土地理院(白地図)': gsiblankLayer
  }

  // レイヤ切り替えコントール設定
  Object.keys(baseMaps).map( key => {
    controlLayers.addBaseLayer(baseMaps[key], key)
  })

  return baseMaps
}


// パネルウインドウ
const initializePanels = (mainMap) => {
  const map = mainMap.map
  // 情報パネル
  const addInfoPanel = (lmap) => {
    const info = L.control()
    const div = L.DomUtil.create('div', 'info')      

    info.onAdd = (map) => {
      return div
    }
    info.update = (props) => {
      div.innerHTML = '<h4>Temperature</h4>' +  
        (props ?
          '<b>' + props.queryDatetime + '<br />' + props.kjName + '<br />' + props.knName + '</b><br />' + props.temp + ' ℃'
          : 'Hover over polygon'
        )
    }
    info.addTo(lmap)  

    return info
  }

  // 凡例パネル
  const addLegendPanel = (lmap) => {
    const legend = L.control({position: 'bottomright'})
    legend.onAdd = (map) => {

      const div = L.DomUtil.create('div', 'info legend')
      temparatureColorScale.map( item => {
        div.innerHTML +=
              '<i style="background:' + item.color + '"></i> ' +
              '&gt; ' + item.temp + '<br>'
      })
      div.innerHTML += '<i style="background:' + getTemparatureColor('N/A').color + '  opacity:0.0"></i> ' + 'N/A'
      return div
    }
    legend.addTo(lmap)
    return legend 
  }

  const infoPanel = addInfoPanel(map)
  const legendPanel = addLegendPanel(map)

  return {info : infoPanel, legend: legendPanel}
}


// オーバーレイレイヤ
const initializeOverlayMaps = (mainMap, amedas, infoPanel) => {
  const map = mainMap.map
  const controlLayers = mainMap.controlLayers

  // アメダス観測所レイヤ
  const amedasObservatoryLayer = L.featureGroup([], {
    attribution: '<a href="https://www.jma.go.jp/bosai/map.html#6/41.27/133.308/&elem=temp&contents=amedas&interval=60">気象庁「アメダス」を加工して作成</a>'
  })
  .on('add', (event) => {
    Object.keys(amedas.observatories).map( key => {
      const amedasItem = amedas.observatories[key]
      L.circle([amedasItem.lat, amedasItem.lon], {radius: 200, weight: 2}).addTo(event.sourceTarget).bindPopup(amedasItem.kjName)
    })
  })
  .addTo(map)

  // アメダス気象情報(気温)レイヤ
  const amedasTemperatureLayer = L.geoJSON( amedas.voronoiPolygons, {
    attribution: '<a href="https://www.jma.go.jp/bosai/map.html#6/41.27/133.308/&elem=temp&contents=amedas&interval=60">気象庁「アメダス」を加工して作成</a>',
    onEachFeature: (feature, layer) => {

      layer.on({
        add: (event) => {
          const layer = event.target

          const style = {'color': '#000000', 'weight': 0.5, 'opacity': 1, 'fillOpacity': 0.8 }
          const weather = amedas.weatherInfos[layer.feature.properties.key]
          if( weather && weather.temp ) {
            style.fillColor = getTemparatureColor(weather.temp[0]).color
            layer.feature.properties.temp = weather.temp[0]
          } else  {
            style.fillOpacity = 0
            style.fillColor = getTemparatureColor(30).color
            layer.feature.properties.temp = 'N/A'
          }
          layer.originalStyle = style

          layer.setStyle(style)
        },
        mouseover: (event) => {
          const layer = event.target

          layer.setStyle({
              weight: 5,
              color: '#666666'
          })

          infoPanel.update(layer.feature.properties)

          if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
              layer.bringToFront()
          }          
        },
        mouseout: (event) => {
          const layer = event.target
          layer.setStyle(layer.originalStyle)

          infoPanel.update()

        }
      })
    }
  })
  .addTo(map)

  const overlayMaps = {
    "アメダス観測所": amedasObservatoryLayer,
    "気温": amedasTemperatureLayer
  }

  // レイヤ切り替えコントール設定
  Object.keys(overlayMaps).map( key => {
    controlLayers.addOverlay(overlayMaps[key], key)
  })

return overlayMaps

}

// メイン
const main = async () => {
  const amedas = await fetchAmedasThenGenerateVoronoi({isClipping: true})
  const mainMap = initializeMainMap()
  const panels = initializePanels(mainMap)
  const baseMaps = initializeBaseMaps(mainMap)
  const overlayMaps = initializeOverlayMaps(mainMap, amedas, panels.info)
}

main()

}())

</script>

<style>
.info {
  padding: 6px 8px;
  font: 14px/16px Arial, Helvetica, sans-serif;
  background: white;
  background: rgba(255,255,255,0.8);
  box-shadow: 0 0 15px rgba(0,0,0,0.2);
  border-radius: 5px;
}
.info h4 {
  margin: 0 0 5px;
  color: #777777;
}

.legend {
  line-height: 18px;
  color: #555555;
}
.legend i {
  width: 18px;
  height: 18px;
  float: left;
  margin-right: 8px;
  opacity: 0.7;
}
</style>


このコードは、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