Google Maps For Rails - updating markers via ajax only when changing search results

I worked on writing a small application with a lot of help from stackoverflow. The basic premise is simple, and I have seen such functionality all over the Internet: I am trying to build a list of locations on a Google search / customized map. Places are stored on the backend, and the controller passes these locations to the view. AJAX is involved because I do not want to reload the entire page. Here are the scenarios: a) A user searches for a location via zipcode => map, loads a new location, a search request is sent to the server, and the map loads any markers, if there is any of the set radii, the map sets the default zoom level; b) Custom panning / zooming around => the map remains where it was left, the search with the bounding box of the viewport is sent to the server and the results are compared. The map will be the default for Seattle at boot time, and the first thing it tries is to bind the user to geolocation ...

Using the gmaps4ails wiki page and basically a modified version of the answer to this question: Google Maps for Rails - update markers using AJAX . I am very close. It works, in fact, just with a hitch. Here's 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; })(); 

There is a problem here, and it is also related to this specific example, like my seeming misunderstanding of how javascript works (I'm new to it). When the user rotates and zooms in, but the search result is the same, I would prefer NOT to call the resetMarkers function, but rather leave the map alone. Currently, the map will always reload Markers / sidebar / etc, and this causes a little flickering of markers on the screen.

I tried several different versions of this, but it does not 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 retain its value over the life of the page (we use it to remove old markers before setting new ones), I thought I could just create another variable to test it. However, this is always false, even if I think it should be true.

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

Or is there something that I am missing completely and should do instead?

+1
source share
1 answer

Your problem is comparing both token lists to decide whether you should update or not.

The fact is that although _.isEqual(__oldmarkers, __markers) does a deep comparison, there may be things in your list of Marker instances that change even for identical points (id, timestamps, ...).
Or maybe it's just because in the beginning both __markers and __oldMarkers are null , so they are equal, which means you never __oldMarkers up in an if block.

In any case, I think that in-depth comparison here may become too costly. Instead, I will compare things that are easily comparable, such as a flat list of coordinates for each set of markers.

Something like that:

 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 String arrays that you need to quickly compare and give the expected results.
In an ordered comparison using _.isEqual both arrays are sorted in advance.

NB: old code used _.difference, but it was wrong (see discussion in comments)
(Note that I use _.difference , although it may be more expensive than _.isEqual , but with a bonus that does not depend on the order of the returned tokens.)

edit: Oh, and of course you can stop sending this "oldHash" to your search query parameters now;)

+2
source

Source: https://habr.com/ru/post/1266543/


All Articles