醒來跑去申請氣象資料開放平臺的帳號,還真的解決問題了。
可能政府資料開放平臺給的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='© <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的時候秀出雨勢動畫,沒下就不顯示。用簡單的條件渲染就可以實現。
這張地圖好像稍微有點用處了。
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='© <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,不愧是挑戰者。