讓我們來做個互動天氣地圖吧!(直播筆記)

這次的直播要做一個互動地圖,當滑鼠滑過的時候顯示當地天氣資訊!

這次的直播內容主要有幾個部分:

  1. 取得地圖的 svg 的檔案,並修改成我們可以用的檔案類型。
  2. 取得台灣的地圖資料,包含行政區域名稱與代號,並把資料對應到地圖上。
  3. 做出互動的頁面,包含滑鼠滑過時的移動、變色,還有右側的資料顯示。

筆記會著重在 3. 程式實作的部分,並把相關的資料與圖片連同程式碼一起放在 github 上面,供大家參考💻。

要跟著影片一起做也沒問題:


程式實作

主要會用到的工具與知識:

  1. HTML, CSS 與 JavaScript 的基礎觀念
  2. svg 的基本操作
  3. Vue.js 框架的操作
  4. 使用 axios 串接中央氣象局 open data API

讀取地圖,並操作樣式

要在我們的頁面讀取 svg 有幾種方法,使用 <img> tag 讀取檔案,或是直接把 <svg> 包覆的內容直接貼在 html 裡面,但是如果直接讀取檔案的話就沒有辦法對 svg 進行操作,所以我們選擇後者。

svg 檔案的格式跟 html 很像,都是一層一層的往下排列,不過相對於 html 裡面的內容是 <html> 包覆所有物件,svg 則是在 <svg> 包覆繪圖軟體裡面使用的「群組」<g>、「路徑」<path> 、「線段」<line> 或是 <ploygon><circle> 圖形等元件。

讀進 svg 的 html 大概會長這樣:

<html>

<head>
...
</head>

<body>
    <svg data-name="圖層 1" xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 595.28 841.89">
        <defs />
        <title>
            image.svg
        </title>
        <path id="161d ... 09.27h0Z" />
        ...
        <path id="41139c2a ... 1.22-1.29Z" />
    </svg>
</body>

</html>

我們先在 CSS 加上一些樣式與高度的限制,才不會讓圖片太大。另外把 svg 裡面的 path 元件加上顏色跟滑鼠移過(hover)時的變化。

這邊要注意,用 css 操作 svg 顏色的方式跟一般 dom 元件的方式不太一樣,如果要改變 path 的填色,不能用 background,而是要用 fill,線條的話不是 border,而是 stroke 喔!想要知道更多 svg 的屬性與使用方式可以參考這個 CSS-TRICK 的整理:SVG Properties and CSS

:root {
    --color-gold: #B99362;
}

body {
    background-color: #222
}

svg {
    height: 100vh;
}


path {
    stroke: white;
    fill: transparent;
    transition: 0.5s;
    cursor: pointer;
}

path:hover {
    fill: var(--color-gold);
    transform: translate(-5px, -5px);
}

到這邊,我們就有一個 hover 時會變色與區域浮起來的地圖囉!

篩選滑鼠移動過地區的地理資料

緊接著我們需要把地圖畫面跟資料連在一起,當滑動過某個地區的時候,要先知道現在滑鼠在哪一個縣市,才有辦法在畫面上顯示相對應的地理資訊~

我們先觀察一下 svg 裡面各個縣市 <path> 的結構,以台北市為例:

<path id="1e48e0bb-8964-4121-b347-b900162cf771" data-name="taipei_city" class="96fdfe13-4732-40bb-9e9c-cdc6e310fcb9" d="M466.27,77.17,465.42,79l-.85.85-.24.49-.85,1v1.83l-1.22.73L462,85.47l.49,1.59,1.22.85,3.9.49,2.44,2.32,1,1.83.12,5.61-1.83,2.56-1.22,1.1-.61,1.34.37,3.54.73,1.46,1.46-.12,1.34-.73.85-1.22.85-.12,2,.85,1.1.85.49,1.34v1.83l.85,1.46,1.58,1.1,1.71.12.73,2.93,2.56,1.71,6.83.73,1.46-.61h2l.12-1.22-3.29-1.59-.24-1.71.12-1.46-.85-2.8v-1.58l.85-1.34,1.22-.73,3.41.24,1.1-.73,1.46-.37,4.63.24.37-1-1.1-.49-1.58.49L497.86,103l-3.29-2.44L494,99.25l.85-1.1-.24-1.34-1-1,.73-.85,1.59-3.29-.37-3.17L490.29,84l.37-1.59,1-1.1-.37-1.22-2-1.71-.73-1.1-.12-3.54-2-2V70l.49-1.34V66.81l-3.17.24-1.34-2.44-1-1.1-1.71,2.68-1.34.61-.61,1.34-2.56,1.46-.61,1.59-1.1.73-1.71.12-1.34.61-2.07,2.44-.61,1.59-1.59.48h0Z" />

發現其實可以直接在 <path> 裡面加上自己定義的 data-* attribute,如此一來,就可以用類似 jQuery 的 attr() 方法,甚至是透過 DOM element 的 data 屬性取得這個名字的內容。

舉例而言,我們可以在直接在瀏覽器加上某個縣市的 onmouseover 監聽器,並在滑鼠移動的時候印出地圖位置的 data-name 的值,就可以這樣做:

// 使用原生 JavaScript 的寫法
// 先抓取台北市的 path 物件
const el_taipei_city = document.getElementById('1e48e0bb-8964-4121-b347-b900162cf771')
// 加上監聽器,打印出我們要的 data-* attribute 內容
el_taipei_city.onmouseover = function({console.log(this.dataset.name)}
// 結果 -> taipei_city

有了地圖的資訊之後,我們只要拿著這個地點的名稱去比對現有的天氣資料,就可以把相對應的資料放到畫面上了。地理資訊的資料型態長這樣:

var place_data=[
  {
   tag: "taipei_city",
   place: "臺北市",
   low: 16,
   high: 24,
   weather: "Rainy"
  },
  {
   tag: "new_taipei_city",
   place: "新北市",
   low: 15,
   high: 22,
   weather: "Rainy"
  }
  ...
]

假設拿著 taipei_city 字串,想要從 place_data 取得整個台北市的物件要怎麼做呢? 我們可以用 for 迴圈,一個一個比對,但是其實 JavaScript 有提供我們更簡潔的寫法,就是陣列的 filter 方法,我們可以直接回傳陣列裡面中,判斷函式結果為 true 的物件。

像是這樣:Do re mi so ~

current_place_obj = place_data.filter((obj)=>obj.tag === 'taipei_city')[0]

因此,我們可以很快速的獲得 tag 是 taipei_city 的整個物件,要小心 filter 方法回傳的也是一個陣列,所以如果要取值的話,要取第0項喔。

將資料用 Vue 綁定,並渲染在畫面上

這邊比較需要注意的地方有兩個,第一個是需要在組件 mounted 的時候把所有的 <path> tag 加上滑鼠移動的監聽器,當滑鼠移動的時候,就更新當前選擇到的 data-name 屬性更新到組件的 filter 資料上。第二個是可以利用 Vue 的 computed 計算屬性即時根據 filter 的變動即時更新要顯示當前資料的物件 now_area

const app = new Vue({
    el: '#app',
    mounted() {
        paths = document.querySelectorAll('path');
        let _this = this // 把這個 vm 本身存在 _this,以供後續函式內部使用
        paths.forEach(e => {
            e.onmouseover = function () {
                _this.filter = this.dataset.name
            }
        })
    },
    data: () => {
        return {
            filter: '',
            place_data: null,
        }
    },
    computed: {
        now_area() {
            let result = place_data.filter((obj) => obj.tag === this.filter)
            if (result.length == 0) {
                return null
            } else {
                return result[0]
            }
        }
    },
})

最後快速的加上標題與內容的樣式,就完成這次的作品了!🎉🎉🎉

加碼小單元-串接中央氣象局的 API 顯示實際的溫度與氣象

我們選擇使用中央氣象局氣象資料開放平台提供的資料,先註冊帳號後到這個網址:https://opendata.cwb.gov.tw/user/authkey,點擊下圖中的取得授權碼之後右邊就會出現你的 API token,要好好保管他這份資料呦。之後只要使用這個 token 就可以通行無阻的拿到我們需要的資料了~

開發指南可以看到呼叫 API 的規範大致上是長這樣:

※ URL: https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/{dataid}?Authorization={apikey}&format={format}
                
{dataid} 為各資料集代碼 (參照:資料清單)  ex.F-A0012-001
                
{apikey} 為會員帳號對應之授權碼  ex.CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678
                
{format} 為資料格式,請參照各資料集頁面確認可下載之檔案格式  ex.XML、CAP、JSON、ZIP、KMZ、GRIB2
                
※ 範例:https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/F-A0012-001?Authorization=CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678&format=XML
                
並請加入快取功能,如上述所示。

因為我們需要的是以JSON格式顯現的鄉鎮天氣預報-台灣未來1週天氣預報,因此呼叫的規格大概是這樣: let url = 'https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/F-D0047-091?Authorization=你的API_token&downloadType=WEB&format=JSON'

我們使用 axios 的 get 方法拿取資料看看 axios.get(url).then(data => {console.log(data)}),可以拿到這個樣子的資料,打開之後發現我們需要的就是在 dataset -> locations 裡面的 location 陣列資料。

再往下看可以看出他的結構,locationName 是縣市名稱,descriptionName 是這個數值的名稱,這才發現,原來氣象局的資料還會依照時段區分,我們尋求最簡單的作法,直接抓離現在最近的時段就好。

weatherElement 裡面有很多資料提供的數值,如果不清楚意思的話也可以查閱檔案的欄位說明表,雖然文件通常又臭又長,但是好好讀一下都能省下不少開發的時間。最複雜的解析部分已經完成,接下來只要把資料源跟篩選方式改成從api抓回來的資料就可以了!

接下來先把 call 到的資料存到 data 的 weather_data 中,另外把 filter 方法換成 find,因為我們只需要第一個符合條件的結果。把回傳的資料格式修整一下,符合原本的資料型態就可以直接呈現囉~

mounted() {
    axios.get(url).then(data => {
        console.log(data)
        this.weather_data = data.data.cwbopendata.dataset.locations.location
    })
...
now_area() {
    let data = {}
    let result = this.weather_data.find((obj) => {
        return obj.locationName === this.filter
    })
    
    if (result) {
        let high = result.weatherElement.find(el => el.elementName === 'MaxT').time[0].elementValue.value
        let low = result.weatherElement.find(el => el.elementName === 'MinT').time[0].elementValue.value
        let weather = result.weatherElement.find(el => el.elementName === 'Wx').time[0].elementValue[0].value
        data = {
            place: this.filter,
            low: low,
            high: high,
            weather: weather
        }
    }
    return data
}

這樣就大功告成了!專案的原始碼可以在這邊查看:https://github.com/Monoame-Design/bosscoding-examples

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

PHP Code Snippets Powered By : XYZScripts.com