Open 18 和 Google Maps 之间的 mashup 让用户可以定位地图中的高尔夫球场目录中的位置。将此球场目录和球场细节页合并起来(并将低层代码 Ajax 化)
可以让您显示球场的细节信息而无需加载新页。将 Spring bean 和 Seam Remoting 相集成让您可以捕获 Google
Maps 位置标记的重定位并能将相关球场的经度和纬度存储到数据库中。如您所见,结果就是会产生所有高尔夫球员都喜欢使用的 Web 2.0
风格的应用程序,这真是让人印象深刻!
@Name("reasonPot") @Scope(ScopeType.SESSION) public class ReasonPotAction { private static String[] reasons = new String[] { "It's the quickest way to get /"rich/".", "It's the easiest way to get started with EJB 3.0.", "It's the best way to leverage JSF.", "It's the easiest way to do BPM.", "But CRUD is easy too!", "It makes persistence a breeze.", "Use annotations (instead of XML).", "Get hip to automated integration testing.", "Marry open source with open standards.", "It just works!" };
private Random randomIndexSelector = new Random();
@WebRemote public String drawReason() { return reasons[randomIndexSelector.nextInt(reasons.length)]; } }
清单 1 中的 Reason Pot 示例(“使用 Seam 的十大理由”)很有趣,但现在是时候该让 Seam Remoting
库大显身手了!将重点重新放到 “无缝集成 JSF,第 2 部分: 借助 Seam 进行对话”
中所介绍的 Open 18 应用程序。您可能记得 Open 18
是个高尔夫球场目录。用户可以浏览球场的列表并随后深入查看单个球场的细节。Open 18 还允许用户创建、更新和删除球场。在其 Web 1.0
版本中,用户和这个应用程序间的每个交互都会使页面重载。
借助 Ajax,可以有多种改进 Open 18 应用程序的方法,在我们继续之前,您可以自己尝试其中的一些方法。第一个可以做的事情是在 Open 18 和 Google Map 之间创建一个 mashup。
时下,若不采用 mapping 实现,在 Internet 就走不多远,而如果添加此特性,您的用户当然会非常高兴。将 Seam Remoting API 和 Google
Maps Geocoder API 结合起来让您可以定位 Google map 上的球场目录中的每个球场。
使用 Google Maps API
要使用 Google Maps API,您必须注册一个免费的 API key。Google 之所以要求申请这个 key 是为了维护有关 API
使用的统计数据和控制对此服务的滥用以便它能保持免费。申请这个 key 需要 Google 账号,但,根据服务条款,此 key
不能共享。为了遵守这些条款,我在示例中使用了一个假
key 。要在自己的计算机上运行 示例应用程序,需要用您自己的 key 替换字符串 GOOGLE_KEY。
地理空间绘制乍听起来需要很多技巧,但若 Google Maps
JavaScript API 能代您完成很多工作的话,那就另当别论了。GMap2 类可以绘制地图并负责视图端口中的滚动和缩放事件。另一个 Google Maps 类 GClientGeocoder 则基于地址字符串解析地理空间的坐标。您的工作只不过就是初始化地图并为每个球场添加标记。
要在地图上放上标记,首先要通过远端方法调用从服务器端组件获取球场集。接下来,用 GClientGeocoder
将每个球场的地址翻译成地理空间点(经度和纬度)。最后,使用该点来在地图的相应坐标上放置标记。作为一个额外的小特性,您还可以将编辑图标旁边的罗盘图
标装备在目录中的每行。当单击了目录行中的罗盘图标时,地图就会缩放直到所选球场出现在视图内。与此同时,此地图还会在标记上呈现一个气球,显示给定球场
的地名、地址、电话和 Web 站点。通过直接单击地图上的一个标记也可以弹出相同的气球。图 2 显示的是完成后的应用程序的预览:
Google Maps 很易于集成和嵌入到 Web 应用程序。正如我已经提到的,它负责了所有呈现地图的所有细节并会提供一个 API 来绘制地图上的地理空间位置。GClientGeocoder 对象担负所有解析地理空间位置和回送所需的经度和纬度数据这样的艰巨任务。
将
地址解析为地理空间点的方法存根与 Seam Remoting
方法存根的工作原理相同。当该方法被调用来获取返回值时,一个回调函数会传递给此方法。在那时,还会向 Google HQ 发送一个 Ajax
请求,而且当响应回至浏览器时,此回调函数会执行。Google Maps API
的智能界面让定位变得非常文字化,因为所基于的只有邮寄地址。您无需再为每个球场维护地理空间的坐标,那样只会增加混乱!这个 API
上的额外的例程之后会使用纬度和经度数据来为地图上的这些位置构建呈现标记。
定制它!
配置 Google map 的显示可用的方法很多。配置此地图本身并不是本文的重点所在,留给您自己尝试练习。有关信息,请参看 参考资料。您可能还会使用高层的 JavaScript 函数,而不是使用面向对象的结构来封装此逻辑。同样地,您尽可以按您自己的意愿自由定制代码。
与地图集成相关的 API 方法有两个:Geocoder.getLatLng() 和 GMap.addOverlay()。首先,Geocoder.getLatLng() 方法将地址字符串解析为一个 GLatLng 点。数据对象只用来包装经度和纬度值对。此方法的第二个实参是一个回调 JavaScript 函数,一旦与 Google
HQ 的通信完成,该函数即会执行。此函数继续执行以通过使用 GMap.addOverlay() 来将一个标记覆盖图添加到地图上。默认地,标记以红色的回形针表示。回形针的顶点指向地图上的地址位置。
清单 5 显示了设置 Google map 并向它添加标记的 JavaScript 代码。函数按执行顺序排列。除了新导入的 Google
Maps API 脚本之外,您还应该识别出 清单 3 中曾经用到的 Seam Remoting 脚本。
/** * Create a new GMap2 Google map and add markers (pins) for each of the * courses. */ function initializeMap() { if (!GBrowserIsCompatible()) return; gmap = new GMap2(document.getElementById('map')); gmap.addControl(new GLargeMapControl()); gmap.addControl(new GMapTypeControl()); // center on the U.S. (Lebanon, Kansas) gmap.setCenter(new GLatLng(38.2, -95), 4); geocoder = new GClientGeocoder(); GEvent.addDomListener(window, 'unload', GUnload); addCourseMarkers(); }
/** * Retrieve the collection of courses from the server and add corresponding * markers to the map. */ function addCourseMarkers() { function onResult(courses) { for (var i = 0, len = courses.length; i < len; i++) { addCourseMarker(courses[i]); }
/** * Resolve the coordinates of the course to a GLatLng point and adds a marker * at that location. */ function addCourseMarker(course) { var address = course.getAddress(); var addressAsString = [ address.getStreet(), address.getCity(), address.getState(), address.getPostalCode() ].join(" "); geocoder.getLatLng(addressAsString, function(latlng) { createAndPlaceMarker(course, latlng); }); }
/** * Instantiate a new GMarker, add it to the map as an overlay, and register * events. */ function createAndPlaceMarker(course, latlng) { // skip adding marker if no address is found if (!latlng) return; var marker = new GMarker(latlng); // hide the course directly on the marker marker.courseBean = course; markers[course.getId()] = marker; gmap.addOverlay(marker);
function showDetailBalloon() { showCourseInfoBalloon(this); }
<h:graphicImage value="/images/compass.png" alt="[ Zoom ]" title="Zoom to course on map" |-------10--------20--------30--------40--------50--------60--------70--------80--------9| |-------- XML error: The previous line is longer than the max of 90 characters ---------| onclick="focusMarker(#{_course.id}, true);" />
/** * Bring the marker for the given course into view and display the * details in a balloon. This method is registered in an onclick * handler on the compass icons in each row in the course directory. */ function focusMarker(courseId, zoom) { if (!GBrowserIsCompatible()) return; if (!mapIsInitialized) { alert("The map is still being initialized. Please wait a moment and try again."); return; } var marker = markers[courseId]; if (!marker) { alert("There is no corresponding marker for the course selected."); return; }
showCourseInfoBalloon(marker); if (zoom) { gmap.setZoom(13); } }
@Name("courseAction") @Scope(ScopeType.CONVERSATION) public class CourseAction implements Serializable { /** * During a remote call, the FacesContext is <code>null</code>. * Therefore, you cannot resolve this Spring bean using the * delegating variable resolver. Hence, the required flag tells * Seam not to complain. */ @In(value="#{courseManager}", required=false) private GenericManager<Course, Long> courseManager;
function addCourseMarker(course) { var address = course.getAddress(); if (course.getPoint() != null) { var point = course.getPoint(); var latlng = new GLatLng(point.getLatitude(), point.getLongitude()); createAndPlaceMarker(course, latlng); } else { var addressAsString = [ address.getStreet(), address.getCity(), address.getState(), address.getPostalCode() ].join(" "); geocoder.getLatLng(addressAsString, function(latlng) { createAndPlaceMarker(course, latlng); }); } }
function createAndPlaceMarker(course, latlng) { // skip adding marker if no address is found if (!latlng) return; var marker = new GMarker(latlng, { draggable: true }); // hide the course directly on the marker marker.courseBean = course; markers[course.getId()] = marker; gmap.addOverlay(marker);
function showDetailBalloon() { showCourseInfoBalloon(this); }
function assignPoint() { var point = Seam.Remoting.createType("com.ibm.dw.open18.Point"); point.setLatitude(this.getPoint().lat()); point.setLongitude(this.getPoint().lng()); var courseActionStub = Seam.Component.getInstance("courseAction"); courseActionStub.setCoursePoint(this.courseBean.getId(), point); }
正
如之前所讨论的,我最初将 Spring 容器集成到 Seam
中所采用的变量解析器方法有其自身的局限。坦白地讲,该方法己经发展到了尽头,现在该是和它说再见的时候了。大多数的 Seam 特性均涉及到了
JSF,而 Seam 的某些属性却工作于 JSF 生命周期之外。要获得与 Spring
的真正集成,需要比定制变量解析器更好的解决方案。所幸的是,Seam 的开发人员已经开始着手解决这种需求,并添加了针对 Spring 的
Seam 扩展。Spring 集成包利用了 Spring 2.0 中的新特性来创建基于模式的扩展点。这些扩展在 bean
定义文件和名称空间处理程序中启用了定制 XML 名称空间,在 Spring 容器的启动过程中对这些标记进行操作。
用于 Spring 的 Seam 名称空间处理程序(您可能需要大声读几遍才能明白其中的含义)有几种与 Spring
容器进行交互的方式。要去除定制的变量解析器,需要将 Spring bean 作为 Seam 组件公开。seam:component 标记可专门用于此目的。将此标记放置于 Spring bean 声明之内会通知 Seam Spring bean 应该被代理或包裹成一个 Seam 组件。在这一点上,Seam
双射机制会将这个 bean 视为仿佛它已经被 @Name 注释,只有现在,您才能不需要 FacesContext 来填补 Seam 和 Spring 间的差距!
配置 Seam-Spring 集成异常容易。第一步都完全无需涉及任何代码更改!所有您需要做的只是确保使用了 Spring 2.0 和 Seam 1.2.0 或后续发布。(自本系列的第一篇文章发表以来,Seam 已经有了迅速发展,所以应该准备好进行 一次升级。)IOC 集成打包成一个单独的 JAR,jboss-seam-ioc.jar,所以除了已经提到的 JAR 之外,应该将它也包含到应用程序的类路径中。
第
二个步骤也不会涉及到 Seam 配置。这次,您看到的是 Spring 配置。首先,向其中定义了 bean 的 Spring 配置 XML 添加
Seam XML 模式声明。这个文件的标准名称是 applicationContext.xml,但示例应用程序使用了一个更为合适的名字
spring-beans.xml。接下来,在任何您想要公开给 JSF 的 Spring
bean 的定义内添加 seam:component 标记。在 Open 18 应用程序中,将此标记嵌套在 courseManager bean 定义内。要查看整个 bean 列表,请参考本文 示例应用程序 的 spring-beans.xml 文件。