2022-11-06

在智能手錶上搜尋香港巴士路線及預計到站時間

巴士是香港常見的公共交通工具,在下都經常乘搭
雖然各大巴士公司都有提供預計抵達應用程式,但由於頻繁的廣告,非常影響操作
而且最近在下較多使用智能手錶,但發現各大巴士公司都沒有製作支援手錶的預計抵達應用程式
如果能夠使用手錶查看資料會更方便,除了可以減省使用電話來查看資料,而且只需要翻一翻手便可以查看資料

巴士應用程式介面

由於各大巴士公司的預計抵達的資料已經公開 應用程式界面 (Application Programming Interface (API))
並開放資料給第三方人士開發非官方應用程式,資料能在 https://data.gov.hk/ 找到

在下使用九巴的API為例子

巴士路線列表數據
{
	"type": "RouteList",
	"version": "1.0",
	"generated_timestamp": "2022-01-01T08:00:00+08:00",
	"data": [
		{
			"route": "1",
			"bound": "O",
			"service_type": "1",
			"orig_en": "CHUK YUEN ESTATE",
			"orig_tc": "竹園邨",
			"orig_sc": "竹园邨",
			"dest_en": "STAR FERRY",
			"dest_tc": "尖沙咀碼頭",
			"dest_sc": "尖沙咀码头"
		},
		{......},
		.
		.
		.
	]
}
假設將傳回的資料保存到變數 json 中
  • json.type 為 資料的類型,暫時為 RouteList
  • json.version 為 資料的版本,暫時為 1.0
  • json.generated_timestamp 為 建立此資料的時間,格式為 [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}
  • json.data 為 巴士路線陣列資料
    • json.data[n].route 為 巴士路線編號
    • json.data[n].bound 為 巴士路線方向, I 或 O
    • json.data[n].service_type 為 巴士路線類型,通常路線為 1 ,其他路線為 2 或以上整數
    • json.data[n].orig_en 為 巴士路線起點英文名稱
    • json.data[n].orig_tc 為 巴士路線起點正體中文名稱
    • json.data[n].orig_sc 為 巴士路線起點簡體中文名稱
    • json.data[n].orig_en 為 巴士路線終點英文名稱
    • json.data[n].orig_tc 為 巴士路線終點正體中文名稱
    • json.data[n].orig_sc 為 巴士路線終點簡體中文名稱

巴士車站數據
  • $stop_id 為 有效的巴士車站編號,參考 巴士車站列表數據 的 json.data[n].stop
{
	"type": "Stop",
	"version": "1.0",
	"generated_timestamp": "2022-01-01T08:00:00+08:00",
	"data": {
		"stop": "18492910339410B1",
		"name_en": "CHUK YUEN ESTATE BUS TERMINUS",
		"name_tc": "竹園邨總站",
		"name_sc": "竹园邨总站",
		"lat": "22.345415",
		"long": "114.192640"
	}
}
假設將傳回的資料保存到變數 json 中
  • json.type 為 資料的類型,暫時為 StopList
  • json.version 為 資料的版本,暫時為 1.0
  • json.generated_timestamp 為 建立此資料的時間,格式為 [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}
  • json.data 為 巴士陣列資料
    • json.data.stop 為 16位16進制編號 巴士車站編號
    • json.data.name_en 為 巴士車站英文名稱
    • json.data.name_tc 為 巴士車站正體中文名稱
    • json.data.name_sc 為 巴士車站簡體中文名稱
    • json.data.lat 為 巴士車站的緯度
    • json.data.long 為 巴士車站的經度

巴士路線-巴士車站數據
  • $route 為 有效的巴士路線編號,參考 巴士路線列表數據 的 json.data[n].route
  • $bound 為 有效的巴士路線方向,參考 巴士路線列表數據 的 json.data[n].bound ,I 使用 inbound ,O 使用 outbound
  • $service_type 為 有效的巴士路線類型,參考 巴士路線列表數據 的 json.data[n].service_type
{
	"type": "StopList",
	"version": "1.0",
	"generated_timestamp": "2022-01-01T08:00:00+08:00",
	"data": [
		{
			"route": "1",
			"bound": "O",
			"service_type": "1",
			"seq": "1",
			"stop": "18492910339410B1"
		},
		{......},
		.
		.
		.
	]
}
假設將傳回的資料保存到變數 json 中
  • json.type 為 資料的類型,暫時為 RouteStop
  • json.version 為 資料的版本,暫時為 1.0
  • json.generated_timestamp 為 建立此資料的時間,格式為 [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}
  • json.data 為 巴士路線與巴士車站陣列資料
    • json.data.route 為 巴士路線編號
    • json.data.bound 為 巴士路線方向, I 或 O
    • json.data.service_type 為 巴士路線類型,通常路線為 1 ,其他路線為 2 或以上整數
    • json.data.seq 為 巴士車站於該巴士路線的次序
    • json.data.stop 為 16位16進制編號 巴士車站編號

巴士預計到達時間數據(巴士車站)
  • $stop_id 為 有效的巴士車站編號,參考 巴士車站列表數據 的 json.data[n].stop
{
	"type": "StopETA",
	"version": "1.0",
	"generated_timestamp": "2022-01-01T08:00:00+08:00",
	"data": [
		{
			"co": "KMB",
			"route": "1",
			"dir": "O",
			"service_type": 1,
			"seq": 1,
			"dest_tc": "尖沙咀碼頭",
			"dest_sc": "尖沙咀码头",
			"dest_en": "STAR FERRY",
			"eta_seq": 1,
			"eta": "2022-01-01T08:00:00+08:00",
			"rmk_tc": "",
			"rmk_sc": "",
			"rmk_en": "",
			"data_timestamp": "2022-01-01T08:00:00+08:00"
		},
		{......},
		.
		.
		.
	]
}
假設將傳回的資料保存到變數 json 中
  • json.type 為 資料的類型,暫時為 StopETA
  • json.version 為 資料的版本,暫時為 1.0
  • json.generated_timestamp 為 建立此資料的時間,格式為 [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}
  • json.data 為 巴士路線與巴士車站資料
    • json.data[n].co 為 巴士公司代號
    • json.data[n].route 為 巴士路線編號
    • json.data[n].dir 為 巴士路線方向, I 或 O
    • json.data[n].service_type 為 巴士路線類型,通常路線為 1 ,其他路線為 2 或以上整數
    • json.data[n].seq 為 巴士車站 於 該巴士路線 的 次序
    • json.data[n].dest_tc 為 巴士路線終點正體中文名稱
    • json.data[n].dest_sc 為 巴士路線終點簡體中文名稱
    • json.data[n].dest_en 為 巴士路線終點英文名稱
    • json.data[n].eta_seq 為 巴士抵達巴士車站的次序
    • json.data[n].eta 為 巴士抵達巴士車站的時間,格式為 [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}
    • json.data[n].rmk_tc 為 正體中文備註
    • json.data[n].rmk_sc 為 簡體中文備註
    • json.data[n].rmk_en 為 英文備註
    • json.data[n].data_timestamp 為 巴士抵達巴士車站的資料更新時間,格式為 [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}

編寫預計抵達程式

由於 JavaScript 具備拆解 JSON 的功能,配合 HTML 便可以做到簡單的互動操作,而且不需要額外安裝特殊軟件便可以編寫
另外應用時只需要使用網頁瀏覽器便可以執行應用程式,便可以電腦、電話、手錶、不同作業系統都能夠使用

////////// javascript.js //////////

function loadRoutes(select) {
	var xhr = new XMLHttpRequest();
	xhr.addEventListener("readystatechange", function (eventReadyStateChange) {
		if (this.readyState == XMLHttpRequest.DONE && this.status == 200) {
			while (select.options.length > 0) {
				select.remove(select.options[0]);
			}
			select.appendChild(document.createElement("option"));
			var json = JSON.parse(this.responseText);
			for (var i in json.data) {
				if (json.data[i].service_type > 1) {
					json.data[i].service_name = "特別班次";
				} else {
					json.data[i].service_name = "";
				}
				var option = document.createElement("option");
				option.value = ["KMB", json.data[i].route, json.data[i].bound, json.data[i].service_type].join(",");
				option.textContent = ["九巴", json.data[i].route, json.data[i].dest_tc, json.data[i].service_name].join(";");
				select.appendChild(option);
			}
		}
	});
	xhr.open("GET", "https://data.etabus.gov.hk/v1/transport/kmb/route", false);
	xhr.send();
}

function loadStops(select, co, route, bound, service_type) {
	bound = ((bound == "I") ? "inbound" : "outbound");
	var xhr = new XMLHttpRequest();
	xhr.addEventListener("readystatechange", function (eventReadyStateChange) {
		if (this.readyState == XMLHttpRequest.DONE && this.status == 200) {
			while (select.options.length > 0) {
				select.remove(select.options[0]);
			}
			select.appendChild(document.createElement("option"));
			var json = JSON.parse(this.responseText);
			for (var i in json.data) {
				var option = document.createElement("option");
				option.value = ["KMB", json.data[i].stop].join(",");
				option.textContent = [["第", json.data[i].seq, "站"].join(""), getStop(co, json.data[i].stop).name_tc].join(";");
				select.appendChild(option);
			}
		}
	});
	xhr.open("GET", ["https://data.etabus.gov.hk/v1/transport/kmb/route-stop", route, bound, service_type].join("/"), false);
	xhr.send();
}

function getStop(co, stop_id) {
	var stop = {};
	var xhr = new XMLHttpRequest();
	xhr.addEventListener("readystatechange", function(event) {
		if (this.readyState == XMLHttpRequest.DONE && this.status == 200) {
			var json = JSON.parse(this.responseText);
			stop = json.data;
		}
	});
	xhr.open("GET", ["https://data.etabus.gov.hk/v1/transport/kmb/stop", stop_id].join("/"), false);
	xhr.send();
	return stop;
}

function loadETA(etaContainers, co, stop_id, route, bound, service_type) {
	var xhr = new XMLHttpRequest();
	xhr.addEventListener("readystatechange", function(event) {
		if (this.readyState == XMLHttpRequest.DONE && this.status == 200) {
			var json = JSON.parse(this.responseText);
			var index = 0;
			var now = new Date();
			for (var i in json.data) {
				if (json.data[i].eta != null && json.data[i].eta != "" && json.data[i].route ==route && json.data[i].dir == bound && json.data[i].service_type == service_type) {
					var text = ["第", json.data[i].eta_seq, "班"];
					var seconds = parseInt((new Date(json.data[i].eta) - now) / 1000);
					var hours = parseInt(seconds / 3600);
					var seconds = seconds % 3600;
					var minutes = parseInt(seconds / 60);
					var seconds = seconds % 60;
					if (seconds <0) {
						seconds *= -1;
						text.push("超時");
					} else {
						text.push("還有");
					}
					if (hours > 0) { text.push(hours + "小時"); }
					if (minutes > 0) { text.push(minutes + "分鐘"); }
					if (seconds >= 0) { text.push(seconds + "秒"); }
					etaContainers[index].innerHTML = text.join("");
					index++;
				}
			}
			while (index < etaContainers.length) {
				etaContainers[index].innerHTML = "";
				index++;
			}
		}
	});
	xhr.open("GET", ["https://data.etabus.gov.hk/v1/transport/kmb/stop-eta", stop_id].join("/"), false);
	xhr.send();
}

<!DOCTYPE html>
<html lang="zh-yue-HK">
<!-- ////////// index.html ////////// -->
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, minimum-scale=1"/>
		<title>HK Bus for Watch</title>
		<script src="javascript.js"></script>
		<script>
//<!--
window.addEventListener("load", function(event) {
	var routeSelect = document.getElementById("route");
	var stopSelect = document.getElementById("stop");

	loadRoutes(routeSelect);

	routeSelect.addEventListener("input", function(eventInput) {
		var routeValues = this.value.split(",");
		loadStops(stopSelect, routeValues[0], routeValues[1], routeValues[2], routeValues[3]);
	});

	window.setInterval(function() {
		var routeValues = routeSelect.value.split(",");
		var stopValues = stopSelect.value.split(",");
		loadETA(document.getElementById("eta-container").getElementsByTagName("div"), stopValues[0], stopValues[1], routeValues[1], routeValues[2], routeValues[3]);
	}, 1000);
});
//-->
		</script>
	</head>
	<body>
		<select id="route">
		</select>
		<select id="stop">
		</select>
		<div id="eta-container">
			<div></div>
			<div></div>
			<div></div>
		</div>
	</body>
</html>

見下文
見下文
見下文
見下文
這並非最理想的效果,但可以選擇顯路線、車站及顯示預計抵達
由於在下以每1秒更新時間,因此預計抵達時間能顯示及準確至秒
但實際的抵達時間仍然受交通情況、天氣等因素影響

編寫手錶程式介面

見下文
見下文
在下將測試網頁上載到 https://hkgoldenmra.bitbucket.io/html5-hkbus-for-watch/
由於要編寫手錶程式的介面使用純 HTML 的介面,會有很大限制,而且不夠美觀
因此在下還將 SVG 嵌入至 HTML 中,讓介面更加貼合錶面限制
例如錶面通常使用圓形LED熒幕,SVG 就能方便地製作圓形的圖案
(SVG 可以使用 Inkscape 等開源向量圖軟件製作,可以更加精美)

見下文
在手錶上的實際效果
與在電腦或電話顯示的效果略有分別,而且手錶上並沒有太多網頁瀏覽器可以選擇
由於在下使用 Samsung 的手錶,因此在下在 Play Store 選擇使用 Samsung Internet Browser ,感覺比較安全

總結

見下文
各巴士公司的API操作大致相同,但「大致相同」亦即是有不同
九巴的路線及車站資料沒有提供 co 屬性的公司縮寫名稱,而城巴及新巴有提供
九巴的路線及車站資料有提供路線方向及服務類型,而城巴及新巴沒有,但城巴新巴搜尋車站時卻需要提供方向
九巴雖然有傳回路線方向的資料,但只是 I 或 O ,而需要提供方向參數搜尋資料時卻需要使用 inbound 或 outbound ,城巴及新巴同樣使用 inbound 或 outbound
九巴搜尋路線顯示的路線方向的資料屬於名稱是 bound ,但搜尋預計抵達時間的資料屬性名卻是 dir

雖然資料是從政府的 data.gov.hk 獲取,但單單是 bound 及 dir 已經顯示相同公司的資料沒有統一
枉論不同公司的資料更加沒有一致的結構
城巴及新巴的API頁面,基本的例子都中沒有提供,參數有錯誤卻沒有任何提示

非官方的巴士預計抵達應用程式,其實已經一早有人製作,閣下有興趣可以到 https://hkbus.app/
由於該網頁的目標使用裝置是智能電話,但智能手錶的熒幕比智能電話小,並不適合智能手錶使用
但如果閣下使用電腦或電話查看預計抵達時間,又不希望觀看巴士公司的廣告, hkbus.app 是非常適合取代巴士公司的應用程式

在下非常討厭這類查找資料的工具需要安裝特定應用程式才能使用
還只能在智能電話上使用,反而功能全面的電腦無法使用
而且智能電話應用程式還需要向平台公司申請將應用程式上架,需要額外成本
如果將來平台有的使用條款有變動,又要重新編寫,甚至需要學習新的程式語言
要支援不同平台又要多重開發,開發時間又要延長發佈時間
如果只是使用 WebView 將網頁包裝成應用程式,到不如直接使用網頁來得更方便,不需要依靠平台
如果有收費服務,網頁還不需要被平台分成

參考資料

沒有留言 :

張貼留言