2018年7月14日 星期六

TStrings, TStringList, UTF8 與 BOM

在 Delphi 2009 推出以前的年代,要處理 Unicode 的資料只能透過 tntWare 的元件,但在 Delphi 2009 之後,從 VCL 到 RTL 全部都已經相容於 Unicode 了,甚至連變數跟 Class 的名字都可以用 Unicode (你喜歡繁中簡中日文夾雜也可以) 來命名,所以程式內部對於 Unicode 的顯示、處理完全沒有問題。

但Unicode 的影響真的是深入到每個細縫,否則我也不會跟它奮鬥了將近20年......

文字檔的處理

對於 Delphi 的資深使用者來講,尤其是越資深的程式人員越有可能使用了這樣的 Work around,也就是把文字檔當成資料庫來使用。

在沒有 SQLite 的 2000 前後,我們可以用 DBase, MDB (Access 的檔案格式),但這些都需要透過驅動程式或者 BDE 來處理。因此有更多的前輩使用的是固定長度文字作為模擬資料庫來使用,這是一把兩面刃,他處理問題、解決問題很快,但製造出來的問題更深更遠,尤其是歷史久遠的程式碼。

在 Delphi 3 到 Delphi 7 的年代,以及一直沒有升級到 Delphi 2009之後版本的前輩,除了會使用固定長度文字作為資料儲存之用,還有一個很糟糕的現象,很普遍,也很嚴重,就是會把 PChar 當成指標,透過 AssignFile 對檔案進行開啟與讀取,然後用 PChar 對檔案內容做 offset 的位置指定與讀取。

乍聽之下這好像沒有任何問題,但是這個『沒有問題』也僅限於 ASCII 與 Single Byte Char的文字檔,也就是在 Unicode 出現之前的年代。

在沒有 Unicode 的時候,全球的電腦系統發展並不是完全沒有進展的,當時我們已經有了 Unix, Linux, DOS, Windows 也到了 Windows 98,資訊界前輩們的努力是可歌可泣的。當時,使用羅馬字元的西方語系問題比較小,因為透過拼音文字,字母並不多。

但以方塊字為主的中文,以及從中文衍生出來的東亞語系文字,如日文、韓文,在資訊界暱稱為 CKJS (Chinese, Korean, Japanese, S我忘了是哪個語系了....),在顯示上面就不是一件簡單的事情了。

所以中文有 Big5, GB碼,日文有 Shift-JIS,各國都有自己的文字編碼,但主要都以 2 Bytes 的長度來定義字碼,再從系統中找到對應的字型加以顯示。

進入到 Unicode 之後,全球的所有知名的資訊廠商幾乎都加入了該聯盟(Unicode Consortium),因為涵蓋的語系非常多,所以兩個 Bytes 根本不夠用,在 Windows 作業系統中,核心的文字都是轉換為 UCS-4 來顯示的,不同的文字檔案或檔案來源,都有各自的轉換模組,所以 Windows 切換成各國文字來顯示都不成問題。

但是轉換為檔案儲存的時候,問題又來了,Unicode 的文字檔案編碼有 UTF-16 (BE), UTF-16 (LE), UCS-4, UCS-2, UTF8, UTF7 等等,讓人眼花撩亂。

目前比較常見的文字檔編碼,除了原本的非 Unicode 編碼,最常用的應該是 UTF-8了,因為 UTF-8 的編碼當中,在西方文字的編碼與 ASCII 完全相同,但是 CKJS 的文字就不是這樣囉,有的字在 UTF-8 當中用 2 bytes, 有的用 3 bytes 來儲存,但對於系統來講都算是一個字元。

TStrings 的更迭

前面提到過,使用 Delphi 撰寫的專案當中,有些比較有歷史的專案,會使用固定長度的文字檔來儲存資料,並搭配 AssignFile, PChar 來讀取文字,進行資料判別與讀取。這個作法在 Single Byte 的文字檔,以及 Single Byte 的 Delphi 當中,使用上是很方便的,但到了 Multi-Bytes Char 的文字檔,以及支援 Multi-Bytes 的 Delphi (Delphi 2009之後的所有版本),這個作法簡直成了惡夢。

在舊的環境當中,抓一個 Byte當一個字元,聽起來很直覺。但Single Byte 的 Delphi 搭配 UTF-8 的檔案編碼,這樣的讀取在中英文夾雜的資料就會出大問題,因為中文字在 UTF-8 檔案可能是兩個 Bytes, 也可能是 3 Bytes, 抓固定長度?鐵死的!

問題不只如此,寫入也是個大問題,以往的編碼法,例如Big5,可以用字元長度乘以2來當成資料長度,但改為用 UTF-8編碼的時候,哇~~~ 固定長度直接崩潰了!

因為以往定義的固定長度,其實當中有很大的問題,發生在名詞定義上的混淆。到底這裡所指的固定長度,是固定幾個字?還是固定幾個Bytes? 這一點就沒有釐清!而檔案編碼的不同,會讓剛提到的長度定義混淆更亂!

所以,在 Delphi 2009 之後,要正確的讀取文字檔案,我自己都養成習慣,一定使用 TStringList 來處理。

TStringList 在早期的 Delphi 當中,是唯一可用來建立 TStrings 物件的 Class,在 Delphi 2009 之後的版本中,它的 LoadFromFile, SaveToFile 方法,都加入了 Encoding 這個參數,我們可以透過參數的指定,直接用對應的編碼方法載入文字檔,只要編碼指定正確,讀入的文字就會是正確的了,寫法如下:

var
   tmpStrs : TStringList;
begin
    tmpStrs := TStringList.Create;
    try
         tmpStrs.LoadFromFile('要載入的文字檔', TEncoding.UTF8);
    finally
         tmpStrs.Free;
    end;
end;

BOM 的有無

在 UTF-8 的檔案儲存中,BOM (Bytes Order Mark) 是常被用來跟非 Unicode 編碼的檔案區分的符號。但 BOM 在很多場合中又會造成問題,例如 JSON 是不接受 BOM 的。

BOM 跟 TStringList 的處理,在 2010 年前後是很熱門的話題,但 Delphi 的 TStrings 已經加入了 WriteBOM 這個屬性,預設值是 True,所以以下這段程式碼:

var
   tmpStrs : TStringList;
begin
    tmpStrs := TStringList.Create;
    try
         tmpStrs.SaveToFile('寫入文字檔.txt', TEncoding.UTF8);
    finally
         tmpStrs.Free;
    end;
end;
產生出來的文字檔,預設是有 BOM 的,用文字編輯器打開,BOM 這三個 Bytes不會被顯示出來,但是透過 UltraEdit 或類似的可以在文字與16進位模式切換的編輯器,就會看到前面多了 3 Bytes。

想要把這 3 個 Bytes拿掉,只需要把 WriteBOM 設定為 False 即可:
var
   tmpStrs : TStringList;
begin
    tmpStrs := TStringList.Create;
    try
         tmpStrs.WriteBOM := False;
         tmpStrs.SaveToFile('文字檔沒有BOM.txt', TEncoding.UTF8);
    finally
         tmpStrs.Free;
    end;
end;
這樣產生出來的 UTF-8 編碼文字檔,就不會有 BOM 了,很簡單吧。

注意事項

如果我們的 TStringList/TStrings 內容是透過 LoadFromStream 取得的,而原來的 Stream 裡面又有不同的編碼方法,這時候 WriteBOM 就不一定能正常運作。

解決方法是:建立另一個 TStringList,透過 AddStrings 把剛剛使用 LoadFromStream 的 TStringList 的內容加過來之後再存,這樣就可以了:
var
   StrsFromStream, tmpStrs : TStringList;
begin
    StrsFromStream := TStringList.Create;
    tmpStrs := TStringList.Create;
    try
         .....
         StrsFromStream.LoadFromStream(其他的Stream Instance);

         tmpStrs.AddStrings(StrsFormStream);
         tmpStrs.WriteBOM := False;
         tmpStrs.SaveToFile('文字檔沒有BOM.txt', TEncoding.UTF8);
    finally
         StrsFromStream.Free;
         tmpStrs.Free;
    end;
end;
這樣就可以了,這問題煩了我一天,所以寫個筆記跟大家分享,也讓我自己做個記錄,因為最近記憶力變差了......


2018年6月22日 星期五

Generic in Delphi

Some History about Delphi 

Delphi has existed for about 23-24 years, in early version (Delphi 1-Delphi 7), Win32 is the only target Delphi shoots for. In year 2000-2003, Microsoft take .Net and C# as next star, Delphi also tried to get ahead, but failed in Delphi 8.

And in the next 10 years, Delphi 2005, 2006, 2007, CodeGear was spinned off from Borland, they tried to shoot for software lifetime management, so we had starteam boundled in these versions.

However, Windows didn’t get real improvement in those years, the only milestones we can see, could be multi-user, 64bit support, and UAC (User Access Control). And Delphi keep silent for years.

Delphi 2009 is a keystone for next couple versions, we have native Unicode support (RTL & components), Generic support, new TTask supports multi-core execution. These new features are great, and makes applications get remarkable performance improvement.

And Delphi XE series help programmers crossing to iOS, MacOSX, Android, and 64bit Linux server in “one code base”, with different compile configures, that’s awesome.

It’s a pity that the wonderful features are seldom used, it might because the sample codes are not enough in quantity or quality. So, let’s have a simple code sample to show you how to use “Generic” with Delphi.

Gernic? Template? What The Hell?

C++ is the very first language which makes object oriented concept well-defined. (C++ was defined earlier then JAVA, so we don’t discuss about the differences between C++ and JAVA in this article)

C++ defines multiple inheritance, Template, and several important concepts, it’s very powerful, and too powerful to implement in common codes.

Delphi takes object pascal as core language, and object pascal evolved for several times in paste decades with Delphi. Object pascal is single root concept, which means all classes in object pascal inherited from the same root class named “TObject”, no exceptions.

If we wish make a class with 2 classes, we need to implement the features with “interface”, and with well-defined interfaces, multiple features of specific classes can be adopted, or we can say to be implemented. Inheritance and interface are another long story, I will write another article to introduce.

What we say “Generic” or “Template”, is to replace specific type name with the keyword “<T>“ in the codes, with this modification, we can reduce the effort to adopt the class for other types.

If you do understand the above sentence, congratulations! You must be an experienced programmer, and you don’t need help from this article.

So, let’s speak English, with some sample codes, you might be understand the concept faster.

When I was a college student, once I was doing my homework from data structure class, or once I was trying to implement a "Stack" with Object Pascal, I will have the following thoughts:

  • Well, Stack, first in last out, or last in first out, so I need an array to store elements.
  • I have to define a push method to put data in.
  • I have to define a pop method to get data out.
 So, the following figure will be helpful to visualize the stack:




As a "stack for storing integer", the basic declaration will look like:

TMyStack = class (TObject)
private
   FElements: array[0..5] of Integer; // Array for storing integer.
public
   function push(element: integer) : integer; // the returned integer will indicate the
                                                                     // position of the pushed element.
   function pop: integer; // return last element, and remove it.

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

I skip the implementation codes, because there are more students searching the codes for their homework in recent years.

A stack class named "TMyStack" is declared in the above codes, but there are still a lot of problems in the above codes. For example, the above class can store only 6 integers, the number of element is limited, and as I mentioned, TMyStack is a "stack for integer", so the argument of push method must be "integer", and return type of pop method must be integer, too.

Let's resolve the limitation of element count first.

I don't remember which version of Delphi exactly, Delphi provide a feature named "variable length of array", we can change the length of an array with setLength function, the declaration of a variable length of array might like this:

var
   varLengIntArray : array of Integer;

and we can change the length of the array in run-time with the following codes:

setLength(varLengIntArray, 20);

The second argument "20", is the length we wish to make the array to be.


With this implementation, we can set the stack class free from the limitation of count of element:
TMyStack = class (TObject)
private
   FElements: array of Integer; // The array for storing elements.
   FElementCount: integer;
public
   function push(element: integer) : integer; // the returned integer will indicate the
                                                                     // position of the pushed element.
   function pop: integer; // return last element, and remove it.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

With the modification, push and pop methods will require some changes, constructor Create will require to initial the count:
==================================================================
constructor TMyStack.Create();
begin
   inherited Create();

  FElementCount := 0; // initialization, set the count of element as 0;
  setLength(FElements, 0); // initialization, set the element array as empty.
end;

function TMyStack.push(element: integer) : integer;
begin
    Inc(self.FElementCount); // Increasing the count of element by 1
    setLength(FElements, self.FElementCount); // Increasing the count of array by 1.

   self.FElements[self.FElementCount -1] := element; // saving the element in the
                                                                                    //  added position.
end;

function TMyStack.pop: integer;
begin
    Result := self.FElements[self.FElementCount -1]; // Returning the last element.

    Dec(self.FElementCount); // Decreasing the count by 1
    setLength(FElements, self.FElementCount); // Removing the last element of array.
end;
==================================================================

With the modification, the stack class was set free from limitation of count.

But, will we need a stack for integer only? Will we need a stack for string tomorrow? or will we need a stack for customized record or class?

If every time when we need a stack for particular type, we need to copy & paste the above codes, and then modify the all the "type" field, it will kill me, am I right?

What if customers require some "outstanding" features for particular types, we will need to modify all the stack classes one by one....... That will drive me crazy in second.

So, is there someway, we can make a stack for all type?

Dr. Alfred Lanning: *That*, Detective, is the right question. -- quoted from "I, robot".

"Generic" is the redemption.

For using the features of generic provided by Delphi, we need to use the "System.Generics.Collections" unit, there are so many types, functions ready for use.

First, let's modify the previous declaration, replace "array of integer" with "TArray<T>", hence, we can save any type of data in TMyStack class.

TMyStack<T> = class (TObject)
private
   FElements: TArray<T>; // New array type with element of all types.
   FElementCount: integer;
public
   function push(element: T) : integer; // the returned integer will indicate the
                                                                     // position of the pushed element.

   function pop: T; // return last element, and remove it.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;


And the implementation should be modified as:
==================================================================
constructor TMyStack<T>.Create();
begin
   inherited Create();

  FElementCount := 0; // initialization, set the count of element as 0;
  setLength(FElements, 0); // initialization, set the element array as empty.
end;

function TMyStack<T>.push(element: T) : integer;
begin
    Inc(self.FElementCount); // Increasing the count of element by 1
    setLength(FElements, self.FElementCount); // Increasing the count of array by 1.

   self.FElements[self.FElementCount -1] := element; // saving the element in the
                                                                                    //  added position.

end;

function TMyStack<T>.pop: T;
begin
    Result := self.FElements[self.FElementCount -1]; // Returning the last element.

    Dec(self.FElementCount); // Decreasing the count by 1
    setLength(FElements, self.FElementCount); // Removing the last element of array.
end;
==================================================================

Oh my Goodness, that's all? Is that simple? And then? How can I use the class?
Just as this way:

var
   integerStack : TMyStack<Integer>;
begin
    integerStack := TMyStack<Integer>.Create;
    try
        integerStack.push(79);
        integerStack.push(7);
        integerStack.push(21);
        integerStack.push(13); 
    finally
         integerStack.Free;
    end;
end; 

In the above code, a stack will be built as the following figure:





And we can create a stack for saving strings:

var
   stringStack : TMyStack<String>;
begin
    stringStack := TMyStack<String>.Create;
    try
        stringStack.push('Woo');
        stringStack.push(Th');
        stringStack.push('at');
        stringStack.push('is'); 
        stringStack.push('sta');  
        stringStack.push('ck');  


        // p.s. I create the figure first, but the space is not long enough, so
        // I split the string into many pieces. 
    finally
         stringStack.Free;
    end;
end;



The built stack looks like the following figure:

With the above codes, the class implementation is not changed. What we do is to declare, create the instance of TMyStack<T>, then the class can be adopted with different data types, it's convenient, right?

There is a class named TList<T> in System.Generics.Collections. We have to create TObjectList to store any object derived from TObject, and we have to add additional codes to mapping different object instance.

With TList<T>, we can save the extended codes, works. In System.Generics.Collections, there are even TStack<T>, TQueue<T>, are you very interested in that unit now?

With a simple conclusion, Generic is to replace the data type with keyword <T> when we try to have a class implementation, and make it clear when we use the class in real execution codes. In this way, we will save so many time, and we, developers, will have extra time for our life. I HOPE SO.....

2018年6月20日 星期三

談一談從 Delphi 2009 之後就支援的重要功能 - 泛型 (Generic)

前言

在C++的語言基礎當中,除了物件導向、事件驅動的概念之外,模版設計(Template)也是非常重要的一環。然而,C++的開發人員能夠善用模版設計的並不多。模版設計這個好物,一般還有一個名稱,就是泛型 (Generic),這個好物在Delphi 2009 之後,也已經被加入到 Object Pascal裡面了,只是我們實在很少用到它。

然而,江湖一點訣,說破沒秘訣,大家對於泛型的少用,很多是因為不知道有這個功能,其次是知道有這個功能卻不知道怎麼使用。

所以,我們這一篇就來深入淺出的介紹一下『泛型』是什麼,順便用幾個簡單的範例來使用『泛型』吧。

泛型? 樣板? 揭起它的神祕面紗

所謂的泛型、樣板,其實就是在寫code的時候,把需要先定義好型別的宣告用一個關鍵字 <T> 來取代,未來真正在使用的時候,把T改成真正的型別,就可以讓這段code適用於多種不同的型別了。

這樣說明,如果您就聽懂了,那應該也不需要來看這篇文章,表示您的悟性頗高,屬於非常有能力的Programmer。(謎之音:喵的,聽的懂我跟你姓! 這不是跟我大學資料結構或物件導向程式設計老師說的一樣嘛?)

用實例來說明吧,我們說地球話,才不會被趕回火星..........

以前,寫資料結構作業的時候,或者用 Object Pascal 寫程式的時候,如果我們要用Delphi 來實作一個堆疊,我們通常會這麼想:

  • 堆疊嘛,資料要先進先出,所以要宣告一個陣列來儲存資料
  • 然後要定義一個 Push 方法,把資料放進去
  • 也要定義一個Pop方法,把資料取出來
  • 因為是堆疊,所以是後進先出 (最後放進去的要最先被取出,只有一個進出口)

可以用底下這張圖片來幫助思考:
以一個存放『整數』的堆疊來說,最最基本的宣告一般就會寫成這樣:
TMyStack = class (TObject)
private
   FElements: array[0..5] of Integer; // 用來存放元素的陣列.
public
   function push(element: integer) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: integer; // 直接傳回最後一個元素.

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

實作的程式碼我就不寫了,最近實在太多學生上網到處找作業的答案範本。

這段程式碼宣告了一個名為 TMyStack 的堆疊類別 (Stack Class),裡面是有很多問題的,例如 FElements 只能放 6 個整數,有元素個數的限制,因為我們前面說過這是一個存放『整數』的堆疊,所以 push 方法的參數是整數型別,pop 方法所回傳的資料也是整數型別。

先來解決資料長度限制的問題

我記不清是從 Delphi 5 還是 Delphi 7開始,Object Pascal就被賦予了可變長度陣列的功能,可以透過 setLength 來調整陣列的長度,宣告的寫法可以寫成:

var
   varLengIntArray : array of Integer;

調整長度的作法則是:
  setLength(varLengIntArray, 20);

後面的數字就是陣列調整後的長度。

這樣的作法,讓上面的整數堆疊陣列脫離了固定長度的限制,改寫過的 Class 宣告就會變成:
TMyStack = class (TObject)
private
   FElements: array of Integer; // 用來存放元素的陣列.
   FElementCount: integer;
public
   function push(element: integer) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: integer; // 直接傳回最後一個元素.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;
這樣修改以後,push跟pop方法裡面也都要有相對應的程式修改,例如在 Create的時候,就要先對 FElementCount 做初始化,push 跟 pop 方法裡面,也得調整長度:

==================================================================
constructor TMyStack.Create();
begin
   inherited Create();

  FElementCount := 0; // 初始化,把元素個數設為 0;
  setLength(FElements, 0); // 初始化,把陣列長度也設為 0;
end;

function TMyStack.push(element: integer) : integer;
begin
    Inc(self.FElementCount); // 把元素個數加一
    setLength(FElements, self.FElementCount); // 把陣列長度也多加一個

   self.FElements[self.FElementCount -1] := element; // 把要 push 的元素放在新增的
                                                                                    //  陣列位置上
end;

function TMyStack.pop: integer;
begin
    Result := self.FElements[self.FElementCount -1]; // 把最後一個元素回傳.

    Dec(self.FElementCount); // 把元素個數減一
    setLength(FElements, self.FElementCount); // 把陣列長度也多減掉一個
end;
==================================================================

這樣修改完以後,整數堆疊就沒有長度限制了。

但是,我們只需要整數堆疊嗎? 會不會明天要一個字串堆疊? 後天會不會要一個自定 record 或者 class 的堆疊?

如果每次需要堆疊,就要重寫一次上面的程式碼,而要修改的地方只有型別,那不是煩死人了?如果又好死不死遇到堆疊裡面要加一些額外的功能(客戶的想像力永遠走在我們前面, #壽山, 你說是吧?),那所有堆疊的程式碼要一個一個去修改,光想像就很想對電腦下毒手.........

那有沒有什麼方法,可以讓我們寫一個堆疊,就可以存放所有型別?

當然有,泛型,就是我們的救贖啊........
要使用泛型,我們得在 use 區段裡面引入 System.Generics.Collections,這裡面有非常多的好物可以用。

我們首先把前面已經改過的類別宣告,再做一些小調整,使用TArray<T> 這段程式碼來取代 array of Integer,讓 FElements 可以容納各種型別的資料:

TMyStack<T> = class (TObject)
private
   FElements: TArray<T>; // 用來存放元素的陣列.
   FElementCount: integer;
public
   function push(element: T) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: T; // 直接傳回最後一個元素.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

實作的程式碼則需要修改為:
==================================================================
constructor TMyStack<T>.Create();
begin
   inherited Create();

  FElementCount := 0; // 初始化,把元素個數設為 0;
  setLength(FElements, 0); // 初始化,把陣列長度也設為 0;
end;

function TMyStack<T>.push(element: T) : integer;
begin
    Inc(self.FElementCount); // 把元素個數加一
    setLength(FElements, self.FElementCount); // 把陣列長度也多加一個

   self.FElements[self.FElementCount -1] := element; // 把要 push 的元素放在新增的
                                                                                    //  陣列位置上
end;

function TMyStack<T>.pop: T;
begin
    Result := self.FElements[self.FElementCount -1]; // 把最後一個元素回傳.

    Dec(self.FElementCount); // 把元素個數減一
    setLength(FElements, self.FElementCount); // 把陣列長度也多減掉一個
end;
==================================================================

我的老天鵝啊,這真是太方便了吧,程式碼這樣寫就好了? 那使用上要怎麼用?
就這樣:
var
   integerStack : TMyStack<Integer>;
begin
    integerStack := TMyStack<Integer>.Create;
    try
        integerStack.push(79);
        integerStack.push(7);
        integerStack.push(21);
        integerStack.push(13); 
    finally
         integerStack.Free;
    end;
end; 

上述這段程式碼,在 finally執行以前,就會建立出以下圖為範例的堆疊資料了:

我們也可以做字串堆疊:
var

   stringStack : TMyStack<String>;
begin
    stringStack := TMyStack<String>.Create;
    try
        stringStack.push('這');
        stringStack.push('就');
        stringStack.push('是');
        stringStack.push('泛'); 
        stringStack.push('型');  
        stringStack.push('啊');  
    finally
         stringStack.Free;
    end;
end;

上述這段程式碼,在 finally執行以前,建立出來的堆疊資料則如下圖:
這樣一來,程式碼都沒有變,我們只在使用 TMyStack<T> 這個 Class 的時候,在宣告、建立Class的時候指明要使用什麼型別,就能夠自由的把一份程式碼用在各種不同型別上了,是不是很方便?

System.Generics.Collections 裡面,TList<T>更是好用,以前我們得要自己做TObjectList,才能透過所有物件都是從 TObject 衍生出來的特性建立出可以儲存物件的List,而且每次使用的時候還得做型別轉換才能正確使用。

現在透過 TList<T>,這些額外的程式碼、型別轉換的工作就都省下來了,甚至連TStack<T>, TQueue<T>, 也都有提供,是不是也讓您想要玩玩看了呢?

泛型說穿了,就是把原本我們需要先寫明的型別,用<T>這個關鍵字取代掉,而改以在實際宣告、使用的時候才敘明型別,這樣一來,真的省下好多好多程式碼,也省下很多時間可以做其他更有意義的事情了,當然,這些事情還是要我們自己去發掘的,大家加油!
 

2018年6月15日 星期五

Delphi 開發手機 App 與其他工具之間的比較分析

寫在前頭

關於各種手機App開發的工具,從2010年前後到現在已經在很多不同的場合介紹過,在元智大學、中台科技大學、德霖科技大學等不同學校的講座、課程當中,都有類似的主題,所以對我來說,這個主題屬於駕輕就熟的範圍。

在 KTOP 的論壇當中,有很多前輩希望大家能夠貢獻所學,為資訊業共圖未來的榮景。當然,KTOP 的主題當中仍然是以 Delphi 為主軸,所以當 lazarus 前輩提了三個題目給我:
  1. Delphi/Lazarus INDY 網路程式設計
  2. Delphi 資料庫程式設計 (甚麼 FireMonkey 架構 , 工作上沒機會接觸, 完全不懂 ... 我用傳統連線方式一樣可以連資料庫啊 ...)
  3. Delphi 手機 APP 程式開發, 不知 Delphi 是否有比其他手機 APP 專用的開發工具有優勢 ..
這三個都是好題目,只是第一跟第二題需要比較多的篇幅來進行,所以我就先就第三題做了這篇文章。

第一題 Delphi/Lazarus Indy 網路程式設計,在我 2001 年的著述 - Delphi/Kylix Indy 網際網路程式設計當中已經寫了三百多頁,當年寫的是 Delphi 7 + Indy 8/9,歷經了17年,我在2009年曾經就內容版本做過一次更新,使用了Delphi 2009 與 Indy 10.0.52做了一次改版,但沒有出版社有興趣,所以只有 KTOP 副站長,也是 Embarcaderp MVP 的 GrandRuRu,以及少數幾位網友跟我購買了兩三個電子書的檔案,並沒有機會出版實體書,覺得有些遺憾。

第二題 Delphi 資料庫程式設計,從 Delphi 7 時期的 BDE,到 Delphi 2005-2009 時期的 dbExpress,再到 XE2-10.2 時期的 FireDAC/UniDAC,我自己在使用上,覺得除了 Connection 的設定上有所不同,其餘在 Table, Query 的使用上並沒有太大的改變,而且李維在為捷康所著的 Delphi Database 開發手冊當中已經有很詳盡的說明,所以我可能在接下來的幾篇文章當中,提供一些使用上的範例與心得,至於資料庫的使用細節,我就不敢斑門弄斧了。

現有的開發工具概觀

從2008年 iPhone 跟 Android 把行動裝置帶向了沒有微軟的世界之後,開發工具也跟微軟從此沒有那麼大的關係,以下,我把開發工具分成幾大類:
  • 原生工具
  • 網頁轉換工具
  • 其他需編譯的工具

原生工具

顧名思義,是由該作業系統的開發者所提供的開發工具,在 iOS 跟 Android 兩大陣營當中(抱歉,從2009至今,我從沒看到 Windows Phone 有任何復甦的可能,所以直接忽略不計),就是三種工具:
  • iOS: Xcode (包含 Objective-C 跟 Swift 兩種語言)
  • Android: Eclipse 跟 Android Studio (都是 JAVA 語言)
如果您對任何語言都不太熟悉,或者說沒有原有技能的包袱,學習原生工具自然是最跟的上作業系統更新的,因為每次作業系統一更新,都會或多或少有些變動,SDK也會跟著調整,這些調整都會直接包含在原生工具裡面,所以SDK跟的最為緊密,也不用擔心有程式碼或工具需要等開發工具的更新才能跟的上。

Objective-C 是從 2000 年前後就被蘋果作為官方程式語言,原本在 1996-1998之間,Object Pascal一度在蘋果的候選名單內,但後來我也不知道是什麼原因,蘋果還是決定自己定義 Objective-C 這個語言。

Objective-C 跟 C++, 跟 C 都完全不同,在語法上,使用中括號 [] 作為 method invoke 的符號,而使用 . 作為 property 的存取符號,也是因為 method invoke 的 statement 跟 C系列的語言完全不同,所以很多已經習慣 C 系列語言的開發人員完全跨不過去。

但從語言的核心來看,用中括號作為 invoke,以空格加上 prefix descriptor來描述每個參數,也很清楚易懂,可能人一習慣某種語法,就很難變化,所以 Objective-C 後來在推廣上也總是有難以突破的障礙,例如 C# 陣營的開發人員,就很排斥這種語法,所以蘋果後來在 2013 年前後開始推出 Swift 這個語言,語法就跟 C# 很相似,相信因此從微軟陣營轉向 Swift 的人不在少數。

Android 的開發工具則是在 2014 年作為一個分水嶺。
2014年以前,官方開發工具是 Eclipse,很多人會用,但沒有人敢說對 Eclipse 熟悉,因為 Eclipse 只要搭配不同的 SDK,就能變成完全不同的面貌,所以用 Eclipse 開發 Android 雖然是在 2014 年以前的唯一選擇,卻也是讓很多開發人員花費很多時間設定的工具。

2014年起,Google推出了 Android Studio,把很多之前難搞的SDK都整合在一個安裝檔裡面了,很好用,但跟之前用 Eclipse 的專案又有不同。前面我提到過,人習慣了某些東西以後就很難改變,所以目前 Android 的開發陣營裡,原生工具仍舊分為兩個不同的派別:Eclipse 跟 Android Studio.

原生工具的優缺點

原生工具的好處,是能夠跟作業系統緊密的結合,永遠不會落後,也不用等待開發工具的廠商更新什麼套件。但原生工具的致命缺點,就是兩個平台必須維護兩套 Source code,而且因為兩個平台工具上的差異,同一個 App 在兩個平台上的更新速度永遠不可能一樣,而且問題修正也不可能同步。

這就造成了原生工具開發的成本被墊高。一般客戶需要App的,分為兩種,一種是自己有養著開發人員,但開發人員對App不熟悉,所以先外包,再把Source code 接回來自己維護,這種客戶比較能從開發人員的角度想問題。

也就知道養著兩組人,就需要兩份成本,時間雖然可以接近,但兩組人的素質不可能完全相同,業界同時寫 iOS 跟 Android 的人不多,因為兩個平台的概念有不小的差距。

第二種客戶是行銷類的客戶,這類客戶會把兩個平台的 App 看成同一個東西,所以會要求用一份成本,做兩個平台的 App,維護時間也一樣,會要求同時產出、問題同時解決,時程要求短,成本要求低,品質要求高,而且不會有第二個案子,因為行銷類的客戶通常是為某個產品提案,效果沒有在短時間內看到,就不會有下一個案子。

就算有了效果,下一個案子的時間也是遙遙無期,而且會嫌成本太高,轉而用其他方式製作,例如 RWD 網頁。

網頁轉換工具

網頁轉換工具,則是前端工程師的最愛,這類工具只要把一個網站做好,所有網頁做成可以離線瀏覽,經由轉換包裹,就可以分別做成 iOS, Android 平台可以使用的 App,代表性的工具有三個,但其實都是同一個:
  • Adobe PhoneGap
  • Apache Cordova
  • IBM WorkLight
這三個都是同一個,最早都叫做 PhoneGap,但PhoneGap後來被 Adobe 買下,免費版的則轉由 Apache 基金會繼續維護下去,改了名字叫做 Apache Cordova。IBM 則在 2013年也用 Cordova 做了一個版本,給了新名字叫做 WorkLight。

這三個工具都是把網頁直接轉換成 App,也各自提供了很多 JQuery API,讓前端開發人員可以使用手機裝置的內建功能,例如GPS,電話功能、重力感應器功能等。

PhoneGap 有方便的 App 端工具,讓開發人員可以直接對包裹出來的頁面做直接互動式的測試。

Cordova 則是比較陽春,要先透過 Chrome, FireFox 先把內容測試好,再進行包裹。
WorkLight 則是提供了強大的後台,可以不透過 AppStore 直接置換掉網頁內容,換句話說,就是可以不透過 AppStore 的審查直接升級 App,所以國泰世華、中國信託的App後來都改用了 IBM WorkLight 來製作。

網頁轉換工具的優缺點

網頁轉換工具的好處是開發快速,前端設計師就能搞定一切,所以成本低,行銷類的客戶最喜歡這種工具。

缺點則是效能不彰,因為所有功能都是透過瀏覽器來顯示的,瀏覽器所有的限制、效能的侷限,都直接衝擊網頁轉換工具製作的 App,但是成本低,所以從 2015 年以後,大量行銷類的 App 都被使用這類工具的工作室或公司搶走了,行銷類的App用原生工具的比例也大為降低。

其他需編譯的工具

這類工具,通常是為了資深的開發人員而生的,包括有:
  • Delphi - 為了原來對 Delphi 熟悉的開發人員而生
  • Xamarin - 為了原來對 C# 熟悉的開發人員而生
這類工具,通常有自己的 IDE,也可以分別編譯 iOS, Android 的 App,編譯出來的執行檔效能也接近原生工具的效能,但仍舊分別有其優缺點:

Delphi 的優缺點:

先講缺點,Delphi製作App的缺點有兩個。

首先是 Foot Print太大,也就是檔案太肥。
因為 Delphi 製作 App 的時候是使用 FireMonkey 框架在 iOS 跟 Android 系統中產生兩個平台的介面,整個框架必須都編譯到程式中,所以檔案一定會很肥,這是無法避免的。

其次是所有第三方元件的原罪,就是當 iOS 一更新,有可能 Xcode 工具裡有修改功能,會導致 Delphi 需要等原廠出 patch 才能搭配新版的 Xcode 編譯,這個問題從 XE5到 10.2.3 都有,但這是原罪,沒辦法。

其次是優點,由於使用 FireMonkey 框架,所以所有的元件都不依靠作業系統的元件,在 iOS 跟 Android 平台的 App 可以使用同一套原始碼,有問題的時候也比較容易一起解決,兩平台的 App 可以由同一個人維護,成本比兩個人低一些。

Xamarin的介紹與優缺點

Xamarin是在2014年前後由一家獨立軟體公司開發出來的,後來在2016年被微軟收購,成為 Visual Studio的一部分。

Xamarin 的概念跟 Delphi 不一樣,這個工具是要讓所有寫 C# 的開發人員,可以用 C#開發 iOS 跟 Android 的 App。

但 C# 的控制,是在 iOS 上用 C# 操控 iOS SDK,在 Android 上用 C# 操控 Android SDK,因此造成了兩個平台的專案中,介面必須分別寫,然後共用的元件再另外寫,可是 iOS SDK 跟 Android SDK 的差別很大,結果就是使用 Xamarin 的工程師必須把兩個平台的 SDK 都要摸熟,而且一個 App 必須維護三種 code: iOS 介面, Android 介面, 以及核心邏輯程式。

對我們有其他選擇的人來看,這很浪費時間,但對只會 C# 的人來說,這完全得到了救贖。

所以,Xamarin 的優點跟缺點,必須從立場來看,對於不拘於只使用 C# 的人來說,Xamarin 要維護三套程式的作法,很蠢。

但對於只會 C# 的人來說,會了 C# 就可以暢行無阻,而且因為編譯都是用原生工具,效能跟原生工具的產出一樣好,且檔案也不會大到很可怕,所以這些都是優點。

結語

以上就是各種不同的開發工具的優缺點,跟大家分享。

我自己很熟悉 Objective-C跟 Delphi,JQuery也還可以,我不隱藏對JAVA的厭惡,但我也會 JAVA,所以我自己使用過 Delphi, Xcode, Apache Cordova, IBM WorkLight, Eclipse 開發過許多程式,或許這些心得算是開發人員可以有些共鳴的。

以我自己的喜好來看,我一個人要維護多個App的話,我會選擇 Delphi,因為程式邏輯跟介面都只需要一套,執行檔 (ipa, apk)是肥了一點,但效能還不錯,且在這些工具中,要跟資料庫連接的話,Delphi 是不二之選。

Objective-C要直接連 DB? 只有 SQLite,而且寫法挺煩的。透過 JSON 跟 Restful API 來處理還好一點。JQuery也只能透過後端工具來處理,我會選擇PHP。

但前台 SQLite 所有工具都一樣, 如果有需要直接連 MySQL, Delphi 會方便很多。
這些工具中,我獨漏了 Unity 3D,因為這工具我歸類是寫 Game 的,寫一般 App的話會很擾人且很惱人,所以沒有在此提及。

2018年6月13日 星期三

在 Delphi 中使用 TMapView 顯示路徑、使用Google Map API進行兩點或多點之間的導航.

在App當中使用地圖元件,常常用來提供導航、路線規劃等功能,這些功能的展現,端看畫面上在地圖元件中,能夠盡量詳盡的為使用者繪製出適當的路線。

在前一篇文章裡面,我們介紹了在 TMapView 上面標示地點的作法,在本篇文章中,我們要介紹更進一步的功能:在地圖上面畫線

常使用Google Map網頁版開發功能的朋友們大都已經使用過 Google Map API的強大功能之一:direction API, 也就是我們常使用的『導航功能』。通常我們比較常用的是在兩個地點之間試圖繪製出最佳的交通路線,這是 Google Map Direction API 的基本功能之一,且讓我們娓娓道來。

導航功能的思路,一般會包含幾個步驟:
  • 地點的選定(可能只有起點與終點,也可能加入中途點)
  • 交通工具的選定
  • 路線的繪製
而這些也恰好都是 Google Map Direction API的基本功能。要使用Google Map Direction API,跟前一篇文章的介紹中會有些重疊之處,例如需要申請Google API Key,需要在App裡面作些基本設定,這些基本設定請參考前一篇文章:在 Delphi 當中使用 TMapView 顯示地圖之一:程式設定與加上地點標示。

在使用 Google Map Direction API之前,則有一組金鑰得先申請,您可以到 Google Map Direction API 的頁面申請。申請完畢以後,就可以一起來看這組 API 的強大功能了。

Google Map Direction API 的使用
如果是直接透過瀏覽器來使用,我們可以在網址列輸入以下這個網址:
https://maps.googleapis.com/maps/api/directions/json?origin=25.0416460,121.5362860&destination=23.5531950,120.3472670&key=您的GoogleAPIKey

在上面的網址中,我們可以看到主要網址後是以json開始帶入參數,依照Google Map Direction API 的說明,這代表我們是要索取以JSON作為輸出格式的資料。

之後的參數有兩組, origin跟destination,分別是兩個座標代表起點跟終點,如果您在網址列上面輸入了上面的文字,就按下了Enter,則畫面應該會顯示如下圖的資訊:





這些資訊就包含了導航的相關資訊、每一段路之間的座標、一共有多少段路、起始點與目的地的座標等資訊。
 
我們在Delphi當中,取得了這些資訊之後,就可以來找出其中有用的資訊了,先來看看完整的訊息有哪些:
{
"geocoded_waypoints" : [
{
"geocoder_status" : "OK",
"place_id" : "EicxMDblj7DngaPlj7DljJfluILlpKflronljYDlj7A157eaOTTomZ8iGhIYChQKEgkpEFFufalCNBHcc2G4xJNGAxBe",
"types" : [ "street_address" ]
},
{
"geocoder_status" : "OK",
"place_id" : "ChIJKbHLslW9bjQRGOc7frCCI6Q",
"types" : [ "street_address" ]
}
],
"routes" : [
{
"bounds" : {
"northeast" : {
"lat" : 25.0782095,
"lng" : 121.5376204
},
"southwest" : {
"lat" : 23.5496167,
"lng" : 120.3473252
}
},
"copyrights" : "地圖資料©2018 Google",
"legs" : [
{
"distance" : {
"text" : "246 公里",
"value" : 246382
},
"duration" : {
"text" : "2 小時 41 分",
"value" : 9643
},
"end_address" : "616台灣嘉義縣新港鄉中山路116號",
"end_location" : {
"lat" : 23.5531974,
"lng" : 120.3473666
},
"start_address" : "106台灣台北市大安區台5線94號",
"start_location" : {
"lat" : 25.0417488,
"lng" : 121.5363047
},
"steps" : [
{
"distance" : {
"text" : "59 公尺",
"value" : 59
},
"duration" : {
"text" : "1 分",
"value" : 17
},
"end_location" : {
"lat" : 25.0416756,
"lng" : 121.5368846
},
"html_instructions" : "往\u003cb\u003e東\u003c/b\u003e走\u003cb\u003e忠孝東路\u003c/b\u003e/\u003cb\u003e忠孝東路三段\u003c/b\u003e/\u003cb\u003e台5線\u003c/b\u003e朝\u003cb\u003e建國南路一段\u003c/b\u003e前進",
"polyline" : {
"points" : "}}ywC{pxdV@QDi@Dw@"
},
"start_location" : {
"lat" : 25.0417488,
"lng" : 121.5363047
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.1 公里",
"value" : 111
},
"duration" : {
"text" : "1 分",
"value" : 27
},
"end_location" : {
"lat" : 25.0407313,
"lng" : 121.5372438
},
"html_instructions" : "於\u003cb\u003e建國南路一段\u003c/b\u003e向\u003cb\u003e右\u003c/b\u003e轉",
"maneuver" : "turn-right",
"polyline" : {
"points" : "o}ywCotxdVj@Mb@KlBm@"
},
"start_location" : {
"lat" : 25.0416756,
"lng" : 121.5368846
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.2 公里",
"value" : 165
},
"duration" : {
"text" : "1 分",
"value" : 77
},
"end_location" : {
"lat" : 25.0417789,
"lng" : 121.5372723
},
"html_instructions" : "於\u003cb\u003e迴轉道\u003c/b\u003e處\u003cb\u003e迴轉\u003c/b\u003e",
"maneuver" : "uturn-left",
"polyline" : {
"points" : "qwywCwvxdVI{A_@N}@Za@Li@P]H"
},
"start_location" : {
"lat" : 25.0407313,
"lng" : 121.5372438
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.3 公里",
"value" : 297
},
"duration" : {
"text" : "1 分",
"value" : 32
},
"end_location" : {
"lat" : 25.0443566,
"lng" : 121.5366898
},
"html_instructions" : "上匝道,往\u003cb\u003e福爾摩沙高速公路\u003c/b\u003e方向走",
"polyline" : {
"points" : "c~ywC}vxdVSNg@Lm@Pw@Pq@JUBQBo@Da@BE?]@W?U?w@AE@G@G@IBKH"
},
"start_location" : {
"lat" : 25.0417789,
"lng" : 121.5372723
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "1.6 公里",
"value" : 1615
},
"duration" : {
"text" : "1 分",
"value" : 88
},
"end_location" : {
"lat" : 25.058881,
"lng" : 121.5369893
},
"html_instructions" : "走\u003cb\u003e建國高架道路\u003c/b\u003e",
"maneuver" : "merge",
"polyline" : {
"points" : "gnzwCisxdVs@A]?]?w@?uCC_A?S?SAK?W?WAaHE[?w@AcBAy@?UAW?qGEaDA]?_@AgDCcBAcBA{BAI?UAY?eBAO?K?M?gBAO?M?yBCM?K?cCCa@?a@AoA?a@A_@?W?"
},
"start_location" : {
"lat" : 25.0443566,
"lng" : 121.5366898
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "1.0 公里",
"value" : 982
},
"duration" : {
"text" : "1 分",
"value" : 57
},
"end_location" : {
"lat" : 25.0669266,
"lng" : 121.5348117
},
"html_instructions" : "靠\u003cb\u003e左\u003c/b\u003e繼續走\u003cb\u003e建國高架道路\u003c/b\u003e/\u003cb\u003e建國高架道路(國1桃園/濱江街基隆)\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "keep-left",
"polyline" : {
"points" : "_i}wCeuxdVsB?K?M?}BCO?M?w@A{HG_@AA?M?O?sECC?M?K?c@?[@Q@O@e@Fc@FMBYJA?WHQFQHOF[Tc@V_@ZGHQNWZU\\W\\q@bAeAtA_@f@"
},
"start_location" : {
"lat" : 25.058881,
"lng" : 121.5369893
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "1.2 公里",
"value" : 1234
},
"duration" : {
"text" : "1 分",
"value" : 81
},
"end_location" : {
"lat" : 25.0739412,
"lng" : 121.52885
},
"html_instructions" : "靠\u003cb\u003e左\u003c/b\u003e繼續走\u003cb\u003e建國高架道路\u003c/b\u003e/\u003cb\u003e圓山交流道 號出口\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "keep-left",
"polyline" : {
"points" : "i{~wCqgxdVc@v@OTQTs@bAa@j@IJONMHSNIDOFUHQFSBMBQ@W@O?MAYAi@Es@Ec@CgACqACI?QAKAeA@kCAaA?g@@Y@OBaAN[F[Ja@POHk@f@YRGFEFGJGJCJEJELEPGVAJCRA^?V@V@d@BZDZBND`@JbABn@?P?T?J@HALAVARATIl@ALEVGZAHAN?H"
},
"start_location" : {
"lat" : 25.0669266,
"lng" : 121.5348117
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "11.4 公里",
"value" : 11423
},
"duration" : {
"text" : "8 分",
"value" : 467
},
"end_location" : {
"lat" : 25.0641299,
"lng" : 121.4194995
},
"html_instructions" : "走\u003cb\u003e國道一號\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "merge",
"polyline" : {
"points" : "cg`xCibwdVGTITW~@o@dCGVITOj@Oj@K\\_@zA_@nA{@hDGVIVs@lCSt@ER_@~A_A~Dg@dCg@lCSdAIf@_@vBIj@UxA_@zCq@`GKdAI~@Kt@Gz@Gl@Ej@CXAZMdBCd@E~@GbA?BIhBGtBA^A^Cl@A`@Af@?d@Ab@ChAA~AArEEf@@dA?b@?d@@b@?P@p@@n@@RF|ADtBHrBL|CRtDPpCB^D^VjDBRBR@B?D@F@D@L@JDZB\\NrAFd@p@pFNfADRBP`D`Ub@`CVxAFb@Hb@j@|DnA~IBVDTx@rFJt@Lr@dBlLNbAP`A^fCp@~GBRDt@d@vI@~A@n@?l@BlBSzGIfBY~CUvBg@~Da@~BO`AEZ]rBO|@Kl@mApHs@pEg@~C]lCWvBOdBKvBCj@?J@LCVCp@?@KdDAbACjD?t@@jB@`AJ~DFpAJ`BLpBDZDZLxA\\lDt@zEDTBVFX^hBFXFZDRDRdLfk@zIva@zAlHtCfNH`@J`@DTlA|IBTf@bGTxCRzC\\dF@d@Bd@D`ANvGHzCBjBL~IJbFFzBDnAJzATrBFd@XdBNn@R|@XbA\\fA\\bAVp@Nb@BDj@tAj@hAp@nAPZz@xAb@x@j@fAxC|GFLHRHRp@dAh@~@"
},
"start_location" : {
"lat" : 25.0739412,
"lng" : 121.52885
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.3 公里",
"value" : 290
},
"duration" : {
"text" : "1 分",
"value" : 12
},
"end_location" : {
"lat" : 25.0625881,
"lng" : 121.417177
},
"html_instructions" : "靠\u003cb\u003e左\u003c/b\u003e以繼續行駛\u003cb\u003e國道一號\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "keep-left",
"polyline" : {
"points" : "yi~wC{vadV`@p@p@fAR^|BbElApB"
},
"start_location" : {
"lat" : 25.0641299,
"lng" : 121.4194995
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "17 公尺",
"value" : 17
},
"duration" : {
"text" : "1 分",
"value" : 1
},
"end_location" : {
"lat" : 25.0626106,
"lng" : 121.4170086
},
"html_instructions" : "在\u003cb\u003e泰山收费站\u003c/b\u003e出口下交流道\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "ramp-right",
"polyline" : {
"points" : "e`~wCkhadVCT?J"
},
"start_location" : {
"lat" : 25.0625881,
"lng" : 121.417177
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "23.5 公里",
"value" : 23496
},
"duration" : {
"text" : "15 分",
"value" : 879
},
"end_location" : {
"lat" : 24.9788469,
"lng" : 121.2266636
},
"html_instructions" : "靠\u003cb\u003e右\u003c/b\u003e,然後併入\u003cb\u003e國道一號\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "keep-right",
"polyline" : {
"points" : "i`~wCigadV`HbOhKtRXp@Zn@tAbCpCfFxAfCXf@Vh@d@fA\\z@Z|@r@`CVhARhAN~@F`@J~@?P@TBr@?V?^AJ?L?lBA~@KxC_@hCSjAYjA_@fAc@fAk@lAqAtBqAxAsBlB{BdBiFpDyB|AwIrHgBlB_BdBmA~AuAlBq@pAgAbDa@`By@`Fe@bIEzA@hBBhAJhBPdBPlAx@xGBn@@~@ChAGpAEbAOnAUvAYvAQp@i@vB_A~CY|AOdAKx@Kz@S~CCLGtBA~ABvBHpCVfF^hILnCNjD@FBb@Bb@NdBPtBFr@Dp@LzAFpAFhADhAB~@B`C@T?d@DpADlA?B@\\Bj@?Z?\\@vAAzGB|BAn@?j@?B?DCxD@bB?\\CtC?XChDC|DMfDCb@At@Cv@KfDMpFChAEz@Ex@Ad@[tJCjBCvE?fDBvBD~CBfADtAFbALrBRdCR|BXlCHj@L|@f@nC\\dBTnA`@rB`@|Ap@bCvAfErAlDRf@FNn@pANZ|AxCtBjDpApB|BfDnAdBfCbDp@v@xEhFtApArExDpFzD~B|AxA`ApBpAvCnBbAt@|@r@n@h@`A|@hB`BbBfBtDvE|@lAnAjB`B|CbAvBtArCrAjD^fAh@dB^tAdB`HbBrGxAvFn@xBPl@Pl@FRrApE~AfFrBlGHRFTdAdDxAtEzC`JJ\\JZxBvF~CrH^~@P`@JVd@dAd@bALZNZj@rAXl@^x@`@v@h@dAf@dAP^R^Vn@Xl@Tb@Xf@fBpDR\\Xh@Zf@bErH|@xAP^NXpG|JpGzJb@r@`@n@~AbCPTNTRV~FbIfCbDrCxDnEfFnCbDjSzUX\\X\\p@v@h@v@z@dAbFxGxAhBX`@Z`@`EzFb@h@|@hAV`@V`@h@l@h@l@~AxBNT|@hAFHt@x@t@x@`BvBhD`E`@b@^b@dBzBpAfBf@n@LPJPjCrDfNrRTZTZzBbDz@rAhC~C`DrE|F~Hv@hAv@fArF~HtDnF\\`@Zb@pBrClCnDjCpDl@z@j@z@lCtDf@n@~BdD^f@zAnBjBvBbBnB`DpDtExEdD|CHJ|A|AbKhK`@^b@^tDtDbBfBPRPPTTnEdEz@|@VVVXxAxAlFfFnApARRPRzB|BJLLL|@~@vH~HRVJJHLdAlAhAvAnEhFfNlPrDlEpBbCJJx@z@NNn@j@"
},
"start_location" : {
"lat" : 25.0626106,
"lng" : 121.4170086
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "1.5 公里",
"value" : 1466
},
"duration" : {
"text" : "1 分",
"value" : 58
},
"end_location" : {
"lat" : 24.9698095,
"lng" : 121.2160748
},
"html_instructions" : "走\u003cb\u003e國道一號\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "merge",
"polyline" : {
"points" : "ytmwCsa|bV`OrQx@bAV\\V\\dGnHNRNPfIzJd@h@b@j@fBxBpClDdBxB`CxCHL"
},
"start_location" : {
"lat" : 24.9788469,
"lng" : 121.2266636
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "105 公里",
"value" : 104969
},
"duration" : {
"text" : "1 小時 4 分",
"value" : 3865
},
"end_location" : {
"lat" : 24.2754319,
"lng" : 120.6933458
},
"html_instructions" : "分岔路口靠\u003cb\u003e左\u003c/b\u003e以繼續行駛\u003cb\u003e國道一號\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "fork-left",
"polyline" : {
"points" : "i|kwCm_zbVzA|A@BbAjAjExFtDdFtXle@JRJPvBxDR`@T^t@pApFdIh@t@`CxCbBjBxGfGz@n@z@p@^`@PPTNRNpD~BvCdB\\PrCzAf@TbBv@bBt@`DjApA`@vDnAtRvDtFdAnAVpSxDhIzBJDdDjA~An@rCnAtC`BhGpEvArA`@\\@@tBvBlArA~BvCbChDf@`A|B~DrA`DdC~GbBpGHXtA|F|G`WdDdHrC`FtCfEr@dAbAnAv@`AbB`BnD`DjFtDlC`BtAn@xB`AlBr@`@NjCz@zPpEjAZf@Jf@LnMfDfDz@rA\\TFVFXHVHrBh@jBX`OxDf@Lf@LlEhAhBn@^Lh@T`Ah@r@d@FBh@\\h@\\LHlA~@\\\\zCpD~@lA`AvAbAbB|ApCvBdFdAnDnAtFdApFLr@Hd@Hd@h@|CbD`RbCpMbAbGpCvORhAH^F`@hCzNDRDRhBjKJf@Hf@~Mhv@@HBFh@~CN|@DXTrATdBXvBR|ARdBLjA@@JfAFz@H`APrBNxBNnDHtDDpDBxA?DAR?R@r@AtBCnDGfDAR?NGhCIhBK~BKbDIlBGxBKlL?X?V?xBVtJL~BXxBBP@RdArIXbBh@bDXzA~@jEvApFtBjGhBvExC~FtNfXrB`FnAdDDLFN^hAjAdFXxAN|@d@vCB\\DZTnCPzBLdERvLLvFLnDThDN|ATtBLhABJJz@RlA\\fB^jB\\zAd@pBZfADRFR@Bp@|Bb@lA@@HTJTPb@p@bBl@nAd@z@r@rAj@dAn@bA~AjCpB`DzB~Dv@zAn@lA~@tB`@dALZJZ@?lAjD`AzCn@|Bl@dCXpA\\hBXbBNdANnANnA?@Fn@Dj@PtCNdDJxEBhA@b@?`@DxB@bC?d@?v@CvEM`HMlEC~@Ah@]xKUnGIpBKdCMbDEx@M`ECr@C|@C|@APItDKnFE|CCdG?bEBvDDfEJ|CLlDLrB`@zHH~@@H@F@Td@~EhAtJjAvGDTNt@`@vBp@zCVnAFZXlA\\rA`@|A\\tAp@lC|@rC|A|DPd@dB~Dt@~A|ArCjB`D`A~AfAvA^h@^f@l@v@d@h@fBpBzAbBl@l@r@p@\\Z^\\|AlAvBbB`BpA`Ar@`Ar@tDvBvHnDlAb@j@Tj@PdBr@dB`@fA\\pB\\lBTd@D~AHd@BdB?vAEzBI`@ENAdBYv@Of@K`@Mr@Wz@]nAm@fAm@tA{@vAkA`BwAn@q@TW`@e@p@w@tBiC|DaF|EmGZ_@hAsAnA}AxAmBz@kAdAcAx@w@rAgA|AkAfAq@RM\\QZQp@Y~@_@lBo@vCu@xB_@zJiAnDSRCTAt@C~AIlBCnCEpED|HVzF\\V@PB`@DN@hEl@dALzHtAhIxB~DpAfE~Al@Xl@XZL`@Lt@\\hDfBzBnAVLVN`HvDZPZPdE|BJFh@Z~ElC|@b@TJTL`HdD~E~BhFnBXJXJpFpBRFRFpEtA`Cr@hDv@lDx@xAXbDj@LBLBv@NzEh@zATdCXtCZ`@F`@BdBNnFh@~@HzALD?h@DjBNfAHbBN|APt@FL@xAP`Fh@zARdALjBVfBZhDl@hCh@xBf@tDz@h@NfBd@fIfC~Br@fJtCPFRFjFbBbD`AxCz@v@TxDv@tEr@`AJjGb@tBJfDH~EAfQQtDChDEh@?bA?tD?`DDn@?zFVjC^FBrBZnDh@|FrAbF~A^JtBz@tB`AvAt@dBbA@@t@b@DBvA|@nB`B~A~Aj@r@l@t@|@pA|@|AfAvBf@pAL\\^jAt@lCr@xCZhBn@rCv@bEj@lCn@tB`@jAZx@b@|@HNJPf@~@x@pAlAvAdBdB`Ax@x@j@nBzAvAz@j@b@DBxBzA|@f@~B|ArBtAvA`Ab@Zl@`@^ZZT^XZTb@\\ZXZXvAzAtAdBRTfAfBhArBjB|D`BrDtAvCJNpArBvAnBfAlA^\\?@l@h@VV^X^XZTTPTPZPZRXPXPXN^PZN\\N\\Lf@Pf@R^L`@L`A\\p@RbAXp@R`@LD@b@LRDNFx@Tj@P`@LXJbA^d@Pf@Td@R\\Np@Zb@Td@V`@TZRf@Zd@\\b@Zb@Zf@`@VP`@\\XVZX^ZZX|B|BtCtD`CxDBDTb@hGfL`CvDtBpCd@d@VZNLLNn@p@jGvFxCvCd@j@JLJNp@t@vAvBfAjB~A|C|@zBHNFNLXr@rBf@fBh@xB`@dCN~@BZDXDZr@dHFlAFlAF`BDv@BfCJfM?T@RRjV@R?RBzBZdH^fFNzAB\\BZ@RLdA^tBr@dDp@hCx@fCjAdDXn@HTHR@Bz@hBfBfDxA|BdAvAzDvEPPPPbA~@dCzBdAp@\\Vh@b@v@b@v@f@dAj@lBz@vB|@fA^VJVHz@TtD`AnB`@|Cf@nDb@dCLtIb@~PNrEPT?TB|BRnBXrDv@rEvAxAn@fCjAdElC`D`CZVXXjIpIr@t@vBvBbC|BzFvE~E~CzA|@d@Rb@T|Av@xE|BtDdBxCpAbCjA`CjAt@^pAt@n@\\l@^vDbC`@\\b@\\pC|Br@n@dBfBjCrCd@n@vAfBlBpCf@|@JRHJFJ|C~FlGvMtBvDPZPZlBbDbAvArA`B~BlCx@v@pNlNpBtBbCzCtAhB~ArCh@bAPX~@pB|AfEbAvDv@nD\\tBjAzKDVBVTvBh@nDt@tDFRJ^Vz@`@rAd@nAZn@`@x@NZNXJRXf@z@tAv@|@^d@l@p@TTJLRP@@nAfA`BpA~AjALHd@XTNZLfAf@~Al@LDjB`@lB\\B?rBb@fItAj@Fz@L|KvA`ALX@X@bNvAl@Jl@JhDf@xFlAjAX|DdAr@RB@vCdAt@V~@Zr@ZdAf@TLjAl@zBtAlAx@nAfA`Ax@r@r@t@r@pA|AnB~BZ^`BjBx@`A\\b@VZ\\`@^f@`@f@p@`A~@lAl@t@j@p@hApA|@`A|@bATTTTvEhFd@f@lIfJZZLNh@j@nBlBt@j@JHJH\\XvA|@hBbAxB`AnA\\vBh@pC`@zAPdCH\\@N@P?P@L?t@C`AA\\Cd@El@Gj@Gb@GbC_@hDs@JCJC|Dy@zAWfASzAQ`AKzAIn@EP?TATAB?jACn@?dD?hCLbBJjCZ~AX~AZtAZb@Ll@Nn@TlCdAfBv@^RfFlCfAl@n@Zt@^VJ\\Nb@N|@Zj@PVFd@LXD|@P^Fp@HZBf@DTBH?B?H@F?n@@nA@jAAv@Eh@Cr@GZClAQt@MlCi@xA]bFqAx@S~A]rBa@pBYn@GfCQjAEdA?lBBfCJhCVF@F@bF|@tA`@nGbCr@^pMtGfAh@XLdC~@HBhBn@xFbBdCd@lC^fF`@bDNbCAH?fBCV?PAP?F?tAI`CKbBMzBS~AUrDYJ?LAN?`@ChBC~A@jADnAJlANl@L`AN|A\\|Af@nAd@TJRHPFt@\\lBz@vBlA~A|@pA`AfAx@~BdBJFPLdH~ExDxBdCnAf@Rt@\\F@pBp@TFRFJ@tBn@fBh@`KrCDBF@bAXlA^dDdA~BdA`Ah@pBrAPNDFtBjB~@hAt@bAbBnCbB~ClArB|@vAbA|AbBtBlArAPNPNJN`A|@rElDPLPLNLNJbBnA`H`ElHnDjGfC~FpBXH^H\\HpLzCpH`Bj@LD@|Bj@rBj@tCz@tA`@THTHfDlAfAd@v@Zx@`@lCtAv@b@PJPJNJn@^fBjAjAz@pB`BfCvBjCzBRPRPr@l@`@\\|CjCr@n@PNPN|@x@tAhAdCrBbDhC|AdAnCzAjAj@d@Tv@ZvAj@B@\\LZLLBdBh@jBh@dATz@R\\JF@lCf@nBb@~AZnB`@xBf@jBd@JDj@N\\JzAd@l@Tj@Rl@Vv@\\fB|@JFJFzCpBzC`CfBzAl@l@nD~DRVPV~@lAd@v@zApCnA~B|A`DXj@Xj@d@dAnAfC|AzCnAtBP\\Rd@Vf@^n@d@r@z@hA`AjAPRfDxDjDnDzE|DbG~D|@d@FB~At@NHZN\\N`Bt@pMxFn@XdEbCpBrAHDdBnAfCxBz@|@bEtE~@rAFHPXRX~BtDjA~BP`@l@lAdBlEdAdDpBxH@D`CzIFXHXhB|GfB|G~@tDLd@Ld@DNTfAp@`Dt@tC@@d@~An@fBHR^fATj@Rd@Th@vAvC`AbBRZb@t@b@n@h@p@h@t@v@|@NLLNNNj@j@~AhBXVXXNLNJJH~ApAVRl@`@`@Zj@X~@j@|@f@f@T|@b@^PTJnAh@rAd@`Bh@p@Tj@NLDNDjCn@vB^`APrATz@Hn@Jt@Fj@Ft@FN@H@H?fAHt@D|AHj@@lBHf@?rA?xBA`AAj@CfACrAGH?PAPCnGq@fDc@bCc@pAYjBa@pMiDNELGzNyFZKXM`@On@Up@UlAa@xOqFpEmApEaA^KXIXGnAYrCa@z@OlEa@xDObEG~BB\\B\\@^B|CNfDZhCd@nAXLBNDtBd@zE`B`CfAjC|AFBFDdCvA~E`DnGvDdFzBFBXFVFhFdA~Er@tFf@D?tEZN?N@jKn@vEn@zFlAjEhA|FtBp@VnJvDdA^~DnApDx@tGz@`DPvCHtNBpBFfBLz@F|APjBVrCj@bI|ATDRDHB|Cf@tBNfEP~EHhCLlDZbDh@rFjAb@J`@JfCf@xB^`CX|Eh@H@J?ZBf@FP@PBP@n@JtEl@zCj@jHpBB@JBHDjHzC~Ax@`DlBh@ZpCrBdCtBdCbCjLrM\\^Z\\jApAhBpBfBbBxAlA|BlBnAz@`C`B`BdARJlYjOxDzB`UjNzJbGhDjBnDfBtHfDVJlQdHfOpFr^zMtClAVJ~@`@|GlCpEvB|E`CzHlEtHnEbHvEfLtIfF`Eh^rZx@n@x@n@x@v@z@v@JH~DvDTRdHzF^XfBxAp@h@xb@h^VTXTbLnJxEvD|CxBfFlDVPTNb@XdBfAhE`CvCxAfDdBzMjGvOnHtBfA`@T`@Rn@\\|BnAzGjExFdEdEhD^X|DdDvL`LfSxQpFrEdLzH`KfGtGdD`@R^RPHhQtH`RvG`JfCjUfGPDPBbGrAlUfEfDf@pJhAbD\\hP~ApYpBlN~@~Hd@rRhAlFZdBLP@R@t@Dv@FxO|@`CNP@R@"
},
"start_location" : {
"lat" : 24.9698095,
"lng" : 121.2160748
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "24.1 公里",
"value" : 24090
},
"duration" : {
"text" : "15 分",
"value" : 881
},
"end_location" : {
"lat" : 24.1189755,
"lng" : 120.5824256
},
"html_instructions" : "靠\u003cb\u003e左\u003c/b\u003e以繼續行駛\u003cb\u003e國道一號\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "keep-left",
"polyline" : {
"points" : "mhdsCm|s_VV?z@DlCNdQbAvId@zHb@fF^rJj@vO|@l@Dl@D~Hb@N@P@lV|AlKt@P@P@nMz@R@T@bCNpQfAz@Dz@FZBtFZfCNzALjHl@fJz@H@d@FzBRhFj@RBR@l@Hl@HZD\\DzAP~B\\lANdAPNBNB|IzAnB`@jIhBdDz@XHXHbCl@zDnA~G~BzFxBnChA|Ar@VJvFtCXPTLTN`@VXNTLVJz@j@z@d@dGfEfBrAVPTPvAfA@?zBjBhErDdBdBd@b@Z\\ZZVX|A~ABB|AfBrChD|CxDfCnDpDpF|ClFXf@~BrEvAxCpAtClBpErAzCbBhETh@`AjCj@bBrGxQTj@tCnIbFfNx@vBRd@Pd@dHhQBHn@rA\\t@vBrEnBnDpEjI`CxDxBdDrC`ELPLP~EdGnJtJb@d@JHxIlHhBrAZT\\VtBzArErCtKzFtDbBRJlAf@hCjArCbAvBr@\\J\\JzHbCnA^jJjC\\J\\HdAXlJzCt@TLDdBn@`@NfBr@NFVLVLdAf@p@\\fCpAzCfBVPf@\\v@h@^X^Xf@`@h@b@v@n@j@f@`GfFFFBFHRJHHHHHtAlAtBlBh@f@j@f@j@h@p@n@VTfAbAvBpB\\ZrAfALJLJXTtA~@vBvAxEvCdB`ATJrCnAjChAvBt@b@N`@Nb@PnDdAtBj@dGpA^HdANf@HjALvCXTBVDbALz@JN@P?l@HdJ^dBFzBHP@T?R@fA?`AD~@BfADfADhADvERfG\\bIj@lBNVBXBbBR`Ed@VBbFd@@?~@D|CX`CV~AHtBTzCPzD\\p@DnAJlAJr@F~I`@`@DjBFf@@X?p@D^Bf@@v@@`@@f@B|@FpABD?bGNzLVnDFlEJxBHjERnAHH?J@XBlBNhANd@DlCNbEj@rGdAv@HXFx@Px@VrC|@n@RRHRFp@ThAd@bDtAlAn@dBdANJJHLFj@d@~BlBlBxA~B`CvAdBdAxAz@nAHLHNd@v@\\n@f@dAR^\\r@b@bA|@`CBDBLJZHXBHPh@n@lCn@`CVvANx@Hn@Hf@NbBDjAB`BHtA@t@?lAC\\CfBQvBARKx@Ij@Gh@OdA_@jBo@fCa@lAo@bBiAhC_ArBeBhDq@pAGLGLq@lAaAhBmBxDy@dByBfFk@tAoAnDqAdEg@hBe@pBGTERCHu@pDUzAg@bD]lDKbAWtCK|BAXCVIhC"
},
"start_location" : {
"lat" : 24.2754319,
"lng" : 120.6933458
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "67.8 公里",
"value" : 67754
},
"duration" : {
"text" : "40 分",
"value" : 2383
},
"end_location" : {
"lat" : 23.5588388,
"lng" : 120.4177033
},
"html_instructions" : "靠\u003cb\u003e左\u003c/b\u003e以繼續行駛\u003cb\u003e國道一號\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "keep-left",
"polyline" : {
"points" : "sverCeg~~UCpB?N?N?j@AlB?`ADhEHdCJdCJ~AFj@@LXnCHj@PbALp@DVDTRdA\\`Bf@lBp@tBTn@L\\JZBH|@tB^z@b@|@bAfBx@rAhA`B^l@vAnBdAhAFFFHpBtBNNzDjDRPRRHHvPfN`VrRj@d@HFb@^xAjA~MjLnBtB~@x@z@~@fC`D^h@Zh@Xb@|@|AdBpDv@nBfBhFf@pB^`Bx@zEVzBT|BNtD@V@TDn@RtEZlI@X@VRvE`@`FVbBBPDRh@xCf@nBRz@DNL^J`@L\\~@nCdB`ExC`Gv@|A@DTd@LP~AjCl@~@jAbBzAtBd@l@xAlB`@d@PRRRd@l@~AdBtCrCLNNLdAdAjBbBxGnFjCjBvCtBdHnEhDjB~Ax@lCpAvGxCbBj@lE|AbCv@pA\\hAZjEdAZH^HTDx@N|FdA~B^dEj@jBRbBLL@ZBXBnDTpCNP@RBl@?lEDP?p@?n@?vGB`GI@?pBE^A^AfAEN?fDQb@Ad@C~Z{AjCS`I_@PANA|@EPAP?xBIxAITA~FOvHG`@AJ?xJElLLjVhA`ADPBnK|@XBNBbBLxHdANBPB`KtApKnB|N`DzBl@RFRDtMlDTFTFjEhAn@Pl@PbAVPDRF`KlCVFXHzXnHn\\nGdRnC^D^FdVjDXDXF~O|BPBPBxB\\fOvBNBLBjFt@nG~@F@H@vYfEJBL@|O|BrVtDz@H`AN~HdAnAPTDnAPzAVx@TdC\\nBXNBb@Dd@Fh@Fh@HN@LBxHt@N@tBRtD`@zAP^BL@N@zFZhEVxM^d@@f@@vJLd@@d@?n@@nA?vD@hDApOStBAxACn@AhCEhAATAR?v@C|BExGMdFGT?RApAAvCGfHCfGBhDBf@?R?T@l@?bDFdDHf@B\\@^BrFRtDPz@FT@TB@?nDXt@D`CRpCTb@DvCZbGp@hANfBVh@FzDj@fDj@|Cj@zCh@~FlAlGvA~A`@pA\\bAVz@TlA\\bBb@bBf@`AXvA`@p[pJPFPFtBj@|Cz@hBf@jCp@`HxArB^bCb@vNfB`BLdF^`NVt@@dMMzM]dHYzL_@tEG|FCjEDrEJJ@J?H?xDR|Hl@lHz@F?D@\\DtAPp@L`LtBdCl@JBHDd@JVHLBLBxI~BPDPDhD|@p@RhOfE`AZnEfAtHbBj@LnMdCbARjBZZD@?nFp@bM|ApJx@p@BjBLP@N@\\@bb@zB`Jl@L?L@tG^zD`@~C`@rC\\NBLBzARdAN|Er@bKdBfH`BdARbARrI`Bz@Pz@PnKpBx@NNDbOfC`CZj@FtBVxBZjFf@|@HrDZpF`@`CPP?pEPv@Bv@DtDNbL?X?zADn@?d@@rFGvGAXAL?LAlDIxKUlAEP?RAnCIvV}@pGQZ?LAN?rDIhHSbEKzHM|@?PARAdCAxB?r@?r@AD?bE@`PPhFLR@R?rELpVhAjQtAjNjA`CTtVdDxDn@LBN@fKfBF@F@lB\\VDH@LDhEbAXFXFxAZx@Rz@PrD~@rCr@`KdCVHXHdFvA|Br@pDfAtAb@XJZHfBj@tFhBnFfBB@PFNFHDLBxTzHv@Vv@X`ErAjDlA~KxDtC|@JBTH~Aj@tAd@nErANDLDtBn@nHvBdOxDNDfEdAPDPBND`LzBH@NDNBlMvBp@FJBJ@tN`BlBNdIl@~Kf@nABt@BV@T@|CH|IFb@?L?L?pGAfJMFAN?LAlFOtLu@`BKLALAJAjAIPCPAjGi@xCa@~HcAfAONCHALATEpDo@vDu@LCNCdB_@lEaAtAYlBg@\\I^KlBc@hCq@rCy@JCJCZK~C{@vFaBRGhEmA~C}@d@MBArD_AtA_@hCs@vA]lCo@XGXINEzBg@bCg@|Ey@pAWvB]`Eg@dD_@~BWLANC\\EnFe@fEWdCOhCIP?PAPAd@Ar@CrPMxNNzAHP@R@fNr@jBPpDZTBTBd@DpGz@pGz@NBND`F`ANDNBjCh@TDRDpCj@nAZ~HnBxAd@vA^LDJDzC|@vKnDZJTHTJjJjDdC`AFDHB^NbCdA`Bx@@@@?HDHBBB~G~CdE|BNFLHbCpANHLHrH`EPHNHlTlLJDHFlM~GNHNFzDtB`ExBPJLFpJlFh@Xd@XzBlAlAp@tGjD\\R\\PrMbHnAp@nAp@|EfCrDjB^P`@R`@RdMdGvGzCh@T`@Rb@PxGvCfChA|EjBf@Rf@RpHtCdFjBZJZLLFrHjCxE~ANFNDhA^hA`@bFbBrE|AhH|BjD~@jOlEt@Rr@R|@TpCl@rB^vCh@nAVbBZ|Cr@bDr@`Cf@lDr@~Cl@nDr@rCf@vAXRBRDF@vCd@bEp@~Dp@`El@hDh@vEn@|En@fCZn@JvEh@dEd@jEd@pKdARBTBdAL|BRVBdAJdAJtCXhGj@t@HZDF@H@H?RBn@JrUxB`@Db@DfAJjDXtCV~BT~Eh@pBPF@H@R@RBt@HnBRnCVlCVrMlATBVDbPdBZDXDfFn@`Df@VDVDtEn@~Cd@VFVDXFjQfDTDRDjB^fAVNFPDjTzFvBp@THVHvBn@zNfFpChARJRHhV`KbM~FfH`DNFNH~PxHdCbAVJVJdLtElE`BlItCZLZJhBl@fFxApD~@xQbFRDRF~DnAnA`@"
},
"start_location" : {
"lat" : 24.1189755,
"lng" : 120.5824256
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.7 公里",
"value" : 667
},
"duration" : {
"text" : "1 分",
"value" : 41
},
"end_location" : {
"lat" : 23.5546293,
"lng" : 120.4140711
},
"html_instructions" : "在\u003cb\u003e257-民雄\u003c/b\u003e出口下交流道,朝\u003cb\u003e新港\u003c/b\u003e前進\u003cdiv style=\"font-size:0.9em\"\u003e收費路段\u003c/div\u003e",
"maneuver" : "ramp-right",
"polyline" : {
"points" : "wixnCsa~}Ul@\\LFTHp@NXJZJFB~CdAPF|Aj@|CtANFLF|@`@r@ZBHFFLJFHDDFJT^Rl@Fd@D`@Ab@Gj@Sz@"
},
"start_location" : {
"lat" : 23.5588388,
"lng" : 120.4177033
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.2 公里",
"value" : 171
},
"duration" : {
"text" : "1 分",
"value" : 15
},
"end_location" : {
"lat" : 23.5555297,
"lng" : 120.4127124
},
"html_instructions" : "走\u003cb\u003e縣道164號\u003c/b\u003e",
"maneuver" : "merge",
"polyline" : {
"points" : "mownC}j}}UEHeBtCc@x@c@t@"
},
"start_location" : {
"lat" : 23.5546293,
"lng" : 120.4140711
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "6.1 公里",
"value" : 6139
},
"duration" : {
"text" : "7 分",
"value" : 443
},
"end_location" : {
"lat" : 23.5559742,
"lng" : 120.3543138
},
"html_instructions" : "繼續直行,並繼續走\u003cb\u003e縣道164號\u003c/b\u003e",
"maneuver" : "straight",
"polyline" : {
"points" : "auwnCmb}}Uq@dAuErIa@t@U\\UZo@r@_EvCA?g@`@[TKFEDSJOD@RBjA?DBvA?B?hD@j@@TDbD@VHvG@XNdKD`D@h@?hDLnD?^D`BJvFH|C@LHvC?RVlFR~EPdDNvCXdFJlBHjA@NDdAXvHNfD?PHhBB\\?Fh@lLB`@VlGBb@XpHBb@VvFN|CHzA@\\dA~T@\\P~DJnBBd@^nIB\\j@|L@\\`@jIPdF@TFrA?D@RN~CFpB?|@An@Ar@CpACh@EvAIrCANCz@Aj@Cp@EbBE`AC`ACn@?JAVWfJEhAEfBAJAXG~BAf@ClA"
},
"start_location" : {
"lat" : 23.5555297,
"lng" : 120.4127124
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "1.0 公里",
"value" : 1034
},
"duration" : {
"text" : "2 分",
"value" : 111
},
"end_location" : {
"lat" : 23.5496167,
"lng" : 120.3476798
},
"html_instructions" : "於\u003cb\u003e嘉民路\u003c/b\u003e向\u003cb\u003e左\u003c/b\u003e轉",
"maneuver" : "turn-left",
"polyline" : {
"points" : "ywwnCmuq}Uf@ZfAl@~AbAzCjBxA|@h@ZfC~AxD`CdAj@z@j@xBxAVTNPX\\JPR\\LXN^Ph@FXHd@`@dE@dB"
},
"start_location" : {
"lat" : 23.5559742,
"lng" : 120.3543138
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.4 公里",
"value" : 403
},
"duration" : {
"text" : "2 分",
"value" : 108
},
"end_location" : {
"lat" : 23.5531974,
"lng" : 120.3473666
},
"html_instructions" : "於\u003cb\u003e中山路\u003c/b\u003e向\u003cb\u003e右\u003c/b\u003e轉\u003cdiv style=\"font-size:0.9em\"\u003e目的地在左邊\u003c/div\u003e",
"maneuver" : "turn-right",
"polyline" : {
"points" : "cpvnC_lp}U{@XoB^oAJE?q@@yAEc@?eACiACiCB"
},
"start_location" : {
"lat" : 23.5496167,
"lng" : 120.3476798
},
"travel_mode" : "DRIVING"
}
],
"traffic_speed_entry" : [],
"via_waypoint" : []
}
],
"overview_polyline" : {
"points" : "}}ywC{pxdV`DgGiIfCuKn@wcAk@mt@]aJdEwLtOcN?qRpCo@bVkVpmAkBxu@xQjcBjJf}@kLxdA_A`h@|Epa@|a@ltB~Bb]bGh}@j~@phBfFfX_DxV{b@t`@aQz]bAjc@}Gri@pEltAyAhrB`Oxt@zTv]rb@z]nWbVfP|\\t_@|pAbp@rrAbgEnwFt|AnaB`oBl~B~k@z~@b\\j`@~N|Jdl@tQfu@dR~YjWfUvk@lVpm@rGxHt_@jTxl@pOdl@dQdO`QrMnc@di@lzC`Gvf@Ixp@Qzi@pEv]je@piAlEzSvG~bAhLb\\|Z`r@hEbk@sChkAt@`jArNzx@b`@|p@lPvMf\\fNdUv@hKaCbj@}k@fNmLx`@oHpz@|Dvw@h]lw@h\\vu@tK`l@xGrkAh[xrAhBhb@xLvN`LnUls@|^v\\dY|XnWz_@rc@pPnR`Ovk@fv@|Qne@~DnsAfOrh@hQlShP~IfXzFho@dCf_@lQxe@fa@zq@|_@zg@ry@jd@dh@~Lpg@vJ|^xTrRb}@|Mhn@|TzYd]zb@td@nWxI|IHrc@qGtb@rDd_@pOhQL|YmGzXQfd@fQh_@lIbe@aBr]`Fpm@v]fk@dUh`@hf@xf@~Uda@xJva@hRtg@hb@|~@`ZbVzTxMpWlJjPl]pZr]lPpTtS|MlYxO`m@zNxb@`O`QpZhO|YdE`j@}BnkAs^~[wCzVxBlw@h]th@xE~o@fSxl@jCj[hFxz@lJfW`Fd[rQb^j_@ncApm@lbB~r@lp@`\\b}BlkBbb@|[|v@f`@l^lVnn@tj@~a@|VnhAd_@xnB|Td{AvIhyB|MvkA|Iro@hLbp@rWnc@v]h]~d@~Rlc@bt@~gB~_@ff@hf@d\\f_Ad[p[jPzX|Vtf@d\\lh@zKphB`LbbBdH~\\zFjPlHpXh[~It]eDpd@m^nz@mGzk@~Bzd@`Sjd@rjA~aAfOfRtLtd@jGrv@fXrh@zs@xl@va@`Pz[`Gdo@vA||@cE`Zo@``A`D`tAjXhgAdWp~@xM`mBvXfk@tHv|@zDh{B[pr@xGtrAt\\t`@fJ`h@lD|f@cApi@Wx]dDl_AtUnf@~IvbAjHviA`OxsAnRhz@ZnwAsDxnBlHt{@vNzeCbw@`dApUbjAhFp_AiGpd@iJ|r@sRls@yKfR{@t|@pBnr@hMpq@hVdu@h`@xeClpAd_At\\liAxXh~BrZv|B|U~~@fTbzB`~@rgAx_@gB`LmMlT{HnFLlL`BjbAfFphAxJ|gCwAvf@OnGjJxFxUbO`C|El@pIaGdAiMG"
},
"summary" : "國道一號",
"warnings" : [],
"waypoint_order" : []
}
],
"status" : "OK"
}



裡面的 legs, steps, 就是每個轉折點的路徑, 但是用這個路徑資訊來畫圖, 地圖只會變成很醜的線段相連, 不能呈現沿著路畫出漂亮曲線的結果。

但裡面也包含有完美曲線的資訊, 就隱藏在 overview_polyline 這個屬性當中,只是overview_polyline 的資訊已經經過編碼,所以無法直接拿來套用在地圖上。

為了這個遺憾,我把以前在 iOS SDK裡面看過的程式碼改寫成了TMapPolylineDescriptorHelper 這個 class Helper (請參閱 Object Pascal Handbook 的 record helper 與 Class Helper, 中文版Object Pascal程式語言手冊第358頁):

type
   TMapPolylineDescriptorHelper = record helper for TMapPolylineDescriptor
      class function CreateByGME(GoogleMapEncodingString: String):
               TMapPolylineDescriptor; static;
   end;

實作程式碼:
class function TMapPolylineDescriptorHelper.CreateByGME(GoogleMapEncodingString: String)
    : TMapPolylineDescriptor;
var
   dataStr: RawByteString;
   coordCount, rawLength: integer;
   idx, currentCoordIdx: integer;
   APoint: TMapCoordinate;
   latitude, longitude, finalLat, finalLon, deltaLat, detlaLon: Double;
   Abyte, shift: Byte;
   res: Int32;
   offset: integer;
begin
   dataStr := GoogleMapEncodingString;
   rawLength := Length(dataStr);
   latitude := 0;
   longitude := 0;

   {$IFDEF WINDOWS}
      offset := 1;
   {$ELSE}
      offset := 0;
   {$ENDIF}

   currentCoordIdx := 0;
   idx := 0;
   while idx < rawLength do begin
      AByte := 0;
      res := 0;
      shift := 0;

      repeat
         AByte := Byte(dataStr[idx+offset]) - 63;
         Inc(idx);

         res := res or ((AByte and $1F) shl shift);
         Inc(shift, 5);
      until (Abyte < $20);

      if (res and 1) = 1 then begin
         deltaLat := not(res shr 1);
      end
      else begin
         deltaLat := res shr 1;
      end;
      latitude := latitude + deltaLat;


      shift := 0;
      res := 0;
      repeat
         AByte := Byte(dataStr[idx+offset]) - 63;
         Inc(idx);

         res := res or ((AByte and $1F) shl shift);
         Inc(shift, 5);
      until (Abyte < $20);

      if (res and 1) = 1 then begin
         detlaLon := not(res shr 1);
      end
      else begin
         detlaLon := res shr 1;
      end;
      longitude := longitude + detlaLon;

      finalLat := latitude * (0.00001);
      finalLon := longitude * (0.00001);

      Inc(currentCoordIdx);
      setLength(Result.Points.Points, currentCoordIdx);

      APoint := TMapCoordinate.Create(finalLat, finalLon);
      Result.Points.Points[currentCoordIdx - 1] := APoint;
   end;
end;

然後在製作地圖的程式碼裡面,把 overview_polyline 的所有點放進去:

overview_polyline := lv0.GetValue('overview_polyline')
                as TJSONObject;
MapPolyline := TMapPolylineDescriptor.CreateByGME
                (overview_polyline.GetValue<String>('points'));
MapPolyline.StrokeWidth := 12.0;
MapPolyline.StrokeColor := TAlphaColor($FF5CC2BE);


AMapPolyline := self.MapView2.AddPolyline(MapPolyline);
AMapPolyline.SetVisible(TRUE);

這樣就可以畫出完整、完美的曲線了。

規劃路線的完整 function 如下:
procedure TFormNovMain.btnTrafficRouteClick(Sender: TObject);
var
   targetURL, mode: string;
   directionJsonStr: string;
   routeObj, lv0, leg0, step, startP, endP, overview_polyline: TJSONObject;
   route, legs, steps: TJSONArray;
   idx: integer;
   APoint: TMapCoordinate;
   Points: TArray<TMapCoordinate>;
   MapPolyline: TMapPolylineDescriptor;
   mapCenter: TMapCoordinate;
   MyMarker: TMapMarkerDescriptor;
begin
   if (self.ComboEditStart.Text <> '') and (self.ComboEditTarget.Text <> '')
   then begin
      if (txtTrafficTitle.Text = self.TxtPublic.Text) then begin
         mode := 'transit';
      end
      else if (txtTrafficTitle.Text = self.TxtDriveTo.Text) then begin
         mode := 'transit';
      end
      else begin
         mode := 'bicycling';
      end;

      if self.ComboEditStart.ItemIndex = 3 then begin
         targetURL := Format('https://maps.googleapis.com/maps/api/directions/'
             + 'json?mode=%s&origin=%g,%g&destination=%s&key=AIzaSyCsQG8bTrKou0FRH0IDOvNdk3iWHDcq1iA',
             [mode, self.Here.Latitude, self.Here.Longitude,
             self.ComboEditTarget.Text]);
      end
      else begin
         targetURL := Format('https://maps.googleapis.com/maps/api/directions/'
             + 'json?mode=%s&origin=%s&destination=%s&key=AIzaSyCsQG8bTrKou0FRH0IDOvNdk3iWHDcq1iA',
             [mode, self.ComboEditStart.Text, self.ComboEditTarget.Text]);
      end;

      self.RESTClient1.baseURL := targetURL;
      self.RESTRequest1.Execute;

      routeObj := self.RESTResponse1.JSONValue as TJSONObject;
      if Assigned(routeObj) then begin
         route := routeObj.GetValue<TJSONArray>('routes');

         lv0 := route.Items[0] as TJSONObject;

         legs := lv0.GetValue<TJSONArray>('legs');

         leg0 := legs.Items[0] as TJSONObject;
         startP := leg0.GetValue<TJSONObject>('start_location');
         mapCenter := TMapCoordinate.Create
             (StrToFloat(startP.GetValue<String>('lat')),
             StrToFloat(startP.GetValue<String>('lng')));
         startCood := mapCenter;
         self.MapView2.Location := mapCenter;

         if bUsePolyline then begin
            legs := lv0.GetValue<TJSONArray>('legs');

            leg0 := legs.Items[0] as TJSONObject;
            steps := leg0.GetValue<TJSONArray>('steps');

            SetLength(Points, steps.Count + 1);
            for idx := 0 to steps.Count - 1 do begin
               step := steps.Items[idx] as TJSONObject;

               startP := step.GetValue<TJSONObject>('start_location');
               APoint := TMapCoordinate.Create
                   (StrToFloat(startP.GetValue<String>('lat')),
                   StrToFloat(startP.GetValue<String>('lng')));
               Points[idx] := APoint;
            end;

            step := steps.Items[steps.Count - 1] as TJSONObject;
            startP := step.GetValue<TJSONObject>('end_location');
            APoint := TMapCoordinate.Create
                (StrToFloat(startP.GetValue<String>('lat')),
                StrToFloat(startP.GetValue<String>('lng')));
            Points[steps.Count] := APoint;

            MapPolyline := TMapPolylineDescriptor.Create(Points);
            MapPolyline.StrokeWidth := 12.0;
            MapPolyline.StrokeColor := TAlphaColor($FF5CC2BE);
         end
         else begin
            overview_polyline := lv0.GetValue('overview_polyline')
                as TJSONObject;
            MapPolyline := TMapPolylineDescriptor.CreateByGME
                (overview_polyline.GetValue<String>('points'));
            MapPolyline.StrokeWidth := 12.0;
            MapPolyline.StrokeColor := TAlphaColor($FF5CC2BE);
         end;

         AMapPolyline := self.MapView2.AddPolyline(MapPolyline);
         AMapPolyline.SetVisible(TRUE);
      end;
   end
   else if self.ComboEditTarget.Text = '' then begin
      ShowMessage('請輸入目的地');
   end
   else begin
      ShowMessage('請輸入出發地點');
   end;
end;

大家可以試試看用 Android 手機拉一個簡單的範例試試看, 立刻就可以看到完美的地圖出現喔.