在日常生活中,我们都离不开位置识别类应用程序。Foursquare、Facebook等应用程序帮助我们和我们的家人朋友分享当前位置(或者正在参观的景点)。而像Google Local这样的应用则帮助我们找到当前位置附近有哪些自己需要的服务设施或业务场所。如此,如果我们需要找到一家离自己最近的咖啡厅,完全可以通过Google Local快速获取建议并立刻动身前往。这不仅大大方便了日常生活,还能够帮助企业将自己的产品推销给更理想的受众群体。无论是对消费者还是对企业,这都堪称完美的双赢局面。
要创建这样一款应用程序,大家首先需要获取用户的地理位置信息。根据维基百科的解释,“地理信息是指某个对象所处的现实地理位置”。就目前来看,Web应用程序中还没有出现标准化的用户地理位置获取方式。虽然Google Gears这样的开源库能够从用户处获取位置信息,但这套库已经停止发展、只能运行在旧版本浏览器当中而且不支持W3C地理位置API。W3C GeoLocation API提供了一套规范,能够通过标准化脚本访问与托管设备相关的地理信息。Geo Location并不提供对HTML 5的官方支持,但这仍然无法阻止人们的热情,而且我们经常听说开发人员将GeoLocation API与HTML 5相对接。该API以用户所收集的地理信息为基础建立抽象层,从而保证所有浏览器都支持地理定位API。大家可以访问http://caniuse.com/#feat=geolocation获取下列图表。
应用程序用例——找工作应用
在本文中,我们将创建一款能够感知地理位置的找工作应用。应用程序将根据用户的特定技能(例如Java、Scala以及MongoDB等)寻找最近的求职地点。应用将利用W3C GeoLocation API实现用户定位。接下来,应用程序将用户位置绘制在谷歌地图当中。大家可以访问http://localjobshtml5-cix.rhcloud.com/获取这款应用。用户图标对应用户当前地理位置,公文包图标则对应目标求职地点。
如果大家点击任何公文包图标,地图会如下图所示自动放大。而当我们关闭信息窗口,画面会再次缩小。另外,大家可以在标记中查看求职场所与当前位置之间的距离、对应职务以及其它相关资料。用户位置与工作位置之间的距离由MongDB的地理空间功能所支持,我们会在后面的文章中进一步讨论这个话题。
应用程序技术堆栈
这款应用的创建需要使用以下技术堆栈:
Java EE 6 : 我们将使用数项Java EE 6规范——JAX-RS以及CDI。JAX-RS属于针对Restful Web服务的Java API,其作用在于根据REST架构模式为网络服务创建提供Java API。CDI则是Context and Dependency Injection(背景与关联性注入)的缩写。CDI允许开发者将Java EE组件与生命周期背景进行绑定、注入,而后通过事件触发与观察机制以松散的耦合方式实现交互。
MongoDB : MongoDB是一套面向文档的NoSQL数据存储机制。我们将把工作数据保存在MongoDB当中并在应用程序中使用其地理空间功能。
HTML 5 : 我们将利用HTML 5创建应用程序客户端,并利用W3C GeoLocation API获取用户的当前位置。
谷歌地图 : 应用程序将利用谷歌地图来处理用户位置以及求职信息。
OpenShift : 应用程序将被部署到OpenShift公共PaaS当中。
应用程序源代码
这款应用程序的源代码被发布在GitHub当中,地址为:https://github.com/shekhargulati/localjobshtml5
前续条件
在我们着手创建应用程序之前,首先需要进行以下几项设置任务:
1. 注册一个OpenShift账户。账户注册完全免费,而且红帽将为每位用户免费提供三套Gear用于运行应用程序。截至本文截稿时,该账户可以获得1.5GB内存容量与3GB磁盘存储空间。
2. 在设备上安装rhc客户工具。rhc是一套ruby gem包,因此大家需要在设备上安装ruby 1.8.7或者更高版本。要安装rhc,大家需输入:
sudo gem install rhc
如果当前已经安装过ruby,请确保其处于最新版本。要更新rhc工具,请执行如下所示命令:
sudo gem update rhc
如需其它相关rhc命令行工具设置说明,请点击下列网址查看相关资料:https://openshift.redhat.com/community/developers/rhc-client-tools-install
1 利用rhc setup命令设置OpenShift账户。这条命令将帮助大家创建一个命名空间并将自己的ssh密钥上传至OpenShift服务器。
#p#
开始创建应用程序
现在我们已经完成了全部前续设置工作,现在开始创建应用程序。我们将从创建OpenShift应用程序开始。在与PaaS协作时,大家首先需要明确一点:PaaS是用来创建应用程序的。因此,现在我们要摆脱过去以虚拟机或者服务器为中心的理念,将全部精力集中在应用程序身上。
创建JBossEAP MongoDB OpenShift应用程序
要创建名为“localjobs”且使用JBossEAP与MongDB的应用程序,我们首先要执行以下命令:
rhc app create localjobs jbosseap mongodb-2.2
这将为我们创建一套应用程序容器,也就是所谓gear,并为其配置全部必要的SELinux政策以及cgroup配置。OpenShift还将为我们设置一个私有git库,并将该库克隆到本地系统当中。最后,OpenShift会将DNS发送至外部环境。大家可以通过http://localjobs-domain-name.rhcloud.com访问该应用。将其中的域名替换为您自己的独特域名即可。
上述命令将创建一套标准化Maven项目模板。有趣的是,在pom.xml文件中存在一段名为openshift的配置信息,如下所示。因此,当大家将自己的源代码推送至OpenShift时,该Maven配置文件将付诸执行。该配置文件不会引发任何影响——而只是创建一个名为ROOT的war文件,从而保证我们的应用程序可用于root背景之下。
<profiles>
<profile>
<id>openshift</id>
<build>
<finalName>localjobs</finalName>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<outputDirectory>deployments</outputDirectory>
<warName>ROOT</warName>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
接下来,我们将把index.html与snoop.jsp两个文件从自己的git库中移除——它们的历史使命已经完成。如果大家不太熟悉git的运作方式,请点击此处阅读由Lars Vogel撰写的上手指南。
git rm -f src/main/webapp/index.html src/main/webapp/snoop.jsp
git commit -am "deleted template files"
添加MongoDB Java驱动程序关联性
由OpenShift创建的pom.xml文件已经拥有全部与Java EE 6相关的关联性。为了使用MongoDB,我们还需要添加MongoDB Java驱动关联性。我使用的是MongoDB Java驱动的最新版本。将下列关联性内容添加到pom.xml文件当中。大家可以点击此处在github上查看完整的pom.xml文件。
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.10.1</version>
</dependency>
启用CDI
CDI代表背景与关联性注入。之所以在应用程序中使用CDI,是因为我们需要利用关联性注入来代替手动创建对象。CDI容器将管理bean生命周期,这样我们作为开发者只需要编写业务逻辑即可。为了让JBossEAP应用程序服务器了解到我们正在使用CDI,我们需要在WEB-INF文件夹下创建一个beans.xml文件。该文件可以保持空白,但它的存在会使容器了解到需要加载CDI框架。Beans.xml文件的内容如下所示:
<?xml version="1.0"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://jboss.org/schema/cdi/beans_1_0.xsd"/>
编写MongDB数据库连接类
接下来,我们将创建一个应用程序作用域bean,用于管理MongoDB数据库连接。该连接类同时起效于本地系统与OpenShift端。大家可以点击此处在github中查看该类的完整内容。
@ApplicationScoped
public class DBConnection {
private DB mongoDB;
@PostConstruct
public void afterCreate() {
System.out.println("just see if we can say anything");
String host = System.getenv("OPENSHIFT_MONGODB_DB_HOST");
if (host == null || "".equals(host)) {
// Create Local MongoDB Connection
} else {
String mongoport = System.getenv("OPENSHIFT_MONGODB_DB_PORT");
String user = System.getenv("OPENSHIFT_MONGODB_DB_USERNAME");
String password = System.getenv("OPENSHIFT_MONGODB_DB_PASSWORD");
String db = System.getenv("OPENSHIFT_APP_NAME");
int port = Integer.decode(mongoport);
Mongo mongo = null;
try {
mongo = new Mongo(host, port);
} catch (UnknownHostException e) {
System.out.println("Couldn't connect to Mongo: "
+ e.getMessage() + " :: " + e.getClass());
}
mongoDB = mongo.getDB(db);
if (mongoDB.authenticate(user, password.toCharArray()) == false) {
System.out.println("Failed to authenticate DB ");
}
}
}
@Produces
public DB getDB() {
return mongoDB;
}
}
在应用程序运行过程中,@ApplicationScoped bean将始终存在,并在应用程序关闭的同时被删除。这正是我们希望通过MongoDB驱动所达到的连接池对象保留效果。
编写RESTful后端
现在我们开始利用JAX-RS为自己的应用程序编写RESTful后端。我们将通过创建一个用于扩展javax.ws.rs.ApplicationPath的类激活JAX-RS。大家需要指定一条基础url,并将其作为网络服务的访问地址。要实现这一目的,我们需要利用ApplicationPath注释为这个类添加注释。如下列代码所示,我利用“/api”作为基础URL:
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/api")
public class JaxRsActivator extends Application {
/* class body intentionally left blank */
}
在成功激活了JAX-RS之后,我们现在可以编写自己的REST服务。大家可以访问http://localjobs-domain-name/api/jobs/{skills}?longitude={longitude}&latitude={latitude}以查看REST端点。该REST端点将搜寻周边经纬度范围内全部与求职者技能相符的工作岗位。
@Path("/jobs")
public class JobsRestService {
@Inject
private DB db;
@GET
@Path("/{skills}")
@Produces(MediaType.APPLICATION_JSON)
public List<Job> allJobsNearToLocationWithSkill(
@PathParam("skills") String skills,
@QueryParam("longitude") double longitude,
@QueryParam("latitude") double latitude) {
String[] skillsArr = skills.split(",");
BasicDBObject cmd = new BasicDBObject();
cmd.put("geoNear", "jobs");
double lnglat[] = { longitude, latitude };
cmd.put("near", lnglat);
cmd.put("num", 10);
BasicDBObject skillsQuery = new BasicDBObject();
skillsQuery.put("skills",
new BasicDBObject("$in", Arrays.asList(skillsArr)));
cmd.put("query", skillsQuery);
cmd.put("distanceMultiplier", 111);
CommandResult commandResult = db.command(cmd);
BasicDBList results = (BasicDBList)commandResult.get("results");
List<Job> jobs = new ArrayList<Job>();
for (Object obj : results) {
Job job = new Job((BasicDBObject)obj);
jobs.add(job);
}
return jobs;
}
}
上面所示的代码会创建一条MongoDB附近位置查询,其结果文件数量被限制为10个。MongoDB返回的结果将作为数据中的数值。由于我们利用经度与纬度进行定位,返回的数据也以经纬度为基础。不过MongoDB还提供一套距离换数选项,允许我们将经纬度结果换算成更易理解的公里或者英里。在上面的代码中,我将经纬度结果转换为111公里。最后,我们将数据转换为一个名为Job的域对象并将其返回。@Produces注释将负责将数据转换至JSON当中。
#p#
将数据载入至MongoDB当中
执行下列命令将数据载入至运行在OpenShift gear中的MongoDB。
在本地设备上,运行rhc app show。这条命令将返回应用程序的详细信息,如下所示:
$ rhc app show -a localjobs
localjobs @ http://localjobs-newideas.rhcloud.com/ (uuid: 5195d8fe5973ca386f000083)
-----------------------------------------------------------------------------------
Created: 12:45 PM
Gears: 1 (defaults to small)
Git URL: ssh://5195d8fe5973ca386f000083@localjobs-newideas.rhcloud.com/~/git/localjobs.git/
SSH: 5195d8fe5973ca386f000083@localjobs-newideas.rhcloud.com
jbosseap-6.0 (JBoss Enterprise Application Platform 6.0)
--------------------------------------------------------
Gears: Located with mongodb-2.2
mongodb-2.2 (MongoDB NoSQL Database 2.2)
----------------------------------------
Gears: Located with jbosseap-6.0
Connection URL: mongodb://$OPENSHIFT_MONGODB_DB_HOST:$OPENSHIFT_MONGODB_DB_PORT/
Database Name: localjobs
Password: qySukKdKrZQT
Username: admin
记录下SSH URL并利用scp命令将jobs-data.json文件复制到我们的应用程序gear当中。大家可以点击此处下载jobs-data.json文件。
$ scp jobs-data.json <ssh url>:app-root/data
接着将SSH插入到应用当中,使用如下所示的rhc app ssh命令:
$ rhc app ssh -a localjobs
将ssh导入至应用程序gear中后,将目录变更为app-root/data,也就是我们复制jobs-data.json文件的目录。
$ cd app-root/data
下面运行mongoimport命令将数据导入至MongoDB数据库当中。
$ mongoimport -d localjobs -c jobs --file jobs
data.json -u $OPENSHIFT_MONGODB_DB_USERNAME -p
$OPENSHIFT_MONGODB_DB_PASSWORD -h
$OPENSHIFT_MONGODB_DB_HOST -port
$OPENSHIFT_MONGODB_DB_PORT
上面显示的代码将把159个job对象导入至MongoDB当中。
最后,我们需要在工作集合中创建地理位置索引。MongoDB只支持二维地理位置索引。大家只能为每个集合匹配一套地理位置索引。在默认情况下,二维地理位置索引假设经度与纬度数值在-180(含180)到180(不含180)之间(即[-180,180])。要创建地理信息索引,需要执行下列命令:
$ mongo
$ use localjobs
$ db.jobs.ensureIndex({"location" : "2d"})
测试RESTful服务
下面,我们将提供源代码并向OpenShift推送变更内容,即创建项目、创建新的war文件并将其部署到运行在OpenShift上的JBossEAP当中。
$ git add .
$ git commit -am "RESful backend done"
$ git push
在代码创建与war文件部署工作完成后,我们就可以利用curl命令对REST服务进行测试了。
curl -i -H "Accept: application/json" http://localjobs-newideas.rhcloud.com/api/jobs/java,scala?longitude=-121.894955&latitude=37.339386
HTTP/1.1 200 OK
Date: Fri, 17 May 2013 08:39:11 GMT
Server: Apache-Coyote/1.1
Content-Type: application/json
Vary: Accept-Encoding
Transfer-Encoding: chunked
[{"companyName":"CyberCoders","jobTitle":"Embedded Java Applications Engineer","distance":4153.025944882882,"skills":["java"],"formattedAddress":"1400 North Shoreline Boulevard, Mountain View, CA, United States","longitude":-122.078488,"latitude":37.414198},{"companyName":"CyberCoders","jobTitle":"Embedded Java Applications Engineer","distance":4153.025944882882,"skills":["java"],"formattedAddress":"1400 North Shoreline Boulevard, Mountain View, CA, United States","longitude":-122.078488,"latitude":37.414198}
.....
]
美化应用程序
现在我们已经证实了应用程序的REST服务工作正常,接下来要做的是构建应用的用户界面。在本文中,我们只需创建一套非常简单的应用用户界面,即提供一套表单,用户可以借助它输入个人技能,并通过div承载谷歌地图渲染完成的求职场所与用户位置。如下所示在src/main/webapp文件夹中创建一个index.html文件:
<!DOCTYPE html>
<html>
<head>
<title>LocalJobs : Find jobs near to you</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<style>
body {
padding-top: 60px;
padding-bottom: 100px;
}
#map-canvas {
height: 500px;
width: 100%;
}
.job_info {border: 1px solid #000;padding: 15px;width: 300px}
.job_info h3 {margin-bottom: 10px}
</style>
<link href="css/bootstrap.css" rel="stylesheet">
<link href="css/bootstrap-responsive.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="css/jquery.tagsinput.css" />
<link href="css/jquery.loadmask.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse"
data-target=".nav-collapse">
<span class="icon-bar"></span> <span class="icon-bar"></span> <span
class="icon-bar"></span>
</button>
<a class="brand" href="#">LocalJobs</a>
<div class="nav-collapse collapse">
<ul class="nav">
<li class="active"><a href="#">Home</a></li>
</ul>
</div>
<!--/.nav-collapse -->
</div>
</div>
</div>
<div id="main" class="container">
<form id="jobSearchForm" class="form-horizontal">
<div class="control-group">
<div class="controls">
<input type="text" id="skills" name="skills" class="input-xlarge"
placeholder="Enter skills for which you want to search jobs"
required>
</div>
</div>
<div class="control-group">
<div class="controls">
<button id="findJobsButton" type="submit" class="btn btn-success">Find
Jobs</button>
</div>
</div>
</form>
</div>
<div id="map-canvas"></div>
<script type="text/x-mustache-template" id="job-template">
<div class="jobBox">
<h3>{{jobtitle}}</h3>
<p> {{company}} </p>
<address> {{address}} </address>
<p> {{skills}}</p>
<p> {{distance}} </p>
</div>
</script>
<script src="js/jquery.js"></script>
<script src="js/jquery.tagsinput.js"></script>
<script type="text/javascript" src="js/jquery.loadmask.min.js"></script>
<script
src="https://maps.googleapis.com/maps/api/js?sensor=true"></script>
<script src="js/bootstrap.js"></script>
<script src="js/underscore.js"></script>
<script src="js/backbone.js"></script>
<script src="js/mustache.js"></script>
<script type="text/javascript">
$( document ).ready( function() {
$('#skills').tagsInput({
defaultText : "add skills"
});
});
</script>
<script src="js/app.js"></script>
</body>
</html>
上面显示的index.html是一个HTML 5文件,而且使用HTML 5的文档类型。我们的应用使用Twitter Bootstrap,这是一款免费工具集合,用于创建网站以及web应用程序。它包含了以HTML以及CSS为基础的设计模板,提供全套排版、表格、按钮、图表、导航、其它界面组件以及备选JavaScript扩展。大家可以点击此处从本项目的github库中获取全部相关css.js文件。
检查GeoLocation支持
由于我们的应用程序以用户位置为基础,因此在进一步调整应用程序之前需要首先检查GeoLocation API。为了检查用户浏览器对GeoLocation API的支持效果,需要将如下所示记录准备函数添加进来。如果用户浏览器支持GeoLocation,那么导航对象中将具有geolocation对象。大家还可以利用Modernizr等开源库检测HTML 5功能。如果用户浏览器不支持geolocation,大家需要禁用表单提交按钮。
<script type="text/javascript">
if(!navigator.geolocation){
alert('Your browser does not support geolocation. Please download latest browser version.');
$("#findJobsButton").attr("disabled", "disabled");
}
</script>
#p#
在提交表单中查找工作
现在我们已经确认用户浏览器能够支持GeoLocation API,接下来要做的就是根据用户的个人技能为其查找理想工作。此项目利用Backbone.js为我们的客户端代码添加结构。如果大家对backbone.js不太熟悉,可以点击此处查看我之前发表的博文《利用Backbone.js、JaxRS、MongoDB以及OpenShift创建单页面Web应用程序》,那里提供了与利用backbone.js创建应用有关的详细说明。请将app.js文件考虑到src/main/webapp目录下的js文件夹当中。下面展示的是经过精简的app.js文件内容,这是为了适当缩减本文的篇幅。
// app.js
(function($){
var LocalJobs = {};
window.LocalJobs = LocalJobs;
var template = function(name) {
return Mustache.compile($('#'+name+'-template').html());
};
LocalJobs.HomeView = Backbone.View.extend({
tagName : "form",
el : $("#main"),
events : {
"submit" : "findJobs"
},
render : function(){
console.log("rendering home page..");
$("#map-canvas").empty();
return this;
},
findJobs : function(event){
event.preventDefault();
$("#map-canvas").empty();
$("#jobSearchForm").mask("Finding Jobs ...");
var skills = this.$('input[name=skills]').val().split(',');
console.log("skills : "+skills);
var self = this;
var mapOptions = {
zoom: 3,
center: new google.maps.LatLng(-34.397, 150.644),
mapTypeControlOptions: {
style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
},
mapTypeId: google.maps.MapTypeId.ROADMAP,
zoomControlOptions: {
style: google.maps.ZoomControlStyle.SMALL
}
};
var map = new google.maps.Map(document.getElementById('map-canvas'),
mapOptions);
navigator.geolocation.getCurrentPosition(function(position){
var longitude = position.coords.longitude;
var latitude = position.coords.latitude;
console.log('longitude .. '+longitude);
console.log('latitude .. '+latitude);
$("#jobSearchForm").unmask();
self.plotUserLocation(new google.maps.LatLng(latitude, longitude),map);
$.get("api/jobs/"+skills+"/?longitude="+longitude+"&latitude="+latitude , function (results){
$("#jobSearchForm").unmask();
self.renderResults(results,self,map);
});
}, function(e){
$("#jobSearchForm").unmask();
// handle error
},
{ timeout: 45000 }
);
},
plotUserLocation : function(latLng , map){
},
renderResults : function(results,self,map){
var infoWindow = new google.maps.InfoWindow();
_.each(results,function(result){
self.renderJob(result,map , infoWindow);
});
},
renderJob : function(result , map , infoWindow){
}
});
LocalJobs.Router = Backbone.Router.extend({
el : $("#main"),
routes : {
"" : "showHomePage"
},
showHomePage : function(){
console.log('in home page...');
var homeView = new LocalJobs.HomeView();
this.el.append(homeView.render().el);
}
});
var app = new LocalJobs.Router();
Backbone.history.start();
})(jQuery);
下面我们一起来解读代码的具体含义。
1. 上面展示的代码旨在创建一个backbone路由实例,并将其作为root DOM的主div。下面我们点击基础url,路由机制会调用映射HomeView的showHomePage函数。渲染函数中的HomeView用于通过id map-canvas清空div。
2. 在HomeView当中,我们拥有一套针对表单提交的事件侦听器。因此,当用户输入个人技能并按下“提交”按钮后,findJobs函数将被调用。
3. findJobs函数是一切运行的基础。
3.1 我们首先利用技能名称获取输入值,然后利用逗号将内容分割,这样就构成了一套技能数组。
3.2 我们接着创建一个谷歌地图对象并为其设置一些默认值。
3.3 下面我们调用navigator.geolocation对象上的getCurrentPosition方法。此方法只有一项必要参数success_callback与两项可选参数error_callback,外加可选对象PositionOptions。
3.4 如果getCurrentPosition被调用成功,则继续调用success_callback。这条回调函数拥有一项参数——position。这个position对象负责保留用户的经伟度结果,并在地图上绘制用户的当前位置。
3.5 在用户位置绘制完成之后,则通过jQuery进行获取调用。
3.6 最后所有结果都将经过迭代并显示在地图之上。
推送代码
现在大家可以将代码推送至OpenShift处并查看应用程序在云中的运行效果。
git add .
git commit -am "localjobs app with UI"
git push
按照我所罗列的提示内容,应用程序将运行在https://localjobs-domain-name.rhcloud.com/位置。大家可以将具体域名替换为自己的命名空间。
总结
在本篇博文中,我们共同探讨了如何利用HTML 5 GeoLocation API以及MongoDB Geo位置索引功能创建位置感知类应用程序。希望文章内容能给大家的开发工作带来启示,感谢阅读。