網頁中顯示世界地圖,可以使用 MapBox 所提供的圖資。請先到 https://mapbox.com 註冊帳號,然後取得access token。註冊不需付費,但需輸入信用卡號。
MapBox javascript 的官方教學網及最新版本查詢網址為 https://docs.mapbox.com/mapbox-gl-js/guides/。
Mapbox 費付方式
免費額度(Free Tier)
-
網頁地圖載入(Map Loads for Web):每月前 50,000 次免費。
-
靜態或瓦片 API 請求(Static/Vector Tiles API):每月前 200,000 次免費。
-
移動裝置月活躍使用者(Mobile MAUs):每月最高 25,000 名使用者免費。
超出免費額度之後
-
Map Loads(網頁地圖載入)
-
超過 50,000 次後,每額外 1,000 次收費約 5 美元(隨使用量增加,費率逐級下降)。
-
-
Directions、Geocoding、Vector Tiles API
-
Directions API 超過某範圍後:每 1,000 次約 2 美元。
-
Vector Tiles API:200,001 次以上,每 1,000 次約 0.25 美元。
-
-
Mobile SDK(iOS/Android)使用月活躍使用者計費
-
超過免費 MAU 後,按使用者數計費。也可能依 SDK 版本搭配 API 使用量一起計價
-
使用心得
所謂「網頁地圖載入」,是指在 javascript 中執行 new mapboxgl.Map({…}),這樣就算一次。而每月前 50,000 次不用付費,一天也要載入 1,600 多次才可能超出額度。
而 Tiles (瓦片)請求是指離開原本區域重繪地圖,一張地圖約要 24 塊圖磚,還好瓦片額度有 200,000 片。
總結以上,這比 Google Map 動不動就要收錢好太多了。
所以要超出 Mapbox 免費額度蠻困難的,除非您是撰寫 app 供大量的用戶使用。如果真的超出免費額度,一定是利用這個圖資賺了不少錢,付點小錢給 Mapbox 也是不痛不癢。
本人在很早時期就註冊 Mapbox 帳號,那時還不用填入信用卡號。所以就算超出額度,Mapbox 應該也沒辦法扣到本人的錢。
初始化地圖
開啟新的 mapbox.html,內容如下
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 載入 mapbox 函數及 css -->
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.js"></script>
<!-- 加入圖資語言切換外掛 -->
<script src='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-language/v1.0.0/mapbox-gl-language.js'></script>
</head>
<style>
#map{
height:100%;
width:100%;
}
</style>
<body>
<div id="map"></div>
<script>
const map=initMap();
function initMap(){
let lng=120.5163944;
let lat=24.0603655;
mapboxgl.accessToken = '你的 access token';
let map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',//12的版本中文才正常
projection: 'globe',
zoom: 12,
center: [lng, lat]
});
//將圖資切換成中文
map.addControl(new MapboxLanguage({
defaultLanguage:'zh-Hant'
}));
return map;
}
</script>
</body>
</html>
html 需注意的地方如下 :
- map 區塊必需設定寬高,否則整個區塊會縮起來而看不到地圖。
- access token 請至官網申請。
- 中文切換請加入上述藍色部份代碼。
- 如果access_token 由 Django 模板傳入,需寫成 mapboxgl.accessToken = “{{mapbox_access_token}}”,前後都需加雙引號。
軌跡路線
軌跡路線採用 Ajax 的方式,向伺服器查詢經緯座標後,再傳回 javascript 進行繪製。
Python 查詢經緯座標,請在 urls.py 新增如下
path("load_routes/", mapbox.load_routes),
然後在 views.py 新增如下藍色代碼。
import json
from django.http import HttpResponse
from django.shortcuts import render
from G import G
# Create your views here.
def html(request):
return render(
request,
"mapbox.html",
{"mapbox_access_token":G.mapbox_access_token}
)
def load_routes(request):
conn, cursor=G.connect("ip", "帳號", "密碼", "cloud")
cursor.execute("select lng, lat from 旅遊軌跡 order by eventTime")
rs=cursor.fetchall()
conn.close()
routes=[[lng, lat] for lng, lat in rs]
return HttpResponse(
json.dumps({'routes':routes}),
content_type="application/json"
)
mapbox.html 模板如下。請注意載入路徑 loadRoutes() 需寫在 map.once(‘load’) 內。
map.on(‘style.load’) 表示載入地圖樣式後要執行的任務,第一次切換樣式會執行一次,第二次切換會疊加執行二次,以此類推。而 map.once(‘style.load’) 則只會執行一次,不會疊加。
map.on(‘load’) 表示地圖樣式、初始化等動作全部完成才觸發。通常只有獨發一次的機會。
<script>
const map=initMap();
animations=[];
const dashArraySequence = [[0, 4, 3],[0.5, 4, 2.5],[1, 4, 2],[1.5, 4, 1.5],[2, 4, 1],[2.5, 4, 0.5],[3, 4, 0],[0, 0.5, 3, 3.5],[0, 1, 3, 3],[0, 1.5, 3, 2.5],[0, 2, 3, 2],[0, 2.5, 3, 1.5],[0, 3, 3, 1],[0, 3.5, 3, 0.5]];
//要指定在 map.once 後才開始動作。地圖未 ready 就開始畫線,會出錯。
map.on('load', () => {
loadRoutes(today);
});
function initMap(){
//同上
}
function loadRoutes(){
url="/load_routes";
conn=connector(url);
conn.onreadystatechange=function(){
if(conn.readyState==4 && conn.status==200){
routes=JSON.parse(conn.responseText)["routes"];
draw_routes(routes);
let cx=routes[routes.length-1][0];
let cy=routes[routes.length-1][1];
map.flyTo({
center: [cx, cy],
essential:true
});
}
};
conn.send();
}
function draw_routes(lonlat){
map.addLayer({
id: "_background",
type: "line",
source: {
type: "geojson",
data: {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates: lonlat
}
}
},
paint: {
'line-color': 'blue',
'line-width': 6,
'line-opacity': 0.5
}
});
map.addLayer({
id: "_dashed",
type: "line",
source: {
type: "geojson",
data: {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates: lonlat
}
}
},
paint: {
'line-color': '#ffff00',
'line-width': 3,
'line-dasharray': [0, 4, 3]
}
});
let step = 0;
animation(1000);
function animation(timestamp) {
newStep = parseInt((timestamp / 50) % dashArraySequence.length);
if (newStep !== step) {
map.setPaintProperty(
"_dashed",
'line-dasharray',
dashArraySequence[step]
);
step = newStep;
}
timer=requestAnimationFrame(animation);
animations.push(timer);
}
}
function connector(url){
if (window.XMLHttpRequest) conn=new XMLHttpRequest();
else conn=new ActiveXObject("Microsoft.XMLHTTP");
conn.open("GET", url, true);
return conn;
}
</script>
loadRoutes() 是繪制路徑的函數,一定要寫在 map.on 區塊。如果省略此區塊,會造成地圖尚未載入就開始繪製路徑而發生錯誤。
map.on('style.load', () => {
loadRoutes(today);
});
繪制地標 Marker
在 python 的 views.py 取得資料庫地標的資料,如下所示。
def load_markers(request):
conn, cursor=G.connect("ip", "帳號", "密碼", "travel")
cmd = f"select * from 旅遊案件 where eventDay = '{day}' and userId=1 order by eventTime"
cursor.execute(cmd)
rows = cursor.fetchall()
markers=[]
for row in rows:
year=row[7].strftime("%Y")
url=f'{year}/{day}/{row[10]}'
markers.append([row[3],row[4], row[6], f'{row[7]} {row[8]}', row[9], url])
return HttpResponse(
json.dumps({'markers': markers}),
content_type = "application/json"
)
最後 urls.py 裏增如下網址
path("load_markers/", mapbox.load_markers),
mapbox.html 模板將 css style 改成如下,然後將 icon1.png、icon2.png、icon3.png 複制到 static/images 目錄下。
<style>
#map{
height:100%;
width:100%;
}
.marker1 {
background-image: url('/static/images/icon1.png');
background-size: cover;
width: 15px;
height: 30px;
border-radius: 50%;
cursor: pointer;
}
.marker2 {
background-image: url('/static/images/icon2.png');
background-size: cover;
width: 15px;
height: 30px;
border-radius: 50%;
cursor: pointer;
}
.marker3 {
background-image: url('/static/images/icon3.png');
background-size: cover;
width: 15px;
height: 30px;
border-radius: 50%;
cursor: pointer;
}
.photo{
width:600px;
height: 337px;
}
table{
border: 1px solid #0000ff;
border-collapse:collapse;
margin-top:10px;
}
tr{
border:1px solid #000000;
}
td{
border:1px solid #000000;
padding:1px;
}
</style>
loadRoutes 加入藍色一行
function loadRoutes(){
.......
map.flyTo({
center: [cx, cy],
essential:true
});
loadMarkers();
.........
}
然後新增如下函數
function loadMarkers(){
url="/load_markers";
let conn=connector(url);
conn.onreadystatechange=function(){
if (conn.readyState==4 && conn.status==200){
markers=JSON.parse(conn.responseText)["markers"];
console.log(markers.length);
for (i=0;i<markers.length;i++){
addMarker(markers[i]);
}
}
}
conn.send();
}
function addMarker(marker){
const el = document.createElement('div');
if (marker[4]=='風景')
el.className = 'marker1';
else if (marker[4]=='人物')
el.className = 'marker2';
else
el.className = 'marker3';
let m = new mapboxgl.Marker(el, {offset: [0, -15]})
.setLngLat([marker[0], marker[1]])
.addTo(map);
//點選 m 後才會載入圖片
//popup 是獨立的圖層物件,沒有跟 m 綁定,所以還要設定 lnglat, 也要獨自加入 map
m.getElement().addEventListener('click', () => {
const popup = new mapboxgl.Popup({ offset: 25 })
.setLngLat(m.getLngLat())
.setHTML(`
<table>
<tr>
<td width='50px'>日期</td>
<td width="40%">${marker[3]}</td>
<td width='50px'>類別</td>
<td width="40%">${marker[4]}</td>
</tr>
<tr>
<td width='50'>經度</td>
<td>${marker[0]}</td>
<td width='50'>緯度</td>
<td>${marker[1]}</td>
</tr>
<tr>
<td width='50'>地址</td>
<td colspan='3'>${marker[2]}</td>
</tr>
<tr>
<td colspan='4'>
<a href="https://mahalbot.ddns.net/travel_photo/${marker[5]}" target="_blank">
<img src="https://mahalbot.ddns.net/travel_photo/${marker[5]}" class="photo"/>
</a>
</td>
</tr>
</table>
`)
.addTo(map)
.setMaxWidth("650px");
});
}
新增預設 marker
使用 mapboxgl.Marker 產生新的圖標,然後傳入 coord [經度, 緯度] 座標,再 addTo(map) 即可。
function addMarker(coord){
const marker = new mapboxgl.Marker({ color: 'blue', rotation: 0 })
.setLngLat([coord[0], coord[1]])
.addTo(map);
}
todo
