URLから地図上にピンを表示する

DQウォークという位置ゲーを始めたのですが、47都道府県に各4つずつ設置されているランドマークを巡るスタンプラリー的な要素が旅行のお供に面白そうで、攻略サイトの一覧で確認したランドマークに寄った帰宅後に、直ぐ近くにもう一つあることに気が付いて後悔したということがあって、ランドマーク名称とGoogleマップのリンクがある一覧の表示からGoogleマップ上にピンマーカーで表示するように変換したという作業のお話です。

まず PHP を使いウェブスクレイピングし、形式が hreftextという二つのキーを持つオブジェクトを配列とするデータを作ります。file_get_contentsしてから正規表現でAタグをマッチさせて…と思いゴニョゴニョやっていましたが、DOMを利用した方が簡単だということで方針を転換しました。

一覧ページを確認すると取り出したいURLにはgoo.glで短縮されたものとそうでないものが混ざっていましたが、いずれも/maps/文字列が含まれていて、var_dumpで配列の長さが188あるのを確認しつつ、全てのhref属性値をstrpos($href,'/maps/')の条件で分岐し、短縮URLを戻しhrefキーの値としてオブジェクトを構成していきます。

$html = file_get_contents('https://appmedia.jp/dqwalk/3864508');
 
$dom = new DOMDocument;
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$xpath = new DOMXPath($dom);
 
$links = [];
$query = '//a[
  @href != ""
  and not(starts-with(@href, "#"))
  and normalize-space() != ""
]';
foreach ($xpath->query($query) as $node) {
  $href = $xpath->evaluate('string(@href)', $node);
  if(strpos($href,'/maps/')){
    $links[] = [
      'href' => getFollowUrl($href),
      //'href' => $href,
      'text' => $xpath->evaluate('normalize-space()', $node),
    ];  
  }
}
$json = json_encode($links, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE/*|JSON_UNESCAPED_SLASHES*/);
var_dump($json);
 
function getFollowUrl($string) {
  preg_match('#^https://goo.gl/maps/#', $string, $match);
  if(!$match) {
    return $string;
  }
  $headers = get_headers($string, true);
  return $headers['Location'][0];
}

短縮されたものから元のURLを取得する方法は、以前ツイッターボットを作った時に経験があったので再利用したのですが、パフォーマンスが非常に悪かったためにこの時点で表示側のAjaxによる動的なデータ生成読み込みを諦め、var_dump($json)で出力したJSONデータをコピペして静的なファイルにすることにしました。更に短縮URLから元URLを取得する方法について、curlを使う方法に加えてget_headersを使う方法を知り、今回は後者を試してみましたがパフォーマンスの比較は別途検証しようと思います。

データができたところで次に表側ですが、緯度経度を動的にparseFloatする必要があるところでコンソールがエラーを吐いて若干ハマりました。

<div id="maps"></div>
<script src="data.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=####################&amp;callback=initMap" async=""></script>
<script>
const posFrom = { 'lat': 36.5431868, 'lng': 135.9460074 }
const minZoomLevel = 5.7
 
function initMap() {
  map = new google.maps.Map(document.getElementById('maps'), {
    center: {lat: posFrom.lat, lng: posFrom.lng},
    zoom: minZoomLevel,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
  })
 
  for (let i = 0, max = DQWLandmarks.length; i < max; i++) {  
    const url = DQWLandmarks[i].href
    const regex = new RegExp('@(.*),(.*),')
    const lng_lat_match = url.match(regex)
    const lat = lng_lat_match[1]
    const lng = lng_lat_match[2]
    //console.log(DQWLandmarks[i].text + ': ' +lon +' , '+ lat)
    //console.log(DQWLandmarks[i].text + ': ' +parseFloat(lon) +' , '+ parseFloat(lat))
 
    const marker = new google.maps.Marker({
      position: {'lat': parseFloat(lat), 'lng': parseFloat(lng)},
      map: map
    })
 
    google.maps.event.addListener(marker, 'click', (function(marker, i) {
      return function() {
        infowindow = new google.maps.InfoWindow({
            content: DQWLandmarks[i].text/*+'<br>'+lon+' , '+lat*/
        })
        infowindow.open(map, marker)
      }
    })(marker, i))
  }
}

gistに置いたJSONデータは、間違いを見つけた4箇所ほど座標を調整しています。GPXに流用するなりご自由にお使いください。

更新: 位置ゲーでWi-Fi環境を推奨するとか、ゲームデータをサーバに自動で置かないとかの不便さもあり、不慮の事故によりレベル30くらいまできてやり直すのが面倒で引退することにしました涙。

投稿者: hkitago

個人事業主でウェブと iOS, Android アプリの開発者で一児の父親。JavaScript, ActionScript, AppleScript, PHP, SQL, ObjC, Swift, Java の読書実行試験運用管理を生業とし、Bind, Postfix, Apache を MacOS で使い、エディタは Vi, mi, Kod, Smultron, TextWrangler を経て Coda, Xcode, Android Studio といった IDE と CotEditor を重用しています。Pokémon GO Trainer Code: 2491 5857 6842