iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0

醒來跑去申請氣象資料開放平臺的帳號,還真的解決問題了。
可能政府資料開放平臺給的Api只是測試用吧。
正常來說這個key不該曝光,要ignore一下。但既然接的只是開源資料而非機敏訊息,那倒還好。

import "./styles.css";
import "leaflet/dist/leaflet.css";
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
import { useState, useEffect } from "react";

import L from "leaflet";
L.Icon.Default.imagePath = "https://unpkg.com/leaflet/dist/images/";

export default function App() {
  const [name, setName] = useState("");
  const [rain, setRain] = useState(0);
  const [la, setLa] = useState(0);
  const [lo, setLo] = useState(0);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === "嘉義")[0]
            .StationName
      )
      .then((resJson) => setName(resJson))
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === "嘉義")[0]
            .RainfallElement.Now.Precipitation
      )
      .then((resJson) => setRain(resJson))
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === "嘉義")[0]
            .GeoInfo.Coordinates[1].StationLatitude
      )
      .then((resJson) => setLa(resJson))
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === "嘉義")[0]
            .GeoInfo.Coordinates[1].StationLongitude
      )
      .then((resJson) => setLo(resJson))
      .catch((err) => console.log(err));
  }, []);

  return (
    <MapContainer center={[23.6, 121]} zoom={8} scrollWheelZoom={true}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker position={[la, lo]}>
        <Popup>
          {name}, {rain}mm
        </Popup>
      </Marker>
    </MapContainer>
  );
}

進入最期待的環節:幫地圖加上雨勢動畫。
Leaflet的外掛不少,我本來想用Demo畫面看起來很美的Leaflet.Rain。
但卻遇上警告:Unable to determine current node version。
怎麼調整都像是渡不了這個難關,似乎也不能排除外掛本身有狀況的可能?
(Github上也看到有開發者回報的isuue還沒被解決)

可惡,我自己用SVG的SMIL Animation畫啦。

const bounds = [
    [23.5, 120.36],
    [23.6, 120.49],
  ];

<SVGOverlay bounds={bounds}>
    <defs>
      <symbol id="drop">
        <line stroke="#4ea6e9" strokeWidth="1%">
          <animate
            attributeName="x1"
            from="30"
            to="0"
            dur="1s"
            repeatCount="indefinite"
          />
          <animate
            attributeName="y1"
            from="0"
            to="60"
            dur="1s"
            repeatCount="indefinite"
          />
          <animate
            attributeName="x2"
            from="30"
            to="15"
            dur="1s"
            repeatCount="indefinite"
          />
          <animate
            attributeName="y2"
            from="0"
            to="30"
            dur="1s"
            repeatCount="indefinite"
          />
        </line>
      </symbol>
    </defs>
    <use xlinkHref="#drop" x="0" y="0" />
    <use xlinkHref="#drop" x="10%" y="0" />
    <use xlinkHref="#drop" x="20%" y="0" />
    <use xlinkHref="#drop" x="30%" y="0" />
    <use xlinkHref="#drop" x="40%" y="0" />
    <use xlinkHref="#drop" x="50%" y="0" />
    <use xlinkHref="#drop" x="60%" y="0" />
    <use xlinkHref="#drop" x="70%" y="0" />
    <use xlinkHref="#drop" x="80%" y="0" />
    <use xlinkHref="#drop" x="90%" y="0" />
    <use xlinkHref="#drop" x="0" y="45%" />
    <use xlinkHref="#drop" x="10%" y="45%" />
    <use xlinkHref="#drop" x="20%" y="45%" />
    <use xlinkHref="#drop" x="30%" y="45%" />
    <use xlinkHref="#drop" x="40%" y="45%" />
    <use xlinkHref="#drop" x="50%" y="45%" />
    <use xlinkHref="#drop" x="60%" y="45%" />
    <use xlinkHref="#drop" x="70%" y="45%" />
    <use xlinkHref="#drop" x="80%" y="45%" />
    <use xlinkHref="#drop" x="90%" y="45%" />
</SVGOverlay>

測試過程中自然也是踩坑連連。像是遇到namespace tag error,意識到是自己沒有把xlink:href改成駝峰式大小寫的xlinkHref;還有SVG動畫不讓我用百分比當單位等。
目前產出之雨勢效果也還不夠漂亮,而且太多重複。
因此動用了viewbox優化程式碼。

<SVGOverlay bounds={bounds}>
    <defs>
      <symbol id="drop" viewBox="0 -10 90 10">
        <line stroke="#4ea6e9" strokeWidth="1%">
          <animate
            attributeName="x1"
            from="30"
            to="0"
            dur="1s"
            repeatCount="indefinite"
          />
          <animate
            attributeName="y1"
            from="0"
            to="60"
            dur="1s"
            repeatCount="indefinite"
          />
          <animate
            attributeName="x2"
            from="30"
            to="15"
            dur="1s"
            repeatCount="indefinite"
          />
          <animate
            attributeName="y2"
            from="0"
            to="30"
            dur="1s"
            repeatCount="indefinite"
          />
        </line>
      </symbol>
    </defs>
    <use xlinkHref="#drop" x="0" y="0" />
    <use xlinkHref="#drop" x="10%" y="0" />
    <use xlinkHref="#drop" x="20%" y="0" />
    <use xlinkHref="#drop" x="30%" y="0" />
    <use xlinkHref="#drop" x="40%" y="0" />
    <use xlinkHref="#drop" x="50%" y="0" />
    <use xlinkHref="#drop" x="60%" y="0" />
    <use xlinkHref="#drop" x="70%" y="0" />
    <use xlinkHref="#drop" x="80%" y="0" />
    <use xlinkHref="#drop" x="90%" y="0" />
</SVGOverlay>

然後偷偷把圖資換成Carto上的好看主題。
至此,畫面已是相當還原。

做完視覺,不小心把歪腦筋動到資料上。
想說要讓測站定位可以完全依靠API傳來的經緯。

如果「直接」取用API獲得的資料,會發現MapContainer和Marker的position並不一致。這種時候就要引入useMap啦,可以設定初始視圖(setView);而bounds(雨勢動畫綁定的位置)則用標準的useState + useEffect處理。

不要忘記幫SVG加上Key值!
此時如果把程式碼中的測站名全都從嘉義改成另一個,就可以重新定位了——

再加上一個簡單的邏輯判斷:雨量大於0的時候秀出雨勢動畫,沒下就不顯示。用簡單的條件渲染就可以實現。

這張地圖好像稍微有點用處了。

Edit Leaflet

import "./styles.css";
import "leaflet/dist/leaflet.css";
import {
  MapContainer,
  Marker,
  Popup,
  SVGOverlay,
  TileLayer,
  useMap,
} from "react-leaflet";
import { useState, useEffect } from "react";

import L from "leaflet";
L.Icon.Default.imagePath = "https://unpkg.com/leaflet/dist/images/";

function ChangeView({ position }) {
  const map = useMap();
  useEffect(() => {
    map.setView(position);
  }, [position, map]);
  return null;
}

export default function App() {
  const [name, setName] = useState("");
  const [rain, setRain] = useState(0);
  const [la, setLa] = useState(0);
  const [lo, setLo] = useState(0);
  const [bounds, setBounds] = useState([
    [0, 0],
    [0, 0],
  ]);

  const position = [la, lo];

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === "嘉義")[0]
            .StationName
      )
      .then((resJson) => setName(resJson))
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === "嘉義")[0]
            .RainfallElement.Now.Precipitation
      )
      .then((resJson) => setRain(resJson))
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === "嘉義")[0]
            .GeoInfo.Coordinates[1].StationLatitude
      )
      .then((resJson) => setLa(resJson))
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === "嘉義")[0]
            .GeoInfo.Coordinates[1].StationLongitude
      )
      .then((resJson) => setLo(resJson))
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then((resJson) => {
        const station = resJson.records.Station.find(
          (s) => s.StationName === "嘉義"
        );
        const la = station.GeoInfo.Coordinates[1].StationLatitude;
        const lo = station.GeoInfo.Coordinates[1].StationLongitude;
        setBounds([
          [la, lo - 0.07],
          [la + 0.07, lo + 0.05],
        ]);
      })
      .catch((err) => console.log(err));
  }, []);

  return (
    <MapContainer center={position} zoom={12} scrollWheelZoom={true}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
      />
      <Marker position={position}>
        <Popup>
          {name}, {rain}mm
        </Popup>
      </Marker>
      <ChangeView position={position} />
      {rain > 0 && (
        <SVGOverlay key={JSON.stringify(bounds)} bounds={bounds}>
          <defs>
            <symbol id="drop" viewBox="0 -10 80 10">
              <line stroke="#4ea6e9" strokeWidth="1%">
                <animate
                  attributeName="x1"
                  from="30"
                  to="0"
                  dur="1s"
                  repeatCount="indefinite"
                />
                <animate
                  attributeName="y1"
                  from="0"
                  to="60"
                  dur="1s"
                  repeatCount="indefinite"
                />
                <animate
                  attributeName="x2"
                  from="30"
                  to="15"
                  dur="1s"
                  repeatCount="indefinite"
                />
                <animate
                  attributeName="y2"
                  from="0"
                  to="30"
                  dur="1s"
                  repeatCount="indefinite"
                />
              </line>
            </symbol>
          </defs>
          <use xlinkHref="#drop" x="0" y="0" />
          <use xlinkHref="#drop" x="10%" y="0" />
          <use xlinkHref="#drop" x="20%" y="0" />
          <use xlinkHref="#drop" x="30%" y="0" />
          <use xlinkHref="#drop" x="40%" y="0" />
          <use xlinkHref="#drop" x="50%" y="0" />
          <use xlinkHref="#drop" x="60%" y="0" />
          <use xlinkHref="#drop" x="70%" y="0" />
          <use xlinkHref="#drop" x="80%" y="0" />
          <use xlinkHref="#drop" x="90%" y="0" />
        </SVGOverlay>
      )}
    </MapContainer>
  );
}

順帶一提:我發現隔壁生態圈也有Vue Leaflet,不愧是挑戰者。


上一篇
【Day28】React Leaflet 1
下一篇
【Day30】React Leaflet 3
系列文
【現在學React還來得及嗎?】30天Takeaway分享30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言