地図で最初に表示する場所はどこにすべきか?

前回からの続き。地図を表示する時、最初のアクセスで表示する地域をどこにするかという悩みがある。Google Maps APIのサンプルコードでは、アメリカのスタンフォード大学とかパロアルト周辺になっていた。おそらくGoogleの始まりがスタンフォード大学学生寮だった、という経緯からだろう。
しかし、日本人の自分としては馴染みもなく、出来れば日本のどこかの地域が表示された方が地理感覚もあるので、その後の操作も楽になる。そう思って、最初は日本の道路の起点としてよく使われる「日本橋」を中心にしていたのだが、それも自分中心の考え方だと気付いた。関東地方に住んでいる人なら許せる範囲かもしれないが、北海道や九州に住んでいる人から見れば、最初に表示される地域としては違和感を感じるかもしれない。
出来ることなら、今その人がいる場所周辺が表示されるようにしたい。特に生活に密着した情報を表示することを目的とした地図の場合は、そっちの方がより多くの人が幸せになると思う。
そんな仕様を実現できるwebサービスAPIが多数、公開されていた。どのサービスを利用するか迷っていたが、何とGoogleも同様のサービスを提供してくれていた。Google AJAX Search APIを利用すれば、接続しているIPアドレスからおよその地域を特定できる。

実験してみる

  • 適当に実験用のコントローラーを作って...
# ---------- ターミナルでの作業 ----------

script/generate controller tests index
  • まずはAPIキーを取得するのだが...
  • 例によってlocalhostから利用する限り、APIキーは設定しなくてもOKなので、以下のように書けた。
<%# ---------- app/views/tests/index.html.erb ---------- %>

<%= javascript_include_tag :defaults %>
<%= javascript_include_tag "http://www.google.com/jsapi?key=" %>

<table>
  <tr><td align="right">     国:</td><td><%= text_field_tag :country      %></td></tr>
  <tr><td align="right">国コード:</td><td><%= text_field_tag :country_code %></td></tr>
  <tr><td align="right">都道府県:</td><td><%= text_field_tag :region       %></td></tr>
  <tr><td align="right">市区町村:</td><td><%= text_field_tag :city         %></td></tr>
  <tr><td align="right">  緯度Y:</td><td><%= text_field_tag :latitude     %></td></tr>
  <tr><td align="right">  経度X:</td><td><%= text_field_tag :longitude    %></td></tr>
</table>

<% javascript_tag do %>
  var loc = google.loader.ClientLocation;
  if (loc == null) {alert("位置情報が取得できませんでした。");}
  $("country").value      = loc.address.country;
  $("country_code").value = loc.address.country_code;
  $("region").value       = loc.address.region;
  $("city").value         = loc.address.city;
  $("latitude").value     = loc.latitude;
  $("longitude").value    = loc.longitude;
<% end %>

  • 素晴らしい!簡単だ。

利用してみる

<%# ---------- app/views/layouts/maps.html.erb ---------- %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>Maps: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'scaffold' %>
  <%= javascript_include_tag :defaults %>

  <!-- Google AJAX Search APIを有効にする -->
  <%= javascript_include_tag "http://www.google.com/jsapi?key=ABQIAAAAubV6ipVCO8c_dPnrcPjEihSEmJJDZxFvackNIw5ej35xzhqArRR9_FNVUYdp6D3Z-VpYyf-Zv6e84A" %>

  <%= GMap.header(:host => request.host) %>
  <%= @map.to_html %>

</head>
<body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield  %>

</body>
</html>
  • コントローラーは、ym4r_gmを利用しながら、以下のようになった。(IPアドレスから取得した現在地を中心にして、不動の中央マーカーを表示する。)
    • [1]によって、引数のjavascript"var lat = google..."をそのまま生成して、グローバル変数latにIPアドレスから取得した緯度情報を代入している。
    • [2]によって、Rubyコード中のlatが、javascriptコード中のグローバル変数latを指し示すことをym4r_gmに教えてあげる。
      • こうしておかないと、[3]@map.center_zoom_init([lat, lng] ,14)のlatやlngnilになってしまう...。
      • Rubyコードとしては未定義なので当り前の結果だが、javascriptのコード生成が絡んでくると悩みのポイントになった。
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  def index
    @map = GMap.new("map_div")

    # IPアドレスから位置情報を取得する
    @map.record_global_init("var lat = google.loader.ClientLocation.latitude") #<------[1]
    @map.record_global_init("var lng = google.loader.ClientLocation.longitude") #<------[1]
    lat = Variable.new('lat') #<------[2]
    lng = Variable.new('lng') #<------[2]
    @map.center_zoom_init([lat, lng] ,14) #<------[3]

    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # 地図が移動した時(moveイベント)、drawCenterMarkerを実行する
    @map.event_init(@map, 'move', 'function(){drawCenterMarker(map);}')
    
    # 中央マーカーをグローバル変数center_markerとして定義して、表示する
    @map.overlay_global_init(center_marker, "center_marker", :local_construction => true)
  end

  private
    
    def center_marker
      icon   = GIcon.new(:image => "http://www.google.com/mapfiles/dd-start.png", :copy_base => GIcon::DEFAULT)
      center = @map.getCenter
      GMarker.new(center, :clickable => false, :icon => icon)
    end
end
<%# ---------- app/views/maps/index.html.erb ---------- %>

<%= @map.div(:width => 500, :height => 300) %>
  • これで、IPアドレスから取得できる現在地を表示するようになった!

不測の事態を考える

  • ひとまず満足したが、場合によってはIPアドレスから位置情報が取得できない可能性もあるかもしれない。そんな時のことを考えて、コントローラーを以下のように修正した。
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  def index
    @map = GMap.new("map_div")

    # IPアドレスから位置情報を取得する
    @map.record_init(<<-END)
      var lat = google.loader.ClientLocation.latitude;
      var lng = google.loader.ClientLocation.longitude;

      // 日本橋の座標を設定
      map.setCenter(new GLatLng(35.6840432111695,139.774460792542),14);//<------[1]

      if (lat && lng) {
        // IPアドレスから位置情報を取得できたらその位置を設定する
        map.setCenter(new GLatLng(lat,lng),14);
      } else {
        // 位置情報を取得できなかったら住所検索の結果を設定する
        new GClientGeocoder().getLatLng('新宿区', function(latlng){ if(latlng){map.setCenter(latlng,14);} });
      }
    END

    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # 地図が移動した時(moveイベント)、drawCenterMarkerを実行する
    @map.event_init(@map, 'move', 'function(){drawCenterMarker(map);}')
    
    # 中央マーカーをグローバル変数center_markerとして定義して、表示する
    @map.overlay_global_init(center_marker, "center_marker", :local_construction => true)
  end

  private
    
    def center_marker
      icon   = GIcon.new(:image => "http://www.google.com/mapfiles/dd-start.png", :copy_base => GIcon::DEFAULT)
      center = @map.getCenter#<------[2]
      GMarker.new(center, :clickable => false, :icon => icon)
    end
end
  • ym4r_gmとうまく連携させる方法が思い付かなかったので、javascriptを直接書いてしまった...。
  • [1]の日本橋の座標を設定するコード行は削除したい衝動に駆られるが...
    • ここであらかじめmap.setCenterを設定しておかないと、IPアドレスから位置情報を取得できなかった場合に、地図が正常に表示されなくなってしまった。
    • おそらく、位置情報を取得できなかった場合のmap.setCenterが、コールバック関数の中で処理されているのが原因のようだ。
    • indexアクションのコードがすべて実行された後に、コールバック関数の中の処理が実行されるみたい。(少なくともcenter_markerメソッドの方が先に処理されている。)
    • すると、map.setCenterが未定義のため、[2]のcenter = @map.getCenterが正常に処理できなくて、予期しない結果になっているようだ。
    • もっと良い別の解決方法があると思う...。

地図の表示状態を保存しておく

さらに使い勝手を考えていくと、訪問したユーザーごとに地図の最終状態を保存しておいて、次回アクセスした時には前回の地図の状態を復元するようにすると良さそう。そうすれば、自分専用の地図を使っている感覚で利用できる。sessionを利用して、以下のようにやってみた。(オレンジ色の部分を追記)

  • [3]で、地図の移動(ズームの変化も含む)が終了した時に発生するmoveendイベントで、ajax呼び出しでstore_mapアクションを実行するように設定しておいた。
    • [6]は、上記ajax呼び出しの定義。:withオプションでlat,lng,zoom(地図の中心座標とズームの状態)のパラメーターも渡している。
    • [4]は、上記moveendイベントで処理される内容。sessionに地図の中心座標とズームを保存する。
  • [1]で、 indexアクションの最初にrestore_mapを実行して、前回の地図の状態をパラメーターとして取り込んでおく。
    • [5]は、restore_mapで処理される内容。sessionをparamsにコピーする。
  • [2]で、地図の中心座標が設定されていたら、その位置を地図で表示する。設定されていなければ、IPアドレスから取得した位置を表示する。
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  def index
    restore_map#<------[1]

    @map = GMap.new("map_div")

    lat  = params[:lat]
    lng  = params[:lng]
    zoom = (params[:zoom] || 14).to_i
    if !lat.blank? && !lng.blank?#<------[2]
      @map.center_zoom_init([lat, lng], zoom)
    else
      # IPアドレスから位置情報を取得する
      @map.record_init(<<-END)
        var lat = google.loader.ClientLocation.latitude;
        var lng = google.loader.ClientLocation.longitude;
        // 日本橋の座標を設定
        map.setCenter(new GLatLng(35.6840432111695,139.774460792542), #{zoom});
        if (lat && lng) {
          // IPアドレスから位置情報を取得できたらその位置を設定する
          map.setCenter(new GLatLng(lat,lng), #{zoom});
        } else {
          // 位置情報を取得できなかったら住所検索の結果を設定する
          new GClientGeocoder().getLatLng('新宿区', function(latlng){ if(latlng){map.setCenter(latlng, #{zoom});} });
        }
      END
    end

    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # 地図が移動した時(moveイベント)、drawCenterMarkerを実行する
    @map.event_init(@map, 'move', 'function(){drawCenterMarker(map);}')
    
    # 地図の移動が終了した時(moveendイベント)、store_mapを実行する(地図の状態を保存する)
    @map.event_init(@map, 'moveend', "function(){#{remote_function_store_map}}")#<------[3]
    
    # 中央マーカーをグローバル変数center_markerとして定義して、表示する
    @map.overlay_global_init(center_marker, "center_marker", :local_construction => true)
  end

  def store_map#<------[4]
    session[:lat]  = params[:lat]
    session[:lng]  = params[:lng]
    session[:zoom] = params[:zoom]
    render :update do |page|
      # page.replace_html(:temp, session.inspect)
    end
  end
  
  def restore_map#<------[5]
    params[:lat]  = session[:lat]  if params[:lat].blank?
    params[:lng]  = session[:lng]  if params[:lng].blank?
    params[:zoom] = session[:zoom] if params[:zoom].blank?
  end

  private
    
    def center_marker
      icon   = GIcon.new(:image => "http://www.google.com/mapfiles/dd-start.png", :copy_base => GIcon::DEFAULT)
      center = @map.getCenter
      GMarker.new(center, :clickable => false, :icon => icon)
    end
    
    def remote_function_store_map#<------[6]
      @template.remote_function(
        :url  => {:action => 'store_map'}, 
        :with => "'lat=' + map.getCenter().lat() + '&lng=' + map.getCenter().lng() + '&zoom=' + map.getZoom()")
    end
end

とりあえず、地図の状態は保存できるようになった!

  • さらに、クッキーの有効期限を設定して、ブラウザを閉じても最終アクセス日から1ヶ月以内であれば地図の状態を復元できるようにしてみた。(オレンジ色の部分を追記)
  • 以下の設定については、詳しくは以前の日記「sessionに有効期限を設定する時の試練」もどうぞ。
# ---------- ターミナルでの作業 ----------

script/plugin install http://svn.codahale.com/dynamic_session_exp/trunk
# ---------- config/environment.rb ----------

...(中略)...
Rails::Initializer.run do |config|
...(中略)...
end

CGI::Session.expire_after 1.month
# ---------- app/controllers/maps_controller.rb ----------

...(中略)...
  def store_map
    session[:lat]  = params[:lat]
    session[:lng]  = params[:lng]
    session[:zoom] = params[:zoom]
    session[:updated_at] = Time.now
    render :update do |page|
    end
  end
...(中略)...

これで最終アクセス日から1ヶ月間は自分専用の地図になった!状態が保存されていない場合でも、可能な限りユーザーの現在地周辺を表示するように努力する地図になった!