2016年10月8日 星期六

Simple Location Sender: 團隊動態追蹤的好範例


這個是什麼? What



這是一個使用者GPS位置回報系統的App端範例



為什麼需要這個? Why



這個範例嘗試用最簡單的方式來撰寫,也因此不論是修改還是學習都非常容易



什麼地方會用到? Where



當你在開發任何需要使用者回報地理位置給伺服器的應用



什麼時候會用到? When



在處理使用者回報資訊給伺服器的時候



誰會需要? Who



任何想要開發相關追蹤應用的開發者或著任何對於使用者端與伺服器端之間溝通有興趣的人



這個如何實作的? How



Project: https://github.com/lienching/LocationReportSender


這個範例使用了幾個開源函式庫
  • geojson : 用來處理 json
  • okhttp3 : 用來處理網路操作
  • realm : 資料庫

這整個範例可以分為四個部份
  1. LocationManager : 負責處理GPS位置,這樣就能得到使用者的位置
  2. JSONBuilder : 建立JSON檔,作為與伺服器溝通的格式
  3. TrackingService : 讓使用者位置的發送可以在Background執行
  4. BookMark(BookMarkHolder) : 這個範例使用Realm作為背後的資料庫,並使用AutoCompleteTextView來實現書籤選單,這個範例也支援將書籤輸入與輸出(格式為csv)

2016年6月12日 星期日

地圖圖層隨便你疊,地圖樣式你決定 - TileStache (基本介紹篇)


前言Preface



使用OpenStreetMap有一個很大的優勢,就是基本上地圖的樣式都是可以自主來去做定義。 但是其實OpenStreetMap原本的自訂圖塊( Custom Tiles ) 並不是我們想像中的那麼的容易操作而且有其限制( 沒辦法將多圖層合併為單一圖層,資源消耗大 ),但是感謝廣大的開源社群相關的工具如雨後春筍一般的一個個冒出來( 如 TileCache, OpenLayer , TileStache ),讓 OpenStreetMap 的地圖多樣性有了革命性的改變,因為可以將多圖層合併在雲端完成,也就可以讓設計者與地圖製作者們可以專心繪製自己的圖層,剩下的合併交給那些工具就好。

TileStache介紹
Brief introduction of TileStache



這次要介紹的TileStache它的前身是TileCache,它們都是用來做圖層渲染(多圖層渲染成一個),這對於設計者與地圖製作者都非常的方便,當然對於一些效能不太好的客戶端也可以帶來一些不錯的效能提升,而TileStache的設計目的是為了讓整個工具更貼近設計者與地圖製作者,讓它們可以在越少的步驟中產生他們所需要的東西。


TileStache設定
Installation of TileStache




TileStache取得請到 官網 or My Github , 目前這個工具只能用在Linux或OSX上


TileStache 是以Python做為開發語言,所以我們所需要的套件如下:




TileStache兩種使用方式:


  1. 直接用Script資料夾內的python script
    # 用來啟動伺服器(服務)
    tilestache-server.py
    
    # 用來快取圖檔(網路圖資)
    tilestache-seed.py
    
    # 用來產生自定義圖層
    tilestache-render.py
    
  2. 將tilestache工具加入指令集
    python setup.py install
    



從TileStache取得圖塊
Requesting Tiles from TileStache




  • 從 HTTP 取得
    1. 取得單一圖塊 Single Tile
      格式 : /{圖層名稱}/{縮放等級}/{x}/{y}.{副檔名(e.g. png, jpg}
      範例 : http://example.org/osm/12/656/1582.png
    2. 動態地圖(類似Google Map)
      格式 : /{圖層名稱}/preview.html
      範例 : http://example.org/osm/preview.html

  • 使用TileStache的API
    1. TileStache.getTile(layer, coord, extension[, ignore_cached=FALSE]): 直接取得圖塊 Get Tile Directly
      • layer: 想要取得得圖層( Core.Layer )
      • coord: 傳入所要取得的位置( ModestMaps.Core.Coordinate )
      • extension: 副檔名("jpg", "png") (String)
      • ignore_cached: (可有可無,預設為FALSE)是否忽略快取圖塊 (Boolean) 
    2. TileStache.requestHandler(config, path_info[,query_string=None][, script_name='']): 透過HTTP Requesting取得圖塊 Get Tile by HTTP Requesting
      • config: JSON設定檔的路徑(String) 或 TileStache.Config.Configuration物件(需要 cache, layers, dirpath 三個參數)
      • path_info: 傳入Request URL, 格式同從HTTP取得之取得單一圖塊(範例: /osm/12/656/1582.png )
      • query_string: (可有可無,預設為None)使用 JSONP 才會用到
      • script_name: (可有可無,預設為'') 給CGI的腳本,用來判斷是否轉址成功

2016年4月7日 星期四

創造自己的App市集( Using F-Droid )


介紹Introduction


F-Droid根據官方說法是一個在Android平台上的FOSS(Free and Open Source Software 免費與開源軟體)可安裝目錄程式,顧名思義就是非常類似我們所熟知了App市集囉。
那肯定會有人問說,既然有了Google Play為什麼還要另外自己去建立一個市集呢? 那是因為有些地方是沒辦法連得上Google Play,或著不喜歡目前市面上所有App市集的上架規則。 那用F-Droid架自己的市集有什麼好處呢?
  • 私人的程式庫,小小程式樂園
  • 自己的市集,自己說的算
  • 開源軟體,自己的模式自己建立



環境建立Setup



首先一定是要下載好工具囉

  • Linux

  • 有套件管理工具的好處就是一件完成安裝!

     sudo apt-get install fdroidserver
    

    當然還有一些進階的操作,例如如果有希望讓F-Droid自己把Source code編譯起來的話,可以用以下的指令(假設你什麼都沒有...)

    # 安裝一些用來編譯程式程式的套件
    sudo apt-get install openjdk-7-jdk libstdc++6:i386 libgcc1:i386 zlib1g:i386 libncurses5:i386 
    # 從android官網用wget抓android-sdk
    cd ~
    # 筆者目前最新版本是r24.4.1,建議讀者到 ( http://developer.android.com/sdk/index.html#Other ) 去查看最新的SDK
    wget http://dl.google.com/android/android-sdk_r24.4.1-linux.tgz
    # 用tar解壓縮我們剛剛的檔案
    tar zxcf android-sdk_r24.4.1-linux.tgz
    # 接下來我們要把ANDROID_HOME設定起來(假設你解壓縮出來的資料夾名字為 android-sdk-linux )
    export ANDROID_HOME=~/android-sdk-linux
    # 再來就是要把ANDROID_HOME丟進我們的 .bashrc (如果是zsh就會是 .zshrc ) 這樣我們的設定就會被記錄住了
    echo 'export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools' >> .bashrc
    

    還有其他一些有的沒有的選項設定,可以參考這邊

  • OSX

  • Using MacPorts

    port install fdroidserver
    

    Using HomeBrew

    brew install fdroidserver
    

    Using easy_install

    sudo easy_install fdroidserver
    
  • Windows

  • Windows基本上Command-Line base的東西就蠻殘缺的所以會需要借助Cygwin,不囉嗦~建立方式如下

    1. 下載 Cygwin (載點)
    2. 安裝 Cygwin , 並在選擇安裝套件的時候選擇安裝以下套件
      • gcc-core
      • git
      • openssh
      • python
      • python-pyasn1
      • python-imaging
      • python-magic
      • python-paramiko
      • python-requests
      • python-setuptools
      • rsync
      • wget
    3. 開啟一個Cygwin的bash介面,然後輸入
    4. easy_install fdroidserver
      
  • From Source

  •  git clone https://gitlab.com/fdroid/fdroidserver.git
     cd fdroidserver
     virtualenv --system-site-packages env/
     . env/bin/activate
     pip install -e .
     python setup.py install
    

接下來就是建立環境囉 (用Linux做示範)

# 伺服器這邊筆者選用Nginx,當然Apache Web Server也是可以
sudo apt-get install nginx
# 建立F-Droid專用的資料夾
sudo mkdir /usr/share/nginx/www/fdroid
sudo chown -R $USER /usr/share/nginx/www/fdroid
cd /usr/share/nginx/www/fdroid
# 初始化環境
fdroid init

然後在當前的資料夾裡面就會多了一個repo的資料夾,這邊就是我們放程式(.apk)的地方,程式放完就可以輸入下面的指令,來產生MetaData(程式敘述資料)

fdroid update --create-metadata

這樣我們的 Repo Server 已經建立完成囉~ 接下來把位置放進F-Droid的Client就可以連接到了



Config.py 設置


基本上要改的就這幾個

# Repo Server的網址
repo_url = "https://MyFirstFDroidRepo.org/fdroid/repo"
# Repo Server的名字
repo_name = "My First F-Droid Repo Demo"
# Repo Server的Icon
repo_icon = "fdroid-icon.png"
# Repo Server的敘述
repo_description = ""

改完之後要重置伺服器,用下方的指令

fdroid server update -v

2016年3月5日 星期六

Step By Step 簡易Mapsforge App開發:MapUpdateManager


目標Achievements


這個類別是用來做圖資更新檢查與更新


程式碼Code


package lien.ching.maptracker.api;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.PowerManager;
import android.util.Log;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import lien.ching.maptracker.Constant;

/**
 * Created by lienching on 12/1/15.
 * In this class, I had no idea how to update a file by using DownloadManager
 * so I just try to use standard HTTP Download as the method
 */
public class MapUpdateManager extends AsyncTask {

    private ProgressDialog dialog;
    private PowerManager.WakeLock wakeLock;
    private Context context;
    private Activity activity;
    public MapUpdateManager(Context context, Activity activity){
        super();
        this.activity = activity;
        this.context = context;
    }
    @Override
    protected Boolean doInBackground(String... params) {
        //For Details, please check http://stackoverflow.com/a/3028660
        String continent = params[1];
        InputStream input = null;
        OutputStream output = null;
        HttpURLConnection connection = null;
        File outputFile = new File(Constant.PATH_MAPSFORGE+params[0]);
        try{
            URL url = new URL("http://download.mapsforge.org/maps/"+params[0]);
            connection = (HttpURLConnection) url.openConnection();
            connection.connect();

            if(connection.getResponseCode() != HttpURLConnection.HTTP_OK){
                Toast.makeText(context,"Connection Fail..."+connection.getResponseCode(),Toast.LENGTH_SHORT).show();
                return false;
            }
            input = connection.getInputStream();


            if (outputFile.lastModified() < connection.getLastModified()){ //if the date is older than the latest file then close the download process
                input.close();
                output.close();
                return true;
            }
            dialog.setMessage("Map Source Updating");
            outputFile.delete();
            outputFile.createNewFile();
            output = new FileOutputStream(outputFile);
            byte data[] = new byte[8192];
            int count;
            while((count = input.read(data)) != -1){
                if(isCancelled()){
                    input.close();
                    return false;
                }

                output.write(data,0,count);
            }
        }catch (Exception e){
            Log.e("MapDownloadManager","Writing Data:"+e.toString());
            return false;
        }finally {
            try {
                if (output != null)
                    output.close();
                if (input != null)
                    input.close();

            } catch (IOException ignored) { }

            if (connection != null)
                connection.disconnect();
        }
        return false;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        PowerManager pm = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, activity.getClass().getName());
        wakeLock.acquire();
        dialog = new ProgressDialog(activity);
        dialog.setMessage("Map Source Checking");
        dialog.setCancelable(false);
        dialog.setIndeterminate(true);
        dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        dialog.show();
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        super.onPostExecute(aBoolean);
        if(dialog.isShowing()){
            dialog.dismiss();
        }
        if(wakeLock.isHeld())
            wakeLock.release();
        try {
            if (!aBoolean)
                Toast.makeText(context, "Download error", Toast.LENGTH_LONG).show();
            else
                Toast.makeText(context, "File downloaded", Toast.LENGTH_SHORT).show();
        }
        catch (Exception e){
            Log.d("MapUpdateManager", e.toString());
        }
    }
}



說明Explanation


這個整體程式使用的下載方式請參照這裡
這邊程式運作的方式是Async,可以在這邊瞭解更多
第57~61行:檢查圖資是不是比資料庫的還要舊,如果是就不繼續做更新的動作,否則就繼續做

Step By Step 簡易Mapsforge App開發:EnvCheck


目標Achievements


這個類別的工作是去偵測使用者是否有我們所需的地圖資料,若有那就去偵測是否有新的版本,否則就去下載。這個類別還做了一件事就是將圖資進行渲染(為了讓圖資檔案比較小,所以使用了向量圖的儲存方式)並喂給MapView去做顯示。


程式碼Code


package lien.ching.maptracker.api;

import android.app.Activity;
import android.app.DownloadManager;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;

import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.util.AndroidUtil;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.labels.LabelLayer;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.rendertheme.InternalRenderTheme;

import java.io.File;
import java.util.concurrent.TimeUnit;

import lien.ching.maptracker.Constant;
import lien.ching.maptracker.R;
import lien.ching.maptracker.overlay.NowLocationLayout;

/**
 * Created by lienching on 11/27/15.
 * This class is use for checking if the device have the required map file or not.
 */
public class EnvCheck {
    private Context context;
    private String externalpath = Environment.getExternalStorageDirectory().getPath();
    private MapDownloadManager mapDownloadManager;
    private MapUpdateManager mapUpdateManager;
    private mapDownloadManager downloadManger;
    private Activity activity;
    private MapView mapView;
    private NowLocationLayout locationLayout;
    public EnvCheck(MapView mapView,NowLocationLayout locationLayout,Context context,Activity activity){
        this.activity = activity;
        this.context = context;
        this.mapView = mapView;
        this.locationLayout = locationLayout;
    }
    public EnvCheck(Context context,Activity activity){
        this.activity = activity;
        this.context = context;
        this.mapView = null;
    }

    public void CheckAndDownload(String continent, String sourcefile) {
        String mapfile = continent+"/"+sourcefile;
        if(!this.isMapResourceExist(mapfile)) {
            downloadManger = new mapDownloadManager(mapView,locationLayout, context, mapfile);
            Thread thread = new Thread(downloadManger);
            thread.run();
        }
        else{
            MapFile targetFile = new MapFile(new File(Constant.PATH_MAPSFORGE+mapfile));
            TileCache tileCache = AndroidUtil.createTileCache(context, "mapcache", mapView.getModel().displayModel.getTileSize(), 1f, mapView.getModel().frameBufferModel.getOverdrawFactor());
            TileRendererLayer tileRendererLayer = AndroidUtil.createTileRendererLayer(tileCache,mapView.getModel().mapViewPosition,targetFile, InternalRenderTheme.OSMARENDER,true,true);
            tileRendererLayer.setXmlRenderTheme(InternalRenderTheme.OSMARENDER);
            mapView.getLayerManager().getLayers().add(tileRendererLayer);
        }
    }


    public boolean isMapResourceDirExist(final String continent){
        File dir = new File(Constant.PATH_MAPSFORGE+continent);
        if(!dir.exists()) {
            if (!dir.mkdirs()) {
                Log.e("EnvCheck","Dir create failed");
                return false;
            }
            Log.d("EnvCheck","Dir Create Sucess!");
        }
        return true;
    }
    public boolean isMapResourceExist(final String mapfile){
        if(!(new File(externalpath+"/mapsforge").exists()))
            createDirectory();
        File searchFile = new File(externalpath+"/mapsforge/maps/"+mapfile);
        if(searchFile.exists())
            return true;
        return false;
    }

    public void createDirectory(){
        File target = new File(externalpath+"/mapsforge/maps");
        target.mkdirs();
        if(target.exists()&&target.isDirectory()){
            Toast.makeText(context, R.string.toast_dir_success, Toast.LENGTH_SHORT).show();
        }
        else{
            Toast.makeText(context, R.string.toast_dir_fail_retry,Toast.LENGTH_SHORT).show();
            target.mkdir();
            if(target.exists()&&target.isDirectory()){
                Toast.makeText(context, R.string.toast_dir_success, Toast.LENGTH_SHORT).show();
            }
            else{
                Toast.makeText(context, R.string.toast_dir_fail, Toast.LENGTH_SHORT).show();
            }
        }
    }
}


說明Explanation


第64行:建立圖資渲染暫存區,For Detail 第65~66行: 建立圖資圖層並設定地圖風格

Step By Step 簡易Mapsforge App開發:NowLocationLayout


目標Achievements


這個類別顧名思義就是要去顯示目前使用者的地理位置並顯示在我們的地圖上面。


程式碼Code


package lien.ching.maptracker.overlay;

import android.Manifest;
import android.app.ActionBar;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.location.Criteria;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.util.Log;
import android.widget.TextView;

import org.mapsforge.core.graphics.Bitmap;
import org.mapsforge.core.graphics.Canvas;
import org.mapsforge.core.graphics.GraphicFactory;
import org.mapsforge.core.graphics.Paint;
import org.mapsforge.core.graphics.Style;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.Point;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.layer.Layer;
import org.mapsforge.map.layer.overlay.Circle;
import org.mapsforge.map.layer.overlay.Marker;
import org.mapsforge.map.layer.overlay.Polyline;
import org.mapsforge.map.model.MapViewPosition;
import org.mapsforge.map.android.util.AndroidSupportUtil;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import lien.ching.maptracker.R;

/**
 * Created by lienching on 12/2/15.
 */


//This class is using LocationManager to provided location information it may cause some issue, for more reliable location information use  Google Play services location APIs(http://developer.android.com/training/location/index.html)

public class NowLocationLayout extends Layer implements LocationListener,GpsStatus.Listener,ActivityCompat.OnRequestPermissionsResultCallback {


    private Activity activity;
    private MapViewPosition mapViewPosition;
    private MapView mapView;
    private LocationManager locationManager;
    private List history_path;

    private Boolean track;

    //For drawing purpose (http://mapsforge.org/docs/0.6.0/org/mapsforge/map/android/graphics/AndroidGraphicFactory.html)
    private static final GraphicFactory GRAPHIC_FACTORY = AndroidGraphicFactory.INSTANCE;
    private Circle accuray_circle;
    private Marker loc_marker;
    private Polyline usr_path;




    //Constructor
    public NowLocationLayout(Activity activity,MapViewPosition mapViewPosition,MapView mapView){
        this.activity = activity;
        this.mapViewPosition = mapViewPosition;
        this.mapView = mapView;
        this.locationManager = (LocationManager) activity.getSystemService(activity.LOCATION_SERVICE);

        track = false;

        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L,0, this);
        //Check if location services available
        if(!locationManager.isProviderEnabled (LocationManager.GPS_PROVIDER)){
            activity.startActivityForResult(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0);
        }

        locationManager.addGpsStatusListener(this);//To add satellite listener


        //setDisplayModel(this.displayModel) set circle, polyline, marker etc... in to mapview's display model, so DisplayModel.getTileSize() won't point to a null object.
        accuray_circle = new Circle(null,0,getDefaultCircleFill(),getDefaultCircleStroke());//Circle(LatLong,radius,color,stroke);
        accuray_circle.setDisplayModel(mapView.getModel().displayModel);

        usr_path = new Polyline(getDefaultPolylineStroke(),GRAPHIC_FACTORY);
        usr_path.setDisplayModel(mapView.getModel().displayModel);

        //AndroidGraphicFactory.convertToBitmap() for detail please check AndroidGraphicFactory
        Drawable drawable = activity.getResources().getDrawable(R.drawable.locationmarker);
        Bitmap bitmap = AndroidGraphicFactory.convertToBitmap(drawable);
        loc_marker = new Marker(null, bitmap, 1, 0);
        loc_marker.setDisplayModel(mapView.getModel().displayModel);

        history_path = new LinkedList();//record user path
    }

    //Transfer Function

    private LatLong LocationToLatLong(Location location){
        LatLong latlong = new LatLong(location.getLatitude(),location.getLongitude());
        return latlong;
    }

    //Getter
    protected Paint getPaint(int color, float strokeWidth, Style style){
        Paint paint = GRAPHIC_FACTORY.createPaint();
        paint.setColor(color);
        paint.setStrokeWidth(strokeWidth);
        paint.setStyle(style);
        return paint;
    }

    private Paint getDefaultCircleFill() {
        return this.getPaint(GRAPHIC_FACTORY.createColor(48, 0, 0, 255), 0, Style.FILL);
    }

    private Paint getDefaultCircleStroke() {
        return this.getPaint(GRAPHIC_FACTORY.createColor(160, 0, 0, 255), 2, Style.STROKE);
    }

    private Paint getDefaultPolylineStroke() {
        return this.getPaint(GRAPHIC_FACTORY.createColor(160, 0, 255, 0), 20, Style.STROKE);
    }


    //Enable Location Tracking
    public void startTrack(){
        this.track = true;
    }


    @Override
    public void draw(BoundingBox boundingBox, byte zoomLevel, Canvas canvas, Point topLeftPoint) {
        if(track) {
            loc_marker.draw(boundingBox, zoomLevel, canvas, topLeftPoint);
            accuray_circle.draw(boundingBox, zoomLevel, canvas, topLeftPoint);
        }
        List temp = usr_path.getLatLongs(); //To get all the LatLong that had been drew
        temp.removeAll(temp); //Remove all
        temp.addAll(history_path);//To re-add all path
        usr_path.draw(boundingBox, zoomLevel, canvas, topLeftPoint);
    }

    @Override
    public void onGpsStatusChanged(int event) {

    }

    @Override
    public void onLocationChanged(Location location) {

        LatLong latLong = this.LocationToLatLong(location);
        loc_marker.setLatLong(latLong);
        accuray_circle.setLatLong(latLong);
        history_path.add(latLong);

        accuray_circle.setRadius(location.getAccuracy());

        mapViewPosition.setCenter(latLong);
        mapViewPosition.setZoomLevel((byte) 17);
        requestRedraw();

    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {}

    @Override
    public void onProviderEnabled(String provider) {
        locationManager.requestLocationUpdates(provider, 1000L, 0, this);
        if(locationManager.GPS_PROVIDER.equals(provider)) {
            track = true;
        }
    }

    @Override
    public void onProviderDisabled(String provider) {
        if(locationManager.GPS_PROVIDER.equals(provider)){
            track = false;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {}

}


說明Explanation


第79行:這行是用來設定GPS更新頻率,參數大概為(地理資訊提供(GPS, Network), 更新時間(單位:毫秒), 更新距離(當距離超過就更新), 接收資訊的Listener),For Detail
第90、93、99行:如果不把那些物件的DisplayModel設定成MapView的,會出現NullPointerException
第145~147行:第145行是Pass by Reference所以我們可以直接用temp去操作,然後我們先把原本的List清空,再把歷史紀錄再放回去
第164行:這句是利用GPS準確度來當作圓的半徑,For Detail

Step By Step 簡易Mapsforge App開發:MapDownloadManager


目標Achievements


這個類別是用來去做圖資下載相關工作,我在範例中是使用第一種,但是為了學習方便我把另外一種也寫出來了


程式碼Code


1. 使用DownloadManager
package lien.ching.maptracker.api;

import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.util.Log;

import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.util.AndroidUtil;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.datastore.MapDataStore;
import org.mapsforge.map.layer.Layer;
import org.mapsforge.map.layer.LayerManager;
import org.mapsforge.map.layer.Layers;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.labels.LabelLayer;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.rendertheme.InternalRenderTheme;

import java.io.File;
import java.net.URL;

import lien.ching.maptracker.Constant;
import lien.ching.maptracker.overlay.NowLocationLayout;


/**
 * Created by lienching on 12/21/15.
 */
public class mapDownloadManager implements  Runnable{


    private DownloadManager downloadManager;
    private Context context;
    private Long enqueue;
    public MapView mapview;
    public String target;
    private BroadcastReceiver receiver;
    private NowLocationLayout locationLayout;
    public mapDownloadManager(MapView mapView,NowLocationLayout locationLayout,Context context, final String target){
        super();
        this.context = context;
        this.target = target;
        this.mapview = mapView;
        this.locationLayout = locationLayout;
    }
    @Override
    public void run() {
        downloadManager = (DownloadManager) context.getSystemService(context.DOWNLOAD_SERVICE);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse("http://download.mapsforge.org/maps/" + target));
        request.setDestinationInExternalPublicDir("mapsforge/maps/",target);
        enqueue = downloadManager.enqueue(request); //Active Download Process
        receiver = new LayerAdder(mapview,locationLayout,target,enqueue); //Created exclusive receiver
        context.registerReceiver(receiver,new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); //Registered Reciever
    }


}


2. 使用HTTP Download (Async)
package lien.ching.maptracker.api;

import android.app.Activity;
import android.app.DownloadManager;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.PowerManager;
import android.util.Log;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import lien.ching.maptracker.Constant;
import lien.ching.maptracker.MainActivity;

/**
 * Created by lienching on 11/27/15.
 * In this class, I want to put downloading process in to Async
 * I directly implementing the HTTP Download in doInBackground()
 * In the app I didn't use this class as the download class
 * if you want to see the source code that use in the app
 * please check mapDownloadManager.class for more detail
 */
public class MapDownloadManager extends AsyncTask{
    private Context context;
    private Activity activity;
    private ProgressDialog dialog;
    public PowerManager.WakeLock wakeLock;
    public MapDownloadManager(Context context,Activity activity){
        super();
        this.activity = activity;
        this.context = context;
    }

    @Override
    protected Boolean doInBackground(String... params) {

        //For Details, please check http://stackoverflow.com/a/3028660
        String continent = params[1];
        InputStream input = null;
        OutputStream output = null;
        HttpURLConnection connection = null;
        File outputFile = new File(Constant.PATH_MAPSFORGE+params[0]);

        try{
            URL url = new URL("http://download.mapsforge.org/maps/"+params[0]);
            connection = (HttpURLConnection) url.openConnection();
            connection.connect();

            if(connection.getResponseCode() != HttpURLConnection.HTTP_OK){
                Toast.makeText(context,"Connection Fail..."+connection.getResponseCode(),Toast.LENGTH_SHORT).show();
                return false;
            }
            input = connection.getInputStream();
            if(outputFile.exists()){
                output = new FileOutputStream(outputFile);
            }
            else{
                EnvCheck envCheck = new EnvCheck(context,activity);
                envCheck.isMapResourceDirExist(continent);
                outputFile.createNewFile();
                output = new FileOutputStream(outputFile);

            }

            byte data[] = new byte[8192];
            int count;
            long total = 0;
            while((count = input.read(data)) != -1){
                if(isCancelled()) {
                    input.close();
                    return false;
                }
                total += count;
                output.write(data,0,count);
            }

            Log.d("MapDownloadManager","Write "+total+" bytes");
        }catch (Exception e){
            Log.e("MapDownloadManager","Writing Data:"+e.toString());
            return false;
        }finally {
            try {
                if (output != null)
                    output.close();
                if (input != null)
                    input.close();
            } catch (IOException ignored) {
            }
            outputFile.setLastModified(connection.getLastModified());
            if (connection != null)
                connection.disconnect();
        }
        return false;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        PowerManager pm = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, activity.getClass().getName());
        wakeLock.acquire();
        dialog = new ProgressDialog(activity);
        dialog.setMessage("Map Source Downing");
        dialog.setCancelable(false);
        dialog.setIndeterminate(true);
        dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        dialog.show();
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        super.onPostExecute(aBoolean);
        if(dialog.isShowing()){
            dialog.dismiss();
        }
        if(wakeLock.isHeld())
            wakeLock.release();
        try {
            if (!aBoolean)
                Toast.makeText(context, "Download error", Toast.LENGTH_LONG).show();
            else
                Toast.makeText(context, "File downloaded", Toast.LENGTH_SHORT).show();
        }
        catch (Exception e){
            Log.d("MapUpdateManager", e.toString());
        }
    }
}


說明Explanation


第一種方式非常的單純也比較不需要擔心使用者不小心離開程式的時候會發生問題,但是缺點就是需要用Reciever去做下載完成判斷
第二種就是比較資工系的做法,直接實作HTTP Download,這個方法的好處就是可以對於"下載"這個動作做很多客製化的事情,但是要處理的事情就很多

Step By Step 簡易Mapsforge App開發:LayerAdder


目標Achievements


這個類別目的是要用來做動態新增圖層


程式碼Code


package lien.ching.maptracker.api;

import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.util.AndroidUtil;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.labels.LabelLayer;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.rendertheme.InternalRenderTheme;
import org.mapsforge.map.rendertheme.rule.RenderTheme;

import java.io.File;

import lien.ching.maptracker.Constant;
import lien.ching.maptracker.overlay.NowLocationLayout;

/**
 * Created by lienching on 1/7/16.
 */

//http://developer.android.com/reference/android/content/BroadcastReceiver.html
public class LayerAdder extends BroadcastReceiver {
    private MapView mapView;
    private String target;
    private NowLocationLayout locationLayout;
    private Long enqueue;
    public LayerAdder(MapView mapView,NowLocationLayout locationLayout,String target,Long enqueue){
        super();
        this.mapView = mapView;
        this.target = target;
        this.enqueue = enqueue;
        this.locationLayout = locationLayout;
    }
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
            long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
            if(downloadId == enqueue){
                MapFile targetFile = new MapFile(new File(Constant.PATH_MAPSFORGE+target));
                TileCache tileCache = AndroidUtil.createTileCache(context, "mapcache", mapView.getModel().displayModel.getTileSize(), 1f, mapView.getModel().frameBufferModel.getOverdrawFactor());
                TileRendererLayer tileRendererLayer = AndroidUtil.createTileRendererLayer(tileCache,mapView.getModel().mapViewPosition,targetFile, InternalRenderTheme.OSMARENDER,true,true);
                tileRendererLayer.setXmlRenderTheme(InternalRenderTheme.OSMARENDER);
                mapView.getLayerManager().getLayers().add(tileRendererLayer);
                mapView.getLayerManager().getLayers().remove(locationLayout);//LocationLayout need be the top layout of the mapview, to keep user location visible
                mapView.getLayerManager().getLayers().add(locationLayout);
                Log.d("LayerAdder",target+" Download Completed"+", LayerAdd");
            }
        }
    }
}
 

說明Explanation


BroadcastReceiver是我用來去偵測檔案是否下載完成的類別

2016年2月19日 星期五

Step By Step 簡易Mapsforge App開發:MainActivity


目標Achievements


這個Class是我們App的導入點,我稱這種類別為Main類別,那我們在這個類別中我們就需要去做整個程式大方向的規劃.

程式碼Code


package lien.ching.maptracker;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;

import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.MapPosition;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.util.AndroidUtil;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.datastore.MapDataStore;
import org.mapsforge.map.datastore.MultiMapDataStore;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.rendertheme.InternalRenderTheme;

import java.io.File;

import lien.ching.maptracker.api.EnvCheck;
import lien.ching.maptracker.overlay.NowLocationLayout;

/**
 * Created by lienching on 11/27/15.
 */
public class MainActivity extends Activity {
    private MapView mapView;
    private MapDataStore taiwanMap,worldMap;
    private MultiMapDataStore multiMapDataStore;
    private TileCache tileCache;
    private TileRendererLayer tileRendererLayer;
    private EnvCheck envCheck;
    private NowLocationLayout nowLocationLayout;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AndroidGraphicFactory.createInstance(this.getApplication()); //Initiation Mapsforge Library
        mapView = new MapView(this);
        mapView.setClickable(true);
        mapView.getMapScaleBar().setVisible(true);
        mapView.setBuiltInZoomControls(true);
        mapView.getMapZoomControls().setZoomLevelMin((byte) 5);
        mapView.getMapZoomControls().setZoomLevelMax((byte) 20);
        mapView.getModel().mapViewPosition.setMapPosition(new MapPosition(new LatLong(23.6, 121), (byte) 7));
        //Disable hardware acceleration at mapview, for more detail about this line of code please check (https://goo.gl/v5weg9)
        mapView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        setContentView(mapView);
        nowLocationLayout = new NowLocationLayout(this,mapView.getModel().mapViewPosition,mapView);
        envCheck = new EnvCheck(mapView,nowLocationLayout,this.getApplicationContext(),this);
        //To keep screen on(http://developer.android.com/training/scheduling/wakelock.html)
        this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

    }

    @Override
    protected void onStart() {
        super.onStart();

        envCheck.CheckAndDownload("world", "world-lowres-0-7.map"); //Check if the map source exist
        envCheck.CheckAndDownload("asia", "taiwan.map");
        mapView.getLayerManager().getLayers().add(nowLocationLayout);//Add Location Layer
        nowLocationLayout.startTrack();
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    protected void onStop() {
        super.onStop();
        this.mapView.destroyAll();
    }

}


說明Explanation


第41行:這一行因為Mapsforge所用的androidsvg必須要做一些初始化,所以我們在第一行就將他呼叫避免後續問題
第50行:用來關閉硬體加速( For Detail
第55行:因為這個範例是要用來紀錄使用者的移動軌跡,所以需要使用這行(For Detail)
第63~64行:檢查檔案是否存在或有沒有新版本,(詳細運作請參考EnvCheck)
第65~66行:因為前兩行需要花時間但也必須要讓使用者知道程式有在運作,所以先讓定位開始運作

2016年2月5日 星期五

Step By Step 簡易Mapsforge App開發:Introduction


前言


先前筆者有稍微介紹了Mapsforge,不知道大家目前有沒對於Mapsforge產生了一點興趣呢? 這次筆者打算用一個比較不抽象的方式來教大家Mapsforge要如何使用~ 本次專案的程式碼將會發佈在 https://github.com/lienching/MapTracker

前置準備Preparation


  1. 使用者所需具備能力
    1. 基本的Java概念
    2. 基本的Android APP開發概念
    3. 熱愛開發的心
  2. 專案所需API(mapsforge系列載點androidsvg載點kXML2載點)
    1. mapsforge-map
    2. mapsforge-core
    3. mapsforge-map-android
    4. mapsforge-map-reader
    5. androidsvg
    6. kXML2


AndroidManifest設置Setup


權限部分(詳細請參考這裡)
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>

這個教學將會有6個部分,分別為 然後他們有一個共用的常數庫 Constant.java
package lien.ching.maptracker;

import android.os.Environment;

/**
 * Created by lienching on 11/27/15.
 */
public class Constant {
    public static final String PATH_MAPSFORGE = Environment.getExternalStorageDirectory().toString() + "/mapsforge/maps/";
    public static final String PATH_WORLDMAP = PATH_MAPSFORGE + "world/world-lowres-0-7.map";
    public static final String PATH_TAIWANMAP = PATH_MAPSFORGE + "asia/taiwan.map";

}