在Google地圖設移動式雷達,呈現半徑內的特定目標

online
示意圖

Code is poetry.

WordPress.org

可別以為我也在抓寶。敝人數月來日夜忙忙忙,無暇寫文;目前預計在接近年底時恢復發文。

這篇在計劃之外。它是為回應一位讀者的提問而寫,因需較大篇幅,所以獨立刊出。程式設計是這個部落格的附設攤位,我無意把它變成寫文的主線(況且,三十多年來,我在程式設計領域始終只是個業餘玩票的)。

該讀者希望以程式做到以下的效果:點選Google的My Map上的某地點,藉此在地圖上顯示該點半徑1000公尺內的其它My Map地點。本文先解決這個問題,然後順便介紹另一種功能:點選Google Maps上的任一個點,藉以顯示特定半徑內的My Map地點。為易於辨識,我將這兩種稱為「特定場址式」與「漫遊式」。

以下先呈現第一種,特定場址式。圖內是台灣全國的民航機場地點。若點選任一個地標,則可呈現半徑100公里內的其它機場。建議您先點選台南、台東:

第一階段:準備資料

就我以前所知,雖然我們可以利用Google Map API來顯示My Map的內容,但標記等等的經緯度資料其實是留在Google的伺服器,沒自動傳輸到終端機器(即用戶端)。掃描Google Maps API的說明、另迅速網搜一番,我未見改變。所以,我還是採用我所知的手動解決方式:將欲使用的My Map資料以KML格式匯出。如何匯出?請參閱Google的這篇說明。Google的My Map提供的匯出格式有KMZ與KML。KMZ較麻煩,我們在此不管它。

KML是XML家族的一員。我將下載的KML檔案之附檔名改為xml,免得未來的程式解讀出現問題。

然後,把這個KML檔案上傳到自己的網站。

第二階段:基本佈置

先將以下這一行放在網頁的body區塊內。視您的需要而調整其中的style設定。


<div id="map-canvas" style="width: 640px; height: 540px; margin: 0; padding:0;"></div>

然後在head區塊中加入這兩行:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?key=AIzaSyBeA7WvHxLMHMKQ2Cn92Q2kNbQZ9U4E-xk&libraries=geometry"></script>

第1行是載入jQuery。網址可改為其它來源。

特別注意:將第2行內的YOUR_API_KEY取代為您自己的API金鑰。若未有API金鑰,請向Google申請。只要有Google帳戶,即可免費取得,而且不到五分鐘就可搞定。請參閱Google的相關說明

此外,請勿刪改YOUR_API_KEY後面的&libraries=geometry。其作用是加掛距離計算工具。

第三階段:程式

最後,在head區塊中加入以下程式,並視需要改變設定,尤其是襯灰底的四行:

<script type="text/javascript">
//KML檔案(網址)
var kmlFile = 'kml.xml';

var g = google.maps;
var map, clickPoint, clickMarker, clickCircle;
//地圖中心座標
var mapCenter = new g.LatLng(23.972583, 120.979361);
//地圖縮放比例(數值越大,細節越清楚)
var mapScale = 7;
//搜尋半徑,單位為公尺
var rad = 100000;
	
//以圓圈顯示搜尋範圍;true=是;false=否
var circleDisplay = true;
//圓周顏色
var circleOutlineColor = '#c0c0c0';
//圓內顏色
var circleFillColor = '#e0e0e0';

//顯示點擊處;true=是;false=否
var clickPointDisplay = true;

//目標資料、目標圖示
var topoMarker = [];
//範圍外的目標圖案;若是ic='',則恢復Google Map預設標誌 
var icon = 'https://www.google.com/mapfiles/marker_white.png';
//範圍內的目標圖案 
var iconTarget = 'https://www.google.com/mapfiles/marker_green.png';
//點選的目標圖案
var iconCenter = 'https://www.google.com/mapfiles/marker_purple.png';
	
//是否顯示地圖說明視窗。true=是;false=否
var infoWindowDisplay = true;
//是否自動關閉地圖說明視窗。true=是;false=否
var infoWindowAutoClose = true;
//自動關閉地圖說明視窗預設時間;計時單位為千分之一秒
var infoWindowTimer = 3000;
//地圖總說明文字
var infoWindowText = '請點觸任一標記';
var w = new g.InfoWindow({
	content: infoWindowText, position: mapCenter,
})

//讀取KML檔,製作地點標示,儲存為JSON
function getKML(){
	$.get(kmlFile, function(data){
		//逐筆掃描地點資料
		$(data).find("Placemark").each(function(q){
			//取得地點名稱
			name = $(this).find("name").text();
			//取得地點座標名稱
			coords = $(this).find("coordinates").text();
			//解析座標經緯度
			var c =[];
			c = coords.split(",");
			lng = c[0];
			lat = c[1];
			//將經緯度設為Google Map Class
			topoc = new g.LatLng(lat, lng);
			//將資料儲存 as JSON
			var marker = new g.Marker({
				map: map,
				position: topoc,
				icon: icon,
				title: name
			});
			attachSensor(marker);
			//將此地標加入地標集合
			topoMarker.push(marker);
		});
	});
}
//(重新)尋找地點,建立地標
function updateMarkers(){
	//逐筆比較表列地點
	for (var i = 0; i < topoMarker.length; i++) {
		//計算點擊處至第i筆地標的距離
		var d = g.geometry.spherical.computeDistanceBetween(clickPoint, topoMarker[i].position);
		//依照範圍,改變地標的圖案
		if (d < rad){
			topoMarker[i].setIcon(iconTarget);
		}
		else{
			topoMarker[i].setIcon(icon);
		}
	}
}
//(重新)繪製圓圈
function updateCircle(){
	if (clickCircle){
		clickCircle.setMap(null);
	}
	clickCircle = new g.Circle({
		map: map,
		center: clickPoint,
		radius: rad,
		fillColor: circleFillColor,
		strokeWeight: 1,
		strokeColor: circleOutlineColor,
		zIndex: -100
	});
}
//(重新)繪製疊層
function updateGraph(){
	//(重新)繪製圓圈
	if (circleDisplay = true){
		updateCircle();
	}
	//(重新)尋找地點,建立地標
	updateMarkers();
}
//偵測點擊動作
function attachSensor(marker){
	marker.addListener('click', function(e) {
		//取得被點擊的地標之經緯度
		clickPoint = this.getPosition();
		//(重新)繪製疊層
		updateGraph();
		//使被點擊的地標改變圖案
		this.setIcon(iconCenter);
	});
}
//啟動
$(document).ready(function(){
	var mapDiv = document.getElementById('map-canvas');
	//建立基本地圖
	map = new g.Map(mapDiv, {
		center: mapCenter, 
		zoom: mapScale, 
		mapTypeId: g.MapTypeId.ROADMAP
	});
	//讀取KML檔,儲存為JSON
	x = getKML();
	//顯示地圖說明視窗
	if(infoWindowDisplay==true){
		w.open(map);
		if(infoWindowAutoClose==true){
			var o =setTimeout("w.close()", infoWindowTimer);
		}
	}
});
</script>

漫遊式

如何改變以上程式,讓地圖上的任意點都能被點選?我猜,將來必定有人會如此詢問。

我們只需改變第三階段的程式。以下的改變還更進一步:在載入地圖時先不秀出地點;在點擊地圖後,僅顯示半徑範圍內的地點。如果來自My Map的資料量龐大,這種作法有助於加速載入。

以下即其成果。請在台灣國境內任意點選:

幾乎跟剛才一樣,在head區塊中加入以下程式,並視需要改變設定,尤其是襯灰底的五行:

<script type="text/javascript">
//KML檔案(網址)
var kmlFile = 'kml.xml';

var g = google.maps;
var map, clickPoint, clickMarker, clickCircle;
//地圖中心座標
var mapCenter = new g.LatLng(23.972583, 120.979361);
//地圖縮放比例(數值越大,細節越清楚)
var mapScale = 7;
//搜尋半徑,單位為公尺
var rad = 100000;

//以圓圈顯示搜尋範圍;true=是;false=否
var circleDisplay = true;
//圓周顏色
var circleOutlineColor = '#c0c0c0';
//圓內顏色
var circleFillColor = '#e0e0e0';

//顯示點擊處;true=是;false=否
var clickPointDisplay = true;

//目標資料、目標圖示
var topo = [], topoMarker = [];
//目標圖案;若是ic='',則恢復預設標誌 
var ic = 'img/airport.png';

//是否顯示地圖說明視窗。true=是;false=否
var infoWindowDisplay = true;
//是否自動關閉地圖說明視窗。true=是;false=否
var infoWindowAutoClose = true;
//自動關閉地圖說明視窗預設時間;計時單位為千分之一秒
var infoWindowTimer = 3000;
//地圖總說明文字
var infoWindowText = '請在台灣國境內任意點觸';
var w = new g.InfoWindow({
	content: infoWindowText, 
	position: mapCenter
});

//讀取KML檔,儲存為JSON
function getKML(){
	$.get(kmlFile, function(data){
		//逐筆掃描地點資料
		$(data).find("Placemark").each(function(q){
			//取得地點名稱
			name = $(this).find("name").text();
			//取得地點座標名稱
			coords = $(this).find("coordinates").text();
			//解析座標經緯度
			var c =[];
			c = coords.split(",");
			lng = c[0];
			lat = c[1];
			//將經緯度設為Google Map Class
			topoc = new g.LatLng(lat, lng);
			//將資料儲存 as JSON
			topo.push({
				"name": name,
				"coord": topoc,
			});
		});
	});
}
//(重新)尋找地點,建立地標
function updateMarkers(){
	//清除先前出現的地標
	if (topoMarker[0]){
		for (var j = 0; j < topoMarker.length; j++) {
			topoMarker[j].setMap(null);
		}
	}
	//逐筆比較表列地點
	for (var i = 0; i < topo.length; i++) {
		//計算點擊處至第i筆地標的距離
		var d = g.geometry.spherical.computeDistanceBetween(clickPoint, topo[i].coord);
		//為範圍內之地標建立標記
		if (d < rad){
			var marker = new g.Marker({
				map: map,
				position: topo[i].coord,
				icon: ic,
				title: topo[i].name
			});
			//將此地標加入地標集合
			topoMarker.push(marker);
		}
	}
}
//(重新)繪製圓圈
function updateCircle(){
	if (clickCircle){
		clickCircle.setMap(null);
	}
	clickCircle = new g.Circle({
		map: map,
		center: clickPoint,
		radius: rad,
		fillColor: circleFillColor,
		strokeWeight: 1,
		strokeColor: circleOutlineColor,
		zIndex: -100
	});
	//讀取圓圈內被點擊的座標,並重新繪製疊層
	clickCircle.addListener('click', function(event) {
		clickPoint = event.latLng;
		updateGraph();
	});
}
//繪製最新點擊處的標記
function updateClickMarker(){
	if (clickMarker){
		clickMarker.setMap(null);
	}
	clickMarker = new g.Marker({
		map: map,
		position: clickPoint,
		//圖形,可自訂
		icon: {
			path: g.SymbolPath.CIRCLE,
			strokeColor:'#9932CC',
			scale: 2
		},
		draggable: false,
		zIndex: -1
	});
}
//(重新)繪製疊層
function updateGraph(){
	//(重新)繪製圓圈
	if (circleDisplay = true){
		updateCircle();
	}
	//繪製最新點擊處的標記
	if (clickPointDisplay = true){
		updateClickMarker();
	}
	//(重新)尋找地點,建立地標
	updateMarkers();
}
//啟動
$(document).ready(function(){
	var mapDiv = document.getElementById('map-canvas');
	//建立基本地圖
	map = new g.Map(mapDiv, {
		center: mapCenter, 
		zoom: mapScale, 
		mapTypeId: g.MapTypeId.ROADMAP
	});
	//讀取KML檔,儲存為JSON
	x = getKML();
	//讀取地圖上被點擊的座標
	map.addListener('click', function(event) {
		clickPoint = event.latLng;
		//(重新)繪製疊層
		updateGraph();
	});
	//顯示地圖說明視窗
	if(infoWindowDisplay==true){
		w.open(map);
		if(infoWindowAutoClose==true){
			var o =setTimeout("w.close()", infoWindowTimer);
		}
	}
});
</script>

後話

從以上兩套程式,熟悉javascript者可輕易變化出其它的呈現方式。

礙於時間因素,我無法隨時解答讀者的問題,尤其是「另外要達成其它效果,要如何如何做」之類的詢問。這篇是今年內的最後一次此類答詢。請因此而失望的網友諒解、海涵!

20161009 04:36 修正:
將兩程式之「load」function 均改為jQuery的「document ready」模式。

發表迴響