2013年9月3日 星期二

Google Maps Android API v2-- Android 新版地圖導航路徑開發方法

繼上一篇的"Android 新版地圖開發方法",最近參考了一些網站後透過Google Directions API擷取了JSON做了個比較實用的導航路徑,如下圖:

本篇主要是在Android下解析Google Directions API 所傳回的JSON資料後再繪製導航路徑,程式碼如下:
(記得程式要匯入google-play-services_lib 以及申請Map API Key,詳見 "Android 新版地圖開發方法")

Layout部分:
activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <fragment
        android:id="@+id/map"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        class="com.google.android.gms.maps.SupportMapFragment" />

</RelativeLayout>

程式1:
MainActivity.java
package edu.nkut.myroutemap;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.json.JSONObject;

import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnMapClickListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;

public class MainActivity extends FragmentActivity {

static final LatLng NKUT = new LatLng(23.979548, 120.696745);
GoogleMap map;
ArrayList<LatLng> markerPoints;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Initializing
markerPoints = new ArrayList<LatLng>();

// Getting reference to SupportMapFragment of the activity_main
SupportMapFragment fm = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);

// Getting Map for the SupportMapFragment
map = fm.getMap();
Marker nkut = map.addMarker(new MarkerOptions().position(NKUT).title("南開科技大學").snippet("數位生活創意系"));
// Move the camera instantly to NKUT with a zoom of 16.
map.moveCamera(CameraUpdateFactory.newLatLngZoom(NKUT, 16));

if (map != null) {

// Enable MyLocation Button in the Map
map.setMyLocationEnabled(true);

// Setting onclick event listener for the map
map.setOnMapClickListener(new OnMapClickListener() {

@Override
public void onMapClick(LatLng point) {

// Already two locations
if (markerPoints.size() > 1) {
markerPoints.clear();
map.clear();
}

// Adding new item to the ArrayList
markerPoints.add(point);

// Creating MarkerOptions
MarkerOptions options = new MarkerOptions();

// Setting the position of the marker
options.position(point);

/**
* 起始及終點位置符號顏色
*/
if (markerPoints.size() == 1) {
options.icon(BitmapDescriptorFactory
.defaultMarker(BitmapDescriptorFactory.HUE_GREEN)); //起點符號顏色
} else if (markerPoints.size() == 2) {
options.icon(BitmapDescriptorFactory
.defaultMarker(BitmapDescriptorFactory.HUE_RED)); //終點符號顏色
}

// Add new marker to the Google Map Android API V2
map.addMarker(options);

// Checks, whether start and end locations are captured
if (markerPoints.size() >= 2) {
LatLng origin = markerPoints.get(0);
LatLng dest = markerPoints.get(1);

// Getting URL to the Google Directions API
String url = getDirectionsUrl(origin, dest);

DownloadTask downloadTask = new DownloadTask();

// Start downloading json data from Google Directions
// API
downloadTask.execute(url);
}

}
});
}
}

private String getDirectionsUrl(LatLng origin, LatLng dest) {

// Origin of route
String str_origin = "origin=" + origin.latitude + ","
+ origin.longitude;

// Destination of route
String str_dest = "destination=" + dest.latitude + "," + dest.longitude;

// Sensor enabled
String sensor = "sensor=false";

// Building the parameters to the web service
String parameters = str_origin + "&" + str_dest + "&" + sensor;

// Output format
String output = "json";

// Building the url to the web service
String url = "https://maps.googleapis.com/maps/api/directions/"
+ output + "?" + parameters;

return url;
}

/**從URL下載JSON資料的方法**/
private String downloadUrl(String strUrl) throws IOException {
String data = "";
InputStream iStream = null;
HttpURLConnection urlConnection = null;
try {
URL url = new URL(strUrl);

// Creating an http connection to communicate with url
urlConnection = (HttpURLConnection) url.openConnection();

// Connecting to url
urlConnection.connect();

// Reading data from url
iStream = urlConnection.getInputStream();

BufferedReader br = new BufferedReader(new InputStreamReader(
iStream));

StringBuffer sb = new StringBuffer();

String line = "";
while ((line = br.readLine()) != null) {
sb.append(line);
}

data = sb.toString();

br.close();

} catch (Exception e) {
Log.d("Exception while downloading url", e.toString());
} finally {
iStream.close();
urlConnection.disconnect();
}
return data;
}

// Fetches data from url passed
private class DownloadTask extends AsyncTask<String, Void, String> {

// Downloading data in non-ui thread
@Override
protected String doInBackground(String... url) {

// For storing data from web service
String data = "";

try {
// Fetching the data from web service
data = downloadUrl(url[0]);
} catch (Exception e) {
Log.d("Background Task", e.toString());
}
return data;
}

// Executes in UI thread, after the execution of
// doInBackground()
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);

ParserTask parserTask = new ParserTask();

// Invokes the thread for parsing the JSON data
parserTask.execute(result);

}
}

/** 解析JSON格式 **/
private class ParserTask extends
AsyncTask<String, Integer, List<List<HashMap<String, String>>>> {

// Parsing the data in non-ui thread
@Override
protected List<List<HashMap<String, String>>> doInBackground(
String... jsonData) {

JSONObject jObject;
List<List<HashMap<String, String>>> routes = null;

try {
jObject = new JSONObject(jsonData[0]);
DirectionsJSONParser parser = new DirectionsJSONParser();

// Starts parsing data
routes = parser.parse(jObject);
} catch (Exception e) {
e.printStackTrace();
}
return routes;
}

// Executes in UI thread, after the parsing process
@Override
protected void onPostExecute(List<List<HashMap<String, String>>> result) {
ArrayList<LatLng> points = null;
PolylineOptions lineOptions = null;
MarkerOptions markerOptions = new MarkerOptions();

// Traversing through all the routes
for (int i = 0; i < result.size(); i++) {
points = new ArrayList<LatLng>();
lineOptions = new PolylineOptions();

// Fetching i-th route
List<HashMap<String, String>> path = result.get(i);

// Fetching all the points in i-th route
for (int j = 0; j < path.size(); j++) {
HashMap<String, String> point = path.get(j);

double lat = Double.parseDouble(point.get("lat"));
double lng = Double.parseDouble(point.get("lng"));
LatLng position = new LatLng(lat, lng);

points.add(position);
}

// Adding all the points in the route to LineOptions
lineOptions.addAll(points);
lineOptions.width(5);  //導航路徑寬度
lineOptions.color(Color.BLUE); //導航路徑顏色

}

// Drawing polyline in the Google Map for the i-th route
map.addPolyline(lineOptions);
}
}
}


程式2: 
DirectionsJSONParser.java

package edu.nkut.myroutemap;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.android.gms.maps.model.LatLng;

public class DirectionsJSONParser {

/** 接收一個JSONObject並返回一個列表的列表,包含經緯度 */
public List<List<HashMap<String,String>>> parse(JSONObject jObject){

List<List<HashMap<String, String>>> routes = new ArrayList<List<HashMap<String,String>>>() ;
JSONArray jRoutes = null;
JSONArray jLegs = null;
JSONArray jSteps = null;

try {

jRoutes = jObject.getJSONArray("routes");

/** Traversing all routes */
for(int i=0;i<jRoutes.length();i++){
jLegs = ( (JSONObject)jRoutes.get(i)).getJSONArray("legs");
List path = new ArrayList<HashMap<String, String>>();

/** Traversing all legs */
for(int j=0;j<jLegs.length();j++){
jSteps = ( (JSONObject)jLegs.get(j)).getJSONArray("steps");

/** Traversing all steps */
for(int k=0;k<jSteps.length();k++){
String polyline = "";
polyline = (String)((JSONObject)((JSONObject)jSteps.get(k)).get("polyline")).get("points");
List<LatLng> list = decodePoly(polyline);

/** Traversing all points */
for(int l=0;l<list.size();l++){
HashMap<String, String> hm = new HashMap<String, String>();
hm.put("lat", Double.toString(((LatLng)list.get(l)).latitude) );
hm.put("lng", Double.toString(((LatLng)list.get(l)).longitude) );
path.add(hm);
}
}
routes.add(path);
}
}

} catch (JSONException e) {
e.printStackTrace();
}catch (Exception e){
}


return routes;
}


/**
* 解碼折線點的方法
* */
    private List<LatLng> decodePoly(String encoded) {

        List<LatLng> poly = new ArrayList<LatLng>();
        int index = 0, len = encoded.length();
        int lat = 0, lng = 0;

        while (index < len) {
            int b, shift = 0, result = 0;
            do {
                b = encoded.charAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
            lat += dlat;

            shift = 0;
            result = 0;
            do {
                b = encoded.charAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
            lng += dlng;

            LatLng p = new LatLng((((double) lat / 1E5)),
                    (((double) lng / 1E5)));
            poly.add(p);
        }

        return poly;
    }
}

AndroidManifest.xml 內容:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="edu.nkut.myroutemap"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />

    <uses-permission android:name="android.permission.INTERNET" />

    <permission
        android:name="in.wptrafficanalyzer.locationroutedirectionmapv2.permission.MAPS_RECEIVE"
        android:protectionLevel="signature" />

    <uses-permission android:name="in.wptrafficanalyzer.locationroutedirectionmapv2.permission.MAPS_RECEIVE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="edu.nkut.myroutemap.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="你所申請的API Key" />
    </application>

</manifest>

執行結果:

上圖執行結果是點選地圖起點(南開科技大學)及終點(逢甲大學),藍色的線就是程式所繪出的導航路徑。
(別忘了,預設模擬器不支援本程式執行,另外程式出處請看參考1項目。)



參考:
1.Showing current location using OnMyLocationChangeListener in Google Map Android API V2 (有提供程式碼,本篇參考出處)
http://goo.gl/3UEmPV

2.Google Directions API |  Maps API 網路服務
https://developers.google.com/maps/documentation/directions/?hl=zh-TW&csw=1

3.編碼折線演算法格式 | Google Maps API
https://developers.google.com/maps/documentation/utilities/polylinealgorithm?hl=zh-TW&csw=1

4.Android程式設計 - Google Maps API v2在地圖上繪圖
http://nkeegamedev.blogspot.tw/2013/05/android-google-map-v2.html

==============延伸閱讀=====================
1.Google Maps Android API v2-- Android 新版地圖開發方法
http://www.cheng-min-i-taiwan.blogspot.tw/2013/04/google-maps-android-api-v2-android.html

11 則留言:

  1. 請問一下,可以做出導航路線。能夠顯示出行車路線嗎??
    雖然能夠A點到B點。如果能夠有導航說明更好,這種功能做得出來嗎??

    回覆刪除
    回覆
    1. 我倒是沒做過行車路線,不過這個功能應該不困難,首先你先把行車紀錄在檔案或資料庫,大致上GPS每一秒會接收訊號一次。所以你只要將點跟點用線連起來就行了,點跟點用線連起可以參考:
      http://nkeegamedev.blogspot.tw/2013/05/android-google-map-v2.html

      刪除
  2. 老師 想請問一下 如果在點南開科技大學出現的框
    裡面加一張圖片在南開科大學的
    大概要加哪個程式碼關鍵字

    回覆刪除
  3. 不好意思老師 可以請問一下 起點和終點的經緯度是在哪裡決定的嗎?

    回覆刪除
  4. 老師,請問一個困擾我的問題,是關於google map 地址轉座標,發表在
    http://apk.tw/thread-452117-1-1.html

    文章如下:
    我的PC環境: window7 /Eclipce JUNO

    Android 軟體是:
    android API level 15
    google play service revision 13
    googlr-play-services_lib 中的 res/values/version.xml 是 4030500
    google maps android api v2


    android 程式中的map宣告:
    map = ((MapFragment) getFragmentManager().findFragmentById(R.id.map)).getMap();
    LatLng latlng = new LatLng (latitude,longitude);
    Marker here = map.addMarker(new MarkerOptions().position(latlng));
    map.moveCamera(CameraUpdateFactory.newLatLngZoom(latlng, 16));

    如果直接給予座標,地圖呈現沒問題。
    但是,我的程式想以地址取出座標,再在地圖呈現地點位置。
    我用HttpURLconnect方式,取出JSON資料
    再解析JSON資料取得座標。


    URL url = new URL("http://maps.google.com/maps/api/geocode/json?address=" + address + "&sensor=false&components=country:TW&language=zh-TW");
    HttpURLConnection urlConn = (HttpURLConnection)url.openConnection();

    結果取出的JSON資料,有世界各國的座標,加上components=country:TW 後得出的座標式是在台灣內,但位置不對

    譬如: 地址輸入"台南市東區勝利路10號" 卻出現台灣台中市台10線等其他的座標。

    如果,我直接在Chrome瀏覽器下輸入
    http://maps.google.com/maps/api/geocode/json?address=台南市東區勝利路10號&sensor=false&components=country:TW&language=zh-TW
    出現的JSON資料則沒問題。


    請問,我該如何解決?

    回覆刪除
    回覆
    1. 您參考一下下列網址教學,應該是跟你要做的功能很接近,我猜測你的程式可能忘了轉換座標值乘以10的6次方。網址:http://goo.gl/e6B0U4

      刪除
    2. 該範例應是google map api v1,我使用 API v2
      座標值從JSON解析取得是Double沒有乘10的6次方問題
      我程式可以取得座標值,地圖也可以顯示在該位置
      我的問題是,我給的地址,maps.google.com無法給我正確的座標值。
      而且速度很慢才回應JSON資料。

      刪除
    3. 我解決了! 我將連線方式從 HttpURLConnection 改成 HttpClient 得到的JSON資料就正確的。 本以為HttpURLConnection 比較好用,但還是HttpClient少臭蟲

      刪除
  5. 老師您好,我目前利用您此篇文章來開發導航系統

    而我想利用手機接收本身的GPS座標,來做一個即時的導航

    請問有什麼辦法可以讓導航地圖上座標的起點不斷的更新

    更新成自己目前的GPS座標呢?

    謝謝老師的分享。

    回覆刪除
  6. 請問蚊子老師
    我下載您說的範例 並且更改成您所改的程式碼
    但是他就是不能執行 一執行就出現錯誤直接結束程式
    出現的問題似乎是連線逾時?!
    我把我的api key改過了
    我發現新版的map是直接繼承Activity
    而且xml的fragment的class屬性是MapFragment
    而不是SupportMapFragment
    因為之後要使用到fragment做出tabhost的樣子
    所以可能會用到Mapfragment
    現在已經不能使用了嗎?
    還是有另外的方法可以實作了?

    回覆刪除