Google Maps For Rails - 仅在搜索结果发生变化时通过 ajax 更新标记

Google Maps For Rails - updating markers via ajax only when search result changes

在 Whosebug 的大力帮助下,我一直在编写一个小应用程序。基本前提很简单,我在整个网络上都看到过这种功能:我正在尝试将位置列表绘制到 searchable/pannable google 地图上。位置存储在后端,控制器将这些位置提供给视图。 AJAX 是因为我不想重新加载整个页面。以下是场景:a) 用户通过邮政编码搜索位置 => 地图加载新位置,将搜索结果发送到服务器,如果设置半径内有任何标记,地图将加载任何标记,地图设置默认缩放级别; b) User pans/zooms around => map 停留在用户离开的地方,带有视口边界框的搜索被发送到服务器并映射结果。该地图将在初始加载时默认为西雅图,它尝试的第一件事是对用户进行地理定位...

使用 gmaps4ails wiki 和这个问题答案的主要修改版本: 我已经非常接近了。实际上,它的工作原理很简单。这是它的样子:

sightings_controller.rb

  def search
    if params[:lat]
      @ll = [params[:lat].to_f, params[:lng].to_f]
      @sightings = Sighting.within(5, origin: @ll).order('created_at DESC')
      @remap = true
    elsif search_params = params[:zipcode]
      geocode = Geokit::Geocoders::GoogleGeocoder.geocode(search_params)
      @ll = [geocode.lat, geocode.lng]
      @sightings = Sighting.within(5, origin: @ll).order('created_at DESC')
      @remap = true
    elsif params[:bounds]
      boundarray = params[:bounds].split(',')
      bounds = [[boundarray[0].to_f, boundarray[1].to_f], [boundarray[2].to_f, boundarray[3].to_f]]
      @ll = [params[:center].split(',')[0].to_f, params[:center].split(',')[1].to_f]
      @sightings = Sighting.in_bounds(bounds, origin: @ll).order('created_at DESC')
      @remap = false
    else
      search_params = '98101'
      geocode = Geokit::Geocoders::GoogleGeocoder.geocode(search_params)
      @ll = [geocode.lat, geocode.lng]
      @sightings = Sighting.within(5, origin: @ll).order('created_at DESC')
      @remap = true
    end
    @hash = Gmaps4rails.build_markers(@sightings) do |sighting, marker|
      marker.lat sighting.latitude
      marker.lng sighting.longitude
      marker.name sighting.title
      marker.infowindow view_context.link_to("sighting", sighting)
    end
    respond_to do |format|
      format.html
      format.js
    end
  end

search.html.haml

= form_tag search_sightings_path, method: "get", id: "zipform", role: "form", remote: true do
  = text_field_tag :zipcode, params[:zipcode], size: 5, maxlength: 5, placeholder: "zipcode", id: "zipsearch"
  = button_tag "Search", name: "button"
  %input{type: "button", value: "Current Location", onclick: "getUserLocation()"}
#locationData

.sightings_map_container
 .sightings_map_canvas#sightings_map_canvas
   #sightings_container

- content_for :javascript do
  %script{src: "//maps.google.com/maps/api/js?v=3.13&sensor=false&libraries=geometry", type: "text/javascript"}
  %script{src: "//google-maps-utility-library-v3.googlecode.com/svn/tags/markerclustererplus/2.0.14/src/markerclusterer_packed.js", type: "text/javascript"}

  :javascript
    function getUserLocation() {
      //check if the geolocation object is supported, if so get position
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(setLocation);
      }
      else {
        document.getElementById("locationData").innerHTML = "Sorry - your browser doesn't support geolocation!";
      }
    }

    function setLocation(position) {
      //build text string including co-ordinate data passed in parameter
      var displayText = "Latitude: " + position.coords.latitude + ", Longitude: " + position.coords.longitude;

      //display the string for demonstration
      document.getElementById("locationData").innerHTML = displayText;
      //submit the lat/lng coordinates of current location
      $.get('/sightings/search.js',{lat: position.coords.latitude, lng: position.coords.longitude});
    }
    // build maps via Gmaps4rails
    handler = Gmaps.build('Google');
    handler.buildMap({
      provider: {
      },
      internal: {
      id: 'sightings_map_canvas'
      }
    },

    function() {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(setLocation);
      }

      var json_array = #{raw @hash.to_json};
      var latlng = #{raw @ll};

      resetMarkers(handler, json_array);
      resetMap(handler, latlng);

      // listen for pan/zoom and submit new coordinates
      (function gmaps4rails_callback() {
        google.maps.event.addListener(handler.getMap(), 'idle', function() {
          var bounds = handler.getMap().getBounds().toUrlValue();
          var center = handler.getMap().getCenter().toUrlValue();
          $.get('/sightings/search.js',{bounds: bounds, center: center, old_hash: #{raw @hash.to_json}});
        })
      })();
    });

search.js.erb

(function() {
  var json_array = <%= raw @hash.to_json %>;
  if (<%= @remap %>) {
    var latlng = <%= raw @ll %>;
    resetMarkers(handler, json_array);
    resetMap(handler, latlng);
  }
  else {
    resetMarkers(handler, json_array);
  }
})();

map.js

(function() {

  function createSidebarLi(json) {
    return ("<li><a>" + json.name + "</a></li>");
  };

  function bindLiToMarker($li, marker) {
    $li.on('click', function() {
      handler.getMap().setZoom(18);
      marker.setMap(handler.getMap()); //because clusterer removes map property from marker
      google.maps.event.trigger(marker.getServiceObject(), 'click');
    })
  };

  function createSidebar(json_array) {
    _.each(json_array, function(json) {
      var $li = $( createSidebarLi(json) );
      $li.appendTo('#sightings_container');
      bindLiToMarker($li, json.marker);
    });
  };

  function clearSidebar() {
    $('#sightings_container').empty();
  };

  function clearZipcode() {
    $('#zipform')[0].reset();
  };

  /* __markers will hold a reference to all markers currently shown
  on the map, as GMaps4Rails won't do it for you.
  This won't pollute the global window object because we're nested
  in a "self-executed" anonymous function */

  var __markers;

  function resetMarkers(handler, json_array) {
    handler.removeMarkers(__markers);
    clearSidebar();
    clearZipcode();
    if (json_array.length > 0) {
      __markers = handler.addMarkers(json_array);
      _.each(json_array, function(json, index){
        json.marker = __markers[index];
      });
      createSidebar(json_array);
    }
  };

  function resetMap(handler, latlng) {
    handler.bounds.extendWith(__markers);
    handler.fitMapToBounds();
    handler.getMap().setZoom(12);
    handler.map.centerOn({
      lat: latlng[0],
      lng: latlng[1]
    });
  }

// "Publish" our method on window. You should probably have your own namespace
  window.resetMarkers = resetMarkers;
  window.resetMap = resetMap;

})();

这就是问题所在,这个具体示例与我似乎对 javascript(我是新手)变量的工作方式的误解一样重要。当用户平移和缩放时,但搜索结果相同时,我宁愿不调用 "resetMarkers" 函数,而宁愿只留下地图。地图目前总是 resetMarkers/sidebar/etc,这会导致屏幕上的标记有点闪烁。

我试过几个不同的版本,但都不行。在 map.js 中:

var __markers;
var __oldmarkers;
function resetMarkers(handler, json_array) {
  if(!(_.isEqual(__oldmarkers, __markers))) {
    handler.removeMarkers(__markers);
    clearSidebar();
    clearZipcode();
    if (json_array.length > 0) {
      __markers = handler.addMarkers(json_array);
      _.each(json_array, function(json, index){
        json.marker = __markers[index];
      });
      createSidebar(json_array);
    }
    __oldmarkers = __markers.slice(0);
  }
};

因为 __markers 似乎在页面的整个生命周期中保持它的值(我们在设置新标记之前用它来删除旧标记),我想我可以简单地创建另一个变量来检查它.然而,即使我认为它应该是真的,它也总是错误的。

我尝试过的另一件事是重新提交旧哈希作为每个搜索请求的参数,然后设置一个标志,但这看起来很复杂,而且 string/hash/array 操作变得如此混乱我放弃了。我真的不认为这是最好的方法,但也许我应该那样做?

或者,是否有什么我完全遗漏了而应该做的事情?

你的问题在于比较两个标记列表来决定你是否应该更新。

问题是,尽管 _.isEqual(__oldmarkers, __markers) 确实执行了深度比较,但列表中的标记实例中的某些内容可能会发生变化,即使对于相同的点(id、时间戳...)也是如此。
或者可能只是因为在开始时,__markers__oldMarkers 都是 null,因此相等,这意味着你永远不会进入 if 块。

无论如何,我认为这里的深度比较可能会变得太昂贵。相反,我会做的是比较容易比较的东西,比如每组标记的平面坐标列表。

像这样:

var __markers, __coordinates = [];
function resetMarkers(handler, json_array) 
{
  var coordinates = _.map(json_array, function(marker) {
    return String(marker.lat) + ',' + String(marker.lng);
  });

  if(_.isEqual(__coordinates.sort(), coordinates.sort()))
  {
    handler.removeMarkers(__markers);
    clearSidebar();
    clearZipcode();
    if (json_array.length > 0) 
    {
      __markers = handler.addMarkers(json_array);
      _.each(json_array, function(json, index){
        json.marker = __markers[index];
      });
      createSidebar(json_array);
    }
    __coordinates = coordinates;
  }
};

这里 __coordinatescoordinates 只是 String 的平面数组,应该快速比较它们并给出预期的结果。
为了使用 _.isEqual 进行比较,两个数组都预先排序。

注意:使用了旧代码 _.difference 但那是不正确的(请参阅评论中的讨论)
(请注意,我使用的是 _.difference,可能比 _.isEqual 更昂贵,但具有独立于返回的标记顺序的好处。)

编辑:哦,当然你现在可以停止在搜索查询参数中发送 "oldHash" ;)