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 手機拉一個簡單的範例試試看, 立刻就可以看到完美的地圖出現喔.


2018年2月28日 星期三

讓 ComboBox 的每個項目顯示不同顏色

在一般的應用程式中,使用 ComboBox 提供下拉選單的功能,讓使用者選擇不同項目,一般而言, ComboBox 的項目沒有什麼特別的,但在特定的應用程式中,有時候會希望每個項目有一些效果呈現,例如:
  • 每個項目都能有個 icon 對應
  • 每個項目的文字顏色可以不一樣
第一個需求,FireMonkey的 TComboBox已經幫我們透過 images (TImagelist) 提供了,所以在本篇文章裡面不討論。

第二個需求,則需要加一些程式碼跟設定來完成:
  1. 先把 ComboBox 的 DropDownKind 設定成 Custom.
  2. 把 ComboBox 裡面的每個字串設定為不同的顏色, 加入以下程式碼, 可以把第一個項目設定成紅色, 第二個項目設定成藍色.
   ComboBox1.ListBox.ListItems[0].TextSettings.FontColor := TAlphaColorRec.Red;
   ComboBox1.ListBox.ListItems[0].StyledSettings := ComboBox1.ListBox.ListItems
       [0].StyledSettings - [TStyledSetting.ssFontColor];

   ComboBox1.ListBox.ListItems[1].TextSettings.FontColor := TAlphaColorRec.Blue;
   ComboBox1.ListBox.ListItems[1].StyledSettings := ComboBox1.ListBox.ListItems
       [1].StyledSettings - [TStyledSetting.ssFontColor];

設定畫面如下圖:
執行結果如圖:
這樣就完成了,分享給大家.

2017年12月11日 星期一

在 Delphi 當中使用 TMapView 顯示地圖之一:程式設定與加上地點標示

在 Delphi Seattle 10 之前,還沒有提供封裝好的地圖元件,因此在那之前的案子,筆者也大多用 TWebBrowser 搭配 JavaScript 土法煉鋼來提供手機上的地圖功能。

在 Delphi Seattle 之後,TMapView總算出現了,但一直沒有實際的機會可以拿來用,最近有個機會,需要在 App 當中提供標示地點、路線的功能,因此正好把多年以前在 iOS 上面製作過的桃園福利地圖App當中的導航跟標示地點的功能拿來複習一下。

在 Delphi 上面要製作具備有 Google Map功能的地圖,請先服用官方說明:http://docwiki.embarcadero.com/CodeExamples/Tokyo/en/FMX.Map_Sample

如果您懶得看,我整理一下懶人包(如何設定你的專案,讓它具備Android平台上能顯示地圖的功能),只有幾點而已:

第一大項:建立 Google Map API 的 Key:

1. 連線到 Google API Console: https://code.google.com/apis/console/?noredirect
2. 在專案列表中,建立一個新的專案
3. 把 App 的 id 跟名字輸入進去
4. 完成,畫面上會出現 Google API Key,這是一長串的字。

第二大項:設定專案的權限:

1. 先把專案切換到 Android 平台

2. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Uses Permissions
3. 確定以下三個權限有勾選起來:
  • Access coarse location
  • Access fine location
  • Access network state 

4. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Entitlement List 確定 Map Service 有勾選起來


5. 點選 Project Manager 上的專案,滑鼠右鍵點擊,選擇 Project > Options > Version Info 把 Google API Key 貼到apikey 欄位上,也確認 id 跟在 Google API Console 輸入的一致


這樣就設定完成了。

設定 TMapView

接著就可以從元件盤上面拉一個 TMapView 到 Form 上面了,MapView 要放多大都可以,但放太小的話使用者操作起來會不方便,所以盡量能留個半邊螢幕給它比較恰當。

使用了 TMapView,第一個需要操作的行為,一定是把地圖的中心點拉到我們想要顯示的地方,並且把地圖放大到『適當的大小』。

以上兩個需求,可以透過設定 TMapView 的兩個 Property 來達成,一個是設定大小用的 Zoom,另一個則是設定中心點的 Location。

先介紹 Zoom,它比較單純,是一個 Single 型別的數字,跟 Google Map 的網頁版一樣,數字越大,街道越大,數字越小,街道越小,我自己習慣的大小,是設定在 16.0,這個大小很適合作為導航之用。

只需一行指令:
Self.MapView1.Zoom := 16;

中心點就稍微麻煩一點,因為 Location 屬性的型別是 TMapCoordinate,所以我們得先建立出一個 TMapCoordinate 實體:

var
    mapCenter: TMapCoordinate;
begin
    .....
    Self.MapView1.Zoom := 16;

    mapCenter := TMapCoordinate.Create(23.459567, 120.323351);
    self.MapView1.Location := mapCenter;
    .....
end;

透過這段程式碼,就能夠把地圖的中心點拉到嘉義高鐵站,並且把大小調整為適合導航的大小。

加一個標示點在 TMapView 上

如果您使用過 Google Map網頁版進行開發,這裡就會很熟悉了,在Google Map網頁版上,要加一個標示點,是以 Marker 元件來加的,但只能以 Javascript 或後端工具在網頁上添加。

在 Delphi 程式中,則提供了對應的元件 TMapMarker,要建立 TMapMarker,則需透過 TMapMarkerDescriptor 這個 record,當中包含了 Marker 的座標、文字描述,如果我們想要自定圖示,也可以直接透過 TMapMarkerDescriptor 來指定:
var
    mapCenter: TMapCoordinate;
    MyMarker: TMapMarkerDescriptor;
begin
    .....
    Self.MapView1.Zoom := 16;

    mapCenter := TMapCoordinate.Create(23.459567, 120.323351);
    self.MapView1.Location := mapCenter;

    MyMarker := TMapMarkerDescriptor.Create(mapCenter, '嘉義高鐵站');
    MyMarker.Visible := true;
    MapView1.AddMarker(MyMarker);
    .....
end;
這樣就能在 MapView 上面,把地圖中心點拉到嘉義高鐵站、在上面插個標示了,如果想要自定圖示,只需指定一個 TBitMap 給 MyMarker.Icon 即可。




使用 TRESTClient 與 TRESTRequest 作為 HTTP Client 之二 (POST 檔案)

使用 HTML 進行檔案上傳,已經是很平常的應用了,在手機App裡面,也常常會用到這個作業,例如拍照上傳,或是從相簿選取照片上傳,都是很常見的。

在 HTML 的 Form 裡面,要讓使用者選擇檔案上傳,通常會這樣寫:
<input type="file" name="fileId1" id="fileId1"/>
當我們在 HTML 裡面這麼寫,網頁瀏覽器會自動在畫面上顯示一個按鈕,點選之後,會出現檔案選擇的對話框讓使用者選擇檔案。

但是,在手機作業系統中,為了提供檔案系統的Sandbox 功能,並無法從網頁直接提供相簿照片選擇或拍照上傳的功能,所以大多數類似的功能,還是得透過 App 提供才行。

在這個範例中,為了讓使用者能直覺使用檔案選擇的功能,所以也同時介紹了如何從手機相簿選擇與拍照上傳的功能。

如何讓Delphi程式提供相簿選照片的功能

在Delphi環境中,要提供使用者從相簿選相片,或是啟動相機拍照,都要透過 TAction 來完成,只是從相簿選照片跟啟動拍照功能,是以兩個不同的 TAction 來完成的。雖說是不同的 TAction 處理不同的程序,但取得照片的時候,回傳給程式的結果都是用TBitMap 來儲存相片。

換句話說,取得了 TBitMap 之後,我們想要對TBitMap做任何影像處理、格式處理,都可以由我們的程式碼來操作。

從 Delphi XE6 之後,這些直接使用行動裝置功能的程式碼,都已經被載入到TAction裡面了,我們只需要在使用上了解如何把TAction跟按鈕事件綁在一起即可。

首先,我們需要在 Form 上面放上一個 TActionList 元件,然後雙擊這個元件,就可以顯示出 ActionList 的編輯畫面:
在這畫面中,我們要新增Media Library 的兩個 Standard Action,就是上圖的TakePhotoFromLibraryAction (從相簿取得照片),以及TakePhotoFromCameraAction (從相機拍攝照片)。


建立兩個 Action 之後,我們就需要為這兩個 Action 設定 onDidFinish 跟 onDidCancel 的事件,前者是用來處理確定拍攝/挑選照片的事件,後者則是用來處理取消拍攝/挑選照片的事件。

在這個範例中,onDidFinish 的時候,會把回傳來的圖片資料直接放在畫面上的 TImage 元件顯示,所以它的程式碼很單純:

procedure TForm2.TakePhotoFromCameraAction1DidFinishTaking(Image: TBitmap);
begin
   self.Image1.Bitmap.Assign(Image);
end;

procedure TForm2.TakePhotoFromLibraryAction1DidFinishTaking(Image: TBitmap);
begin
   self.Image1.Bitmap.Assign(Image);
end;

就是直接把 Image Assign 給 Image1.Bitmap 屬性即可。

最後,點選按鈕上傳,完整的程式碼如下:

procedure TForm2.btnUploadClick(Sender: TObject);
var
   newItem: TRESTRequestParameter;
   allPass: boolean;
   imgPath, houseNum, floorIdx: string;
   nameStr: String;
   obj: TJSONObject;
begin
   allPass := True;

   if self.EditName.Text = '' then begin
      allPass := False;
      ShowMessage('請填寫住戶名字');
   end
   else if (Length(self.EditCardNo.Text) < 5) or
       (Length(self.EditCardNo1.Text) < 5) then begin
      allPass := False;
      ShowMessage('請填寫悠遊卡卡號10碼');
   end
   else if self.Image1.Bitmap.IsEmpty then begin
      allPass := False;
      ShowMessage('請提供悠遊卡感應卡號照片');
   end;

   if allPass then begin
      self.RectWaiting.Visible := True;
      self.AniIndicator1.Enabled := True;

      imgPath := System.IOUtils.TPath.Combine
          (System.IOUtils.TPath.GetDocumentsPath, 'tmp123.png');
      self.Image1.Bitmap.SaveToFile(imgPath);

      self.RESTClient1.BaseURL :=
          'http://testURL/acceptNewCard.php';

      self.RESTRequest1.Params.Clear;
      RESTRequest1.Method := rmPOST;

      nameStr := self.EditName.Text;

      self.RESTRequest1.AddParameter('cardNum', self.EditCardNo.Text + ':' +
          self.EditCardNo1.Text);
      nameStr := TIdURI.ParamsEncode(nameStr, IndyTextEncoding_UTF8);
      self.RESTRequest1.AddParameter('name', nameStr,
          TRESTRequestParameterKind.pkGETorPOST,
          [TRESTRequestParameterOption.poDoNotEncode]);
      // self.RESTRequest1.AddParameter('name', nameStr, TRESTRequestParameterKind.pkGETorPOST, [TRESTRequestParameterOption.poDoNotEncode]);

      case self.ComboBoxHouseNum.ItemIndex of
         0: begin
               houseNum := '1';
            end;
         1: begin
               houseNum := '374';
            end;
      end;

      houseNum := TIdURI.ParamsEncode(houseNum, IndyTextEncoding_UTF8);
      self.RESTRequest1.AddParameter('houseNum', houseNum);

      floorIdx := IntToStr(self.ComboBoxFloor.ItemIndex + 1);
      self.RESTRequest1.AddParameter('floorIdx', floorIdx);
      self.RESTRequest1.AddFile('picFilename', imgPath);
      self.RESTRequest1.Execute;

      obj := TJSONObject.ParseJSONValue(self.RESTRequest1.Response.JSONText)
          as TJSONObject;
      ShowMessage(obj.GetValue<String>('result'));

      self.RectWaiting.Visible := False;
      self.AniIndicator1.Enabled := False;
   end;
end;

上面的程式碼裡面,處理檔案的重點只有一行:
self.RESTRequest1.AddFile('picFilename', imgPath);

在更前面一點的程式碼中,imgPath 是由TImage.Bitmap即時存下來的照片,這個範例只適用於 Delphi 10.2 (Tokyo) 以後的版本,因為在 Berlin 當中,對於檔案的 MIME Type 還沒有完全支援,所以在 Berlin 或 Seattle 當中要使用這個方法上傳檔案的朋友,請升級吧,我也沒辦法直接提供 TRESTClient 在新版的程式碼給大家。


2017年6月9日 星期五

使用 TRESTClient 與 TRESTRequest 作為 HTTP Client 之一 (POST 字串參數)

在 Delphi XE 推出以前的年代,Delphi的發展方向是筆直朝向資料庫連結Windows 應用程式這個目標不斷前進的,從Delphi 1開始,到Delphi 7,Delphi奠定了VB Killer的外號,主要依靠的就是與資料庫的連接功能超越其他開發工具,而且超越的距離不只一個世代。

在 .NET開始發展,Delphi 8, Delphi .NET 不斷延遲的時候,與資料庫連接功能的方便性,仍然讓許多ERP廠商、軟硬體廠商持續愛用 Delphi.

直到 Web 開發與 App 開發超越了 Windows 應用程式的需求,VC, VB, Delphi 也開始隨著這波潮流,漸漸不再像 1990年代那麼廣受愛戴了。

在1990到 2010年之間,Delphi的網路連線功能,主要是藉由第三方元件來提供的,其中知名度最高,全球使用人數也最多的,應該就是 Indy 這套元件了。

這套元件在 2000 年前,叫做 WinShose,從第八版之後才改名為Indy,全球投入這套元件開發的開發人員,前後超過40人,從最基礎的 TCP/IP 功能到各種協定的Client與Server 端元件,筆者從中得益非常多,也開發了當中的DNS Server元件,對通訊協定的深入了解,Indy團隊可說是我不可或缺的師長。

隨著Delphi 工具走入了多平台開發的領域,Indy的侷限性也在這兩三年凸顯了出來,主要是在各個作業系統上面對於SSL與加密功能的支援無法緊密結合到作業系統內建的功能所致。

由於這個局限性,Delphi XE6開始,REST Client系列元件漸漸開始成為 Delphi 團隊的重點開發項目之一,所以我們從 Delphi XE6, Delphi XE7 之後的版本,可以發現到,使用 TRESTClient, TRESTRequest, TRESTResponse 系列組合的應用程式越來越多了,原廠也不斷鼓勵大家使用這套元件來提供 REST API 的連線功能。

REST API 的基礎是 HTTP 協定,大多以 HTTP 的 POST 方法把 JSON 編碼形式的參數傳遞到 Server,而 Server 再以 JSON 形式的參數回傳。

有時作法也會稍有變化,例如以 POST 方法把 Web-Form 編碼形式的參數傳遞給 Server,Server 再以 JSON 形式把資料回傳。

形式不一而足,但相同的是 HTTP 協定,最常用的也是以 POST 方法把參數傳給 Server 端。

今天要跟大家分享的主題,則是如何『使用 TRESTClient 與 TRESTRequest 作為 HTTP Client』。

前面已經提到過,在沒有 TRESTClient 整組元件以前,我們通常用的是 Indy 系列的元件來提供網路傳輸的功能,而現在有了 TRESTClient 整組元件,我們在行動平台上面就可以不需要另外配置函式庫,也能夠直接使用 https 與 server 連線了,在勒索病毒氾濫的今天,使用 https 會讓使用者比較安心。

POST作業說明

在 HTTP 的 POST 作業當中,參數跟 GET 作業一樣,Client端需要以 name=value&name2=value2 這種形式進行字串連接,再傳送到 Server 端去。Get 跟 POST的差異,在於 Get 方法是把所有參數當做 URL 的一部分,發送 HTTP GET 指令的時候,參數連同 URL  一起傳送。

而 POST 作業則是發送完 POST 指令後,把所有的參數與資料隨之傳送。依照 HTTP 型定的規範,GET 作業的 URL 是無法加密的,而且長度也有限制。因此,當需要傳遞的資料比較多,或者有機敏性,透過 HTTPS 傳送,就是最直接,也最方便,更是目前最通用的資料保護方法。

透過 POST 傳遞的參數,除了字串以外,還常常包含了檔案傳遞。我們很常看到在網頁上面以按鈕提供使用者選擇要上傳的檔案,也常看見提供以拖拉的方式把檔案上傳到遠端系統,尤其網頁郵件系統最常見到這種作法。

過去以 TIdHTTP 元件的 POST 方法發送參數時,呼叫方式如下:
var
    httpClient : TIdHTTP;
    url, params, httpResultStr : string;
begin
    url := 'http://mytestURL.com/test.php';
    params := 'name=我的名字&test=測試';

    httpClient := TIdHTTP.Create(self);
    try
         httpResultStr := httpClient.Post(url, params);
         showMessage(httpResultStr);
    finally
        httpClient.Free;
    end;
end;
  
這樣就可以把 params 字串的眾參數傳到 server 去了。理論上是這樣沒錯,但事情並沒有這麼簡單,在 HTTP 協定當中要傳參數給 Server,如果這些字串包含了特殊字元,則必須要先經過編碼,而編碼,是我們一生都需要與之對抗的繁複程序。

在 HTTP GET 方法當中,所有的參數除了要以 name=value 對每一個參數做描述,以及需要用 & 來連接各組參數,所有的 value 都需要以 url encode 來擺脫 URL 保留字元的糾纏。name 是否需要編碼呢?筆者建議,name 就乖乖的用英文吧,可以省下很多問題,以及處理這些可避免的問題所需要的時間!

那麼同樣的功能,以 TRESTClient 跟 TRESTRequest 要怎麼達成呢? 也很容易,作法如下:
1. 在 form 裡面放上 TRESTClient 跟 TRESTRequest 元件各一。
2. 把要傳遞的參數加到 TRESTRequest 實體的 params 屬性裡面去,這個屬性的型別是 TArray,所以可以存放多組參數。
3. 設定 TRESTClient 要傳送參數的URL,注意,URL 是設定在TRESTClient 哦!
4. 設定 TRESTRequest 要使用的傳輸方法,要設定為POST(因為我們正在介紹的是POST方法,請按照您的需求調整)
5. 呼叫 TRESTRequest 實體的 execute 方法,就可以把資料送去 server 了。

寫成 Delphi 的程式碼,會像以下這樣:
self.RESTClient1.BaseURL :=
          'http://我的網址/acceptNewCard.php';
self.RESTRequest1.Params.Clear;
self.RESTRequest1.Method := rmPOST;

self.RESTRequest1.AddParameter('test', self.EditCardNo.Text);
self.RESTRequest1.AddParameter('name', self.EditName.Text);

是不是很容易呢?的確很容易,裡頭的問題我們等下再深入探討,先來看 server 端要怎麼接收這些個參數,我們用 PHP 當範例,需要用 C#,JSP的讀者朋友們請自行轉譯喔⋯⋯

PHP Server 端接收 POST 參數的方法
從 1994 年開始,筆者就陸續撰文說明 HTTP POST 方法如何接參數,包含了CGI 用C,perl等語言實作,也包含 ISAPI 以 Delphi 實作,近幾年比較流行的是 PHP,JSP,C#,但 PHP 程式碼讀起來比較簡潔易懂,所以我就選擇 PHP 來做範例了。

在 PHP 裡面,透過GET 跟 POST 方法傳遞的參數,會被分別存放在 $_GET 跟 $_POST 這兩個陣列變數裡面,如果要偷懶,不想區分 GET 或 POST 方法,也可以從 $_REQUEST 這個變數試著讀取,當中有些安全性考量,最好勤勞一點,把它們區分開來。

以剛剛的例子來看,我們傳了一個名為 name,以及一個名為 test 的字串,用的是 POST 方法,所以我們得用以下兩個變數來存取這兩個字串:
  • $_POST['name']  這個變數可以取得 Client 端發送出來的 name  
  • $_POST['test']  這個變數可以取得 Client 端發送出來的 test
所以在 server 端,我們可以這樣寫,來抓到這兩個資料:
$name = $_POST["name"];
$test = $_POST["test"];

這樣寫會不會出問題呢? 答案是不會!如果使用者不輸入中文的話!

中文資料的編碼處理 

Delphi的開發人員絕大多數都是英美語系的人,我推測因此對於亞洲語系的文字顯示與傳輸比較沒有辦法完整的測試,但對於我們以中文為母語的人來說,從電腦誕生的那個年代,中文的顯示在每個作業系統、每種通訊協定的設計都比英文協定來的困難。

以上面的例子來看,如果我們直接拿這個例子來測試,筆者寫的範例程式,執行傳輸資料時,Server 所抓到的文字並不是正確的中文字,如下圖所示:

可以看得出來,傳到 server 的時候,server 是讀不到資訊的。這是怎麼回事呢?筆者屬於不認輸的好奇寶寶,使出了渾身解數,終於解決了這個問題。

寫過 Web 程式的讀者們一定可以立刻推測出來,這絕對是文字編碼出問題了,然而,是什麼地方出問題?可能出問題的點我列出來跟大家分享:
  • HTTP Client 的 charset 設定錯了
  • HTTP Request 裡面的文字編碼出問題
檢查的方向也是從這兩個關鍵點出發,第一點的檢測很容易,從Object Inspector檢查一下 RESTRequest1的設定:

AcceptCharset 確定是 UTF-8,沒錯,所以設定不是問題。

接著,就要從 Client 端發出去的資料下手了。有讀者或許會問『你怎麼不懷疑Server端程式寫錯了?』這個問題很好,之所以排除了這個問題,是因為同一個 Server 端的 PHP 程式,我用了 Postman 做過比對測試,回傳的結果是正確的,因此判定是 Client 端程式的問題。

接著筆者從 TRESTRequest.AddParameter 的各種多載形式來嘗試,AddParameter 這個方法有以下幾種多載的形式:
  • procedure AddParameter(const AName, AValue: string); overload;
  • procedure AddParameter(const AName: string; AJsonObject: TJSONObject; AFreeJson: boolean = true); overload;
  • procedure AddParameter(const AName, AValue: string; const AKind: TRESTRequestParameterKind); overload;
三種形式我都測試過,從 AddParameter 的執行中 trace 進去看各個可能性,由於 TRESTRequest 的參數中,Get 跟 Post 的加入方法是混用的,在程式碼裡面編碼又會有點不同。

在 REST.Client.pas 裡面,我曾經懷疑過編碼錯誤,所以也在執行階段對各個變數都進行觀察,最後,找到了原因與解法,至於過程,就不多說了,花了我兩天咧。

原因:編碼錯誤

用HTTP傳遞中文的時候,務必用UTF-8編碼,但一定要記得,中文字在作業系統中,都是UCS32編碼,這個現象在Windows裡面如此,在Android裡面如此,在iOS跟Mac我不確定,但處理方法是一樣的。

直接以 AddParameter('name', '中文測試'); 把參數加進 TRESTRequest 的時候,REST.Client.pas 的程式碼是把 '中文測試' 這個字串直接抓 Ord 的資料來做編碼的,然而,這個作法,是錯的!!!!!!!!

在 HTTP 傳遞 UTF-8 資料的時候,我們要傳遞的是 UTF-8 文字的二進位資料,但直接把 '中文測試' 這個字串直接拿來轉成二進位? 當時編碼並不是 UTF-8 啊,當然怎麼編碼送到 server 都是錯的!!!!

解法:AddParameter之前先做 UTF-8 轉換

這個解法,筆者第一天就已經想到,只是很想像以前改 Indy 程式一樣,直接改好 REST.Client.pas 之後,回饋給原廠使用,所以花了不少時間找方法,最後發現這個方法不用動到 REST.Client.pas,又能正確處理,就直接這麼跟大家分享了,寫成 Delphi 程式碼如下:
var
     nameStr : String;
begin
   ...
   nameStr := TIdURI.ParamsEncode(nameStr, IndyTextEncoding_UTF8);
   self.RESTRequest1.AddParameter('name', nameStr,     
             TRESTRequestParameterKind.pkGETorPOST,    
             [TRESTRequestParameterOption.poDoNotEncode]);
   ...
end;
在把字串透過 AddParameter 加入參數陣列之前,我先把字串做個 UTF-8 轉換,在這裡用的是 TIdURI 的類別方法 ParamsEncode,這個方法只有兩個參數,第一個參數是字串內容,第二個參數則是要文字編碼的種類,在這裡我選擇了 UTF8,寫法就是上面範例程式的第一行。

接著,在呼叫 AddParameter 的時候,我使用了多載形式當中的第三種,要求 AddParameter 處理資料的時候不要再動我的編碼,因為我已經處理好了。

這麼修改過之後,在各個作業系統當中,執行結果都是正確的,上面兩個圖以 Windows 作業系統為例,現在我們拿 Android 截圖來做為例子:


讀者可以看到右邊的截圖裡面,server 回傳的資料已經是正確的中文字了。

最後,我把 PHP 程式碼也附上來給大家參考:
<?php
date_default_timezone_set("Asia/Taipei");
header('Content-Type: charset=utf-8');

include("public/DBClassPDO.php");
$objDBPDO = new DBClassPDO();

$cardNum = $_POST["cardNum"];
$floorIdx = $_POST["floorIdx"];
$name = $_POST["name"];
$houseNum = $_POST["houseNum"];

$params = array();
$params['name'] = $name;
$params['cardno'] = $cardNum;
$params['houseNum'] = $houseNum;
$params['floorIdx'] = $floorIdx;
$params['picFilename'] = $fileSaveName;
$params['created'] = 0;

$result["resultCode"] = "0";
$result["result"] = "成功";
$result["sqlcmd"] = $name;

$jsonStr = json_encode($result);
echo $jsonStr;
?>
這是最基本的傳遞字串,下次再給大家示範怎麼傳檔案,透過 TRESTRequest來做也是很簡單的,TRESTRequest 跟 TRESTClient 的確是取代 TIdHTTP 的好工具。

2017年5月22日 星期一

在 Android 的 Edit 中, 如何按下 Enter 即隱藏虛擬鍵盤

在 Windows 的應用程式中,我們常常為了讓使用者能夠快速輸入,在Edit元件中的onKeyUp或者 onKeyDown 事件中主動偵測使用者輸入的字元是否有換行符號 (Enter),當使用者按下了Enter,程式碼就主動把游標 Focus 帶到下個欄位,但在行動裝置中,又多了一個課題:『如果是多個欄位,就帶到下個欄位。但如果是單一欄位,或是最後一個欄位,就隱藏虛擬鍵盤』

這個課題筆者在 2014九月的筆記『Virtual Keyboard 的顯示與隱藏』裡面已經有介紹過一次,只是當時是聚焦在iOS系統上,因為當時筆者開發的平台大都聚焦在iOS,對Android系統的關切並不多。

最近在一些專案中,也把觸角伸到了Android平台,但筆者仍堅持『不到最後關頭,絕不輕言JAVA』的原則,認識筆者的朋友們也都知道,筆者會寫,但非不得已,絕不寫JAVA的原則。

加上Delphi從XE6之後,對Android的支援也很深入了,筆者就更能堅定這個立場。

言歸正傳,在TEdit元件中要在使用者按下虛擬鍵盤的Enter鍵時,隱藏虛擬鍵盤的話,要怎麼做呢?

顯然用2014九月那個作法(把focus轉移到別的元件), 對Android平台沒用,不然筆者也不用多寫這篇筆記了。

是的,熟習Android的使用者都知道,按下Android的實體Back鍵,就能隱藏虛擬鍵盤。所以這個作法也很簡單,就是『在onKeyUp事件中,把Key 改成 137 即可』

onKeyUp 事件中,有兩個 call by Reference 的參數,一個是 Key, 另一個是 KeyChar, 都讓我們可以在處理完鍵盤事件後,重新賦予新的值給系統。在Delphi Seattle, Berlin, Tokyo (前面的版本我已經移除了,無從驗證),這三個比較新的版本中,都定義有 vkHardwareBack這個值,其數值就是 137。

只需在 onKeyUp 事件中把 137 指派給 Key 這個變數即可隱藏鍵盤了,但通常我們程式碼可能會用於多種平台上,所以我的範例程式碼會加上 {$IFDEF Android} 這樣的判別式,以利多平台的程式維護:

procedure TFormMain.Edit_changeDeviceNameKeyUp(Sender: TObject; var Key: Word;
    var KeyChar: Char; Shift: TShiftState);
begin
   if Key = vkReturn then begin
      self.btn1.SetFocus;

{$IFDEF ANDROID}
      Key := vkHardwareBack
{$ENDIF}
   end;
end;
這樣應該很容易理解,對嗎?

2017年5月19日 星期五

在 iOS 10 之後, 使用手機相機與相簿應注意事項

iOS 的 SDK 每一年至少都會有一次大改版,從 2009 到 2016 年,版號已經到了第 10 版了,很輕易的就追上了 Mac OSX. 每一次的大改版都會有不少新的功能或新的規範,在 iOS 7的改版算的上是幅度最大的,把 iOS 1.0 到 iOS 6.0 以實物視覺與立體視覺的基礎來了個髮夾彎,轉向去跟 Android, Windows 一起走平面化極簡風。(不知道賈柏斯天上有靈會怎麼反應......)
  • iOS 8的改版中,要求使用地理資訊的App需要在 info.plist 當中自己寫出提示的文字
  • iOS 10 的改版中,更進一步要求使用相機、相簿的App也要自己提示。
如果沒有在 info.plist 裡面定義提示文字,App執行到該功能的時候,會直接發生閃退,這對於使用者來說很不方便,所以App開發的人員也只好為了使用者對 App 進行改版。

如果是使用 Xcode 來開發,就需要編輯 info.plist, 加入以下圖片中兩個灰色背景的設定,一個是使用相機,另一個是使用相簿。



這跟是否使用第三方的 Framework 無關喔,即使您使用了 Cocos2D, 或是 CocoaPad, 這些宣告也是跑不掉的。

如果使用的是 Delphi,則需要從專案設定選項 (Project Option)來設定,設定視窗如下圖所示:


叫出這個視窗的方法有兩個,一個是從Delphi的視窗中點選 Project 選項,選擇裡面的 Option。另一個則是用滑鼠右鍵點選視窗右邊的專案名稱,選擇 Menu 裡面的 Option 選項,都可以顯示出這個視窗。

設定好這兩個選項之後,在 Delphi 裡面呼叫 TTakePhotoFromCameraAction 或
TTakePhotoFromLibraryAction的時候,就不會閃退了.

要留意一點,透過 Delphi 取用相機或相簿的時候,OnDidFinishTaking 所取得的照片是 TBitMap 元件,我們可以先對圖片做一些簡單的處理,例如可以把圖片的 解析度弄小一點,或者改變圖片的大小,甚至是儲存成不同格式 (PNG或JPG)。

對 iOS 模擬器處理時,由於模擬器通常沒有相機,所以不用特別設定模擬器的相機使用描述。Delphi 的設定會依照不同的裝置平台做個別的設定,而且還分成 Debug/Release,所以我自己通常會直接編輯 All Configuration。

從 iOS 5S 之後,也已經沒有 32 bit 的新裝置了,所以如果您的 App 不打算提供給 32 bit 裝置使用的話,iOS Device - 32 bit platform 也可以不用理他了。