寫在前頭
完成六角學院「JavaScript 入門篇 - 學徒的試煉」,一定能看懂這系列文章八成以上,因為我也是在這個學習進度後完成口罩地圖的。
正如前一篇所說,在這裡你目前不會看到太深入的技術探討,與其說是教大家如何寫出口罩地圖,更趨近於,將我自己的實作過程留下記錄,但因為我一向秉持洧杰老師強調的,找自己「看得懂的程式碼」來參考,所以我使用的語法確實相對易懂,我預計用幾篇來記錄這個主題,如果你迫不及待想要知道我如何實作,也可以直接到我的 GitHub 參考原始碼。
本篇記錄,會講到利用 AJAX 將藥局資料倒入地圖中,並標上 marker,以及如何使用定位按鈕取得使用者的位置。
載入地圖
這裡,我們要使用的是 Leaflet 搭配 OpenStreetMap 來開發,首先你要先載入 Leaflet 的 CSS 及 JavaScritp。
將以下這一段放進 HTML 的 <head>
區塊內:
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""/>
以下這段依照官方指引,必須確保放在前一段的後方,我則是把它放在 <body>
區塊裡 </body>
的前方。
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
crossorigin=""></script>
接著在 HTML 區塊裡建立一個 <div id="map"></div>
(id 命名可隨意更改,我這邊是直接命名為 map),而為了之後更方便調整版面,我在外面又包了一個 <div class="container"></div>
結構如下:
<div class="container">
<div id="map"></div>
</div>
接下來,我們建立一個 all.js 檔案,並把它引入 HTML,你在官方指引裡一樣可以找到這些內容,請在 all.js 裡面新增以下內容:
var map = L.map('map').setView([51.505, -0.09], 13);
// ('map') 要記得改成自己命名的 id 才不會錯誤
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// contributors 後方可加入自己的名字及網址,如 GitHub 網址或個人網頁網址
上面的程式碼 setView([51.505, -0.09], 13) 以白話文解釋,[51.505, -0.09] 是你地圖中心的座標位置,而 13 則是縮放程度,數值越大代表放大程度越高,數值越小則相反,但有其極限,可以依照自己的需求測試看看。
接著我們新增一個 style.css 檔案,我們必須將 #map
設定大小,才能讓圖資正確顯示,不過為了之後響應式的考量,我從官方指引的範例 #mapid { height: 180px; }
改成如下:
html,body{
width: 100%;
height: 100%;
}
.container{
width: 100%;
height: 100%;
}
#map{
width: 100%;
height: 100%;
}
由於是相對數值,所以以上三個元素都要設定寬高,否則,地圖會呈現一片空白,無法正常載入,地圖載入之後,我們會發現上下左右都留有一些空間,此時只要設定 CSS Reset 即可,我們就會得到一個全螢幕的地圖了。
當然,若是你習慣看繁體中文,我們也可以讓地圖回到台灣,只要在 Google Map 搜尋 台北 101,在網址列上,你就可以拿到座標了。
將 all.js 裡的以下內容填上我們得到的座標,我們就來到台灣了!
var map = L.map('map').setView([25.033976, 121.5623502], 13);
歡迎光臨台灣~XDDDD
取得全國藥局資料
我們運用 AJAX 來取得全國藥局資料,資料來源是江明宗大大整合政府資料開的 API。
let data;
function getData(){
const xhr = new XMLHttpRequest;
xhr.open('get','https://raw.githubusercontent.com/kiang/pharmacies/master/json/points.json',true)
xhr.send(null);
xhr.onload = function(){
data = JSON.parse(xhr.responseText).features;
addMarker();
}
}
將 data 取至全域變數這一步非常重要!
將 data 取至全域變數這一步非常重要!
將 data 取至全域變數這一步非常重要!
因為很重要,所以要說三遍,這一步可以讓我們在往後持續取用 data 裡的資料,之前就是不知道,所以卡了好久,因為洧杰老師的直播特訓班(AJAX 與函式應用教學)才幫助我脫離苦海~
接著老師在影片中提到使用 init 函式,讓網頁載入時可以預設執行 init 裡的函式。(附上相關討論)
// 1.新增 init 函式,讓網頁載入時可以預設執行 init 裡的函式
function init(){
getData();
}
let data;
function getData(){
const xhr = new XMLHttpRequest;
xhr.open('get','https://raw.githubusercontent.com/kiang/pharmacies/master/json/points.json',true)
xhr.send(null);
xhr.onload = function(){
data = JSON.parse(xhr.responseText).features;
// 3.這是我們下一步要用的函式,為地圖上的藥局加上 marker
addMarker();
}
}
// 2.別忘了執行 init
init();
function addMarker(){
// 4.在地圖上加上 marker 前,先看看資料有沒有載入成功
console.log(data);
}
打開開發人員工具,等一下下,看到資料進來,就代表成功了。
在地圖標上 marker
接著,我們在地圖標上 marker 之前,先訂義我們要的 marker 顏色。(marker 專案 leaflet-color-markers)
// 1.定義 marker 顏色,把這一段放在 getData() 前面
let mask;
// 2.我們取出綠、橘、紅三個顏色來代表口罩數量的不同狀態
const greenIcon = new L.Icon({
iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
// 3.只要更改上面這一段的 green.png 成專案裡提供的顏色如:red.png,就可以更改 marker 的顏色
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
const orangeIcon = new L.Icon({
iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-orange.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
const redIcon = new L.Icon({
iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
定義完 marker 顏色之後,我們就要運用 addMarker 函式,將 marker 根據每一間藥局的座標,標至地圖上的對應位置。(API 的資料使用說明)
function addMarker(){
for(let i = 0;i<data.length;i++){
// 1.由於我們已能存取 data 裡的資料,所以我們就按照 API 的使用說明來取用資料
const pharmacyName = data[i].properties.name;
const maskAdult = data[i].properties.mask_adult;
const maskChild = data[i].properties.mask_child;
const lat = data[i].geometry.coordinates[1];
const lng = data[i].geometry.coordinates[0];
// 2.下判斷式,依據不同的口罩數量,來顯示不同的 marker 顏色
if(maskAdult == 0 || maskChild == 0){
mask = redIcon;
}else if (maskAdult < 100 && maskAdult !== 0 || maskChild < 100 && maskChild !== 0){
mask = orangeIcon;
}else{
mask = greenIcon;
}
// 3.最後,將 marker 標至地圖上
L.marker([lat,lng], {icon: mask}).addTo(map);
}
}
然後你就會得到密密麻麻的 marker!
收納 marker
為了避免密集恐懼症發作效能的考量,我們馬上來使用另一個套件 Leaflet.markercluster!把 marker 收納起來~
我們先在 HTML 的 <head>
裡將套件的 CSS 引用:
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.css"></link>
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css"></link>
在 </body>
的前方引用套件的 JavaScript:
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script>
引用完之後,我們來調整一下我們的 all.js,首先在 function addMarker() 前放上這一段:
const markers = new L.MarkerClusterGroup({ disableClusteringAtZoom: 18 }).addTo(map);
接著將 function addMarker() 原本的這一段,
L.marker([lat,lng], {icon: mask}).addTo(map);
修改為:
markers.addLayer(L.marker([lat,lng], {icon: mask}).bindPopup());
在 function addMarker() 結尾前補上:
map.addLayer(markers);
修改後,完整的 function addMarker() 長這樣~
function addMarker(){
for(let i = 0;i<data.length;i++){
const pharmacyName = data[i].properties.name;
const maskAdult = data[i].properties.mask_adult;
const maskChild = data[i].properties.mask_child;
const lat = data[i].geometry.coordinates[1];
const lng = data[i].geometry.coordinates[0];
if(maskAdult == 0 || maskChild == 0){
mask = redIcon;
}else if (maskAdult < 100 && maskAdult !== 0 || maskChild < 100 && maskChild !== 0){
mask = orangeIcon;
}else{
mask = greenIcon;
}
markers.addLayer(L.marker([lat,lng], {icon: mask}).bindPopup());
}
map.addLayer(markers);
}
接著來看看成果~
你可以看到很隱約的黑色數字,滑鼠游標移上去,會有 hover 效果,點擊之後會展開如下圖。
到這邊已經成功收納 marker 了,接著我們來補上一些 CSS 來改善視覺效果。在我們的 style.css 裡新增以下的程式碼。
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}
.container.leaflet-right {
right:0px;
}
千萬別焦慮,為什麼我知道要加上這一段,因為這只是套件預設的 CSS,專案上面就找得到 CSS 檔案~ 你只要視自己的需求微調即可,成果如下,真的是清楚多了~
最後,我們在這一段的 bindPopup() 裡用 ES6 的語法加上一些內容:
markers.addLayer(L.marker([lat,lng], {icon: mask}).bindPopup(`<h3>${pharmacyName}</h3>`));
這樣,我們點擊地圖上的 marker 時,就會出現該藥局的名稱~
到這邊,口罩地圖的雛型已經完成了!
找到自己的定位,並標上 marker
接著來到本文的最後一個 part,如何找到自己的人生定位。這一段我真的卡了好一段時間,後來靠著兩段影片,自己實作出了這個功能,解掉這個困境的時候,真的是通體舒暢啊!!!
先附上影片連結,能夠看這個影片,一部分是影片本身淺顯易懂,另一部分也是英文學習的成果,不過這算支線任務了,我之後會再寫文談談這一塊~
• 2.2 Geolocation Web API - Working with Data and APIs in JavaScript
• 1.5 Mapping Geolocation with Leaflet.js - Working with Data and APIs in JavaScript
總之,從上面影片你可以抓到一個關鍵字 Geolocation,事不宜遲,我們就來看看該怎麼寫這個功能~
我們將原本的定位程式碼:
const map = L.map('map').setView([25.033976, 121.5623502], 13);
改成如下:
const map = L.map('map').setView([0, 0], 16);
用意是先將定位歸零,等獲取使用者位置時,再去修改 setView 中 [0, 0] 裡的數值,此時,畫面會呈現一片藍…
先別慌!這不是我們搞砸了,只要把畫面縮小,我們就會發現我們來到了非洲的西部~
接著,我們先定義代表使用者 marker 的色彩,並新增到地圖上。
const violetIcon = new L.Icon({
iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-violet.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
const marker = L.marker([0, 0] , {icon:violetIcon}).addTo(map);
結果如下:
接著,我們來定位使用者位置。
if ('geolocation' in navigator) {
// 如果定位可以運行,就印出 'geolocation available'
console.log('geolocation available');
// 取得使用者位置的經緯度
navigator.geolocation.getCurrentPosition(position => {
userLat = position.coords.latitude;
userLng = position.coords.longitude;
// 印出使用者位置的經緯度
console.log(userLat, userLng);
// 以使用者的經緯度取代 [0, 0]
map.setView([userLat, userLng], 13);
// 在使用者所在位置標上 marker
marker.setLatLng([userLat,userLng]).bindPopup(
`<h3>你的位置</h3>`)
.openPopup();
});
} else {
// 如果定位無法運行,就印出 'geolocation not available'
console.log('geolocation not available');
}
用瀏覽器預覽,會跳出視窗詢問你是否允許存取你的位置資訊,這邊我們可以看到 marker 仍然是在非洲西部的海上。
按下「允許」後,我們就可以發現使用者定位改變了。此時,開發人員工具裡的 Console 裡也會印出 geolocation available 及使用者的經緯度位置。
這邊一點小提醒,電腦版偶爾會發生定位不準確的情況,搜尋相關討論得到一些說法:「因為電腦因為沒有定位相關的硬體,所以定位通常只靠 IP 來判斷」,因此不準確是很正常的,相較之下,手機的定位會精確得多。
最後,我們要來新增一個「定位按鈕」,當我們滑動 map 時,只要點擊這顆按鈕,就可以跳到使用者的位置。
第一步,我們先在 HTML 新增一個按鈕:
<div id="map"></div>
<input type="button" class="GeoBtn" id="jsGeoBtn" value="定位鈕">
接著,設定 CSS 讓按鈕可以固定在地圖上。
.GeoBtn{
position: fixed;
top: 80px;
z-index: 999;
}
最後,我們用 JavaScript 來監聽按鈕,只要點擊,就可以抓到目前使用者的位置。
let geoBtn = document.getElementById('jsGeoBtn');
geoBtn.addEventListener('click',function(){
map.setView([userLat, userLng], 13);
marker.setLatLng([userLat,userLng]).bindPopup(
`<h3>你的位置</h3>`)
.openPopup();
},false);
我們先故意把畫面移動到別處:
此時,只需要點擊「定位鈕」,就可以回到使用者的位置了!
以上,就是本文要探討的主要內容,我們下一篇再見~