2014/06/03

android 使用google map API v2 顯示地圖

android 使用google map API v2 顯示地圖

要利用google 所提供的map api來顯示地圖。

在測試程式之前,有一先準備工作要先完成,可以先參考這個網址: http://www.cnblogs.com/mengdd/archive/2013/01/01/2841390.html

準備好後,就可以寫個測試程式來踹看看。

先建立一個android project,將它命名為googlemaptest。

AndroidManifaset.xml

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

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

    <permission
        android:name= "com.example.mapdemo.permission.MAPS_RECEIVE"
        android:protectionLevel= "signature" />

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

    <uses-permission android:name= "com.example.mapdemo.permission.MAPS_RECEIVE" />
    <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-permission android:name= "android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name ="android.permission.INTERNET" />
    <uses-permission android:name= "android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:allowBackup= "true"
        android:icon= "@drawable/ic_launcher"
        android:label= "@string/app_name"
        android:theme= "@style/AppTheme" >
        <activity
            android:name= "com.example.googlemaptest.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.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name= "com.google.android.maps.v2.API_KEY"
            android:value="yourAPI_KEY" />
    </application >

</manifest>

要加入的東西不少

<permission
    android:name= "com.example.mapdemo.permission.MAPS_RECEIVE"
    android:protectionLevel= "signature" />

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

<uses-permission android:name= "com.example.mapdemo.permission.MAPS_RECEIVE" />
<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-permission android:name= "android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name = "android.permission.INTERNET" />
<uses-permission android:name= "android.permission.ACCESS_NETWORK_STATE" />

在application 裡加入

<meta-data
    android:name= "com.google.android.gms.version"
    android:value ="@integer/google_play_services_version" />
<meta-data
    android:name= "com.google.android.maps.v2.API_KEY"
    android:value ="yourAPI_KEY" />

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" >

    <RelativeLayout
        android:id= "@+id/topbar1"
        android:layout_width= "fill_parent"
        android:layout_height= "wrap_content"
        android:background= "#00666666" >

        <Button
            android:id= "@+id/btn_query"
            android:layout_width= "wrap_content"
            android:layout_height= "wrap_content"
            android:layout_alignParentRight="true"
            android:text= "查詢"
            android:textSize= "16sp" />

        <EditText
            android:id= "@+id/et_address"
            android:layout_width= "wrap_content"
            android:layout_height= "wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:layout_toLeftOf= "@+id/btn_query"
            android:ems= "10"
            android:inputType= "textMultiLine" />

    </RelativeLayout >

    <fragment
        android:id= "@+id/map"
        android:layout_width= "match_parent"
        android:layout_height= "match_parent"
        android:layout_below= "@+id/topbar1"
        class= "com.google.android.gms.maps.MapFragment" />

在xml中配置一個輸入地址的文字框,一個查詢按鈕,和一個顯示地圖的map fragment。

MainActivity.java

 private EditText et_address ;
 private Button btn_query;
 private GoogleMap map;
 private LocationManager mgr;
 private LocationListener mLocationListenerGPS;
 private LocationListener mLocationListenerNETWORK;
 private final int TIMEOUT_SEC = 20;
 private int currentTime = 0;
 private Handler handler = new Handler();
 private boolean getLocationFlag = false;
 private LatLng START = null; // 啟始座標

 @Override
 protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout. activity_main);
       et_address = (EditText) this.findViewById(R.id. et_address);

       // 先打開GPS定位
       map = ((MapFragment) getFragmentManager().findFragmentById(R.id.map ))
                 .getMap();
       map.setMyLocationEnabled( true);
       mgr = (LocationManager) getSystemService(LOCATION_SERVICE );
       getGps();

 }

 public void getGps () {

       getLocationFlag = false;
       initLocationListener();
       startTimeoutMechanism();

       if (!mgr.isProviderEnabled(LocationManager. GPS_PROVIDER)) {

            Log. d("GpsTools!" , "gps not open!!!" );
            Toast. makeText( this, "GPS衛星定位尚未開啟\n請先開啟GPS衛星定位尚未開啟" ,
                       Toast. LENGTH_LONG).show();
       } else {
             mgr.requestLocationUpdates(LocationManager. GPS_PROVIDER, 0, 0,
                        mLocationListenerGPS);
       }

       NetworkTools networkTools = new NetworkTools();
       if (!networkTools.checkInternetConnection( this)) {
            Log. d("GpsTools!" , "network not open!!!" );
       } else {
             mgr.requestLocationUpdates(LocationManager. NETWORK_PROVIDER, 0, 0,
                        mLocationListenerNETWORK);
       }
 }

 private void stopGps() {
        mgr.removeUpdates( mLocationListenerNETWORK);
        mgr.removeUpdates( mLocationListenerGPS);
 }

 private void initLocationListener() {
        mLocationListenerGPS = new LocationListener() {
             @Override
             public void onLocationChanged(Location location) {
                 //設定位置
                 setLocation(location);
            }

             @Override
             public void onProviderDisabled(String provider) {
                 Log. d("mLocationListenerGPS" , "onProviderDisabled" );
            }

             @Override
             public void onProviderEnabled(String provider) {
                 Log. d("mLocationListenerGPS" , "onProviderEnabled" );
            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {
                 Log. d("mLocationListenerGPS" , "onStatusChanged" );
            }
       };

       mLocationListenerNETWORK = new LocationListener() {
             @Override
             public void onLocationChanged(Location location) {
                 //設定位置
                 setLocation(location);
            }

            @Override
            public void onProviderDisabled(String provider) {
                 Log. d("mLocationListenerNETWORK" , "onProviderDisabled" );
            }

            @Override
            public void onProviderEnabled(String provider) {
                 Log. d("mLocationListenerNETWORK" , "onProviderEnabled" );
            }

            @Override
            public void onStatusChanged(String provider, int status,
                       Bundle extras) {
                 Log. d("mLocationListenerNETWORK" , "onStatusChanged" );
            }

       };
 }

 private Runnable timeOutRunnable = new Runnable() {
        public void run() {
            Log. d("GpsTools" , "" + currentTime );
             currentTime++;
             if ( currentTime > TIMEOUT_SEC) {
                 //停止定位
                 stopGps();
                 getLocationFlag = true;
            }

             if (! getLocationFlag) {
                  // 如為false,表示尚未取得,再試一次
                  handler.postDelayed( this, 1000);
            } else {

            }

       }
 };

 private void startTimeoutMechanism() {
        handler.postDelayed( timeOutRunnable, 1000);
 }



 private void setLocation (Location location) {
       String addrline = null;
       Double longitude = null;
       Double latitude = null;

       Log. d("GpsTools" , "longitude" + location.getLongitude());
       Log. d("GpsTools" , "latitude" + location.getLatitude());

       try {
            Geocoder gc = new Geocoder(MainActivity. this,
                       Locale. TRADITIONAL_CHINESE); // 地區:台灣
             // 自經緯度取得地址
            List<Address> lstAddress = gc.getFromLocation(
                       location.getLatitude(), location.getLongitude(), 1);
            Address address = lstAddress.get(0);
            addrline = address.getAddressLine(0);

            et_address.setText(addrline); //顯示定位後的地址

            longitude = address.getLongitude();
            latitude = address.getLatitude();
       } catch (Exception e) {
            e.printStackTrace();
       }

       if (addrline != null) {
            stopGps();
             // 取到位置了,將flag設為true,讓timeout機制不要再跑了
             getLocationFlag = true;
             START = new LatLng(latitude, longitude);
             setMapMarker( "現在位置" );
       }
 }

 private void setMapMarker(String addrStr) {
       if ( map != null) {
            map.clear();
            Marker note1 = map.addMarker( new MarkerOptions().position(START).title(addrStr));
       }

       // 設定中心點
       CameraUpdate center = CameraUpdateFactory.newLatLng(START);
       map.moveCamera(center);
       map.animateCamera(CameraUpdateFactory. zoomTo(15), 1000, null );

       // 也可以用這個方法取代上面設定中心點
       // map.moveCamera(CameraUpdateFactory.newLatLngZoom(START, 15));
       //

 }

加一個class NetworkTools.java

package com.example.googlemaptest;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;

public class NetworkTools {
     public boolean checkInternetConnection(Context c){
          ConnectivityManager cm=(ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE);
          NetworkInfo ni=cm.getActiveNetworkInfo();
          if(ni!=null && ni.isConnected()){
               // System.out.println("ni.isConnected() = "+ni.isConnected());
               return ni.isConnected();
          }else{
               // System.out.println("ni.isConnected() = "+ni.isConnected());
               return false;
          }
     }
}

執行測試程式,如果順利的話,應該可以定位到目前位置,在欄位顯示大略地址,並在地圖上顯示節點和抬頭說明。

我們也可以輸入地址來查詢地址: 接著在MainActivity, 也加入一個Method:

private void getLatLngByAddr() {

    Geocoder gc = new Geocoder(this, Locale.TRADITIONAL_CHINESE); // 地區:台灣

    try {
        List<Address> addresses = gc.getFromLocationName(et_address
                .getText().toString().trim(), 1);

        if (addresses != null && !addresses.isEmpty()) {
            double latitude = ((Address) addresses.get(0)).getLatitude();
            double longitude = ((Address) addresses.get(0)).getLongitude();
            String addrline = ((Address) addresses.get(0))
                    .getAddressLine(0);

            if (addrline != null) {
                START = new LatLng(latitude, longitude);
                setMapMarker(et_address.getText().toString().trim());
            }
        }

    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

}

在onCreate Method中加入

btn_query = (Button) this.findViewById(R.id. btn_query);

        btn_query.setOnClickListener( new OnClickListener() {

             @Override
             public void onClick(View v) {
                 //getLatLngByAddr();
            }
        });

執行程式再試試看,應該可以用地址查詢位置了。

erlang dialyzer

dialyzer 是 DIscrepancy AnalYZer for ERlang programs 的縮寫,這是靜態分析工具,可在執行前,用來偵測一些錯誤狀況,例如資料型別定義錯誤、dead or unreachable code、不必要的測試等等。

plt: persisten lookup table

為了分析程式,我們可產生 plt (persistent lookup table) 檔案,用來減少程式碼分析的時間,plt 裡面儲存了資料型別與函數的資訊。

首先產生一個常用函式庫的 plt,裡面包含了 erts, kernel, stdlib, mnesia, crypto, sasi。

在 linux 可直接產生 plt 檔案,但在 Windows 會因為缺少環境變數 HOME 或 DIALYZER_PLT,而產生錯誤訊息:The HOME environment variable needs to be set so that Dialyzer knows where to find the default PLT,必須先設定環境變數,然後再產生 plt 檔案。

set HOME=%USERPROFILE%

再用以下指令產生 .dialyzer_plt 檔案

> dialyzer --build_plt --apps erts kernel stdlib mnesia crypto sasl

  Creating PLT c:/Users/username/.dialyzer_plt ...
Unknown functions:
  compile:file/2
  compile:forms/2
  compile:noenv_forms/2
  compile:output_generated/1
  xref:add_application/3
  xref:analyze/2
  xref:set_default/3
  xref:set_library_path/2
  xref:start/2
  xref:stop/1
Unknown types:
  compile:option/0
 done in 4m22.58s
done (passed successfully)

從錯誤訊息看出還缺少了一些函數,可以使用用以下指令,再把一些缺少的 module 增加到 plt 裡面。但增加後,還是會持續產生一些錯誤訊息,我們就不再增加那些函式庫的 plt

> dialyzer --add_to_plt --plt "%HOME%/.dialyzer_plt" --apps compiler tools webtool debugger inets

也可以使用以下這個方式,直接用 -c 指定 ebeam 的目錄。

> dialyzer --add_to_plt --plt "%HOME%/.dialyzer_plt" -c "%HOME%/lib/ssl-5.3.3/ebin"

如果要從 plt 中移除某個函式庫,則是用

dialyzer --remove_from_plt --plt "%HOME%/.dialyzer_plt" -c "%HOME%/lib/ssl-5.3.3/ebin"

進行 dialyzer 分析

以一個簡單的 test.erl 為例

%% test.erl
-module(test).
-export([bar/1, foo/0]).

bar(List) ->
    abc.

foo() ->
    V1 = bar([]),
    io:format("length: ~p~n", [length(V1)]).

由原始檔分析

> dialyzer --src -c test.erl
  Checking whether the PLT c:/Users/username/.dialyzer_plt is up-to-date... yes
  Proceeding with analysis...
test.erl:8: Function foo/0 has no local return
test.erl:10: The call erlang:length(V1::'abc') will never return since it differs in the 1st argument from the success typing arguments: ([any()])
Unknown functions:
  erlang:get_module_info/1
  erlang:get_module_info/2
  io:format/2
 done in 0m0.44s
done (warnings were emitted)

由以下兩行得知,程式發生資料型別的錯誤

test.erl:8: Function foo/0 has no local return
test.erl:10: The call erlang:length(V1::'abc') will never return since it differs in the 1st argument from the success typing arguments: ([any()])

將程式修改一下

-module(test).
-export([bar/1, foo/0]).

bar(List) ->
    [1,2,3].

foo() ->
    V1 = bar([]),
    io:format("length: ~p~n", [length(V1)]).

分析就會成功

> dialyzer --src -c test.erl
  Checking whether the PLT c:/Users/username/.dialyzer_plt is up-to-date... yes
  Proceeding with analysis...
Unknown functions:
  erlang:get_module_info/1
  erlang:get_module_info/2
  io:format/2
 done in 0m0.42s
done (passed successfully)

由目的檔分析

如果直接編譯,分析時會出現類似這樣的錯誤

Could not scan the following file(s):
  Could not get abstract code for: d:/temp/code/test.beam
  Recompile with +debug_info or analyze starting from source code

因此我們要在編譯時,增加參數 +debug_info

> erlc +debug_info test.erl

接下來就可以對 ebeam 進行分析

> dialyzer -r .
  Checking whether the PLT c:/Users/username/.dialyzer_plt is up-to-date... yes
  Proceeding with analysis...
test.erl:8: Function foo/0 has no local return
test.erl:10: The call erlang:length(V1::'abc') will never return since it differ
s in the 1st argument from the success typing arguments: ([any()])
Unknown functions:
  erlang:get_module_info/1
  erlang:get_module_info/2
  io:format/2
 done in 0m0.42s
done (warnings were emitted)

2014/05/26

erlang - edoc

對於一個 erlang library 來說,因為 erlang 習慣把 client 跟 server 使用的 code 都放進同一個 module 裡面,而且程式裡面只能利用 export 的宣告,來知道這個 module 提供什麼 API 介面,至於要怎麼使用,就只能靠文件說明來輔助。

edoc 是 erlang 程式的 document generator,它是因應 Java 語言裡 Javadoc 的概念啟發,語法跟 javadoc 類似。

多國語言

因為 erlang R17 才預設 erl 原始程式碼都要使用 utf8 編碼,以 R16 的狀況,我們必須根據 Using Unicode in Erlang 文件的說明,因為 R16B 預設coding 為 bytewise (or latin1) encoding,如果在 module 的程式碼裡面要寫上中文字,而檔案要存成 utf-8 時,erl module source file 的第一行就必須要寫上

%% -*- coding: utf-8 -*-

如果沒有加上這個,產生 edoc 的文件就會直接變成亂碼。

edoc 實例分析

我們以 rabbitmq erlang client 為例,直接看一個正式的 erlang library 是怎麼使用 edoc。

首先,要先下載原始程式碼,通常我們會直接在官方網站 下載 原始程式碼,目前是 rabbitmq-server-3.2.4.tar.gz,但是下載後,查閱程式碼,Makefile 裡面只有很簡單的編譯語法,因此,我們就改變一下,到 rabbitmq-erlang-client github 下載程式碼,這裡的 Makefile 就有比較完整的 edoc 處理的資訊。

關於文件的部份,首先要修改 Makefile 前面的 VERSION 變數,原本是 0.0.0,但應該修改成 3.2.4。

# Makefile

VERSION=3.2.4

###############################################################################
##  Documentation
###############################################################################

documentation: $(DOC_DIR)/index.html

$(DOC_DIR)/overview.edoc: $(SOURCE_DIR)/overview.edoc.in
    mkdir -p $(DOC_DIR)
    sed -e 's:%%VERSION%%:$(VERSION):g' < $< > $@

$(DOC_DIR)/index.html: $(DEPS_DIR)/$(COMMON_PACKAGE_DIR) $(DOC_DIR)/overview.edoc $(SOURCES)
    $(LIBS_PATH) erl -noshell -eval 'edoc:application(amqp_client, ".", [{preprocess, true}, {macros, [{edoc, true}]}])' -run init stop

相關的變數是放在 common.mk 裡面,並直接由 Makefile 引用

# common.mk

SOURCE_DIR=src
DIST_DIR=dist
DEPS_DIR=deps
DOC_DIR=doc
......

先直接執行看看,直接 make 或 make all 都沒有看到 doc 文件的結果,所以要再 make documentation

> make documentation
mkdir -p doc
sed -e 's:%%VERSION%%:3.2.4:g' < src/overview.edoc.in > doc/overview.edoc
ERL_LIBS=deps:dist erl -noshell -eval 'edoc:application(amqp_client, ".", [{preprocess, true}, {macros, [{edoc, true}]}])' -run init stop

第一步是建立 doc 目錄
第二步是把 src/overview.edoc.in 移動到 doc/overview.edoc,同時修改版本號碼的內容
第三步是使用 edoc module 裡的 application,ERL_LIBS是 erl 接受的環境變數值,內容是erl執行時,所需要的其他 library 的目錄。接下來就 -eval 一段 erlang srcipt,最後再執行 init module 的 stop method 把erl關掉。

ERL_LIBS=deps:dist erl 的寫法,是因為 bash IFS 預設值為 space, a tab and a newline,寫在同一行,可以讓 erl 使用一個暫時的環境變數 ERL_LIBS,卻又不會永久影響該環境變數。

別的執行方式

> erl -noshell -run edoc_run packages '[""]' '[{dir, "doc"},{source_path, ["src"]}]'

在 Windows 因為參數 parsing 的問題,參數兩頭必須都要用雙引號,因此上面的指令要調成

erl -noshell -run edoc_run packages "[\"\"]" "[{dir, \"doc\"},{source_path, [\"src\"]}]"

在 linux 環境,單引號或雙引號的寫法都可以運作。

也可以這樣執行:

erl -noshell -run edoc_run application "'rabbitmq erlang client'" "\".\"" "[{def,{vsn,\"3.2.4\"}},{source_path, [\"src\"]}]"

edoc module

edoc_run 裡面有把如何從 erlang startup 的時候,就直接呼叫 edoc。但因為 rabbitmq 例子是從 shell 執行一段 script,所以要直接看 edoc 的文件,script是執行 edoc:applation/3 這個 method。

application(Application::atom(), Dir::filename(), Options::proplist()) -> ok

文件會被輸出到第二個參數目錄下的 doc 子目錄。preprocess 設定可讓所有 source files 都先讓 erlang preprocessor (epp) 先處理一次,當程式裡面使用了 macro 的時候,就一定要打開 preprocess 設定。同時要在 macros 列出所需要的 macro definitions,也就是要使用 edoc 裡面定義的巨集。

amqp_rpc_client.erl

amqp_rpc_cient edoc 是 edoc 的頁面,每個文件都有三個部份 (1) Description (2) Function Index (3) Function Details

對應到 amqp_rpc_client.erl 原始程式碼,最前面是版權宣告,在一行空白之後,就是 %% @doc 開頭的一段文字,這段文字的第一句話,會自動變成文件一開始的簡介,然後這一整段文字,都會成為 (1) Description 的內容。

接下來是 API 的部份,amqp_rpc_client 有三個 public function,每個function都有 %% @spec 與 %% @doc 兩段內容,這兩個部份會成為文件的 (3) Function Details 的段落。

而(2) Function Index可看到三個 public function 的 table。

後面的 function 都沒有 @spec 與 @doc 的說明,甚至到了 callback function ,還標記了 %% @private,這表示這些 function 屬於 library 內部使用,使用者不需要在文件上知道這些 function 的說明。

%% The contents of this file are subject to the Mozilla Public License
%% Version 1.1 (the "License"); you may not use this file except in
%% compliance with the License. You may obtain a copy of the License at
%% http://www.mozilla.org/MPL/
%% ...

%% @doc This module allows the simple execution of an asynchronous RPC over
%% AMQP. It frees a client programmer of the necessary having to AMQP
%% plumbing. .....
-module(amqp_rpc_client).

...

%%--------------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------------

%% @spec (Connection, Queue) -> RpcClient
%% where
%%      Connection = pid()
%%      Queue = binary()
%%      RpcClient = pid()
%% @doc Starts a new RPC client instance that sends requests to a
%% specified queue. This function returns the pid of the RPC client process
%% that can be used to invoke RPCs and stop the client.
start(Connection, Queue) ->
    {ok, Pid} = gen_server:start(?MODULE, [Connection, Queue], []),
    Pid.

...

%%--------------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------------

%% Sets up a reply queue and consumer within an existing channel
%% @private
init([Connection, RoutingKey]) ->
....

edoc tags

在 edoc 可使用的完整 tag 列表,可在 edoc user's guide 裡面看到,tags 有分 generic, overview, module, function 四種,文件中也刻意把最複雜的 @spec @type 獨立一個章節 Type specifications。

running edoc

有四種

  1. edoc:application/2 產生一個 application 的文件
  2. edoc:packages/2 產生多個 packages 的文件
  3. edoc:files/2 產生數個 source files 的文件
  4. edoc:run/3 上面三個 functions 的最一般化的函數版本

overview page

在文件的目的目錄放置一個 overview.edoc 檔案,內容可以使用 Overview 與 Generic tags

Generic tags

可用在任何一個地方

  1. @clear
    會把前面所有 tags 都丟棄
  2. @docfile
    Reads a plain documentation file
  3. @end
    標記這是上一個 tag 的結尾
  4. @headerfile
    類似 @dodfile,通常是讀取 heade files: .hrl 檔案
  5. @todo 或 @TODO
    內容是 XHTML text,描述還沒有做完的工作
  6. @type
    a type declaration or definition

Overview tags

文件的目的目錄的 overview.edoc 檔案裡面可使用這些標籤

  1. @author
  2. @copyright
  3. @doc
  4. @reference
  5. @see
  6. @since
  7. @title
  8. @version

以 amqp erlang client 為例,overview.edoc 內容為

@title AMQP Client for Erlang
@author GoPivotal Inc. <support@rabbitmq.com>
@copyright 2007-2013 GoPivotal, Inc.

@version 3.2.4

@reference <a href="http://www.rabbitmq.com/protocol.html" target="_top">AMQP documentation</a> on the RabbitMQ website.

@doc

== Overview ==
....

%% ```f(X) ->
%%       case X of
%%          ...
%%       end'''

== Overview == 是 h3 的縮寫,就像是 Markdown 一樣

   == Heading ==
   === Sub-heading ===
   ==== Sub-sub-heading ====

另外這個語法

 ```...'''

會自動變成 pre,以免跟 xml tag 有衝突

<pre><![CDATA[...]]></pre>

Module tags

這些用在 module 宣告中,有很多標籤都跟 Overview tags 一樣

  1. @author
  2. @copyright
  3. @deprecated
  4. @doc
  5. @hidden
    不會出現在文件中,通常是 sample code, test modules
  6. @private
    標記這是 private module
  7. @reference
  8. @see
    refernce to a module, function, datatype, or application
  9. @since
  10. @version

Function tags

  1. @deprecated
  2. @doc
  3. @equiv
    等同於另一個 function call/expression
  4. @hidden
  5. @private
  6. @see
  7. @since
  8. @spec
    指定 function type
  9. @throws
  10. @type

函數規格 @spec

在 function 中,參數與回傳值的資料型別並不是清楚地寫在 function 的定義上,我們需要一個方式,來告訴使用這個函數的progarmmer,該怎麼使用它。erlang 社群開發了一種記號法,但這個記號法並不是 erlang 程式碼的一部分,而只是一種寫文件的工具。

這個記號法只能用在文件上,在程式碼中,會用 %% 將該行視為註解。通常會這樣寫 %% @spec

-module(math)
-export([fac/1]).

%% @spec fac(int()) -> int().

fac(0) -> 1;
fac(N) -> N * fac(N-1).

在使用此型別記號法時,要定義兩件事:型別與函數的規格。

定義型別

名稱為 typeName 的型別會寫成 typeName()

內建已經預先定義的型別是:

  1. any(): 指任何 erlang 的資料型別,term() 是 any() 的別名
  2. atom(), binary(), float(), function(), integer(), pid(), port(), reference(): erlang 的基本資料型別
  3. bool(): atom(true 或 false)
  4. char(): integer() 的子集合,代表字元
  5. iolist(): 遞迴地定義為 [char() | binary() | iolist()],通常用來產生高效率的字元輸出
  6. tuple()
  7. list(L): 是 [L] 的別名
  8. nil(): 就是 []
  9. string(): list(char()) 的別名
  10. depp_string(): 遞迴地定義為 [char()|deep_string()]
  11. none(): 沒有資料型別,用在不會產生回傳值的函數,例如無窮的接收迴圈,表示此函數不會返回

使用者自己定義型別可以寫成

@type newType() = TypeExpression

範例
@type onOff() = on|off.
@type person() = {person, name(), age()}.
@type people() = [person()].
@type name() = {firstname, string()}.
@type age() = integer().

指定函數的輸入與輸出型別

寫法為
@spec fuinctionName(T1, T2, ..., Tn) -> Tret
T1, T2, ..., Tn 是 參數的型別, Tret 是回傳值的資料型別

每個 T 都有三種可能的形式

  1. TypeVar
    型別變數,這代表未知型別(跟 erlang 的變數無關)
  2. TypeVar::Type
    型別變數後面跟著一個型別
  3. Type
    型別表示式

範例

@spec file:open(FileName, Mode) -> {ok, Handle} | {error, Why}.
@spec file:read_line(Handle) -> {ok, Line} | eof.

file:open/2 意思是,要開啟 FileName,會取得回傳值 {ok, Handle} 或是 {error, Why}
FileName 跟 Mode 是型別變數,但我們不知道它確切的型別是什麼。

範例

@spec lists:map(fun(A)->B, [A]) -> [B].
@spec lists:filter(fun(X) -> bool(), [X]) -> [X].

範例

@spec file:open(FileName::string(), [mode()]) -> {ok, Handle::file_handle()} | {error, Why::string()}.
@type mode() = read|write|compressed|raw|binary| ...

範例

@spec file:open(string(), Modes) -> {ok, Handle} | {error, string()}
    Handle() = file_handle(),
    Modes = [Mode],
    Mode = read|write|compressed|raw|binary| ...

範例

@spec file:open(string(), [mode()]) -> {ok,file_handle()} | error().
@type error() = {error, string()}.
@type mode() = read|write|compressed|raw|binary| ...

where

也可以用變數,搭配 where 來寫函數的定義

Spec ::= FunType "where"? DefList? | FunctionName FunType "where"? DefList?

範例

%% @spec (Connection, Queue) -> RpcClient
%% where
%%      Connection = pid()
%%      Queue = binary()
%%      RpcClient = pid()

上面的範例寫法,是先填上變數名稱,第二行之後,再加上 where 來更明確地限制每一個變數實際上的資料型別。

結論

對於 erlang 來說,applcation 的標準文件 edoc 是很重要的,我們必須透過 edoc 才能知道怎麼使用這個 library。