Google Maps API → OpenLayers API

id:ykhpno1 さんの、この質問。

その前までの一連の質問で、Google Maps API を使ってある程度の形になっていたのだけれど、アクセス数の制限があったりするから OpenStreet Map に変更したいという お話。
Google Maps API のここは、OpenLayers API ではこう書く、みたいな感じページが意外と見つからなかったので、備忘を兼ねて。

長いよ

まずは、ソース

Google Map
<div id="map" style="width: 99%; height: 650px;"></div>
<script type="text/javascript">
(function() {

  var posts = [];
<?php while ($the_query->have_posts()) :
  $the_query->the_post();
  $googlemap = get_field('googlemap');
  if (! $googlemap) {   // マップ住所が登録されていないこともある
    continue;
  }
  $category = get_the_category(); 
?>
  posts.push({
    lat: <?php echo $googlemap['lat']; ?>,
    lng: <?php echo $googlemap['lng']; ?>,
    name: '<?php echo post_custom("Name")?>',
    link: '<?php the_permalink(); ?>',
    title: '<?php the_title(); ?>',
    cat_id: '<?php echo $category[0]->cat_ID; ?>'
  });
<?php endwhile; // End the loop. Whew. ?>
  // PHP によるデータ作成は、ここまで

  // ページの読み込みが終わったら、地図表示の処理を行う
  google.maps.event.addDomListener(window, 'load', function() {   // ■(B)
    // カテゴリーID とマーカー画像の対応

    var icon_map = {
      2: "http://maps.google.co.jp/mapfiles/ms/icons/red-dot.png",
      3: "http://maps.google.co.jp/mapfiles/ms/icons/blue-dot.png",
      4: "http://maps.google.co.jp/mapfiles/ms/icons/yellow-dot.png",
      5: "http://maps.google.co.jp/mapfiles/ms/icons/green-dot.png",
    };

    // 地図の中心、最北西、再南東   // ■(F)-2
    var sum_lat = 0, sum_lng = 0;
    var min_lat = 999, min_lng = 999;
    var max_lat = 0, max_lng = 0;
    for (var i = 0 ; i < posts.length ; ++i) {
      var lat = posts[i].lat, lng = posts[i].lng;
      sum_lat += lat;
      sum_lng += lng;
      min_lat = Math.min(min_lat, lat);
      max_lat = Math.max(max_lat, lat);
      min_lng = Math.min(min_lng, lng);
      max_lng = Math.max(max_lng, lng);
    }
    var pos_center = {
      lat: sum_lat / posts.length,
      lng: sum_lng / posts.length
    };
    var pos_sw = new google.maps.LatLng(max_lat,min_lng);
    var pos_ne = new google.maps.LatLng(min_lat,max_lng);

    var mapdiv = document.getElementById('map');    // ■(C)
    var myOptions = {
      zoom: 11,
      center: new google.maps.LatLng(pos_center.lat, pos_center.lng), // ■(D)
      mapTypeId: google.maps.MapTypeId.ROADMAP,
      scaleControl: true      // ■(E)
    };
    var map = new google.maps.Map( mapdiv, myOptions ); // ■(A)
    map.fitBounds(new google.maps.LatLngBounds(pos_sw, pos_ne));    // ■(F)-1

    // Zoom の最大値は 16 まで  // ■(G)
    var zoom_listener = google.maps.event.addListener(map, "idle", function() { 
      if (map.getZoom() > 16) {
        map.setZoom(16); 
      }
      google.maps.event.removeListener(zoom_listener); 
    });


    var marker = [];
    var infowindow = [];

    for (var i = 0 ; i < posts.length ; ++i) {
      var post = posts[i];
              
      // 緯度、経度を指定して Marker を作成
      var latlng = new google.maps.LatLng(post.lat, post.lng);
      var m = new google.maps.Marker({    // ■(H)
            icon: icon_map[ post.cat_id ],  // ■(I)
            position: latlng,
            map: map,
            title: post.title       // ■(J)
          });
      marker.push(m);

      var iw = new google.maps.InfoWindow({   // ■(K)
        content: '<div style="width : 100%;height : 36px;">' +
              post.name + ':' +
              '<a href="' + post.link + '">' + post.title +
              '</a></div>',
        size: new google.maps.Size(50, 30)
      });
      infowindow.push(iw);

      // ■(L)
      var handler = (function() {
        var m_ = m, iw_ = iw;
        return function() {
          if (iw_.getMap() == null){
            iw_.open(map, m_);
          } else {
            iw_.close();
          }
        };
      })();
      google.maps.event.addListener(m, 'click', handler);
    }

    var markerCluster = new MarkerClusterer( map, marker );     // ■(O)
  });

})();
</script>
OpenStreet Map (OpenLayers API)
<div id="map" style="width: 99%; height: 650px;"></div>
<script type="text/javascript">
(function() {

  var posts = [];
<?php while ($the_query->have_posts()) :
  $the_query->the_post();
  $googlemap = get_field('googlemap');
  if (! $googlemap) {   // マップ住所が登録されていないこともある
    continue;
  }
  $category = get_the_category(); 
?>
  posts.push({
    lat: <?php echo $googlemap['lat']; ?>,
    lng: <?php echo $googlemap['lng']; ?>,
    name: '<?php echo post_custom("Name")?>',
    link: '<?php the_permalink(); ?>',
    title: '<?php the_title(); ?>',
    cat_id: '<?php echo $category[0]->cat_ID; ?>'
  });
<?php endwhile; // End the loop. Whew. ?>
  // PHP によるデータ作成は、ここまで


  // ページの読み込みが終わったら、地図表示の処理を行う
  window.addEventListener("DOMContentLoaded", function () {
      
    // マーカーとツールチップを、店舗情報毎に設定するための overlay
    var overlay = new OpenLayers.Layer.Vector('Overlay', {
      styleMap: new OpenLayers.StyleMap({
        externalGraphic: '${marker_image}',     // ■(I)-1
        graphicWidth: 21,
        graphicHeight: 25,
        graphicYOffset: -25,
        cursor: "pointer",
        title: '${tooltip}',    // ■(J)-1
      })
    });


    var icon_map = {
      2: "http://dev.openlayers.org/img/marker.png",
      3: "http://dev.openlayers.org/img/marker-blue.png",
      4: "http://dev.openlayers.org/img/marker-gold.png",
      5: "http://dev.openlayers.org/img/marker-green.png",
    };

    var epsg4326 = new OpenLayers.Projection("EPSG:4326");
    var epsg3857 = new OpenLayers.Projection("EPSG:3857");

    // マーカーの生成
    var features = [];
    for (var i in posts) {
      var post = posts[i];
      features.push(new OpenLayers.Feature.Vector(    // ■(H)-1
          new OpenLayers.Geometry.Point(post.lng, post.lat)
          .transform(epsg4326, epsg3857), {
            tooltip: post.title,                    // ■(J)-2
            marker_image: icon_map[ post.cat_id ],  // ■(I)-2
            post: post,
          }
      ));
    }

    overlay.addFeatures(features);  // ■(H)-2

    overlay.events.on({     // ■(L)-1
      'featureclick': function(evt) {
        toggle_popup(evt.feature);
      },
    });

    // マップの生成
    var map = new OpenLayers.Map("map");    // ■(A)、■(C)
    
    map.addLayers([
      new OpenLayers.Layer.OSM(),           // ■(A)、
      overlay                               // ■(H)-3
    ]);
    map.addControl(new OpenLayers.Control.ScaleLine({   // ■(E)
      bottomOutUnits: "",
      bottomInUnits: ""
    }));

    
    // マーカーのクリック状態を取得するために、SelectFeature なるものを作る
    var select = new OpenLayers.Control.SelectFeature(overlay);   // ■(L)-2
    map.addControl(select);
    select.activate();

    // マーカーが含まれる範囲の Bounds と、それに合わせたズーミング
    var bounds = new OpenLayers.Bounds();   // ■(F)-2
    for (var i in posts) {
      bounds.extend(new OpenLayers.LonLat(posts[i].lng, posts[i].lat));
    }
    bounds.transform(epsg4326, epsg3857);
    map.zoomToExtent(bounds);   // ■(F)-1、■(D)

    // ズームの最大値は 16 まで // ■(G)
    if (map.getZoom() > 16) {
      map.zoomTo(16);
    }


    // 吹き出しの「×」を押したときの処理
    function close_popup(evt) {
      toggle_popup(this.feature);
    }

    // マーカーをクリックしたときに吹き出しの表示を切り替える
    function toggle_popup(feature) {
      if (! feature.popup) {
        var post = feature.attributes.post;
        var content = '<div>' + post.name + ':' +
              '<a href="' + post.link + '">' + post.title +
              '</a></div>';

        var popup = new OpenLayers.Popup.FramedCloud(       // ■(K)
          "featurePopup",
          feature.geometry.getBounds().getCenterLonLat(),
          new OpenLayers.Size(100, 100),
          content,
          null, true, close_popup
        );
        popup.calculateRelativePosition = function() {      // ■(M)
          return "bl";
        };
        feature.popup = popup;
        popup.feature = feature;
        feature.layer.map.addPopup(popup);    // ■(L)-3
      } else {
        var popup = feature.popup;
        popup.feature = null;
        feature.layer.map.removePopup(feature.popup);       // ■(L)-4
        feature.popup.destroy();
        feature.popup = null;
      }
    }

  });

})();
</script>
<style>
/* 地図領域と地図のサイズが合わないとき : Google Maps は API が設定する */
#map {
  position: relative;     /* ■(N) */
  overflow: hidden;
}
/* style.css の指定が影響して、吹き出しが正しく表示されない */
#map .olPopup img {
  max-width: none;
}
</style>

Google Maps API vs. OpenLayers API

(A) 地図の作成

G: google.maps.Map が地図を表す。
O: OpenLayers.Map は Layer のコンテナで、地図もひとつの Layer 。地図の画像がない Map も作れる。

(B) 地図の作成タイミング

G: 自分で EventListener の API を持っている。
O: DOM の addEventListener を使う。

(C) 地図を表示する要素との紐づけ

G: Map のコンストラクタには要素を指定する。
O: 要素の id を指定する。
OpenLayers.Map では Element も指定できるけど、その場合は Map#render() を呼ばないと表示されない。
Map#render() は引数に Element が必要なので、コンストラクタに要素を指定する意味が解らない。

(D) 中心の位置

どちらも、Map のコンストラクタか、Map#setCenter() で指定するのが基本。
ズーミングの調整をする (F) ので、必要がなくなっているけれども(後述)。
OpenLayers API では、座標は、緯度・経度で指定した後に、transform() で座標変換する必要がある。

ちなみに、座標は、それを扱うクラス名が表すように順序が違う。
G: 緯度、経度 : google.maps.LatLng
O: 経度、緯度 : OpenLayers.LonLat
OpenLayers API は、(x, y) ってことなのかな。

(E) スケールバー

「100m がこのくらい」という あの線。
G: Map のコンストラクタで指定する
O: Map のコントロールという位置づけ(回答の時点では気が付いてなくて、回答には書いてない)。
OpenLayers.Control.ScaleLine は、デフォルトで単位が二つ表示されて、ft なんかが表示されちゃう。

(F) 良い感じにズーム

G: Map#fitBounds() を使う
O: Map#zoomToExtent() を使う

google.maps.LatLngBounds にも extend() があったので、南西、北東の位置を求めて、なんてやらずに、OpenLayers API と同じようなコードにできる。
中心の位置も getCenter メソッドで取得できる(でも、fitBounds() で中心は適切に設定される)。
google.maps.LatLngBounds - Google Maps JavaScript API V3 Reference

(G) ズームの最大値を抑制

表示するマーカーがひとつだけだと、(F) でのズームが大きすぎてよく分からない地図になっちゃうので、ある値まででズームを止めたい。
G: Map のレンダリングが終わってから、ということらしい。ズームの値を調整した後は Listener を削除しておかないと、手動でのズーミングも制限されちゃう。
O: 普通に呼び出せる

(H) マーカーの作成

G: Map に紐づける
O: Map に紐づける Marker もあるけれど、クリックしたときの挙動やらなんやらをやるなら Feature.Vector を使う方が推奨らしい

(I) マーカー画像の指定

G: Marker のコンストラクタで指定する
O: StyleMap で一括で指定する。${text} という表現を使うと Vector の属性を展開できるので、スタイルを一括で指定してるけど、マーカーごとにスタイルを変えるということができる。

OpenLayers.StyleMap を使わずに、スタイルを直接 OpenLayers.Feature.Vector に指定することもできる。

    var overlay = new OpenLayers.Layer.Vector('Overlay');       // StyleMap の指定は要らない

    ...

      features.push(new OpenLayers.Feature.Vector(
          new OpenLayers.Geometry.Point(post.lng, post.lat)
          .transform(epsg4326, epsg3857), {
            post: post,
            // StyleMap で展開するための属性は要らない
          },
          {   // 第3引数で指定する
            externalGraphic: icon_map[ post.cat_id ],   // アイコン画像
            graphicWidth: 21,
            graphicHeight: 25,
            graphicYOffset: -25,
            cursor: "pointer",
            title: post.title,,     // ツールチップ
          }
      ));

マーカー画像は、その位置と画像の左上が一致するっぽい。
なので、回答のコードではちょっと位置がずれているはず。
graphicXOffset = -10 とか必要だったかな。

(J) マーカーのツールチップ

マーカーの画像と同様。

(K) 吹き出しの作成

G: インスタンスを作っただけでは表示されず、InfoWindow#open() で表示。
O: Map#addPopup() すると表示される。
なぜ、Layer ではなく Map にひもづくように設計されているのかは不明。

(L) マーカーをクリックしたときに、吹き出しの表示をトグル

G: Marker の click イベントで処理する。InfoWindow#open() / close() で切り替える
O: Layer.Vector の featureclick イベントで処理する。Map#addPopup() / #removePopup() で切り替える

Map のインスタンスが必要で、スコープが広い変数を使ったけれど、feature.layer.map で、Feature.Vector がひもづいている Map が手に入る。

回答では SelectFeature を使っているが、featureclick イベントだけを使う場合には必要がなかった。
SelectFeature は、サンプルでよく見かける featureselected, featureunselected を使う場合に必要。

(M) 吹き出しがマーカーを覆い隠す

FramedCloud は、指定した位置が吹き出しの髭の先端になり、地図領域の場所によって上にくるか下にくるかが自動で調整される。
地図の下側で吹き出しを表示すると、マーカーの上部に吹き出しが表示され、吹き出し (FramedCloud) の髭の部分がマーカーを覆い隠す。
そのため、マーカーの位置をクリックしても featureclick イベントが発火しない(吹き出しをクリックしてることになってる)。

z-index は、何段か親の方の Layer の要素に対して設定されているので、マーカーの z-index 大きくすることでは、この問題は解決できない。
回避方法としてはかっこ悪いが、吹き出しを常に下に表示することで、吹き出しの画像とマーカーが重ならないようにしている。

(N) 地図のスタイル

Google は、地図を表示する要素に以下のスタイルを埋め込む

#map {
    position: relative;
    overflow: hidden;
}

なので、地図を抱えるエリアが地図よりも気持ち小さいとかの場合でも それっぽく表示されるけど、OpenLayers API の場合は使う側に任されている。

(O) マーカーのクラスタリング

G: MarkerClusterer というのを使う。
O: なんか面倒そうで、回答時点で使うまでに至らず。

OpenLayers API には、OpenLayers.Strategy.Cluster なるものがある。
きっと、それを使うに違いない。

回答に書いたコードから変更したところ

比較がしやすいように、Google Maps APIOpenLayers API の差異以外に記述方法が違っているところは、少し合わせてみた。
後、なるべく横スクロールが出ないように、2TAB で変換して、長すぎる行は少し分割。

Google Maps API
  • 投稿データを持つ変数などもクロージャの中に入れる
  • マーカーの重心や範囲を求める処理を onload のハンドラの中に移動
OpenLayers API
  • ロード時の処理を無名関数で addEventListener の引数に指定
  • スコープが大きすぎる変数 map と select のスコープを狭める

その他に...

マウスホイールを使った拡縮

その後のやり取りで分かったのだけれど、OpenLayers は、マップ上にカーソルがあるときに、マウスホイールで地図の拡縮ができる(ctrl キーを押してなくても)。
ただ、効かないケースがあって、

  • Control.ScaleLine を Map#addControl() だと、マウスホイールの拡縮が効く
  • Control.ScaleLine を、Map のコンストラクタで指定すると、拡縮が効かない

変なの。

OpenLayers 3

OpenLayers 2 is outdated. Go to the latest 3.x version: http://openlayers.org/

Documentation – OpenLayers 2

なんと、OpenLayers は 2 は終息してて、これからは 3 を使えと。

回答するためにいろいろ検索しているときに、名前空間ol. なんてついてるのがあって、気にはなってたんだよ。
全ては、後のフェスティバル (´・ω・`)

参考情報

リンク
元のコードとか





  お腹いっぱい :-)