自製Google MapView範例

地圖程式架構:
地圖影像資料庫透過下載執行緒向GOOGLE MAP伺服器攝取地圖,客戶端遊戲執行緒進行畫面更新時,向地圖模組攝取地圖資料,而地圖模組會依照執行緒提供的經緯度將地圖影像從地圖影像資料庫引用後進行排列及位移。

%E5%9C%B0%E5%9C%96%E7%A8%8B%E5%BC%8F%E6%9E%B6%E6%A7%8B.JPG
圖一:地圖模組


GPS%E9%81%8A%E6%88%B2%E6%B5%81%E7%A8%8B2.png
圖二:麥卡托投影法示意圖
此程式為依照Google Map View功能所自行編寫的程式,下圖為執行結果。
GMapDemo.png
圖三:執行結果

以下為自製Google MapView範例程式碼:
專案下載:GMapDemo1214.zip
程式: GMapDemo.java

package ccc.GMapDemo;
 
import gmap.GMapHandler;
import gmap.GPoint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.os.Bundle;
import android.os.Handler;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
 
public class GMapDemo extends Activity {
 
    // 畫布
    GmapView view;
 
    // 中心點經緯度
    GPoint center = new GPoint(24.448541368875834, 118.32220673561096, 19);
 
    // 靜態地圖Google Map Key
    String key="ABQIAAAAsHkLYX5ar3b-5P7EhsjhXBQ5g4cqN0MUBoV1v7EHe2z9rhP7AxSpu5jlSkMDTRq_cjvF4r10wvd_lQ";
    //String key = "ABQIAAAAz2rjkrilGZTguphQT5RQqxQnA14KtgqpIfJzkpdB1-oq1QYeLRTl0Szkt5Yli5Jv9oBOrf85JNpgIw";
    //String key = "ABQIAAAAz2rjkrilGZTguphQT5RQqxQQIe5j7nozdTIq37ll2BjQmE8XsxTUxidRqBWWrDXy2cwQU686YRJCTQ";
 
    // Google Map 處理器
    GMapHandler gmap;
 
    // 事件處理器
    Handler handle=new Handler();
 
    // 程式進入點
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        // Google Map 處理器
        gmap = new GMapHandler(center, 300, 300, key);
        gmap.setLoadingImage(this.getResources(), R.drawable.loading);
        view = new GmapView(this);
        setContentView(view);
 
        // 設定下載完地圖時的回呼事件
        gmap.setDownloadCallBack(new GMapHandler.DownloadCallback() {        
            public void downloadCallback() {
                handle.post(new Runnable() {                    
                    public void run() {
                        GMapDemo.this.view.invalidate();        
                    }
                });
 
                //Log.i("GMAP", "AsuncDownloadCallback");
            }
        });
    }
 
    // 選單放大跟縮小索引
    protected static final int MENU_ZoomIn = Menu.FIRST;
    protected static final int MENU_ZoomOut = Menu.FIRST+1;
 
    // 選單初始化
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        menu.add(0, MENU_ZoomIn, 0, "放大");
        menu.add(0, MENU_ZoomOut, 0, "縮小");
        return super.onCreateOptionsMenu(menu);
    };
 
    // 選單項目選擇
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int zoom = gmap.getZoom();
        switch (item.getItemId()) {
            case MENU_ZoomIn:
                if (zoom < 19)
                    gmap.setZoom(++zoom);
                break;
            case MENU_ZoomOut:
                if (zoom > 0)
                    gmap.setZoom(--zoom);
                break;
        }
        view.invalidate();
        return super.onOptionsItemSelected(item);
    }
 
    // 觸控事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
 
        int pointCount = event.getHistorySize();
 
        if (pointCount > 0) {
            float dx = event.getHistoricalX(pointCount - 1)
                    - event.getHistoricalX(0);
            float dy = event.getHistoricalY(pointCount - 1)
                    - event.getHistoricalY(0);
            gmap.offset((int) -dx, (int) -dy);
            view.invalidate();
        }
 
        return super.onTouchEvent(event);
    }
 
     // 鍵盤事件
    @Override 
    public boolean onKeyDown(int keyCode, KeyEvent event) {
          if(keyCode==KeyEvent.KEYCODE_DPAD_LEFT){
              gmap.offset(-10, 0);  
          }
          if(keyCode==KeyEvent.KEYCODE_DPAD_RIGHT){
              gmap.offset(10, 0);  
          }
          if(keyCode==KeyEvent.KEYCODE_DPAD_UP){
              gmap.offset(0, -10);              
          }
          if(keyCode==KeyEvent.KEYCODE_DPAD_DOWN){
              gmap.offset(0, 10);  
          }
 
          view.invalidate();
        return super.onKeyDown(keyCode, event);
    }
 
    // 畫布
    class GmapView extends View{
 
        public GmapView(Context context) {
            super(context);
 
        }
        @Override 
        protected void onDraw(Canvas canvas) {
            Paint p=new Paint();
            p.setARGB(255, 255, 255, 255);
            p.setTextSize(12);
            p.setTextAlign(Align.LEFT);
 
            // 測試用的下載靜態地圖 
            //Bitmap tmp= GMapDownloader.getGMap(center, 256, 256, key);
            //canvas.drawBitmap(tmp, 0, 0,p);
 
            // 繪出地圖
            gmap.draw(canvas);
            super.onDraw(canvas);
        }
 
    }
}

程式: GLatLng.java

package gmap;
 
public class GLatLng {
    public double lat;
    public double lng;
    public GLatLng(double lat,double lng){
        this.lat=lat;
        this.lng=lng;
    }
    public GLatLng(int x,int y,int zoom){
        this.lng= MercatorProjection.XToLng(x, zoom);
        this.lat= MercatorProjection.YToLat(y, zoom);
    }
    public GLatLng(GPoint point){
        this.lng= MercatorProjection.XToLng(point.X,point.Zoom);
        this.lat= MercatorProjection.YToLat(point.Y,point.Zoom);
    }
    public GLatLng(GLatLng latlng){
        this.lng= latlng.lng;
        this.lat= latlng.lat;
    }
    public GPoint getGPoint(int zoom){
        return new GPoint(lat, lng, zoom);
    }
 
}

程式: GMapDownloader.java

package gmap;
 
import java.net.URL;
import java.net.URLConnection;
 
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Proxy;
 
public class GMapDownloader {
 
    public static Bitmap getNoLogoGMap(GPoint center, int width, int height,
            String key) {
        GPoint fixedPoint = new GPoint(center.X, center.Y + 15, center.Zoom);
        height += 30;
        Bitmap srcImg = getGMap(fixedPoint, width, height, key);
 
        if (srcImg != null) {
            Bitmap fixedBitmap = Bitmap.createBitmap(srcImg, 0, 0, width,
                    height - 30);
            srcImg.recycle();
            return fixedBitmap;
        } else {
            return null;
        }
 
    }
 
    public static Bitmap getGMap(GPoint center, int width, int height,
            String key) {
 
        GLatLng latlng = center.getGLatLng();
        double lat = latlng.lat;
        double lng = latlng.lng;
        int zoom = center.Zoom;
        Bitmap downloadBitmap = downloadImg(lat, lng, zoom, width, height, key);
        return downloadBitmap;
 
    }
 
    private static Bitmap downloadImg(double lat, double lng, int zoom,
            int width, int height, String key) {
        try {
            String size = String.format("%1sx%2s", width, height);
            // 網址
            String urlString = String
                    .format(
                            "http://maps.google.com/staticmap?center=%1$s,%2$s&zoom=%3$s&size=%4$s&maptype=hybrid&sensor=false&key=%5$s",
                            lat, lng, zoom, size, key);
            URL url ;// use proxy server
            if(Proxy.getDefaultHost()==null)
                url = new URL(urlString);
            else
                url = new URL("http",Proxy.getDefaultHost(),Proxy.getDefaultPort(), urlString);
 
            // 連線
            URLConnection conn = url.openConnection();
            conn.connect();
            return BitmapFactory.decodeStream(conn.getInputStream());
        } catch (Exception ex) {
            return null;
        }
    }
 
}

程式: GMapHandler.java

package gmap;
 
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
 
public class GMapHandler {
    private static Map<String, byte[]> gmapPng = new HashMap<String, byte[]>();
    private static Map<String, Bitmap> gmapBmp = new HashMap<String, Bitmap>();
    private static Thread compressThread = null;
    private static Thread downloadThread = null;
    private static List<GPoint> downloadIndex = new ArrayList<GPoint>();
    Bitmap buffer;
 
    //緩衝器中心的索引直
    String bufferIndex;
    Bitmap loadimg = null;
    GPoint center;
    int width;
    int height;
    String apiKey;
    DownloadCallback callBack = new DownloadCallback() {
        public void downloadCallback() {
 
        }
    };
 
    public GMapHandler(GLatLng centerLatLng, int zoom, int width, int height,
            String apiKey) {
        center = new GPoint(centerLatLng, zoom);
        inti(center, width, height, apiKey);
 
    }
 
    public GMapHandler(GPoint center, int width, int height, String apiKey) {
        inti(center, width, height, apiKey);
    }
 
    private void inti(GPoint center, int width, int height, String apiKey) {
        this.center = center;
        this.width = width;
        this.height = height;
        this.apiKey = apiKey;
        int drawCountX = (width / 256) + 2;
        int drawCountY = (height / 256) + 2;
        buffer = Bitmap.createBitmap(drawCountX * 256, drawCountY * 256,
                Config.ARGB_8888);
 
    }
 
    public void setLoadingImage(Resources res, int DrawableId) {
        loadimg = BitmapFactory.decodeResource(res, DrawableId);
    }
 
    public void setDownloadCallBack(GMapHandler.DownloadCallback callback) {
        this.callBack = callback;
    }
 
    public void draw(Canvas canvas) {
        updateBuffer();
        Paint paint = new Paint();
        paint.setAlpha(255);
        GPoint drawIndex = new GPoint(center);
        drawIndex.offset(-width / 2, -height / 2);
        int offsetX = drawIndex.X % 256;
        int offsetY = drawIndex.Y % 256;
        Rect src = new Rect(offsetX, offsetY, offsetX + width, offsetY + height);
        Rect dst = new Rect(0, 0, width, height);
        canvas.drawBitmap(buffer, src, dst, paint);
    }
 
    public Bitmap getBitmap() {
        updateBuffer();
        return Bitmap.createBitmap(buffer);
    }
 
    private Bitmap getLoadimg() {
        if (loadimg == null)
            loadimg = Bitmap.createBitmap(width, height, Config.ARGB_8888);
        return loadimg;
 
    }
 
    private void updateBuffer() {
        compressGmpBmp();
        if (bufferIndex != getIndexString(center)) {
            bufferIndex = getIndexString(center);
            Canvas canvas = new Canvas(buffer);
            Paint paint = new Paint();
            paint.setAlpha(255);
 
            GPoint drawIndex = new GPoint(center);
            drawIndex.offset(-width / 2, -height / 2);
            int drawCountX = (width / 256) + 2;
            int drawCountY = (height / 256) + 2;
            Rect src = new Rect(0, 0, 256, 256);
            for (int iy = 0; iy < drawCountY; iy++) {
                for (int ix = 0; ix < drawCountX; ix++) {
                    int left = ix * 256;
                    int top = iy * 256;
                    int right = left + 256;
                    int bottom = top + 256;
                    Rect dst = new Rect(left, top, right, bottom);
                    Bitmap indexGmap = getIndexGMap(drawIndex);
                    canvas.drawBitmap(indexGmap, src, dst, paint);
                    drawIndex.offset(256, 0);
                }
                drawIndex.offset(-256 * drawCountX, 256);
            }
        }
 
    }
 
    public int getIndexX(GPoint index) {
        return index.X / 256;
    }
 
    public int getIndexY(GPoint index) {
        return index.Y / 256;
    }
 
    public String getIndexString(GPoint index) {
        int indexX = getIndexX(index);
        int indexY = getIndexY(index);
        return String.format("IGX=%1$s IGY=%2$s GZ=%3$s", indexX, indexY,
                index.Zoom);
    }
 
    // 取得GoogleMap
    public Bitmap getIndexGMap(GPoint index) {
        String indexString = getIndexString(index);
 
        Bitmap reImg = gmapBmp.get(indexString);
        if (reImg != null) {
            if (!reImg.isRecycled()) {
                return reImg;
            }
        }
 
        reImg = pngByteToBitmap(gmapPng.get(indexString));
        if (reImg != null) {
            gmapBmp.put(getIndexString(index), reImg);
            return reImg;
        } else {
            gmapBmp.put(indexString, getLoadimg());
            asyncDownlod(index);
            return gmapBmp.get(indexString);
        }
 
    }
 
    // 壓縮圖片函式
    Runnable compressGmpBmp = new Runnable() {
        public void run() {
 
            int drawCountX = (width / 256) + 2;
            int drawCountY = (height / 256) + 2;
            if (gmapBmp.size() > drawCountX * drawCountY) {
                GPoint drawIndex = new GPoint(center);
                drawIndex.offset(-width / 2, -height / 2);
 
                //尋找不需要使用的圖片
                List<String> needBmpIndex = new ArrayList<String>();
                for (int iy = 0; iy < drawCountY; iy++) {
                    for (int ix = 0; ix < drawCountX; ix++) {
                        needBmpIndex.add(drawIndex.toString());
                        drawIndex.offset(256, 0);
                    }
                    drawIndex.offset(-256 * drawCountX, 256);
                }
 
                String[] gmapBmpKey = new String[gmapBmp.size()];
                gmapBmp.keySet().toArray(gmapBmpKey);
                for (String key : gmapBmpKey) {
                    if (!needBmpIndex.contains(key)) {
                        Bitmap compressBmp = gmapBmp.get(key);
 
                        if (compressBmp != getLoadimg()){
                            //儲存為PNG作為快取用
                            gmapPng.put(key, bitmapToPngByte(compressBmp));
                            compressBmp.recycle();
                            }
                        gmapBmp.remove(key);
 
                    }
                }
                //釋放記憶體
                //System.gc();
            }
 
        }
    };
 
    // 壓縮圖片執行緒
    public void compressGmpBmp() {
 
        if (compressThread == null) {
            compressThread = new Thread(compressGmpBmp);
            compressThread.start();
        } else if (!compressThread.isAlive()) {
 
            compressThread = new Thread(compressGmpBmp);
            compressThread.start();
        }
    }
 
    private Bitmap pngByteToBitmap(byte[] pngByte) {
        if (pngByte != null)
            return BitmapFactory.decodeByteArray(pngByte, 0, pngByte.length);
        else
            return null;
    }
 
    private byte[] bitmapToPngByte(Bitmap bitmap) {
        if (bitmap != null) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            bitmap.compress(CompressFormat.PNG, 0, out);
            byte[] bytes = out.toByteArray();
            return bytes;
        } else {
            return null;
        }
    }
 
    private void asyncDownlod(GPoint index) {
        downloadIndex.add(new GPoint(index));
 
        if (downloadThread == null) {
            downloadThread = new Thread(asyncDownlod);
            downloadThread.start();
        } else if (!downloadThread.isAlive()) {
            downloadThread = new Thread(asyncDownlod);
            downloadThread.start();
        }
 
    }
 
    public void offset(int dx, int dy) {
        center.offset(dx, dy);
    }
 
    public void setCenter(GLatLng centerLatLng, int zoom) {
        center = new GPoint(centerLatLng, zoom);
    }
 
    public void setZoom(int zoom) {
        center.setZoom(zoom);
        updateBuffer();
    }
 
    public int getZoom() {
        return center.Zoom;
    }
 
    public interface DownloadCallback {
        public void downloadCallback();
    }
 
    Runnable asyncDownlod = new Runnable() {
        public void run() {
            while (downloadIndex.size() > 0) {
                GPoint index = downloadIndex.get(0);
                int cx = getIndexX(index) * 256 + 128;
                int cy = getIndexY(index) * 256 + 128;
                GPoint imgCenter = new GPoint(cx, cy, index.Zoom);
                Bitmap downloadImg = GMapDownloader.getNoLogoGMap(imgCenter,
                        256, 256, apiKey);
                if (downloadImg == null)
                    downloadImg = getLoadimg();
                gmapBmp.put(getIndexString(index), downloadImg);
                downloadIndex.remove(0);
                callBack.downloadCallback();
            }
 
        }
    };
 
}

程式: GPoint.java

package gmap;
 
public class GPoint {
        int X;
        int Y;
        int Zoom;
 
        public GPoint(double lat,double lng,int zoom){
            this.X=MercatorProjection.LngToX(lng, zoom);
            this.Y=MercatorProjection.LatToY(lat, zoom);
            this.Zoom=zoom;
        }
 
        public GPoint(int x,int y,int zoom){
            this.X = x;
            this.Y = y;
            this.Zoom = zoom;
        }
 
        public GPoint(GLatLng latlng, int zoom) {
            this.X=MercatorProjection.LngToX(latlng.lng, zoom);
            this.Y=MercatorProjection.LatToY(latlng.lat, zoom);
            this.Zoom=zoom;
        }
 
        public GPoint(GPoint point){
            this.X = point.X;
            this.Y = point.Y;
            this.Zoom = point.Zoom;
        }
 
        // 位移
        public void offset(int dx,int dy){
            this.X += dx;
            this.Y += dy;
        }
 
        // 取得經緯度
        public GLatLng getGLatLng(){
            return new GLatLng(X, Y, Zoom);
        }
 
        // 轉字串
        public String toString(){
            return String.format("GX=%1$s GY=%2$s GZ=%3$s", X, Y, Zoom);
        }
 
        // 設定縮放
        public void setZoom(int zoom) {
            GLatLng latlng = new GLatLng(this);
            this.X = MercatorProjection.LngToX(latlng.lng, zoom);
            this.Y = MercatorProjection.LatToY(latlng.lat, zoom);
            this.Zoom = zoom;
        }
}

程式: MercatorProjection.java

package gmap;
 
public class MercatorProjection {
       // 經度轉成點(point)
    public static int LngToX(double Longitude,int zoom)
    {
        int numberOfTiles = 1 << zoom << 8;
        return (int)((Longitude + 180.0) * numberOfTiles / 360.0);
    }
 
    // 緯度轉成點(point)
    public static int LatToY(double Latitude,int zoom)
    {
        int numberOfTiles = 1 << zoom << 8;
        double projection = Math.log(Math.tan(Math.PI / 4 + ((Latitude / 180 * Math.PI) / 2)));
        double y = (projection / Math.PI);
        y = 1.0 - y;
        y = y / 2.0 * numberOfTiles;
 
        return (int)y;
    }
 
    // 點(X)轉成經度
    public static double XToLng(int pointX,int zoom)
    {
        int numberOfTiles = 1 << zoom << 8;
        return (pointX * (360.0 / numberOfTiles)) - 180.0;
    }
 
    // 點(Y)轉成緯度
    public static double YToLat(int pointY,int zoom)
    {
        int numberOfTiles = 1 << zoom << 8;
        double Latitude = pointY * (2.0 / numberOfTiles);
        Latitude = 1 - Latitude;
        Latitude = Latitude * Math.PI;
        Latitude = Math.atan(Math.sinh(Latitude)) * 180.0 / Math.PI;
        return Latitude;
    }
}

AndroidManifest.xml為使用權限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="ccc.GMapDemo"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".GMapDemo"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
 
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-sdk android:minSdkVersion="4" />
 
</manifest>

程式影片解說

程式手機執行影片