我很菜,所以只會用原生 JS 跟 CSS 寫「口罩地圖 」Ep.01


Posted by ABow_Chen on 2020-05-23

寫在前頭

完成六角學院「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: '&copy; <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);

我們先故意把畫面移動到別處:

此時,只需要點擊「定位鈕」,就可以回到使用者的位置了!

以上,就是本文要探討的主要內容,我們下一篇再見~

資料補充


#口罩地圖 #javascript #css #六角學院 #台灣口罩地圖- 2020 防疫要贏 #JavaScript 入門篇 - 學徒的試煉







Related Posts

Day3 安裝資料庫吧 ! FireBase!

Day3 安裝資料庫吧 ! FireBase!

uuko
可以請你說明一下,前端的資料跟後端是如何交換的嗎?

可以請你說明一下,前端的資料跟後端是如何交換的嗎?

JnTng
使用 ROS 與 Gazebo 模擬一個自動避障機器人

使用 ROS 與 Gazebo 模擬一個自動避障機器人

Po-Jen
[ week 1 & 4 ] 網路基礎概論-🔗

[ week 1 & 4 ] 網路基礎概論-🔗

vick12052002
[DAY9] 初學 Git (下)

[DAY9] 初學 Git (下)

生菜
Day 06 遠交近攻

Day 06 遠交近攻

Justin900429


Comments