Google Maps For Rails - updating markers via ajax

2019-05-31 16:28发布

I have been working through writing a small app with a lot of help from stackoverflow. The basic premise is simple, and I've seen this sort of functionality all over the web: I am trying to plot a list of locations onto a searchable/pannable google map. The locations are stored on the backend, and the controller feeds these locations to the view. AJAX is involved because I don't want to reload the entire page. Here are the scenarios: a) User searches for a location via zipcode => map loads new location, a search get is sent to server and map loads any markers if there are any within set radius, map sets a default zoom level; b) User pans/zooms around => map stays wherever the user has left it, a search with the viewport bounding box is sent to the server and the results are mapped. The map will default to Seattle on initial load and the first thing it tries is to geolocate the user...

Using the gmaps4ails wiki and mostly a modified version of the answer from this question: Google Maps for Rails - update markers with AJAX I have gotten very close. It works, actually, just with a hitch. Here is what it looks like:

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;

})();

Here's the problem, and it is as much to do with this specific example as my seeming misunderstanding of how javascript (I'm new to it) variables work. When the user pans and zooms, but the search result is the same, I would prefer NOT to call the "resetMarkers" function, but would rather just leave the map alone. The map will currently always resetMarkers/sidebar/etc and this causes a little bit of a flicker of markers on the screen.

I've tried several different versions of this, but doesn't work. In 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);
  }
};

Since __markers seems to hold its value through the life of the page (we use it to remove the old markers before we set new ones), I thought I could simply create another variable to check against it. However, it's always false even when I think it should be true.

Another thing I've tried is to resubmit the old hash as a parameter with every search request and then set a flag, but this seems complicated, and the string/hash/array manipulation got so confusing I gave up. I don't really think this would be the best approach, but perhaps I should do it that way?

Or, is there something I am missing completely and should be doing instead?

1条回答
2楼-- · 2019-05-31 17:06

Your problem lies in comparing both lists of markers to decide whether you should update or not.

The thing is, although _.isEqual(__oldmarkers, __markers) does perform a deep comparison, there might be things in Marker instances within your list that change even for identical points (an id, timestamps, ...).
Or perhaps it's simply because at the start, both __markers and __oldMarkers are null, thus equal, meaning you never get inside the ifblock.

Anyway, I think deep comparison here could become too costly. What I'd do instead, is compare things that are comparable easily, like the flat list of coordinates for each set of markers.

Something like this:

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;
  }
};

Here __coordinates and coordinates are just flat arrays of String, which should be compared quickly and give you the expected results.
In ordered to be compared using _.isEqual, both arrays are sorted beforehand.

NB: Old code used _.difference but that wasn't correct (see discussion in comments)
(Note that I'm using _.difference though, probably more costly than _.isEqual but with the bonus of being independent of the returned markers order.)

edit: Oh and of course you can stop sending that "oldHash" in the search query params now ;)

查看更多
登录 后发表回答