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;
}
};
这里 __coordinates
和 coordinates
只是 String 的平面数组,应该快速比较它们并给出预期的结果。
为了使用 _.isEqual
进行比较,两个数组都预先排序。
注意:使用了旧代码 _.difference 但那是不正确的(请参阅评论中的讨论)
(请注意,我使用的是 _.difference
,可能比 _.isEqual
更昂贵,但具有独立于返回的标记顺序的好处。)
编辑:哦,当然你现在可以停止在搜索查询参数中发送 "oldHash" ;)
在 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;
}
};
这里 __coordinates
和 coordinates
只是 String 的平面数组,应该快速比较它们并给出预期的结果。
为了使用 _.isEqual
进行比较,两个数组都预先排序。
注意:使用了旧代码 _.difference 但那是不正确的(请参阅评论中的讨论)
(请注意,我使用的是 _.difference
,可能比 _.isEqual
更昂贵,但具有独立于返回的标记顺序的好处。)
编辑:哦,当然你现在可以停止在搜索查询参数中发送 "oldHash" ;)