位置跟踪器pt。 1:Swift中的离线优先设计

Location Tracker应用程序的目标是向Swift开发人员展示使用Cloudant跟踪,存储和查询位置有多么简单,同时启用脱机优先设计并提供架构指导以扩展解决方案以支持数百万用户。

在本教程中,我们将向您展示如何构建应用程序以及为实现目标所采用的策略。 我们将向您展示如何将Cloudant Sync用于离线支持和数据同步。 我们将向您展示如何使用Cloudant Geo来执行和可视化地理空间查询。 最后,我们将介绍在以后的教程中将使用的替代体系结构和方法,以向您展示如何扩展应用程序以支持数百万用户。

总览

Location Tracker应用程序是在Swift中开发的iOS应用程序,可跟踪用户位置并将这些位置存储在Cloudant中。 当用户移动并记录新位置时,该应用会查询服务器以获取用户位置附近的兴趣点。

以下是“位置跟踪器”应用的屏幕截图。 蓝色图钉标记应用程序记录的每个位置。 在用户行进的路径上绘制了一条蓝线。 每当Location Tracker应用程序记录新位置时,都会在Cloudant中执行基于半径的地理查询,以查找附近的兴趣点(在应用程序中称为“地点”)。 半径由绿色圆圈表示。 位置显示为绿色图钉:

要求

为了帮助实现我们的目标,我们创建了5个关键要求:

  1. 跟踪前台和后台的位置:当应用程序在前台或后台运行时,该应用程序应该能够跟踪用户的位置。
  2. 使用地理空间查询来查找指定半径内的兴趣点:该应用程序应向用户展示如何使用Cloudant Geo执行地理空间查询。
  3. 脱机运行:该应用程序应能够在脱机时跟踪用户位置,并在网络连接可用时将这些位置同步到Cloudant。
  4. 将用户位置信息保密:用户不应有权访问其他用户的位置信息。
  5. 提供整合和分析所有位置的能力:后端工程师或数据科学家在不影响第4条要求的情况下,在所有位置执行分析应该很简单。

建筑

为了满足要求4(用户隐私),使用每个用户的数据库设计模式实现了位置跟踪器。 为只有该用户有权访问的每个用户创建一个专用数据库。 运作方式如下:

  1. 当用户注册时,Location Tracker应用程序会将用户信息发布到我们的Node.js服务器。
  2. Node.js服务器在“用户”数据库中创建一个新用户。
  3. Node.js服务器创建一个特定于用户的数据库来跟踪用户的位置。
  4. Node.js服务器将数据库名称和身份验证信息返回到应用程序。
  5. 该应用程序直接连接到Cloudant以同步位置信息。

稍后我们将详细讨论。 这是系统架构的高级示意图:

除了创建相邻架构图中所示的特定于用户的数据库之外,服务器还为每个特定于用户的数据库配置连续复制到统一数据库( 所有位置 )中。 通过为我们提供一个位置来查询和分析所有用户记录的所有位置,同时又不损害用户安全性和隐私性(没有向用户提供直接访问统一数据库的权限),这满足了要求#5(位置合并和分析)。

注意: “每用户数据库”设计模式使您可以轻松地在iOS应用程序和Cloudant之间同步位置信息,同时确保信息保密。 对于中小型应用程序来说,这是一个很好的解决方案。 在下一个教程中,我们将向您展示复制用户隔离的数据的替代方法,以及如何扩展应用程序以支持数百万个用户。

服务器

Location Tracker Server是一个Node.js应用程序,它提供RESTful API,用于使用Cloudant Geo注册新用户和查询位置。 安装Location Tracker Server时,将在Cloudant实例中创建三个数据库:

  1. lt_locations_all –该数据库用于跟踪所有位置。 用户注册后,将创建一个特定的数据库来跟踪该用户的位置。 每个特定于用户的数据库都将配置为连续复制到lt_locations_all数据库中。
  2. lt_places –此数据库包含“位置跟踪器”应用程序将查询的位置列表。
  3. lt_users –此数据库用于管理用户。 每个用户都有一个用户名,密码和有关该特定用户的位置数据库的信息。

lt_locations_alllt_places数据库将分别使用地理索引创建,从而使您可以进行地理查询并利用Cloudant仪表板中的集成地图视觉效果。 lt_places数据库将填充50个示例位置,这些位置遵循iOS模拟器中“ Freeway Drive”调试位置设置的路径:

遵循Location Tracker Server GitHub页面上的指示信息,以启动Location Tracker Server并在本地或Bluemix上运行。

客户端

Location Tracker客户端是使用Swift开发的iOS应用。 如前所述,Location Tracker应用程序可跟踪和记录用户位置,并向Cloudant查询兴趣点。 当新用户向Location Tracker应用程序注册时,将专门创建一个新数据库来跟踪该用户的位置。

Location Tracker应用程序使用Cloudant Sync for iOS在本地存储位置并将其同步到Cloudant:

按照Location Tracker App GitHub页面上的说明进行操作,以在Xcode中启动并运行Location Tracker App。

怎么运行的

希望此时您已经成功部署了Location Tracker Server,并可以在iOS模拟器或iOS设备上运行Location Tracker应用程序。

在本教程的其余部分,我们将更详细地介绍应用程序的工作方式以及如何满足5个关键要求,包括:

  • 我们如何在iOS中跟踪用户的位置。
  • 我们如何使用“每用户数据库”设计模式隔离和同步用户位置。
  • 我们如何使用Cloudant Sync支持离线位置跟踪以及与Cloudant的双向同步。
  • 我们如何将用户位置复制到统一位置数据库中。
  • 我们如何使用Cloudant Geo在用户位置附近查找兴趣点。

用户注册

这一切都始于用户注册。 如您在下面看到的,我们只需要用户名和密码。 您可以轻松扩展应用程序以添加新字段,例如姓名,电子邮件地址等。

当用户点击“注册”按钮时,该应用将执行HTTP PUT到Location Tracker服务器的操作。 PUT主体是用户的JSON表示形式:

  { 
“用户名”:“ markwatson”,
“ password”:“ passw0rd”,
“ type”:“用户”,
“ _id”:“ markwatson”
}

JSON在RegisterViewControllergetRegisterHttpBody函数中生成。 如您在下面看到的,我们只是在创建字典并使用内置的NSJSONSerialization类:

  func getRegisterHttpBody(_id:String)-> NSData { 
var参数:[String:String] = [String:String]()
params [“ username”] = self.usernameTextField.text
params [“ password”] = self.passwordTextField.text
params [“ type”] =“用户”
params [“ _ id”] = _id
var body:NSData!
做{
正文=尝试NSJSONSerialization.dataWithJSONObject(参数为NSDictionary,选项:[])
}
赶上{
打印(错误)
}
返回身体
}

真正的工作从Node.js服务器收到PUT请求开始。 该请求在api/routes.js中的createUser函数中处理, createUser执行以下步骤:

  1. 检查用户是否存在指定的ID。 如果用户已经存在,则将状态409返回给客户端。
  2. 为用户创建位置数据库。 该数据库将被称为lt_locations_user_USERNAME ,并将仅用于存储该用户的位置。 登录时,数据库名称将返回到应用程序,以允许使用Cloudant Sync进行同步。
  3. 在新创建的数据库上创建地理索引。
  4. 在Cloudant中生成用于访问数据库的API密钥和密码。 API密钥和密码也将在登录时返回给应用程序。
  5. 将API密钥与新创建的位置数据库相关联。
  6. 将用户及其ID,密码,API密钥和API密码存储在用户数据库中(API密码将被加密)。
  7. 将用户的位置数据库配置为连续复制到lt_locations_all数据库。

这是JavaScript代码。 有关每个函数的完整定义,请参考routes.js文件:

  var username = req.params.id; 
var dbName ='lt_locations_user_'+ encodeURIComponent(用户名);
checkIfUserExists(cloudant,req.params.id)
.then(function(){
返回createDatabase(cloudant,dbName);
})
.then(function(){
返回createIndexes(cloudant,dbName);
})
.then(function(){
返回generateApiKey(cloudant);
})
.then(function(api){
返回applyApiKey(cloudant,dbName,api);
})
.then(function(api){
返回saveUser(req,cloudant,dbName,api);
})
.then(function(user){
返回setupReplication(cloudant,dbName,user);
})
.then(function(user){
res.status(201).json({
好的:是的,
id:user._id,
转:user.rev
});
},函数(错误){
console.error(“注册用户时出错。”,err.toString());
如果(err.statusCode && err.statusMessage){
res.status(err.statusCode).json({错误:err.statusMessage});
}
其他{
res.status(500).json({error:'内部服务器错误'});
}
});

用户登录

注册后,用户立即登录。 该应用程序将以下请求发送到Node.js服务器:

  { 
“用户名”:“ markwatson”,
“ password”:“ passw0rd”
}

这是一个示例响应:

  { 
“ ok”:是的,
“ api_key”:“ ytorestenauneexxxxedstoo”,
“ api_password”:“ ffdc36ea8dbaadxxxx94d9d884d0255c56c08e1e”,
“ location_db_name”:“ lt_locations_user_markwatson”,
“ location_db_host”:“ 9f61849d-2884-4463-XXXX-56344789b05c-bluemix.cloudant.com”
}

该响应包含将位置与为此用户创建的Cloudant数据库同步所需的信息。 这些值以及用户的登录信息随后存储在设备上(密码安全地存储在钥匙串中):

  UsernamePasswordStore.saveUsernamePassword(用户名,密码:密码) 

LocationDbInfoStore.saveApiKeyPasswordDbNameHost(
dict [“ api_key”]如! 串,
apiPassword:dict [“ api_password”]为! 串,
dbName:dict [“ location_db_name”]为! 串,
dbHost:dict [“ location_db_host”]为! 串

将这些信息存储在本地可使该应用程序完全脱机运行。 如果用户在登录时杀死了该应用程序,而在脱机时重新打开了该应用程序,则该应用程序将自动使用户登录。

另外,这些值可通过AppState类提供给应用程序。 项目中的任何类都可以随时访问这些值。 例如:

 让凭据=“(AppState.locationDbApiKey!):( AppState.locationDbApiPassword!)” 
让url =“ https://(凭证)@(AppState.locationDbHost!)/(AppState.locationDbName!)”

稍后我们将仔细研究此代码。

追踪地点

在iOS中跟踪位置非常简单,但是在后台跟踪位置可能会有些棘手。 Apple仅允许某些类型的应用程序(健身,GPS等)在后台进行连续的位置跟踪,但它们会为所有应用程序提供重大的位置更改(只要用户批准)。 在位置跟踪器中,当应用程序在后台运行时,我们使用重大更改位置服务。 有关位置跟踪和重大更改服务的更多信息,请参阅iOS文档。

我们创建了一个包装器,该包装器可自动处理监视实时位置和重大更改服务之间的切换。 包装器称为LocationMonitor 。 这是如何使用LocationMonitor的示例:

 类MyViewController:UIViewController,LocationMonitorDelegate 

覆盖func viewDidAppear(动画:布尔){
...
LocationMonitor.instance.addDelegate(个体)
}

func locationUpdated(location:CLLocation,inBackground:Bool){
//用位置做点什么
}

有许多变量指示LocationMonitor何时将新位置通知给订户。 这些变量可以在AppConstants类中找到:

  static let minMetersLocationAccuracy:Double = 25 
static let minMetersLocationAccuracyBackground:Double = 100
static let minMetersBetweenLocations:Double = 15
静态让minMetersBetweenLocationsBackground:双= 100
静态让minSecondsBetweenLocations:双= 15
  1. minMetersLocationAccuracy – iOS位置库报告给定位置的准确性。 此变量指示在前台运行以通知注册订户时位置必须有多精确。
  2. minMetersLocationAccuracyBackground –此变量类似于minMetersLocationAccuracy ,但在跟踪后台位置时使用。
  3. minMetersBetweenLocations – iOS位置库可以报告位置的最小变化。 为了我们的目的,如果用户没有移动,我们不想存储每个位置。 此变量指示用户必须移动以报告位置的最低仪表数。
  4. minMetersBetweenLocationsBackground –此变量类似于minMetersBetweenLocations ,但在后台跟踪位置时使用。
  5. minSecondsBetweenLocations –与minSecondsBetweenLocations类似,此变量指示自上次报告新位置以来必须经过的最少秒数。

同步位置

在将位置同步到Cloudant数据库之前,我们需要配置一个本地数据存储。 在MapViewController类的viewDidLoad函数中,您将看到对initDatastoreManager的调用。 此函数初始化可用于管理一个或多个数据存储的数据存储管理器,并指定本地数据存储应在设备上的位置:

  func initDatastoreManager(){ 
让fileManager = NSFileManager.defaultManager()
让documentsDir = fileManager.URLsForDirectory(.DocumentDirectory,inDomains:.UserDomainMask).last!
让storeURL = documentsDir.URLByAppendingPathComponent(“ locationtracker”)
让路径= storeURL.path
做{
datastoreManager =试试CDTDatastoreManager(目录:路径)

} {
fatalError(“无法初始化数据存储:(错误)”)
}
}

初始化数据存储区管理器后,我们调用initLocationsDatastore函数来初始化用户位置的数据存储区。 在这里,我们创建一个新的数据存储并在created_at属性上建立索引:

  func initLocationsDatastore(){ 
做{
locationDatastore =试试datastoreManager!.datastoreNamed(locationDatastoreName)
locationDatastore?.ensureIndexed([“ created_at”],withName:“ timestamps”)
}
赶上{
fatalError(“无法初始化位置数据存储区:(错误)”)
}
}

现在我们准备开始保存位置。 捕获新位置后,我们将创建LocationDoc类的新实例:

 让locationDoc = LocationDoc(docId:nil, 
纬度:location.coordinate.latitude,
经度:location.coordinate.longitude,
用户名:AppState.username !,
sessionId:AppState.sessionId,
时间戳:NSDate(),
背景:inBackground

然后,我们创建一个CDTDocumentRevision来存储在本地数据存储中:

  func createLocationDoc(locationDoc:LocationDoc)-> Bool { 
...
let rev = CDTDocumentRevision(docId:locationDoc.docId)
rev.body = NSMutableDictionary(dictionary:locationDoc.toDictionary())
做{
尝试locationDatastore!.createDocumentFromRevision(rev)
}
赶上{
print(“创建位置错误:(错误)”)
}
返回真
}

最后,我们将本地数据存储与Cloudant同步。 我们首先配置Cloudant数据库的URL。 我们使用服务器返回的身份验证信息,并将其存储在AppState类中:

 让凭据=“(AppState.locationDbApiKey!):( AppState.locationDbApiPassword!)” 
让url =“ https://(凭证)@(AppState.locationDbHost!)/(AppState.locationDbName!)”

接下来,我们创建一个单向复制作业:

 让工厂= CDTReplicatorFactory(datastoreManager:self.datastoreManager) 

let job = CDTPushReplication(来源:self.locationDatastore !,目标:url)

//创建复制作业。
var job = try factory.oneWay(job)

//将self指定为复制委托(在作业完成时通知)。
工作!。委托=自我

//开始工作
尝试工作!.start()

上面的代码显示了如何将新位置推送到Cloudant,但是我们也可以从Cloudant中提取位置,而当用户首次登录时,我们就可以这样做。 在MapViewControllerviewDidAppear函数中,我们调用syncLocations(.Pull) 。 这将创建从Cloudant到应用程序的单向复制作业。 复制完成后,我们的本地数据存储将包含从Cloudant检索到的位置。 我们调用loadLocationDocsFromDatastore函数加载位置并将其添加到地图中。 这是loadLocationDocsFromDatastore函数:

  func loadLocationDocsFromDatastore(){ 
让查询= [“ created_at”:[“ $ gt”:0]]
让结果= locationDatastore?.find(查询,跳过:0,限制:UInt(AppConstants.locationDisplayCount),字段:无,排序:[[“ created_at”:“ desc”]])
后卫结果!=无其他{
打印(“无法查询位置”)
返回
}
dispatch_async(dispatch_get_main_queue(),{
self.removeAllLocations()
//我们正在加载从最近到最近的文档
//我们希望我们的数组处于相反的顺序
//这样我们就可以绘制路径,并在添加新位置时增加标签
//在这里我们枚举文档并将它们以相反的顺序添加到本地数组中
//然后遍历该局部数组,然后将它们一一添加到地图中
var docs:[CDTDocumentRevision] = []
结果!.enumerateObjectsUsingBlock({(doc,idx,stop)->在
docs.insert(doc,atIndex:0)
})
用于文档{
如果让locationDoc = LocationDoc(aDoc:doc){
self.addLocation(locationDoc,drawPath:false,drawRadius:false)
}
}
self.drawLocationPath()
})
}

当用户离线登录时,将调用相同的功能。 当网络不可用时,这允许用户查看本地数据存储事件中的任何位置存储。

查询地点

记录新位置后,Location Tracker应用程序会在这些位置的半径范围内寻找兴趣点(“地点”)。 位置与GeoJSON几何对象一起存储在lt_places数据库中。 这是一个示例地方:

  { 
“ _id”:“ 94cf2fcb31459b2244bf8ff6140a5282”,
“ _rev”:“ 1-6ded3878845982c77e08c19bfed65d95”,
“几何”:{
“ type”:“ Point”,
“坐标”:[
-122.3162468,
37.4722645
]
},
“名称”:“埃奇伍德公园自然保护区”,
“ type”:“功能”,
“ created_at”:1462910143508
}

lt_places数据库还包括Cloudant地理空间索引,该索引使我们能够执行基于地理的查询。 索引是在安装Location Tracker服务器时创建的,定义如下:

  { 
“ _id”:“ _design / points”,
“ _rev”:“ 1-cd7d85016e88bcc571b7d6e8c2d33768”,
“ language”:“ javascript”,
“ st_indexes”:{
“ pointidx”:{
“ index”:“函数(doc){如果(doc.geometry && doc.geometry.coordinates){st_index(doc.geometry);}}”
}
}
}

Cloudant Geo支持多种基于地理的查询(单击此处了解更多信息)。 我们在位置跟踪器应用中使用了一个简单的基于半径的查询。 该应用程序将查询发送到Node.js服务器,后者又将查询发送到Cloudant。 这是应用程序制定查询的方式:

 让url = NSURL(string:“(AppConstants.baseUrl)/ api / places 
?lat =(lastLocation.geometry!.latitude)
&lon =(lastLocation.geometry!.longitude)
&radius =(AppConstants.placeRadiusMeters)
&relation =包含
&nearest = true
&include_docs = true”

您可以在上方看到我们正在连接到Node.js服务器上的/api/places端点。 我们正在传递最后一个位置的纬度和经度以及AppConstants定义的半径。 我们指定了包含的几何关系,该关系告诉Cloudant返回指定纬度/经度半径内的任何对象。 最后,我们要求Cloudant以最接近最远的顺序返回文档(Nearest nearest=true )。

Node.js服务器在Cloudant上调用地理索引终结点(传递从Location Tracker应用发送的查询字符串)。 请参阅api/routes.jsgetPlaces函数:

  ... 
var url = cloudant.config.url +“ / lt_places / _design / points / _geo / pointidx”;
url + =“?” + querystring.stringify(req.query);
request.get({uri:url},function(err,response,body){
...

当应用程序从服务器接收到地点列表时,它将它们存储在本地数据存储区中,就像我们对位置所做的一样。 这使我们可以查看离线运行时查询的位置。

结论和下一步

在本教程中,我们向您展示了如何动态创建特定于用户的Cloudant数据库,以分隔各个用户的位置。 我们向您展示了如何在iOS中跟踪位置以及如何使用Cloudant Sync将位置与Cloudant同步并在脱机运行时跟踪位置。 我们介绍了如何以编程方式配置连续复制,以将特定于用户的数据库中的位置复制到用于分析所有位置的中央数据库中。 最后,我们向您展示了如何使用Cloudant Geo在纬度/经度半径内索引和查询文档,以及如何在Cloudant信息中心内的地图上查看位置。

为此,我们满足了5个关键要求:

  1. 跟踪前景和背景中的位置。
  2. 使用地理空间查询来查找指定半径内的兴趣点。
  3. 离线运行。
  4. 将用户位置信息保密。
  5. 提供合并和分析所有位置的功能。

在未来的教程中,我们将讨论满足这些要求的替代方法,以及如何使用Cloudant支持数百万用户,包括:

  • 如何使用CouchDB更改提要作为连续复制的替代方法。
  • 如何使用Cloudant Envoy在单个数据库中存储位置,同时保持用户隐私和安全。
  • 如何使用替代地图提供程序(例如Mapbox)以及如何离线存储地图。