2025-09-15

【軟體筆記】MobaXterm 取代 pietty

最近買了 ASUSTOR AS1102TL,看到 AppCentral 中可以安裝 python3,想嚐鮮,但又找不到【終端機】之類的方式能進到命令列文字模式。

想到 AS1102TL 是屬 Linux 系統,就試試看以 SSH 連進去。

老人家之前慣用的 pietty 在建立連線時出現了錯誤訊息


看似是 pietty 不支援新的加密協定,上 pietty 官網,最新的版本仍停留在 0.4.00,跟現在我手上的版本一樣。

於是問了 DeepSeek 推薦有什麼替代軟體,試用了覺得 MobaXterm 不錯,可以記錄多台主機連線,好像還能開多個頁籤,可惜沒有中文界面(或是我沒找到?因為 DeepSeek 說有...)

程式畫面左邊會列出它偵測到的同區網內可連線的裝置
→ 在欲連線的裝置按滑鼠右鍵
→ Edit session


→ 確認是圈選 SSH
→ 輸入【Remote host】、【Specify username
→ 點擊【OK】鈕
→ 提示要您輸入密碼
→ 第一次登入時,程式還會問您要不要儲存(記住)密碼,然後就登入了




















2025-06-28

【Android】Android15 停用無邊框方式顯示內容

參考資料 ----


AndroidAndroid10(Quince Tart, API29) 導入無邊框風格樣式,Android15(Vanilla Ice Cream, API35) 開始,新專案預設會是無邊框風格樣式,而自 Android16(BAKLAVA, API36) 開始 則會強制 APP 預設外觀為無邊框風格樣式。
(題外話...看來 Build.VERSION_CODES 依字母排列只到 API35 為止了)

如果您的 targetSdkVersion 設為 35,當 APP 安裝在 Android15 時就會強制啟用無邊框顯示,若 APP 還未準備好,調整為無邊框,則可以 windowOptOutEdgeToEdgeEnforcement 先緩緩。
 
<resources
    xmlns:tools="http://schemas.android.com/tools">

    <style
        name="Theme.MyTheme"
        parent="Theme.Material3.DayNight.NoActionBar">

        <!-- 關閉 Edge-to-Edge(沈浸式無邊框) 功能 -->
        <item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
		
...
...
</resources>
 


不過 Android16(Baklava, SDK36) 起,windowOptOutEdgeToEdgeEnforcement 就失效了,該面對的跑不掉,開始調整自己的 APP 吧。






2025-06-21

【Android】設定建構變化版本 -- 分開建立測試、正式版本 app

參考資料 ----


老人家的 app 有置入 AdMob 播放廣告,但 Google 要求 app 開發、測試期間,app 內的廣告要採用測試廣告,且抓得頗嚴,所以覺得蠻麻煩的。有時候,app 上架了才發現沒有換回正式廣告,就得再更正後再重上架...😒


偶然間,發現可以【設定建構變化版本】,這功能超好用的!

AdMob 的每種類型廣告都有個廣告 ID,在開發、測試期間要使用測試 IDdebug 時播放的廣告就會是測試廣告,要設定廣告,須在 /res/values/strings.xml 指定該類型廣告的 ID
 
<resources>
...
...

<!-- 上架前要 mark 起來 -->
<!-- adMob app id 測試 -->
<string name="admob_app">ca-app-pub-3940256099942544~3347511713</string>
<!-- 橫幅 測試 -->
<string name="admob_banner">ca-app-pub-3940256099942544/6300978111</string>
<!-- 插頁 測試 -->
<string name="admob_interstitial">ca-app-pub-3940256099942544/8691691433</string>
<!--  影片 測試 -->
<string name="admob_video">ca-app-pub-3940256099942544/5224354917</string>

<!-- 開發時期先 mark 起來,等要上架前再恢復 -->
<!-- adMob app id 正式 -->
<!-- <string name="admob_app">ca-app-pub-blablaXXXXXXXXXX~XXXXXXXXXX</string> -->
<!-- AdMob 橫幅 正式 -->
<!-- <string name="admob_banner">ca-app-pub-blablaXXXXXXXXXX/aaaaaaaaaa</string> -->
<!-- AdMob 插頁 正式 -->
<!-- <string name="admob_interstitial">ca-app-pub-blablaXXXXXXXXXX/bbbbbbbbbb</string> -->
<!-- AdMob 影片 正式 -->
<!-- <string name="admob_video">ca-app-pub-blablaXXXXXXXXXX/cccccccccc</string> -->

...
...
</resources>
 


現在,改在 app 層級build.gradle 設定
 
android {

...
...

    buildTypes {
        debug {
            // 開發、測試期
            applicationIdSuffix ".debug"    // debug 版本的 applicationId 會是 com.example.debug
            
            resValue 'string', 'app_name', '測試版 app'
            
            // adMob app id
            resValue "string", "admob_app_id", "ca-app-pub-3940256099942544~3347511713"
            // 橫幅 測試
            resValue "string", "banner_id", "ca-app-pub-3940256099942544/6300978111"
            // 插頁 測試
            resValue "string", "interstitial_id", "ca-app-pub-3940256099942544/8691691433"
        }

        release {
            // 正式
            resValue 'string', 'app_name', '正式版 app'
            
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

            // adMob app id
            resValue "string", "admob_app_id", "ca-app-pub-blablaXXXXXXXXXX~XXXXXXXXXX"
            // 橫幅 正式
            resValue "string", "banner_id", "ca-app-pub-blablaXXXXXXXXXX/aaaaaaaaaa"
            // 插頁 正式
            resValue "string", "interstitial_id", "ca-app-pub-blablaXXXXXXXXXX/bbbbbbbbbb"
        }
    }

...
...
}
 

不僅如此,還可以設定 測試版 跟 正式版 不同的 applicationId,這麼做,就可以在同一支開發手機同時安裝 測試版 跟 正式版 app

功能表選擇 Build → Select Build Variant,會看到可編譯的版本的選項。


productFlavorsBuildTypes 的進階設定,可以變化出更多的 Build Variant,如下圖:


另有一個常見的情況:
某個特定的 API 在某版本起被棄用,Google 強制規定要改用新的 API(Google 還蠻常做這事的...所以要配合 Google,大約每半年要檢視自己的 app 是否能在各 Android 版本上正常運行);例如:
app 原本在 API SDK30 前都可正常運行,但自 SDK31 起須改變程式寫法,在 build.gradle 設定 minSdkVersion 31,程式改好後,可將新版的 apk 取不同的檔名,上架到 PlayStore,如此,就分出新舊版本了,手機中的 PlayStore app 會自動判別,挑選最適合手機的版本 -- 手機版本在 SDK30 前的 user 會下載安裝舊版的 app,而手機版本在 SDK31 後的 user 則會下載新版的 app,不會因為 app 改版而放棄舊版本手機的 user



2025-06-08

【jQuery】jQuery + CSS 多個 checkbox 勾選

 
<html>
<head>
<title>checkbox lab</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script>
// chkAll 點擊事件
function checkAll() {
    var isChecked = $('#chkAll').prop('checked');
    // 更新所有 nameCheckbox 的狀態
    $('.nameCheckbox').prop('checked', isChecked).trigger('change');
}


// 更新 chkAll 狀態
function updateChkAll() {
    var totalCheckboxes = $('.nameCheckbox').length;
    var checkedCheckboxes = $('.nameCheckbox:checked').length;
    var $chkAll = $('#chkAll');
               
    if (checkedCheckboxes === 0) {
        // 全部未選中
        $chkAll.prop('checked', false);
        $chkAll.removeClass('partial');
    } else if (checkedCheckboxes === totalCheckboxes) {
        // 全部選中
        $chkAll.prop('checked', true);
        $chkAll.removeClass('partial');
    } else {
        // 部分選中
        $chkAll.prop('checked', false);
        // 關鍵修改:部分選中時保持 checked 但變灰色
        $chkAll.prop('checked', true).addClass('partial');
    }
}
</script>
<style type="text/css">
#chkAll {
    margin-bottom: 10px;
    font-size: 10px;
}
/* 部分選中時的灰色打勾 */
#chkAll.partial {
    accent-color: gray;  /* 現代瀏覽器 */
    filter: grayscale(50%) opacity(80%);  /* 舊瀏覽器備用 */
}
</style>
</head>
<body>
<center>
<form name="frmInput" id="frmInput" method="post" action="func.php">
<input type="hidden" name="act" value="input" />
<table border="1">
	<tr>
		<th nowrap="nowrap"><input type="checkbox" id="chkAll" onchange="checkAll()" />全選</th>
		<th nowrap="nowrap">姓名</th>
	</tr>
	<tr>
		<td><input type="checkbox" class="nameCheckbox" id="chk1" onchange="updateChkAll()" /></td>
		<td>張三</td>
	</tr>
	<tr>
		<td><input type="checkbox" class="nameCheckbox" id="chk2" onchange="updateChkAll()" /></td>
		<td>李四</td>
	</tr>
	<tr>
		<td><input type="checkbox" class="nameCheckbox" id="chk3" onchange="updateChkAll()" /></td>
		<td>王五</td>
	</tr>
</table>
</form>
</center>
</body>
</html>
 

2025-02-16

【PHP】以 Telegram bot 取代 LINE notify

* 先準備 2 支手機(在本筆記,2 支手機都是 Android),telegram 沒法建立一人群組(應該說我沒試成功)

* 2 支手機都安裝 telegram,並建立一個群組

* 在尋找聯絡人輸入 @botfather,找到後,將 botfather 加入聯絡人

* 進入 botfather聊天室,會看到 botfather 的招呼語【What can this bot do?

* 輸入【/start】,botfather 會列出一堆跟 bot 有關的指令

* 輸入【/newbot

botfather 會請您為您的 bot 取名字,因為後續的互動會是 json 格式,所以建議這次先以英文命名,較容易在 json 字串中辨識、找到。

* 接下來,botfather 請您設定您的 bot 的 id,bot 的 id 必須以 【bot】結尾,ex:TetrisBottetris_bot,這個 id 必須是在 telegram 唯一的,所以若您設定的名字已經有別人先用了,botfather 會請您改名字。

* 建立您的 bot 後,可輸入【/help】進一步了解

* 輸入【/mybots】,會得到您的 bottoken,格式是 【一串數字:大小寫英數字混合字串】很重要!請好好保存


* 將 bot 加入群組

→ 由群組中的任一人發送一個訊息到群組

→ 點擊標題欄,可以看到這個群組的成員,並且下方有個標題為【Links】的區塊,點擊第一個連結(最後面以 【/getUpdates】結尾),就會看到一段 json 字串,將這段 json 字串複製出來,並尋找其中

 
...
...

"chat":{"id":-0123456789,
        "title":"mytest",
        "type":"group",
        "all_members_are_administrators":false
       }

...
...
 
這個就是群組的 id,通常是負數typegroup
如果找不到群組 id,請多試幾次(群組某人發送訊息,然後看 json 字串),甚至將 bot 踢出群組再重加入。

寫個測試的 PHP 程式

 
<?php
date_default_timezone_set('Asia/Taipei');

$sToken = "0123456789:AAG9Sxwv3zc9YblablablaiBBJ3ZOUO4lFY";    // bot 的 token
$sGroupId = "-9876543210";	// 群組 id
 
// 訊息內容
$sMsg = "Hello, this is oldgrayduck's telegram Bot test, ".date('Y-m-d H:i:s');
 
$url = "https://api.telegram.org/bot$sToken/sendMessage";
$data = [
    'chat_id' => $sGroupId,
    'text' => $sMsg
];
 
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 
$result = curl_exec($ch);
curl_close($ch);

// 顯示回應
echo $result;
 
?>
 


群組就會收到訊息了。

2024-12-30

【LINE Messaging API】主動傳送訊息(推播) 給多位使用者

類似主動傳訊息給 user,不過在傳訊類型定義為 multicast。要傳訊的目標對象是多位 user,所以 UserId 要存在陣列中。


 
<php?
date_default_timezone_set('Asia/Taipei');
$sRootDir = $_SERVER['DOCUMENT_ROOT'];
$channelAccessToken = '您的 channel Access Token';
$bodyMsg = file_get_contents('php://input');

// LINE 不會幫我們記錄, 所以要自己寫 log
// 這段不是必要, 只是方便自己除錯
$sLogfile = 'log_'.date('Ymd').'.log';    // 指定 log 檔名, 一天一個檔, 檔名格式為: log_yyyymmdd.log
$fp = fopen($sLogfile, "a+");
fwrite($fp , print_r(date('Y-m-d H:i:s').', Recive: '.$bodyMsg."\n", true));
fclose($fp);

...
...
// 取得 user id, 管道有很多, 此處不贅述
$arrUserId = ['...','...', ...];    ← 要推播的目標 UserId 存入陣列

$payload = [ 'to' => $arrUserId,
             'messages' => [
                               ['type' => 'text',
                                'text' => '恭喜! 您中了特獎!',
                               ]
                              ]
           ];
SendMsg($payload);


// 傳送訊息給指定使用者
function SendMsg($payload) {
    $ch = curl_init();
    // curl_setopt($ch, CURLOPT_URL, 'https://api.line.me/v2/bot/message/push');
    curl_setopt($ch, CURLOPT_URL, 'https://api.line.me/v2/bot/message/multicast');    ← 注意這裡要改成 multicast
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [  'Content-Type: application/json',
                                            'Authorization: Bearer ' . $GLOBALS['channelAccessToken']
                                        ]);
    $result = curl_exec($ch);
    curl_close($ch);
    // 寫 log
    $sLogfile = 'log_'.date('Ymd').'.log';
    $fp = fopen($sLogfile, "a+");
    fwrite($fp , print_r(date('Y-m-d H:i:s').', send message result: '.$result."\n", true));
    fclose($fp);
}
?>
 



相關筆記 ----


2024-08-28

【Kotlin】隨機取出指定筆數的記錄

題目:

有一含 1000 筆記錄的資料表 table1,要從中取出任意 100

在詢問 Google 的 Gemini、OpenAI 的 ChatGPT、Perplexity 的 Perplexity、微軟的 Copilot,多認識了一些類別及技巧,故做此筆記。


做法一:

 
val mScope = CoroutineScope(Job() + Dispatchers.IO)
mScope.launch {
    val mDbHelper = SQLite(baseContext)
    var db = mDbHelper.readableDatabase

    // 查詢本機資料庫有幾筆記錄
    var sSql = "SELECT * FROM table1 "
    val rs = db.rawQuery(sSql, null)
    val mRecnt = rs.count    // 本機資料庫記錄筆數
    var mSentence = 100   // 迴圈數, 要取出 100 筆記錄

    val mList = mutableListOf<Int>()
    var xx = 1
    // 這種做法, 有可能亂數取得的 mPos 已存在 mList 中
    while(xx<=mSentence) {
        val mPos = (0 until mRecnt).random()    // 每筆記錄在 table1 的位置
        if(mPos !in mList) {
            mList.add(mPos)
            xx++
        }
    }
    mList.sorted()    // 將 mList 內的元素由小到大排序
    // 如此, 在實際自 table1 取出記錄就會是自資料表首一路向資料表尾依序取出記錄
    
    ...
    ...
}
 



做法二:

 
val mScope = CoroutineScope(Job() + Dispatchers.IO)
mScope.launch {
    ...
    ...

    val mRaw = mutableSetOf<Int>()    // mutableSet 的特性是存入時就會檢查每個元素的值都是唯一
    val mRandom = Random(System.currentTimeMillis())
    while(mRaw.size<mSentence) {
        mRaw.add(mRandom.nextInt(mRecnt))
    }
    // 但 mutableSet 不具排序功能, 
    // 故將它轉型態成 list 並排序後存入  mList
    val mList = mRaw.toList().sorted()
    
    ...
    ...
}
 



做法三:

 
val mScope = CoroutineScope(Job() + Dispatchers.IO)
mScope.launch {
    ...
    ...

    // 這個做法最快, 一行指令搞定
    // 以 shuffled() 洗牌後, 再以 take() 取前面幾個元素, 再將元素排序
    // (0 until mRecnt) 是 IntRange
    val mList = (0 until mRecnt).shuffled().take(mSentence).sorted()
    // val mList = (0 until mRecnt).shuffled(Random(System.currentTimeMillis())).take(mSentence).sorted()   // 可以在 shuffled() 內加上時間當做亂數種子, 增強其隨機性
    
    ...
    ...
}
 



2024-07-13

【網頁程式】土砲打造 -- 依數個地點距離由近至遠規劃行程導航

參考資料 ----


當我們要前往數個地點,想依距離由近至遠排序,規劃開車行程,可以請 Google Cloud 服務,不過這是要收費的。

行程含目的地的停靠點在 10 個點以下,是一種費率,11(含)個點以上則會收較高的費用。

本筆記是自行計算及排序停靠點(注意,還是要收費的)。


您可以服務全開,不過我只使用其中 2 項 -- 取得地理位置(經緯度)導航







建立 GMap 類別
 
<?php 
date_default_timezone_set('Asia/Taipei');
  
// 用來查詢地址的經、緯度
class GMap {
    function getPageData($url) { 
        $ch = curl_init(); 
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);  
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect: '));
        curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_NOBODY, false);
        curl_setopt($ch, CURLOPT_FILETIME, true);
        curl_setopt($ch, CURLOPT_REFERER, $url); 
        curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 4);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)");
        $result['data'] = curl_exec($ch);
        curl_close($ch);
          
        return $result;
    }
    
    // 初始化 apikey 金鑰 
    function __construct() {
        $this->apikey = '向 Google map 申請 apikey';
    } 


    // 從 google map 取得地址經緯度
    public function getLngLat($addr='',$sDebug='') {
        $apikey = $this->apikey;
        $url = "https://maps.googleapis.com/maps/api/geocode/json?address=$addr&key=$apikey";
        $geocode = $this->getPageData($url);
        if($sDebug=='debug')
            var_dump($geocode);

        if(isset($geocode['data'])) {
            if(!$geocode["data"])
                // 當 Google map 解析不了時,回應 經/緯度值都是 -1 的虛擬經緯度
                $geocode = '{"results":[{"geometry":{"location":{"lat":-1,"lng":-1}}}]}';
            else
                $geocode = $geocode['data'];
        } else
            // 當 Google map 解析不了時,回應 經/緯度值都是 -1 的虛擬經緯度
            $geocode = '{"results":[{"geometry":{"location":{"lat":-1,"lng":-1}}}]}';

        $output = json_decode($geocode); 
        $latitude = $output->results[0]->geometry->location->lat;
        $longitude = $output->results[0]->geometry->location->lng; 
     
        return array('lat'=>$latitude,'lng'=>$longitude);
    }
}
?>
 


getroute.php,用來依次計算起點至每個目的地的距離,取最近的地址做為下個停靠點,並再以這地址做為下次計算的起點
 
<?php
// 切換至 https
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off") 
{
    $location = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
    header('HTTP/1.1 301 Moved Permanently');
    header('Location: ' . $location);
    exit;
}

include('gmap.php');

class MyDest {
    public $addr;
    public $len = 0;
    public $lng = -1;
    public $lat = -1;
  
    function __construct($aa) {
        $this->addr = $aa;
    }
}


if(isset($_GET['lat'])) {
    // 接收到起點 經/緯 度
    // $sLat = 22.612058525444247;    // 測試用
    // $sLng = 120.3142669919977;   // 測試用
    $sLat = $_GET['lat'];
    $sLng = $_GET['lng'];
    

    $aDest = array();    // 存放上傳的目的地
    $aSort = array();    // 排序後的目的地

    $gmap = new GMap();
    
    if(isset($_GET['dest1'])) {
        $obj = new MyDest($_GET['dest1']);
        $fLngLat = $gmap->getLngLat($obj->addr);    // 上車地點經緯度
        $obj->lat = $fLngLat["lat"];    // 緯度
        $obj->lng = $fLngLat["lng"];    // 經度
        array_push($aDest, $obj);
    }
    if(isset($_GET['dest2'])) {
        $obj = new MyDest($_GET['dest2']);
        $fLngLat = $gmap->getLngLat($obj->addr);    // 上車地點經緯度
        $obj->lat = $fLngLat["lat"];    // 緯度
        $obj->lng = $fLngLat["lng"];    // 經度
        array_push($aDest, $obj);
    }
    if(isset($_GET['dest3'])) {
        $obj = new MyDest($_GET['dest3']);
        $fLngLat = $gmap->getLngLat($obj->addr);    // 上車地點經緯度
        $obj->lat = $fLngLat["lat"];    // 緯度
        $obj->lng = $fLngLat["lng"];    // 經度
        array_push($aDest, $obj);
    }
    if(isset($_GET['dest4'])) {
        $obj = new MyDest($_GET['dest4']);
        $fLngLat = $gmap->getLngLat($obj->addr);    // 上車地點經緯度
        $obj->lat = $fLngLat["lat"];    // 緯度
        $obj->lng = $fLngLat["lng"];    // 經度
        array_push($aDest, $obj);
    }
    if(isset($_GET['dest5'])) {
        $obj = new MyDest($_GET['dest5']);
        $fLngLat = $gmap->getLngLat($obj->addr);    // 上車地點經緯度
        $obj->lat = $fLngLat["lat"];    // 緯度
        $obj->lng = $fLngLat["lng"];    // 經度
        array_push($aDest, $obj);
    }

    while(count($aDest)>0) {
        foreach($aDest as &$val) {    // 注意:$val 前面有 &
            // 計算【各目的地】距離【起點】的長度
            $url = "https://maps.googleapis.com/maps/api/directions/json?origin=$sLat,$sLng&destination=".urlencode($val->addr)."&key=$apiKey";
            
            // 目的地 destination 參數也可以是經緯度
            $url = "https://maps.googleapis.com/maps/api/directions/json?origin=$sLat,$sLng&destination=".urlencode($val->lat).",".urlencode($val->lng)."&key=$apiKey";
            
            // 若希望路線避開上高速公路, 則可加 avoid=highways 參數
            $url = "https://maps.googleapis.com/maps/api/directions/json?origin=$sLat,$sLng&destination=".urlencode($val->addr)."&avoid=highways&key=$apiKey";
            // 此外, 還可避開 ----
            // tolls:避開收費路段
            // highways:避開高速公路
            // ferries:避開渡輪
            // indoor:避開室內路線(主要用於步行路線)
            
            // 初始化 cURL
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            $response = curl_exec($ch);
            curl_close($ch);
    
            // 解析 json 響應
            $data = json_decode($response, true);
            if ($data['status']=='OK') {
                $val->len = $data['routes'][0]['legs'][0]['distance']['value'];
            } else {
                $val->len = -1;
            }
            unset($val);
        }

        // 使用 usort 函數排序陣列,根據 $a 屬性值從小到大排序
        usort($aDest, function($a, $b) {
            return $a->len <=> $b->len;
        });
        // 排序後的第一個物件就是 距離最短的地址
        $minObj = $aDest[0];
        // print_r($aDest);
        array_push($aSort, ['addr' => ($minObj->addr)]);
        
        // 這個地址做為起點
        $sLat = $minObj->lat;
        $sLng = $minObj->lng;
        array_splice($aDest, 0, 1);
    }
    echo json_encode($aSort);   // 回傳依距離排序後的停靠點及目的地
}

?>
 


navi.php,將排好的順序丟給 GoogleMap 導航。
 
<?php
// 切換至 https
if(empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off") 
{
    $location = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
    header('HTTP/1.1 301 Moved Permanently');
    header('Location: ' . $location);
    exit;
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html style="height:100%;">
<head>
    <meta charset="utf-8">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<br />
<button onclick="startNavigation()" style="font-size:40pt;">巡航</button><br />
<span  style="font-size:40pt;">提醒:在 Android Chrome 開啟本網頁</span><br />
<br />
<br />
<script>
function startNavigation() {
    var aRoute = [
        "高雄市苓雅區四維三路2號",
        "高雄市前鎮區中華五路789號",
        "高雄市鼓山區濱海二路1號",
        "高雄市左營區博愛二路777號",
        "高雄市左營區高鐵路107號"
    ];

    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(function(position) {
            var latitude = position.coords.latitude;    // 緯度
            var longitude = position.coords.longitude;  // 經度
            var str = "https://您自己的網址/getroute.php?lat="+latitude+"&lng="+longitude+"&dest1="+aRoute[0]+"&dest2="+aRoute[1]+"&dest3="+aRoute[2]+"&dest4="+aRoute[3]+"&dest5="+aRoute[4];
            // alert('str = '+str);
            // return;

            var destinations = new Array();
            $.getJSON(str, function(res) {
                // alert("res = "+res);
                // 這段是從 client 端向 server 請求查詢
                // server 回傳 json 型態的陣列
                // 陣列內的元素是 addr
                res.forEach(function(element) {
                    destinations.push(element.addr);
                });
                var waypoints = destinations.map(function(destination) {
                    // Google map 規定停靠點以【|】字元分隔
                    // 將 5 個地點以【|】字元分隔
                    return encodeURIComponent(destination);
                }).join("|");
                destination = waypoints.split('|').pop();   // 取得最後【|】右邊的字串做為目的地
                waypoints = waypoints.slice(0, waypoints.lastIndexOf('|')); // 取得最後【|】的左邊字串做為停靠點

                // 構建導航 URL
                var mapsURL = "https://www.google.com/maps/dir/?api=1&origin=" + latitude + "," + longitude + "&destination=" + destination + "&waypoints=" + waypoints + "&travelmode=driving";
                // alert(mapsURL);
                // return;

                // 跳轉到 Google Maps
                window.location.href = mapsURL;
            });
            // return;
            

            
        }, function(error) {
                alert("無法取得地理位置: " + error.message);
            }
        );
    } else {
        alert("瀏覽器不支援地理定位功能。");
    }
}
</script>
<br />
<br />
</body>
</html>

 


排序後,程式呼叫 Android 手機上的 Google Map app 進行導航。

第一次執行這個網頁程式時,會詢問是否同意瀏覽器存取你的地理位置,選擇同意且記住您的選擇,之後執行就不會再跳出詢問視窗了。

測試 Google Chrome、Firefox、Microsoft Edge 這幾個主流瀏覽器
Chrome 效果最佳,直接啟動 Google Map,畢竟都是自家的產品;
Edge 雖可呼叫 Google Map,但詢問視窗一直開著,不會自動關閉
Firefox 最差,啟動了 Google Map,但要導航的地址全不見了!!



另一個有趣的發現:
如果只單純導航前往一個地點,Google Map 並不管目的地是否與您的行車方向順向,只要抵達目的地附近,Google Map 就認為任務完成了。ex:出發點是高雄市中山大學,目的地是高雄女中(雄女),雄女的對面是國軍英雄館,Google 導航只要帶您到國軍英雄館,導航就結束了。
但當設了多個停靠點時,Google Map 就會使命必達,規劃的路線即使繞一圈也會帶您順向抵達雄女。

然後...老人家又好奇了...這個網頁程式在 iPhone 上打開會是什麼結果?
答案是:在 Safari 直接 Deny!! 不給開

2025


相關筆記 ----






2024-07-10

【MySQL】國定假日應用

參考資料 ----


自 政府資料開放平台 下載下來的檔案格式為 csv,內容依序為【西元日期】(格式為 yyyymmdd)、【星期】(國字)、【是否放假】(0=上班, 2=放假)、【備註】 4 個欄位,以逗點【,】分隔。自 01/01 ~ 12/31 一天一列。

比照這個 csv,在資料庫(我採用 MySQL) 中建一個 table,不過我的 table 有多一個【年度】欄位,並將【是否放假】欄位改名為【休假

我需要的只有放假日,也就是【休假】欄位值 = 2,因此先透過 phpMyAdmin 存入資料庫,再將【平日】刪除。

 
-- csv 最前面補加 西元年度欄位

-- 將 csv 上傳存入 holiday_tmp
-- holiday 和 holiday_tmp 差別只在 holiday 有 autoincrement 的 primary key

-- 刪除平日, 只留假日
DELETE FROM holiday_tmp
    WHERE 休假=0

-- 存入 holiday
INSERT INTO holiday
(年度,日期,星期,休假,備註)
SELECT * FROM holiday_tmp

-- 清空 holiday_tmp
DELETE FROM holiday_tmp
 


2024-06-13

【SQLite】以預設值新增一筆記錄

當 CREATE TABLE 的欄位定義有預設值,或無預設值(NULL) 且未限制 NOT NULL 時,可使用下面指令。

 
INSERT  INTO  資料表名  DEFAULT  VALUES;