地図を表示したい!ym4r_gmをちゃんと使いたい

前回の続き。目的地を中心に置いてマーカと吹き出しウィンドウを表示する最も基本的な地図は表示できるようになったが、未だGoogle Maps APIやym4r_gmの使い方はちゃんと理解できていない...。そんな状態で作業しながら考えたこと。

ym4r_gmのgmaps_api_keyを動的に設定する方法

  • ym4r_gmではdevelopment、test、production環境ごとにAPIキーを設定できるようになっている。
  • そして、デフォルトのgmaps_api_key.ymlを見ていると、気になる書き方がある。production環境の設定のところ。
# ---------- config/gmaps_api_key.yml ----------

#Fill here the Google Maps API keys for your application
#In this sample:
#For development and test, we have only one possible host (localhost:3000), so there is only a single key associated with the mode.
#In production, the app can be accessed through 2 different hosts: thepochisuperstarmegashow.com and exmaple.com. There then needs a 2-key hash. If you deployed to one host, only the API key would be needed (as in development and test).

development:
 ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ

test:
 ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ

production:
 thepochisuperstarmegashow.com: ABQIAAAAzMUFFnT9uH0Sfg76Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDmlRT6e90j135zat56yhJKQlWnkaidDIQ
 example.com: ABQIAAAAzMUFFnT9uH0Sfg98Y4kbhGFJQa0g3IQ9GZqIMmInSLrthJKGDmlRT98f4j135zat56yjRKQlWnkmod3TB
  • コメントの英語の説明を読んでみると、どうやら、さらにホスト名ごとにAPIキーを複数設定しておいて、使い分けることができるようだ。やってみた。
<%# ---------- app/views/layouts/maps.html.erb ---------- %>

...(中略)...
  <%= GMap.header(:host => 'example.com') %>
  <%= @map.to_html %>
...(中略)...
  • 上記のように指定すれば、example.com: に設定したAPIキーを利用してくれる。
  • そして、Railsではrequest.hostでホスト名は簡単に取得できるので、以下のようにしておけばとっても便利。
<%# ---------- app/views/layouts/maps.html.erb ---------- %>

...(中略)...
  <%= GMap.header(:host => request.host) %>
  <%= @map.to_html %>
...(中略)...
  • production環境に限らず、development、test環境でも同じように設定できる。
  • localhost以外で開発している時にとても幸せを感じる。

いろいろなマーカーアイコンを利用する

  • 地図は目的の場所と情報が表示されて、初めて立派な地図としての機能を果たす。
  • その場所を指し示すのがマーカーなのだが、現在は地図中心を示すマーカーが一つだけ。そのマーカーはデフォルトで水滴が逆立ちした形状で、赤く、中心に黒い点があるアイコンになっている。

  • 目的の場所が一カ所だけならこれで問題ないのだが、目的の場所が複数箇所の場合、マーカーアイコンも区別したくなる。
  • Google Mapをお手本にすれば、マーカの中にA、B、C...とアルファベットが表示されている。それは以下のようにすれば利用できた。
    • GIcon.newでアイコンオブジェクトを生成する。デフォルトのマーカーアイコンをベースに、:imageオプションでAが表示されている画像を指定している。(googleが用意しているアイコン画像を利用)
    • 上記で生成したアイコンオブジェクトを、GMarker.newの:iconオプションで設定すれば、目的のマーカーが表示された。
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  def index
    @map = GMap.new("map_div")
    @map.center_zoom_init([37.4419, -122.1419], 13)
    
    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # マーカーAと吹き出しウィンドウを表示する
    icon = GIcon.new(:image => "http://www.google.com/mapfiles/markerA.png", :copy_base => GIcon::DEFAULT)
    @map.overlay_init(GMarker.new([37.4419, -122.1419], :title => "Hello", :info_window => "Info! Info!", :icon => icon))
  end
end


  • 複数のマーカーを区別したい時は以下のようにしてみた。
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  POINTS = [
    {:point => [35.678355, 139.715109], :name => "国立競技場"}, 
    {:point => [35.67452 , 139.717083], :name => "神宮球場" }, 
    {:point => [35.676472, 139.699316], :name => "明治神宮" }
  ] 
  def index
    @map = GMap.new("map_div")
    @map.center_zoom_init([35.678982,139.710131], 14)
    
    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # マーカーA,B,Cと吹き出しウィンドウを表示する
    POINTS.each do |item|
      @letter = (@letter.succ rescue 'A')
      icon = GIcon.new(:image => "http://www.google.com/mapfiles/marker#{@letter}.png", :copy_base => GIcon::DEFAULT)
      marker = GMarker.new(item[:point], :title => "Hello #{@letter}", :info_window => item[:name], :icon => icon)
      @map.overlay_init(marker)
    end
  end
end

  • オリジナル画像をアイコンにすることだって出来る。(以下、デフォルトアイコンをベースにした簡略的な方法)
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  def index
    @map = GMap.new("map_div")
    @map.center_zoom_init([37.4419, -122.1419], 13)
    
    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # オリジナルマーカーと吹き出しウィンドウを表示する
    icon = GIcon.new(:image => "../images/my_marker.png", :copy_base => GIcon::DEFAULT)
    @map.overlay_init(GMarker.new([37.4419, -122.1419], :title => "Hello", :info_window => "Info! Info!", :icon => icon))
  end
end

:image => @template.image_path("my_marker.png")
:image => "http://localhost:3000/images/my_marker.png"
    • @template.image_pathで指定するのがベストかもしれない。

マーカーリストとマーカーを連動させる

  • 地図の横にマーカーリストをリンク表示して、そのリンクをクリックしても吹き出しウィンドウを表示するようにしてみた。
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  POINTS = [
    {:point => [35.678355, 139.715109], :name => "国立競技場"}, 
    {:point => [35.67452 , 139.717083], :name => "神宮球場" }, 
    {:point => [35.676472, 139.699316], :name => "明治神宮" }
  ] 
  def index
    @map = GMap.new("map_div")
    @map.center_zoom_init([35.678982,139.710131], 14)
    
    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # マーカーリストと連動させて、マーカーA,B,Cと吹き出しウィンドウを表示する
    @map.record_global_init("var markers = [];")
    POINTS.each_with_index do |item, index|
      @letter = (@letter.succ rescue 'A')
      icon = GIcon.new(:image => "http://www.google.com/mapfiles/marker#{@letter}.png", :copy_base => GIcon::DEFAULT)
      marker = GMarker.new(item[:point], :title => "Hello #{@letter}", :info_window => item[:name], :icon => icon)
      @map.declare_init(marker, 'marker')
      @map.overlay_init(marker)
      @map.record_init("markers[#{index}] = marker;")
    end
  end
end
<%# ---------- app/views/maps/index.html.erb ---------- %>

<div style="float:right">
  <ul>
  <% MapsController::POINTS.each_with_index do |item, index| %>
    <li><%= link_to_function "#{('A'[0] + index).chr}. #{item[:name]}", "GEvent.trigger(markers[#{index}], 'click')" %></li>
  <% end %>
  </ul>
</div>

<%= @map.div(:width => 500, :height => 300) %>
  • これで、右側のリストの「A. 国立競技場」をクリックしても、吹き出しウィンドウが表示されるようになった。
    • もちろん、直接マーカーをクリックしても、今まで通り、吹き出しウィンドウが表示される。


Rubyコードはjavascriptコードを生成するだけ

今まで、まるでRubyコードがGoogle Mapを制御しているような錯覚に陥っていたが、上記でマーカーリストのリンクを作成してみて、javascriptコードが生成されている事実を改めて理解する必要があった。Rubyコードはjavascriptコードを生成し、そのjavascriptコードがGoogle Mapを制御している。生成されたjavascriptコードは以下のようになっていた。

// ---------- app/controllers/maps_controller.rbで設定したjavascriptコード ----------
var markers = [];//<------[1]
var map;

window.onload = addCodeToFunction(window.onload,function() {
  if (GBrowserIsCompatible()) {
    map = new GMap2(document.getElementById("map_div"));
    map.setCenter(new GLatLng(35.678982,139.710131),14);

    var marker = addInfoWindowToMarker(new GMarker(new GLatLng(35.678355,139.715109),{icon : addOptionsToIcon(new GIcon(G_DEFAULT_ICON),{image : "http://www.google.com/mapfiles/markerA.png"}),title : "Hello A"}),"国立競技場",{});//<------[2]
    map.addOverlay(marker);//<------[3]
    markers[0] = marker;//<------[4]

    var marker = addInfoWindowToMarker(new GMarker(new GLatLng(35.67452,139.717083),{icon : addOptionsToIcon(new GIcon(G_DEFAULT_ICON),{image : "http://www.google.com/mapfiles/markerB.png"}),title : "Hello B"}),"神宮球場",{});//<------[2]
    map.addOverlay(marker);//<------[3]
    markers[1] = marker;//<------[4]

    var marker = addInfoWindowToMarker(new GMarker(new GLatLng(35.676472,139.699316),{icon : addOptionsToIcon(new GIcon(G_DEFAULT_ICON),{image : "http://www.google.com/mapfiles/markerC.png"}),title : "Hello C"}),"明治神宮",{});//<------[2]
    map.addOverlay(marker);//<------[3]
    markers[2] = marker;//<------[4]

    map.addControl(new GLargeMapControl());
    map.addControl(new GMapTypeControl());
  }
});
  • マーカーリストと連動させて、マーカーA,B,Cと吹き出しウィンドウを表示する部分に限定して確認してみると、以下のような対応関係になっていた。
No. Rubyコード 対応するjavascriptコード
[1] @map.record_global_init("var markers = [];") var markers = [];
[2] @letter = (@letter.succ rescue 'A')
...(中略)...
@map.declare_init(marker, 'marker')
var marker = addInfoWindowToMarker(...);
[3] @map.overlay_init(marker) map.addOverlay(marker);
[4] @map.record_init("markers[#{index}] = marker;") markers[0] = marker;
  • @map.record_global_initまたは@map.record_initは、引数の文字列をそのままjavascriptコードとして出力する。
    • つまり、ym4r_gmまたはGoogle Maps APIのサポート外のことをやろうとしたら、@map.record_...メソッドに頼るしか無いのだ。
  • ..._initで終わるメソッドは<%= @map.to_html %>で出力するjavascriptコードとして@mapオブジェクトに溜め込まれていく。
  • ..._global_initで終わるメソッドとの違いは...
    • ..._initは、window.onload関数部のコードとして出力される。
    • ..._global_initは、window.onload関数部のコードとして出力される。
    • ..._global_initを利用するのは、そのページ全体で共有されるグローバル変数の設定が出来るから。
      • 今回の例で見ると、markersというグローバル変数にマーカーA,B,Cを配列として順番に保存して、
      • もしA. 国立競技場のリンクがクリックされた時は、"GEvent.trigger(markers[0], 'click')"を実行するように設定している。
  • @map.declare_init(marker, 'marker')も、理解しようとすると頭が混乱してくるメソッドだ。
    • Rubyコードとしての変数markerには、直前の行でGMarkerオブジェクトが代入されている。
    • そのGMarkerオブジェクトをjavascriptコードの変数markerに代入するコードを生成している。
    • 分かりにくいので、@map.declare_initあり・無しの場合を比較してみると...
# @map.declare_initありの場合

marker = GMarker.new([37.4419, -122.1419], :title => "Hello", :info_window => "Info! Info!")
@map.declare_init(marker, 'marker')
@map.overlay_init(marker)

   # 一旦var markerで変数に代入してから、その変数markerをmap.addOverlayの引数に設定している
=> var marker = addInfoWindowToMarker(new GMarker(new GLatLng(35.678355,139.715109),{,title : "Hello"}),"Info! Info!",{});
=> map.addOverlay(marker);
# @map.declare_init無しの場合

marker = GMarker.new([37.4419, -122.1419], :title => "Hello", :info_window => "Info! Info!")
@map.overlay_init(marker)

   # map.addOverlayの引数に直接コードが代入される
=> map.addOverlay(addInfoWindowToMarker(new GMarker(new GLatLng(35.678355,139.715109),{title : "Hello"}),"Info! Info!",{}));


当初、ym4r_gmを全く理解できない状態だったが、上記の要点をイメージできるようになって、一気に理解が深まった。

  • RubyはMapを制御しているんじゃない、Mapを制御するjavascriptコードを生成しているだけだ。
  • init、global_initの違い。
  • declare_initは何をしているのか?

吹き出しウィンドウの中身を設定

吹き出しウィンドウに画像や拡大縮小の操作リンクを表示するようにしてみた。

  • :info_window => @template.balloon(item)として、吹き出しウィンドウの中身をヘルパメソッドでデザインした。
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  POINTS = [
    {:index => 0, :point => [35.678355, 139.715109], :name => "国立競技場", :image_url => "http://upload.wikimedia.org/wikipedia/commons/thumb/7/7f/National_Stadium_of_Japan_Kasumigaoka.jpg/800px-National_Stadium_of_Japan_Kasumigaoka.jpg"}, 
    {:index => 1, :point => [35.67452 , 139.717083], :name => "神宮球場" , :image_url => "http://upload.wikimedia.org/wikipedia/commons/thumb/3/34/Meiji_Jingu_Stadium-4.jpg/800px-Meiji_Jingu_Stadium-4.jpg"}, 
    {:index => 2, :point => [35.676472, 139.699316], :name => "明治神宮" , :image_url => "http://upload.wikimedia.org/wikipedia/ja/5/53/Meiji-jingu_naihaiden.jpg"}
  ] 
  def index
    @map = GMap.new("map_div")
    @map.center_zoom_init([35.678982,139.710131], 14)
    
    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # マーカーA,B,Cと画像や操作リンク付きの吹き出しウィンドウを表示する
    @map.record_global_init("var markers = [];\n")
    POINTS.each_with_index do |item, index|
      @letter = (@letter.succ rescue 'A')
      icon = GIcon.new(:image => "http://www.google.com/mapfiles/marker#{@letter}.png", :copy_base => GIcon::DEFAULT)
      marker = GMarker.new(item[:point], :title => "Hello #{@letter}", :info_window => @template.balloon(item), :icon => icon)
      @map.declare_init(marker, 'marker')
      @map.overlay_init(marker)
      @map.record_init("markers[#{index}] = marker;")
    end
  end
end
  • 吹き出しウィンドウの中身はballoonメソッドに定義した。
# ---------- app/helpers/maps_helper.rb ----------

module MapsHelper
  def balloon(item)
    link_to(
      image_tag(item[:image_url], :border => 1, :size => '80x80', :alt => '画像なし', :align => 'left'),
      item[:image_url], :target => '_blank') +
    item[:name] +
    content_tag(:div, 
      "[" + link_to_function('+地図拡大', "zoom_in(#{item[:index]})") + "]" +
      "[" + link_to_function('−地図戻す', "zoom_out(#{item[:index]})") + "]", 
      :style => 'clear:both'
    )
  end
end
  • 地図拡大zoom_in、地図戻すzoom_outのjavascript関数を定義した。
// ---------- public/javascripts/application.js ----------

// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults
function zoom_in(i) { 
  map.setZoom(17);
  map.panTo(markers[i].getPoint());
}

function zoom_out(i) { 
  map.setZoom(14);
  GEvent.trigger(markers[i], "click");
}
  • 吹き出しウィンドウを表示するとこんな感じ。


中央に不動のマーカーを表示したい

地図をスクロールしても常に中央に位置するマーカーを表示したい。以下のようにやってみた。

  • [1]で、地図が動くとmoveイベントが発生するので、drawCenterMarker関数を呼び出して中央マーカーの位置を修正する。
  • [2]で、中央マーカーを作成して、初期表示する。
    • drawCenterMarkerでも中央マーカーを参照したいので、グローバル変数center_markerを定義した。
    • @map.overlay_global_initによって、グローバル変数center_markerへ代入して、表示するコードが同時に生成される。
  • [3]で、簡潔にするため中央マーカーを作成するプライベートメソッドcenter_markerを定義した。
# ---------- app/controllers/maps_controller.rb ----------

class MapsController < ApplicationController
  POINTS = [
    {:index => 0, :point => [35.678355, 139.715109], :name => "国立競技場", :image_url => "http://upload.wikimedia.org/wikipedia/commons/thumb/7/7f/National_Stadium_of_Japan_Kasumigaoka.jpg/800px-National_Stadium_of_Japan_Kasumigaoka.jpg"}, 
    {:index => 1, :point => [35.67452 , 139.717083], :name => "神宮球場" , :image_url => "http://upload.wikimedia.org/wikipedia/commons/thumb/3/34/Meiji_Jingu_Stadium-4.jpg/800px-Meiji_Jingu_Stadium-4.jpg"}, 
    {:index => 2, :point => [35.676472, 139.699316], :name => "明治神宮" , :image_url => "http://upload.wikimedia.org/wikipedia/ja/5/53/Meiji-jingu_naihaiden.jpg"}
  ] 
  def index
    @map = GMap.new("map_div")
    @map.center_zoom_init([35.678982,139.710131], 14)
    
    # 地図をコントロールする部品を設定(拡大縮小スライダーとボタン、地図と航空写真の切替ボタン)
    @map.control_init(:large_map => true,:map_type => true)
    
    # マーカーA,B,Cと吹き出しウィンドウを表示する
    @map.record_global_init("var markers = [];")
    POINTS.each_with_index do |item, index|
      @letter = (@letter.succ rescue 'A')
      icon = GIcon.new(:image => "http://www.google.com/mapfiles/marker#{@letter}.png", :copy_base => GIcon::DEFAULT)
      marker = GMarker.new(item[:point], :title => "Hello #{@letter}", :info_window => @template.balloon(item), :icon => icon)
      @map.declare_init(marker, 'marker')
      @map.overlay_init(marker)
      @map.record_init("markers[#{index}] = marker;")
    end
    
    # 地図が移動すると発生するmoveイベントで、drawCenterMarkerを実行する
    @map.event_init(@map, 'move', 'function(){drawCenterMarker(map);}')#<------[1]
    
    # 中央マーカーをグローバル変数center_markerとして定義して、表示する
    @map.overlay_global_init(center_marker, "center_marker", :local_construction => true)#<------[2]
  end

  private
    
    def center_marker#<------[3]
      icon = GIcon.new(:image => "http://www.google.com/mapfiles/dd-start.png", :copy_base => GIcon::DEFAULT)
      GMarker.new(@center, :clickable => false, :icon => icon)
    end
end
  • @map.overlay_global_initでlocal_construction => trueは重要。指定した時と、指定しない時の違いは以下のようになる。
  • この場合、local_construction => trueを指定しないと、map.getCenter()で中央の座標を取得できないので、正常に表示できなくなる。
# :local_construction => trueありの場合
@map.overlay_global_init(center_marker, "center_marker", :local_construction => true)

   # 関数外部ではグローバル変数center_markerの定義だけ。値の代入はwindow.onloadの関数内部で行う。
=> var center_marker;
   window.onload = addCodeToFunction(window.onload,function() {
     if (GBrowserIsCompatible()) {
       ...(中略)...
       center_marker = new GMarker(map.getCenter(),{icon : addOptionsToIcon(new GIcon(G_DEFAULT_ICON),{image : "http://www.google.com/mapfiles/dd-start.png"}),clickable : false});
     }
   });
# :local_construction => true無しの場合
@map.overlay_global_init(center_marker, "center_marker")

   # 関数外部でグローバル変数center_markerの定義と値の代入を行う。window.onloadの関数内部では何もしない。
=> var center_marker = new GMarker(map.getCenter(),{icon : addOptionsToIcon(new GIcon(G_DEFAULT_ICON),{image : "http://www.google.com/mapfiles/dd-start.png"}),clickable : false});
   window.onload = addCodeToFunction(window.onload,function() {
     if (GBrowserIsCompatible()) {
       ...(中略)...
     }
   });
  • 地図が移動すると、drawCenterMarker(map) が呼び出されて、中央マーカーの位置を常に中央になるように修正する。
// ---------- public/javascripts/application.js ----------

// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults
...(中略)...
function drawCenterMarker(map) {
  var mapCenter = map.getCenter();
  center_marker.setPoint(mapCenter);
}
  • 緑色のマーカーが常に中央を指し示すマーカー。グリグリ動かしても、常に中央。


少しずつ地図の使い方が分かって、面白くなってきた!しかし、まだ知らないことの方がきっと多いので、もっと便利に使う技がたくさんあるはず。試行錯誤はまだまだ続く...。