Django Mapbox

      在〈Django Mapbox〉中尚無留言

 

網頁中顯示世界地圖,可以使用 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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *