C++ で電源・Wi-Fi利用可能店舗を検索

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

(2024年4月7日)使用ライブラリ更新
(2023年12月17日)使用ライブラリ更新
(2023年7月23日)使用ライブラリ更新

目次

サンプル・プログラム

圧縮ファイルの内容
searchoutletwin.msiインストーラ
bin/searchoutletwin.exe実行プログラム本体
bin/cwebpage.dll
bin/libcurl.dll
実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/searchoutletwin.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ビルド
searchoutletwin.cpp 更新履歴
バージョン 更新日 内容
1.1.4 2024/04/07 使用ライブラリ更新
1.1.3 2023/12/17 使用ライブラリ更新
1.1.2 2023/07/23 使用ライブラリ更新
1.1.1 2023/04/01 使用ライブラリ更新
1.1.0 2022/09/23 ウィンドウ位置保存,ライブラリ更新
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++ 開発環境の準備」をご覧いただきたい。

リソースの準備

今回は32ビット版の開発環境を用いる。
Eclipse を起動し、新規プロジェクト searchoutletwin を用意する。
ResEdit を起動し、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "searchoutletwin.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" を利用してほしい。

解説:定数など

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

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

解説:検索の流れ

本アプリケーションの肝は、検索ボタンをクリックしたときのフローである。
>C++で電源・Wi-Fi利用可能店舗を検索

1092:         //検索
1093:         case IDM_EXEC:
1094:         case IDC_BUTTON_EXEC:
1095:             //検索キーがあればジオコードAPI呼び出し
1096:             Query = getStrEditBox(hDlg, IDC_EDIT_QUERY);
1097:             if (Query.length() > 0) {
1098:                 if (pGC->searchPoints(_SW(Query), UserAgent, 0> 0) {
1099:                     Longitude = pGC->Ppoints[0].longitude;
1100:                     Latitude  = pGC->Ppoints[0].latitude;
1101:                 }
1102:             //検索キーがなければ地図中心座標を取り出す
1103:             } else {
1104:                 if (wBrowser.getInputById(L"longitude", &val)) {
1105:                     Longitude = stod(val);
1106:                 }
1107:                 if (wBrowser.getInputById(L"latitude", &val)) {
1108:                     Latitude = stod(val);
1109:                 }
1110:             }
1111:             if (wBrowser.getInputById(L"zoom", &val)) {
1112:                 Zoom = stoi(val);
1113:             }
1114:             if (wBrowser.getInputById(L"maptype", &val)) {
1115:                 Maptype = _WS(val);
1116:             }
1117:             //カーソルを砂時計に
1118:             SetCursor(LoadCursor(NULL, IDC_WAIT));
1119:             //最寄駅検索
1120:             cnt = pOM->getResults_OasisMogya(Latitude, Longitude, 100);
1121:             if (cnt > 0) {
1122:                 cnt = makeInformation(cnt);
1123:             }
1124:             //ブラウザ・コントロールに表示
1125:             viewBrowser(tmpname, cnt);
1126:             //最寄駅の一覧表示
1127:             makeListView(GetDlgItem(hDlg, IDC_LISTVIEW_OUTLETS));
1128:             //エラー・リセット
1129:             ErrorMessage = "";
1130:             pGC->resetError();
1131:             break;

モバイラーズオアシスAPI

緯度・経度から電源・Wi-Fi利用可能店舗を検索するのに、「モバイラーズオアシスAPI 」を利用する。
WebAPIのURL
URL
https://oasis.mogya.com/api/v1/spots.json

入力パラメータ
フィールド名 要否 内  容
n 必須 検索範囲の北端の緯度(世界測地系)
s 必須 検索範囲の南端の緯度(世界測地系)
e 必須 検索範囲の東端の経度(世界測地系)
w 必須 検索範囲の西端の経度(世界測地系)
lat 任意 検索中心の緯度(世界測地系)
lng 任意 検索中心の経度(世界測地系)
応答データ(json) result id ユニークID title 店名 address 住所 tel 電話番号 latitude 緯度 longitude 経度 url 店舗公式サイトのURL wireless 無線LAN(配列) categories name カテゴリ名 image イメージアイコンのURL importance カテゴリの重要度 parent_id 親カテゴリID tag 電源、Qi、禁煙喫煙などの情報(配列) other その他情報(HTML) distance latitude,longitudeで指定した緯度経度からのおおよその距離 mo_url モバイラーズオアシスでの情報ページUR status レスポンス

解説:モバイラーズオアシスAPIの呼び出し

 375: /**
 376:  * モバイラーズオアシス電源情報API から必要な情報を配列に格納する
 377:  * @param   double latitude  緯度(世界測地系)
 378:  * @param   double longitude 経度(世界測地系)
 379:  * @param   double distance  範囲(メートル)
 380:  * @return  int ヒット数/(-1):エラー発生
 381: */
 382: int pahooOasisMogya::getResults_OasisMogya(double latitude, double longitude, double distance) {
 383:     //モバイラーズオアシス電源情報API呼び出し
 384:     char sn[SIZE_BUFF + 1], ss[SIZE_BUFF + 1];
 385:     char se[SIZE_BUFF + 1], sw[SIZE_BUFF + 1];
 386:     double n, w, s, e;
 387: 
 388:     pGC->getPointDistance(longitude, latitude, distance, 0 - distance, &w, &n);
 389:     pGC->getPointDistance(longitude, latitude, 0 - distance, distance, &e, &s);
 390: 
 391:     snprintf(sn, SIZE_BUFF, "%.5f", n);
 392:     snprintf(ss, SIZE_BUFF, "%.5f", s);
 393:     snprintf(se, SIZE_BUFF, "%.5f", e);
 394:     snprintf(sw, SIZE_BUFF, "%.5f", w);
 395:     this->webapi = "https://oasis.mogya.com/api/v1/spots.json?n=" + (string)sn + "&w=" + (string)sw + "&s=" + (string)ss + "&e=" + (string)se;
 396: 
 397:     //モバイラーズオアシス電源情報API応答
 398:     static string contents = "";
 399:     static wstring ucontents = L"";
 400: 
 401:     bool res = readWebContents(this->webapi, UserAgent, &contents);
 402:     if (res == FALSE) {
 403:         this->errmsg = _SW("モバイラーズオアシス電源情報APIの接続エラーが発生しました");
 404:         return (-1);
 405:     }
 406: 
 407:     //配列の初期化
 408:     for (int i = 0i < __SIZE_PPOINTSi++) {
 409:         this->Poutlets[i].id = 0;
 410:         this->Poutlets[i].title = this->Poutlets[i].address = L"";
 411:         this->Poutlets[i].phone = L"";
 412:         this->Poutlets[i].url = this->Poutlets[i].mo_url = "";
 413:         this->Poutlets[i].latitude = this->Poutlets[i].longitude = 0.0;
 414:     }
 415: 
 416:     wregex re1(_SW("用途\\:充電"));
 417:     wregex re2(_SW("電源\\:"));
 418:     wregex re3(_SW("用途\\:"));
 419:     wregex re4(_SW("NG|ng"));
 420:     wsmatch mt1;
 421: 
 422:     //JSON読み込み
 423:     int cnt = 0;
 424:     try {
 425:         std::stringstream ss;
 426:         ss << contents;
 427:         ptree pt;
 428:         json_parser::read_json(ss, pt);
 429: 
 430:         //応答チェック
 431:         if (optional<string>str = pt.get_optional<string>("status")) {
 432:             if (str.get() == "OK") {
 433:             } else {
 434:                 this->errmsg = _SW("モバイラーズオアシス電源情報APIの応答エラーが発生しました");
 435:                 return (-1);
 436:             }
 437:         } else {
 438:             this->errmsg = _SW("モバイラーズオアシス電源情報APIの応答エラーが発生しました");
 439:             return (-1);
 440:         }
 441: 
 442:         //JSON解釈
 443:         try {
 444:             for (auto it : pt.get_child("results")) {
 445:                 if (cnt >__SIZE_PPOINTS) {
 446:                     break;
 447:                 }
 448:                 //店名
 449:                 if (optional<string>title = it.second.get_optional<string>("title")) {
 450:                     this->Poutlets[cnt].title = _UW(title.get());
 451:                 }
 452:                 //住所
 453:                 if (optional<string>address = it.second.get_optional<string>("address")) {
 454:                     this->Poutlets[cnt].address = _UW(address.get());
 455:                 }
 456:                 //緯度
 457:                 if (optional<string>lat = it.second.get_optional<string>("latitude")) {
 458:                     this->Poutlets[cnt].latitude = stod(lat.get());
 459:                 }
 460:                 //経度
 461:                 if (optional<string>lng = it.second.get_optional<string>("longitude")) {
 462:                     this->Poutlets[cnt].longitude = stod(lng.get());
 463:                 }
 464:                 //URL
 465:                 if (optional<string>url = it.second.get_optional<string>("url")) {
 466:                     this->Poutlets[cnt].url = url.get();
 467:                 }
 468:                 //moURL
 469:                 if (optional<string>mo_url = it.second.get_optional<string>("mo_url")) {
 470:                     this->Poutlets[cnt].mo_url = mo_url.get();
 471:                 }
 472:                 //充電情報
 473:                 this->Poutlets[cnt].charge = L"";
 474:                 for (auto it2 : it.second.get_child("tags")) {
 475:                     if (optional<string>name = it2.second.get_optional<string>("name")) {
 476:                         wstring ws = _UW(name.get());
 477:                         if (regex_search(ws, mt1, re1)) {
 478:                             this->Poutlets[cnt].charge = _SW("可能\");
 479:                     }
 480:                     }
 481:                 }
 482:                 //電源情報
 483:                 this->Poutlets[cnt].powersupply = L"";
 484:                 int key = 0;
 485:                 for (auto it2 : it.second.get_child("tags")) {
 486:                     if (optional<string>name = it2.second.get_optional<string>("name")) {
 487:                         wstring ws = _UW(name.get());
 488:                         if (regex_search(ws, mt1, re4)) {
 489:                             this->Poutlets[cnt].powersupply = L"";
 490:                             break;
 491:                         }
 492:                         if (regex_search(ws, mt1, re2)) {
 493:                             wstring wstr;
 494:                             if (key > 0) {
 495:                                 wstr = _SW(",");
 496:                             } else {
 497:                                 wstr = L"";
 498:                             }
 499:                             wstr +regex_replace(ws, re2, L"");
 500:                             this->Poutlets[cnt].powersupply +wstr;
 501:                             key++;
 502:                         }
 503:                     }
 504:                 }
 505:                 //Wi-Fi情報
 506:                 this->Poutlets[cnt].wireless = L"";
 507:                 for (auto it2 : it.second.get_child("wireless")) {
 508:                     if (optional<string>name = it2.second.get_optional<string>("name")) {
 509:                         wstring ws = _UW(name.get());
 510:                         wstring wstr;
 511:                         if (key > 0) {
 512:                             wstr = _SW(",");
 513:                         } else {
 514:                             wstr = L"";
 515:                         }
 516:                         wstr +regex_replace(ws, re3, L"");
 517:                         this->Poutlets[cnt].wireless +wstr;
 518:                         key++;
 519:                     }
 520:                 }
 521:                 //識別子
 522:                 this->Poutlets[cnt].id = {(char)(65 + cnt)};
 523:                 cnt++;
 524:             }
 525:         //JSON解釈エラー
 526:         } catch(ptree_bad_path& e) {
 527:             this->errmsg = _SW("モバイラーズオアシス電源情報APIの検索エラーが発生しました");
 528:             return (-1);
 529:         }
 530: 
 531:     //読み込みエラー
 532:     } catch(json_parser_error& e) {
 533:         this->errmsg = _SW("モバイラーズオアシス電源情報APIの接続エラーが発生しました");
 534:         return (-1);
 535:     }
 536:     contents.clear();
 537: 
 538:     return cnt;
 539: }

これまでのWebAPIと異なり、応答は json形式である。
C++で json形式データを扱うことは希だが、幸いなことに、Boost C++ライブラリproperty_tree にある json_parserxml_parser とほぼ同じ使い方ができる。
今回も、cURL を使って応答情報を変数に代入し、ストリーミングを使ってjsonパーサーに流し込んでやる。

充電情報、電源情報、Wi-Fi情報は配列になっているため、これらをシリアライズしつつ、余計な文字列を正規表現の置換関数 regex_replace を使って省いている。
その他の関数、マップ描画、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header