C++ で最寄駅を検索

(1/1)
>C++で最寄駅を検索
インターネット経由で HeartRails Express API - 最寄駅情報取得 API にアクセスし、地図や住所、ランドマーク、郵便番号等から最寄駅を検索し、距離順に一覧表示する。一覧情報をクリップボードにコピーしたり、CSV形式ファイルに保存することができる。また、ユーザーがAPIキーを取得することでGoogleマップやGoogle住所検索も利用できるようになる。
PHPで住所・ランドマークから最寄駅を求める」で作ったPHPプログラムをC++に移植したものである。

(2024年3月30日)使用ライラリ更新
(2023年11月23日)使用ライラリ更新
(2023年7月8日)検索に国土地理院ジオコーディングAPIを追加,使用ライラリ更新
(2023年4月1日)使用ライラリ更新

目次

サンプル・プログラム

圧縮ファイルの内容
stationsearchwin.msiインストーラ
bin/stationsearchwin.exe実行プログラム本体
bin/cwebpage.dll
bin/libcurl.dll
実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/stationsearchwin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/application.icoアプリケーション・アイコン
sour/mystrings.cpp汎用文字列処理関数など(ソース)
sour/mystrings.h汎用文字列処理関数など(ヘッダ)
sour/pahooGeocode.cpp住所・緯度・経度に関わるクラス(ソース)
sour/pahooGeocode.hpp住所・緯度・経度に関わるクラス(ヘッダ)
sour/apikey.cppAPIキーの管理(ソース)
sour/apikey.hppAPIキーの管理(ヘッダ)
sour/webbrowser.hppWebブラウザ・クラス(ヘッダ)
sour/makefileビルド
stationsearchwin.cpp 更新履歴
バージョン 更新日 内容
1.1.4 2024/03/30 使用ライラリ更新
1.1.3 2023/11/23 使用ライラリ更新
1.1.2 2023/07/08 使用ライラリ更新
1.1.1 2023/04/01 使用ライラリ更新
1.1.0 2022/09/23 UserAgent追加,ウィンドウ位置保存,ライブラリ更新
pahooGeocode.cpp 更新履歴
バージョン 更新日 内容
1.6.0 2023/07/02 getPointsGSI()メソッド追加
1.5 2022/09/03 デバッグコード埋め込み
1.4 2021/05/01 makeMapLeaflet() 引数追加
1.3 2021/03/07 tokyo_wgs84(), wgs84_tokyo()メソッド追加
1.2 2020/12/20 leafletスクリプトの参照URL変更
mystrings.cpp 更新履歴
バージョン 更新日 内容
1.12 2021/01/31 readWebContents() 引数post追加
1.11 2020/10/17 htmlspecialchars() 追加
1.1 2020/10/17 GetVersion2()追加,readWebContents() 引数ua追加
1.01 2020/10/03 setClipboardData() bug-fix
1.0 2020/09/22 初版

使用ライブラリ

WebAPIにアクセスするために、オープンソースのライブラリ Boost C++ライブラリcURL (カール)  および OpenSSL が必要になる。導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。

MSYS2 コマンドラインからビルドするのであれば、"makefile" を利用してほしい。

リソースの準備

今回は32ビット版の開発環境を用いる。
Eclipse を起動し、新規プロジェクト stationsearchwin を用意する。
ResEdit を起動し、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "stationsearchwin.cpp" を追加する。
リンカー・フラグを -Wl,--enable-stdcall-fixup -mwindows -lgdiplus -static -lstdc++ -lgcc -lwinpthread -lcurl -lssl "(任意のパス)\libcurl.dll" "(任意のパス)\cwebpage.dll" "C:\Windows\system32\GdiPlus.dll" に設定する。
また、コマンド行パターンを ${COMMAND} ${FLAGS} ${OUTPUT_FLAG} ${OUTPUT_PREFIX}${OUTPUT} ${INPUTS} -lole32 -loleaut32 -luuid にすること。

MSYS2 コマンドラインからビルドするのであれば、"makefile" を利用してほしい。

解説:定数など

  36: // 定数など ==================================================================
  37: #define MAKER       "pahoo.org"             //作成者
  38: #define APPNAME     "stationsearchwin"      //アプリケーション名
  39: #define APPNAMEJP   "最寄駅検索"            //アプリケーション名(日本語)
  40: #define APPVERSION  "1.1.4"                 //バージョン
  41: #define APPYEAR     "2020-24"               //作成年
  42: #define REFERENCE   "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-15-01.shtm"   //参考サイト
  43: 
  44: //ListViewItemの最大文字長:変更不可
  45: #define MAX_LISTVIEWITEM    259
  46: 
  47: //ヘルプ・ファイル
  48: #define HELPFILE    ".\\etc\\help.chm"
  49: 
  50: //デフォルト保存ファイル名
  51: #define SAVEFILE    "stationsearchwin.csv"
  52: 
  53: //現在のインターフェイス
  54: HINSTANCE hInst;
  55: 
  56: //アプリケーション・ウィンドウ
  57: HWND hParent;
  58: 
  59: //アプリケーション・ウィンドウ位置
  60: unsigned hParent_X, hParent_Y;
  61: 
  62: //検索キー格納用
  63: string Query;
  64: 
  65: //エラー・メッセージ格納用【変更不可】
  66: string ErrorMessage;
  67: 
  68: //ブラウザ・コントロール
  69: WebBrowser wBrowser;
  70: 
  71: //UserAgent
  72: string UserAgent;
  73: 
  74: //pahooGeocodeオブジェクト
  75: pahooGeocode* pGC;
  76: 
  77: //マップID
  78: #define MAP_ID          "map_id"
  79: //地図の大きさ
  80: #define MAP_WIDTH       530     //地図の幅(ピクセル)
  81: #define MAP_HEIGHT      300     //地図の高さ(ピクセル)
  82: //経度・緯度(初期値)
  83: #define DEF_LONGITUDE   139.766667
  84: double Longitude = DEF_LONGITUDE;
  85: #define DEF_LATITUDE    35.681111
  86: double Latitude  = DEF_LATITUDE;
  87: //地図拡大率(初期値)
  88: #define DEF_ZOOM    13
  89: int Zoom = DEF_ZOOM;
  90: //地図の種類(初期値)
  91: #define DEF_MAPTYPE     "GSISTD"
  92: string Maptype = DEF_MAPTYPE;

とくに注意記載が無い限り、定数は自由に変更できる。

解説:検索の流れ

本アプリケーションの肝は、検索ボタンをクリックしたときのフローである。
>C++で最寄駅を検索

 894:         //検索
 895:         case IDM_EXEC:
 896:         case IDC_BUTTON_EXEC:
 897:             //検索キーがあればジオコードAPI呼び出し
 898:             Query = getStrEditBox(hDlg, IDC_EDIT_QUERY);
 899:             if (Query.length() > 0) {
 900:                 if (pGC->searchPoints(_SW(Query), UserAgent, 0> 0) {
 901:                     Longitude = pGC->Ppoints[0].longitude;
 902:                     Latitude  = pGC->Ppoints[0].latitude;
 903:                 }
 904:             //検索キーがなければ地図中心座標を取り出す
 905:             } else {
 906:                 if (wBrowser.getInputById(L"longitude", &val)) {
 907:                     Longitude = stod(val);
 908:                 }
 909:                 if (wBrowser.getInputById(L"latitude", &val)) {
 910:                     Latitude = stod(val);
 911:                 }
 912:             }
 913:             if (wBrowser.getInputById(L"zoom", &val)) {
 914:                 Zoom = stoi(val);
 915:             }
 916:             if (wBrowser.getInputById(L"maptype", &val)) {
 917:                 Maptype = _WS(val);
 918:             }
 919:             //カーソルを砂時計に
 920:             SetCursor(LoadCursor(NULL, IDC_WAIT));
 921:             //最寄駅検索
 922:             cnt = pHR->getResults_Heartrails(Latitude, Longitude);
 923:             if (cnt > 0) {
 924:                 cnt = makeInformation(cnt);
 925:             }
 926:             //ブラウザ・コントロールに表示
 927:             viewBrowser(tmpname, cnt);
 928:             //最寄駅の一覧表示
 929:             makeListView(GetDlgItem(hDlg, IDC_LISTVIEW_STATIONS));
 930:             //エラー・リセット
 931:             ErrorMessage = "";
 932:             pGC->resetError();
 933:             break;

解説:INPUT要素の取得

以前に作った makeMapLeaflet メソッドにより、マップをドラッグしたり、表示倍率を変更したり、マップタイプを変更した結果は、ブラウザ・コントロールのinputフォームにリアルタイムで反映される。
JavaScriptであればgetElementByIdメソッドを使って取り出せるのだが、C++で同じことをやるのに骨が折れた。

 191: /**
 192:  * Webブラウザ・コントロールにある要素を取り出す(ID指定)
 193:  * @param   wstring id ID
 194:  * @param   IHTMLElement** element 要素を格納
 195:  * @return  bool TRUE:要素があった/なかった
 196: */
 197: bool getIHTMLElementById(std::wstring id, IHTMLElement** pElement) {
 198:     HWND iES = getIES();
 199:     static const UINT WM_HTML_GETOBJECT = ::RegisterWindowMessage(_T("WM_HTML_GETOBJECT"));
 200:     LRESULT res = 0;
 201:     SendMessageTimeout(iES, WM_HTML_GETOBJECT, 0, 0, SMTO_ABORTIFHUNG, 1000, reinterpret_cast<PDWORD_PTR>(&res));
 202:     if (! res) {
 203:         return FALSE;
 204:     }
 205: 
 206:     //DLL 内のエクスポート関数のアドレスを取得
 207:     HINSTANCE hInstance = LoadLibrary(_T("oleacc.dll"));
 208:     LPFNOBJECTFROMLRESULT pfObjectFromLresult = (LPFNOBJECTFROMLRESULT)::GetProcAddress(hInstance, "ObjectFromLresult" );
 209:     if (pfObjectFromLresult == NULL) {
 210:         return FALSE;
 211:     }
 212: 
 213:      //IHTMLDocument3 のポインタを取得
 214:     IHTMLDocument3* pHTMLDocument3;
 215:     (*pfObjectFromLresult)(res, IID_IHTMLDocument3, 0, (void **)&pHTMLDocument3);
 216:     if (! pHTMLDocument3) {
 217:         return FALSE;
 218:     }
 219: 
 220:      //IHTMLElement のポインタを取得
 221:     BSTR bstrId;
 222:     IHTMLElement* element;
 223:     bstrId = SysAllocString(id.c_str());
 224:     pHTMLDocument3->getElementById(bstrId, &element);
 225: 
 226:     *pElement = element;
 227:     if (! element) {
 228:         return FALSE;
 229:     }
 230:     return TRUE;
 231: }

今回はCOMインターフェースを使って取り出しているのだが、IHTMLDocument3 ポインタを取得するのに、ブラウザ・コントロールの奥深くにある IES(InternetExplorer_Server)のハンドルを知らなければならない。
>C++で最寄駅を検索
WinExplorer v1.30 を使って IES の位置を見たところ。かなり深いところにある。

  90: /**
  91:  * IES(InternetExplorer_Server)のハンドルを返す
  92:  * @param   なし
  93:  * @return  HWND ハンドル
  94: */
  95: HWND getIES(void) {
  96:     TCHAR className[128];
  97:     HWND hChild = GetWindow(control, GW_CHILD);
  98:     while (hChild) {
  99:         GetClassName(hChild, className, sizeof(className));
 100:         if (_tcscmp(_T("Internet Explorer_Server"), className) == 0) {
 101:             return hChild;
 102:         }
 103:         hChild = GetWindow(hChild, GW_CHILD);
 104:     }
 105:     return NULL;
 106: }

関数 getIES を用意し、IES を取り出すことができるようにした。

HeartRails Geo API 最寄駅検索

緯度・経度から最寄駅検索するのに、「HeartRails Express API - 最寄駅情報取得 API 」を利用する。
WebAPIのURL
URL
https://express.heartrails.com/api/xml

入力パラメータ
フィールド名 要否 内  容
method 必須 メソッド名:getStation(固定)
x 必須 最寄駅を取得したい場所の経度(世界測地系)。
y 必須 最寄駅を取得したい場所の緯度(世界測地系)。
応答データ(xml) response station name 駅名 line 路線名 distance 検索地点からの距離 x 経度 y 緯度 prefecture 都道府県 postal 郵便番号 next 次の駅 prev 前の駅

解説:HeartRails Geo APIの呼び出し

 365: /**
 366:  * HeartRails Express API から必要な情報を配列に格納する
 367:  * @param   double latitude  緯度(世界測地系)
 368:  * @param   double longitude 経度(世界測地系)
 369:  * @return  int ヒット数/(-1):エラー発生
 370: */
 371: int pahooHeartRailsExpress::getResults_Heartrails(double latitude, double longitude) {
 372:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
 373: 
 374:     snprintf(lng, SIZE_BUFF, "%.5f", longitude);
 375:     snprintf(lat, SIZE_BUFF, "%.5f", latitude);
 376:     this->webapi = "http://express.heartrails.com/api/xml?method=getStations&x=" + (string)lng + "&y=" + (string)lat;
 377:     static string contents = "";
 378:     bool res = readWebContents(this->webapi, UserAgent, &contents);
 379:     if (res == FALSE) {
 380:         this->errmsg = _SW("HeartRails Express APIの接続エラーが発生しました");
 381:         return (-1);
 382:     }
 383: 
 384:     //配列の初期化
 385:     for (int i = 0i < __SIZE_PPOINTSi++) {
 386:         this->Pstations[i].id = 0;
 387:         this->Pstations[i].title = this->Pstations[i].line = this->Pstations[i].distance = L"";
 388:         this->Pstations[i].latitude = this->Pstations[i].longitude = 0.0;
 389:     }
 390: 
 391:     //XML読み込み
 392:     int cnt = 0;
 393:     try {
 394:         std::stringstream ss;
 395:         ss << contents;
 396:         ptree pt;
 397:         xml_parser::read_xml(ss, pt);
 398: 
 399:         //応答チェック
 400:         if (optional<string>str = pt.get_optional<string>("response.station")) {
 401:         } else {
 402:             this->errmsg = _SW("HeartRails Geo APIの応答エラーが発生しました");
 403:             return (-1);
 404:         }
 405: 
 406:         //XML解釈
 407:         try {
 408:             for (auto it : pt.get_child("response")) {
 409:                 if (cnt >__SIZE_PPOINTS) {
 410:                     break;
 411:                 }
 412:                 //読み飛ばし
 413:                 if (it.first !"station") {
 414:                     continue;
 415:                 }
 416:                 //最寄駅名
 417:                 if (optional<string>name = it.second.get_optional<string>("name")) {
 418:                     this->Pstations[cnt].title = _UW(name.value());
 419:                 }
 420:                 //緯度
 421:                 if (optional<string>lat = it.second.get_optional<string>("y")) {
 422:                     this->Pstations[cnt].latitude = stod(lat.value());
 423:                 }
 424:                 //経度
 425:                 if (optional<string>lng = it.second.get_optional<string>("x")) {
 426:                     this->Pstations[cnt].longitude = stod(lng.value());
 427:                 }
 428:                 //路線
 429:                 if (optional<string>line = it.second.get_optional<string>("line")) {
 430:                     this->Pstations[cnt].line = _UW(line.value());
 431:                 }
 432:                 //距離
 433:                 if (optional<string>distance = it.second.get_optional<string>("distance")) {
 434:                     this->Pstations[cnt].distance = _UW(distance.value());
 435:                 }
 436:                 //識別子
 437:                 this->Pstations[cnt].id = {(char)(65 + cnt)};
 438:                 cnt++;
 439:             }
 440:         //XML解釈エラー
 441:         } catch(ptree_bad_path& e) {
 442:             this->errmsg = _SW("HeartRails Geo APIの検索エラーが発生しました");
 443:             return (-1);
 444:         }
 445: 
 446:     //読み込みエラー
 447:     } catch(xml_parser_error& e) {
 448:         this->errmsg = _SW("HeartRails Geo APIの接続エラーが発生しました");
 449:         return (-1);
 450:     }
 451:     contents.clear();
 452: 
 453:     return cnt;
 454: }

解説:パラメータの保存と読み込み

 133: /**
 134:  * パラメータの読み込み
 135:  * @param   なし
 136:  * @return  なし
 137:  */
 138: void loadParameter(void) {
 139:     ptree pt;
 140: 
 141:     //初期値設定
 142:     initParameter();
 143: 
 144:     //XMLファイル読み込み
 145:     try {
 146:         xml_parser::read_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt);
 147: 
 148:         //XML解釈
 149:         try {
 150:             //形式チェック
 151:             if (optional<string>str = pt.get_optional<string>("parameter")) {
 152:             } else {
 153:                 return;
 154:             }
 155:             //パラメータ読み込み
 156:             for (auto it : pt.get_child("parameter")) {
 157:                 string typeit.second.get_optional<string>("<xmlattr>.type").value();
 158:                 if (type == "latitude") {
 159:                     Latitude = stod(it.second.data());
 160:                 } else if (type == "longitude") {
 161:                     Longitude = stod(it.second.data());
 162:                 } else if (type == "zoom") {
 163:                     Zoom = stoi(it.second.data());
 164:                 } else if (type == "maptype") {
 165:                     Maptype = (string)it.second.data();
 166:                 } else if (type == "wx") {
 167:                     hParent_X = (unsigned)stoi(it.second.data());
 168:                 } else if (type == "wy") {
 169:                     hParent_Y = (unsigned)stoi(it.second.data());
 170:                 } else if (type == "query") {
 171:                     Query = utf8_sjis((string)it.second.data());
 172:                 }
 173:             }
 174:         //解釈失敗したら初期値設定
 175:         } catch (xml_parser_error& e) {
 176:             initParameter();
 177:             return;
 178:         }
 179:     //読み込み失敗したら初期値設定
 180:     } catch (xml_parser_error& e) {
 181:         initParameter();
 182:         return;
 183:     }
 184: 
 185:     //アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
 186:     HWND hDesktop = GetDesktopWindow();
 187:     WINDOWINFO windowInfo;
 188:     windowInfo.cbSize = sizeof(WINDOWINFO);
 189:     GetWindowInfo(hDesktop, &windowInfo);
 190:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 191:         hParent_X = 0;
 192:     }
 193:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 194:         hParent_Y = 0;
 195:     }
 196: }

 198: /**
 199:  * パラメータの保存
 200:  * @param   なし
 201:  * @return  なし
 202:  */
 203: void saveParameter(void) {
 204: #ifndef CMDAPP
 205:     //アプリケーション・ウィンドウの位置取得
 206:     WINDOWINFO windowInfo;
 207:     windowInfo.cbSize = sizeof(WINDOWINFO);
 208:     GetWindowInfo(hParent, &windowInfo);
 209:     hParent_X = (unsigned)windowInfo.rcWindow.left;
 210:     hParent_Y = (unsigned)windowInfo.rcWindow.top;
 211:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 212:         hParent_X = 0;
 213:     }
 214:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 215:         hParent_Y = 0;
 216:     }
 217: #endif
 218:     //ブラウザ・コントロールからパラメータを取り出す
 219:     wstring val;
 220:     if (wBrowser.getInputById(L"longitude", &val)) {
 221:         Longitude = stod(val);
 222:     }
 223:     if (wBrowser.getInputById(L"latitude", &val)) {
 224:         Latitude = stod(val);
 225:     }
 226:     if (wBrowser.getInputById(L"zoom", &val)) {
 227:         Zoom = stoi(val);
 228:     }
 229:     if (wBrowser.getInputById(L"maptype", &val)) {
 230:         Maptype = _WS(val);
 231:     }
 232: 
 233:     char lng[SIZE_BUFF + 1], lat[SIZE_BUFF + 1];
 234:     snprintf(lng, SIZE_BUFF, "%.5f", Longitude);
 235:     snprintf(lat, SIZE_BUFF, "%.5f", Latitude);
 236: 
 237:     //XMLファイルへ書き込む
 238:     ptree pt;
 239:     ptree& child1 = pt.add("parameter.param", lat);
 240:     child1.add("<xmlattr>.type", "latitude");
 241:     ptree& child2 = pt.add("parameter.param", lng);
 242:     child2.add("<xmlattr>.type", "longitude");
 243:     ptree& child3 = pt.add("parameter.param", to_string(Zoom));
 244:     child3.add("<xmlattr>.type", "zoom");
 245:     ptree& child4 = pt.add("parameter.param", Maptype);
 246:     child4.add("<xmlattr>.type", "maptype");
 247:     ptree& child5 = pt.add("parameter.param", (string)to_string(hParent_X));
 248:     child5.add("<xmlattr>.type", "wx");
 249:     ptree& child6 = pt.add("parameter.param", (string)to_string(hParent_Y));
 250:     child6.add("<xmlattr>.type", "wy");
 251:     ptree& child7 = pt.add("parameter.param", sjis_utf8(Query));
 252:     child7.add("<xmlattr>.type", "query");
 253: 
 254:     const int indent = 4;
 255:     write_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt, std::locale(),
 256:         xml_writer_make_settings<std::string>(' ', indent));
 257: }

前回状態の保持を目的として、地図の中心点(緯度・経度)、表示倍率、マップタイプ、アプリケーションウィンドウの位置、検索キーをXMLファイルに保存し、次回起動するときにそれを読み込むようにした。
その他の関数、マップ描画、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header