65.9K
CodeProject 正在变化。 阅读更多。
Home

在托管代码中使用 Skyhook Wireless XPS 定位服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (12投票s)

2009年1月6日

CPOL

28分钟阅读

viewsIcon

92448

downloadIcon

1455

包装器和示例程序演示了 Skyhook Wireless XPS SDK 的使用(混合定位系统,使用 GPS、WiFi 定位和蜂窝塔定位)

screenshot

引言

Skyhook Wireless 因提供基于 WiFi 的定位而闻名。该服务已用于 iPhone、用于相机的 Eye-Fi 地理标记存储卡,并可用于 Windows 桌面操作系统、OS X、Linux、Symbian 和 Windows Mobile。最近,Skyhook Wireless 更新了 SDK,以利用蜂窝塔和 GPS 定位,结合这三种定位技术的信息,为应用程序提供极其广阔的覆盖范围。对于 Windows Mobile,该服务通过非托管库访问。通过阅读 Skyhook Wireless 开发者论坛,我发现托管开发者一直无法访问 100% 的功能。我决定自己创建一个包装器。

我第一次尝试创建包装器失败了,很快我就发现了为什么其他开发者没有成功。Compact Framework 中可用的互操作功能并非使用常用技术创建包装器所需的。利用年终假期的一些时间,我成功创建了一个包装器,使服务 100% 的功能可用。在这篇文章中,我重新审视了我去年发布的一个程序,该程序会自动响应短信请求并返回手机位置,并为其添加了基于 XPS 的定位和使用微软 Virtual Earth Web 服务的地图功能。

在我最初发布这篇文章时,Skyhook Wireless SDK 只执行基于 WiFi 的定位。他们现在已经更新了客户端,以利用 GPS 和蜂窝塔位置,允许开发者通过单个函数调用从最精确的可用源获取位置数据。

技术和要求

这里介绍的示例程序需要运行 Windows Mobile 5 或 Windows Mobile 6 的 Windows Mobile Professional 设备。设备上必须存在 .NET Framework 3.5 版本,最后一个示例程序还使用了 SQL Server CE 3.5 版本。此外,您需要一部具有短信套餐和无限流量套餐的手机。示例程序将通过您的数据连接下载地图图像,图像会迅速消耗数据。这里介绍的程序设计用于扩展到 800x800 屏幕。

术语

本文中我将多次使用一些术语。

  • WPS - WiFi 定位服务
  • WPSAPI - Skyhook Wireless 提供的 WiFi 定位 API
  • WpsProxy - 我为处理托管代码和 WPSAPI 之间的一些数据转换而制作的本地 DLL
  • XPS - Skyhook Wireless 解决方案,自动结合 GPS 和 WPS 信息,实现可靠快速的定位。

设置您的开发者账户

我在示例代码中调用的一些 Web 服务需要账户才能使用。在所有情况下,都可以免费获得开发者账户。开发者账户可以进行的调用量有限制。虽然允许的调用次数远超个人使用,但如果我将此代码与我的账户信息一起分发,我相信最大允许调用量会很快达到(我相信您会理解我为什么不想分享我的账户信息)。

这里的示例程序将在一组注册表项中查找账户信息。将账户信息存储在注册表中,允许信息指定一次,然后供所有需要它的程序使用。如果您尝试在未先设置自己的开发者账户的情况下运行这里的程序,那么它们将无法工作。要将您的账户信息保存到您的设备,请运行名为“SetMyKeys”项目中的代码。

您可以通过访问以下每个 URI 来设置您的开发者帐户

为什么要使用 XPS 定位

GPS 设备可以提供令人难以置信的精确位置信息。尽管它们具有高分辨率,但我认为 GPS 设备有两个主要缺点。它们可能需要一些时间才能获取定位,并且在许多条件和环境下,GPS 设备根本无法获取定位(在“城市峡谷”中或建筑物内部)。即使有清晰的 GPS 信号可用,也不是所有 Windows Mobile 设备都具有 GPS 硬件。通过利用其他定位技术,我们可以扩大潜在客户群,并提高程序快速获取位置信息的能力。

XPS 定位如何工作?

Skyhook Wireless 的 XPS 利用三种定位技术:WiFi、蜂窝塔和 GPS。WiFi 路由器不断传输其 ID 号(MAC 地址)。Skyhook Wireless 已经积累了一个庞大的路由器 ID 数据库,以及可以接收其信号的位置。他们雇佣了 500 多名全职司机,驾车四处收集 WiFi 信号,并将其与 GPS 位置相关联。此外,Skyhook Wireless 还有一个蜂窝塔 ID 及其位置的数据库。当无法接收 GPS 信号时,XPS 将提交附近路由器和蜂窝塔的 ID 并返回您的位置。Skyhook Wireless 在此页面上解释了该技术。

WPSAPI 库还利用了智能缓存。开发者可以指定缓存大小,WPSAPI 会将数据库的一个子集下载到本地设备的存储中。下载完成后,WPSAPI 无需再往返 Skyhook Wireless 服务器获取位置信息,直到用户移动到缓存信息覆盖的区域之外。除了获取位置,该服务还可以对位置进行地理编码,提供用户所在州、城市甚至街道的信息。

我所在地区的覆盖情况如何?

找到 XPS 客户端无法定位的地方相当困难。它通过 GPS、蜂窝塔和 WiFi 定位的覆盖范围非常出色。如果您想了解 Skyhook Wireless 在您所在地区的 WiFi 覆盖类型,可以查看覆盖地图。乍一看美国地图,覆盖范围似乎有点稀疏。然后我意识到很多人居住在都会区或其附近,并将覆盖地图与人口密度地图进行了比较。这样一看,我发现覆盖范围相当不错。我住在亚特兰大市郊区,乔治亚州,美国。我开车沿着一条从我家到餐厅的 7 英里路径,手机通过 WPS 轮询我的位置,只遇到了一小段路,我的手机无法看到 WiFi 信号来推断我的位置。即使在没有 WiFi 定位覆盖的区域外,也要记住 XPS 客户端也会使用蜂窝塔和 GPS 定位。

Skyhook Wireless 在美国亚利桑那州凤凰城(美国第五大城市)的 WiFi 覆盖地图和人口密度地图。颜色越深表示人口密度越大。请注意人口密度和 WiFi 覆盖的形状相似。

获取 Skyhook Wireless SDK

Skyhook Wireless SDK 是免费下载的,只需您注册一个开发者帐户。开通帐户时,您需要通过用户 ID 和领域来指定您的身份。没有密码。领域只是您决定用来识别您的公司或团体的一些名称。它可以是实体名称、域名或您选择的任何其他名称。注册并登录后,您将看到许多不同的 SDK 和文件供您下载。本文您需要下载两个文件。一个是适用于手机的 Windows Mobile 5 & 6 PocketPC (arm) 和适用于手机的 Windows Mobile PocketPC (arm) 的 Virtual GPS。它适用于许多其他平台,但为了本文的目的,我只考虑 Windows Mobile 下载。

无论您是否正在使用本文附带的项目,如果您正在使用 Skyhook Wireless WPSAPI SDK,您都需要更新 `WpsProxy` 类的属性,以便它知道 SDK 的头文件和库文件在哪里。要在 Visual Studio 中更改这些设置,请右键单击使用 WPSAPI 库的 C/C++ 项目,然后选择“属性”。附加包含目录的设置在“配置属性”->“C/C++”->“常规”下。附加库的设置可以在“配置属性”->“链接器”->“附加库目录”下找到。

在不更改基于 GPS 的程序的情况下立即使用 WPS

如果您已经有使用 GPS 的应用程序,您可以通过 Virtual GPS 应用程序立即使用 Skyhook Wireless WPS 服务。这对于没有 GPS 硬件的设备尤其有用。Virtual GPS 应用程序模拟 GPS 设备。通过 COM 端口或 GPS 中间驱动程序与 GPS 设备通信的软件可以使用 Virtual GPS。如果您的设备有 GPS 硬件,Virtual GPS 可以配置为同时使用 GPS 硬件和 WPS 来获取您的位置。您的软件将在 GPS 信息可用时接收 GPS 信息,并在 GPS 信息不可用时回退到 WPS 信息。我在这里不详细介绍 Virtual GPS 的设置,因为 Skyhook Wireless 已经提供了详细说明如何设置 Virtual GPS 的文档。

P/Invoke WPS 代码中的挑战

当我第一次尝试为 WPSAPI 创建包装器时,我采用的路径与我为其他已调用的原生代码所采用的路径相同。对于 WPSAPI,我发现这不合适。WPSAPI 使用 ANSI 字符串(单字节字符串),而 Compact Framework 使用 Unicode 字符串(2 字节)。在桌面框架上封送字符串时,对于这种情况,我们可以通过在相关字符串字段中添加带有 `UnmanagedType.LPStr` 的 `MarshalAs` 属性来自动将一种类型的字符串转换为另一种类型。然而,Compact Framework 中不支持此值。

在制作包装器时遇到的另一个复杂问题是,复杂结构正在从 WPS API 传递;一个结构可能包含指向另一个结构的指针,而该结构又可能包含指向另一个实体的结构。当尝试传递这种性质的结构时,CLR 经常会返回熟悉的但非描述性的 `NotSupportedException`。这两个问题共同使得托管开发人员难以访问 WSP API。

重新思考 WPSAPI 库的接口

在遇到字符串转换和复杂结构传递的障碍之后,我想到:“如果 WPSAPI 使用 Unicode 字符串并传递扁平结构,那该多好?”由于我没有 WPSAPI 库的代码,我无法更改库的字符串实现。因此,我制作了一个自己的本地 DLL 来调用 WPSAPI 功能。这个 DLL 在项目 *WpsProxy* 中。该项目是用 C 编写的,主要作为托管代码和本地 API 之间的转换层存在。该层处理 Unicode 和 ANSI 字符串的转换,并将复杂的 WPS 结构转换为扁平结构,以便于从 .NET 语言中进行操作。

`WpsProxy` 的托管包装器位于项目 *J2i.Net.WiFiPositioning* 中的一个名为 `WPS` 的类中。WPS 中包含的几乎所有功能都通过包装器公开。例外是两个用于释放 WPS 调用分配的资源的函数。我没有将非托管资源传递给托管代码并让开发人员负责解除分配,而是从托管代码中分配必要的内存,并将其传递给代理进行填充。填充信息后,WpsProxy 代码将解除分配 WPSAPI 调用分配的内存。

下面是代理通过的最简单调用之一的示例

extern "C" WPSPROXY_API void SetServerUrl(const LPWSTR url)
{
    //Convert unicode string to ANSI string. 
    //this call allocates memory
    LPSTR szUrl = wcstombs(url);
 
    //call WPSAPI function with ANSI string
    WPS_set_server_url(szUrl);
 
    //Free the ANSI string
    delete szUrl;
}

最难封装的功能是涉及回调的函数。通常可以通过将托管委托传递给本机函数并让运行时处理两个代码实体之间的调用封送来实现本机代码的回调。但是,如前所述,Compact Framework 无法处理字符串的自动转换和需要传递的结构的转换。为了解决这个问题,当调用需要回调的 WPSAPI 函数时,它会收到指向我的 WpsProxy 项目中函数的指针。然后该函数将回调它从托管代码收到的委托。当托管代码返回一个值时,WpsProxy 组件将获取该返回值并将其传递回 WPSAPI。在 WPSAPI 文档中列为可选的回调函数的情况下,它仍将从 WpsProxy 接收回调函数。如果 WpsProxy 没有托管委托要调用,它只将控制权返回给 WPSAPI。

以下是 WpsProxy 中由托管代码调用且委托作为参数传递的函数之一。函数体执行从双字节字符到单字节字符的转换,然后调用 WPSAPI 函数。

extern "C" WPSPROXY_API WPS_ReturnCode PeriodicLocation(
    LPWSTR userName, 
    LPWSTR realm, 
    WPS_StreetAddressLookup lookupType, 
    unsigned long period, 
    unsigned long iterations, 
    WiFiLocationDelegate* callback
    //This is the delegate passed in from managed code
    )
{
    //perform conversion from double byte strings to single byte strings
    WPS_SimpleAuthentication simpleAuthentication;
    simpleAuthentication.realm = wcstombs(realm);
    simpleAuthentication.username = wcstombs(userName);
    WPS_ReturnCode result;
 
    result = WPS_periodic_location(
        &simpleAuthentication, 
        lookupType,period, 
        iterations, 
        WiFiLocationCallback,        //Function predefined within WPSAPI
        callback                    //managed delegate. Passed as data parameter
        );                    
    //delete userName;
    //delete realm;
    return result;
}

WpsProxy 代码实现回调的示例如下。为了突出回调机制的实现,我删除了许多不重要的代码。WPSAPI 会回调此函数,然后此函数将回调托管代码(如果托管代码提供了委托),或者在没有传递委托的情况下不执行回调。

WPS_Continuation WiFiLocationCallback(void*  arg, WPS_ReturnCode result, 
                 const WPS_Location* location)
{
    WPS_Continuation retCode;
    retCode = WPS_STOP;
    if(arg!=NULL) // if a callback function has been passed
    {
        WiFiLocationDelegate delegateMethod = (WiFiLocationDelegate)arg;
        
        //Code deleted from here for clarity. At the end of the work that is
        //performed by the code that I've deleted here the callback function
        //is called.
        
        retCode = delegateMethod(result,location->latitude, location->longitude, 
                                 location->hpe, location->nap, location->speed, 
                                 location->bearing, streetNumber, addressLine, 
                                 city, stateName, stateCode, postalCode, county, region, 
                                 countryName, countryCode,province);
    }/*arg!=null*/
    return retCode;
}

使用包装器

要使用包装器,必须引用 *J2i.Net.WiFiPositioning.dll* 程序集。如果您将此引用添加到项目,部署项目,然后尝试调用 WPS 函数,您将收到一个关于 *WPSPROXY.dll* 未找到的错误。您需要手动将该程序集复制到设备上的目标文件夹。执行此操作后,如果您再次尝试运行项目,即使文件存在,您仍将收到完全相同的错误。此错误发生是因为除非 *WpsProxy.dll* 依赖的文件可用,否则无法加载它;您还需要将 *WPSAPI.DLL* 文件复制到目标设备。

如果满足上述要求,您可以通过实例化一个 WPS 对象,然后调用一个函数来获取您的位置。此基本功能在名为 *SimpleClient* 的项目中进行了演示。该项目中的代码如下所示。

// From the project SimpleClient
        
Wps _wps = new Wps(_userName, _realm);
 
void UpdatePosition(WiFiLocation position)
{
   //...this function list out the values of the fields on the WiFiLocation
   //object passed to it. 
}
private void miFullAddress_Click(object sender, EventArgs e)
{
    WiFiLocation loc = _wps.GetWiFiLocation(StreetAddressLookupType.FullStreet);
    this.UpdatePosition(loc);
}
 
void miPosition_Click()
{
    WiFiLocation loc = _wps.GetWiFiLocation(StreetAddressLookupType.NoStreet);
    this.UpdatePosition(loc);
}

WPS 类还有其他可调用的方法。我在这里不详细介绍所有方法的用途。这些信息可以在 Skyhook wireless 网站上的 WPSAPI 文档中找到。我为 WPS 类设置的方法名称与 WPSAPI 库中的函数名称不完全相同,但它们相似,并且将 Wps 类方法映射到 WPSAPI 函数并不困难。在接下来的几周,我将在 Skyhook Wireless 讨论组中提供更详细的文档。

Wps Class Diagram

这两个重载方法(`WPS.GetIPLocation` 和 `Wps.GetWiFiLocation`)允许两种处理无法获取位置情况的方式。您可以选择通过返回代码或处理异常来检测这些情况。

//These methods will throw an exception if a location cannot be acquired
public WiFiLocation GetWiFiLocation(StreetAddressLookupType lookupType);
public IPLocation GetIPLocation(StreetAddressLookupType lookupType)
 
//These methods will return an error code if a location cannot be acquired
public ResultCode GetWiFiLocation(StreetAddressLookupType lookupType, 
                                  out WiFiLocation location)
public ResultCode GetIPLocation(StreetAddressLookupType lookupType, 
                                out IPLocation location)

简单客户端获取基本位置和完整位置信息的屏幕截图。

上述函数调用返回 `WiFiLocation` 的实例。`WiFiLocation` 和 `IPLocation` 都是派生自 `Location` 类的类。这两个类的 WPSAPI 等效项相互独立,但对于我的包装器实现,我让它们派生自一个公共类。

如果您需要定期更新用户位置,WPSAPI 可以轮询用户位置,并以您选择的频率向您报告其位置。我已通过 `BeginPeriodicLocation` 方法使此功能可访问。这是一个阻塞调用;调用者在 WPS 对象没有更多位置可报告之前不会重新获得控制。如果您不想阻塞主线程,则需要为定期更新创建自己的线程。

ResultCode rc = _wps.BeginPeriodicLocation(
        StreetAddressLookupType.NoStreet, //leve of positioning detail required
        5000,   //period (in milliseconds) desired between updates
        10      //The number of times that a position is to be reported
);

缓存位置数据

Skyhook Wireless 通过一种称为平铺(tiling)的机制提供路由器 ID 及其位置的缓存。开发人员可以指定缓存整体可以消耗多少内存,以及单个会话中可以从 Skyhook Wireless 服务器下载多少数据。您需要指定缓存数据将保存的文件路径。以下代码将在“\temp”文件夹中保存最多略低于 4 MB 的缓存数据。每个会话下载的数据不会超过 500,000 字节。

rc = _wps.BeginTiling("\\temp\\", 500000, 4000000);

请注意,平铺仅在您仅请求位置信息,不包含地址数据时才有效。请求地址数据总是会导致对 Skyhook Wireless 服务器的调用。

微软的基于位置的 Web 服务

有许多可供您使用的基于位置的 Web 服务。最容易使用的服务之一是 Live Search Web 服务。我不会在本文中描述 Live Search,但如果您想开始,可以在这篇 CodeProject 文章中找到代码示例。Live Search 的位置相关功能最能描述为像电话簿。最重要的是,该服务易于使用。MapPoint 和 Virtual Earth Web 服务提供更多的信息,但使用起来需要更多的努力。MapPoint 和 Virtual Earth 服务的开发者帐户是同一个帐户,我甚至发现,在某些情况下,我可以将一个程序从一个服务重定向到另一个服务,它仍将按设计工作,而无需更改任何其他代码。

服务划分

Virtual Earth 和 MapPoint Web 服务没有将服务功能放在一个单一的巨型 Web 服务中,而是将其划分为功能组。

  • Common - 包含一般功能和身份验证所需的调用
  • Geocode - 包含在地址、地理坐标和其他命名地点方法之间进行转换的功能
  • Imagery - 用于获取区域地图
  • Route - 用于获取路线指示
  • Search - 用于查找区域内的商家。

在这些示例程序中,我对 Imagery、Search、Geocode 和 Common 服务进行了一些调用。以下是 Virtual Earth WSDL 的链接。

Service URI
Common http://staging.common.virtualearth.net/find-30/common.asmx
地理编码 http://staging.dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc?wsdl
图像 http://staging.dev.virtualearth.net/webservices/v1/imageryservice/imageryservice.svc?wsdl
路线 http://staging.dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc?wsdl
搜索 http://staging.dev.virtualearth.net/webservices/v1/searchservice/searchservice.svc?wsd

使用搜索服务

打开名为 *MapPoint.SimpleClient* 的解决方案。此项目利用 NavTech 的数据通过搜索功能查找属于特定类别的商家(程序已预配置为使用北美数据源;如果您在欧洲,则需要更改程序设置以使用欧洲数据源)。选择一个搜索类别,然后选择“搜索”按钮,您将看到您所在地区属于该类别的商家列表。

private void miSearch_Click(object sender, EventArgs e)
{
    MapPoint.Service.FindServiceSoap findService = 
             new MapPoint.SimpleClient.MapPoint.Service.FindServiceSoap();
 
    //To prevent to annoying URI Exception in the debug output. Optional.
    findService.Proxy = System.Net.GlobalProxySelection.GetEmptyWebProxy(); 
 
    //Specify our credentials for authorization
    findService.Credentials = new NetworkCredential(
                                      _registrySettings.MapPointAppID, 
                                      _registrySettings.MapPointPassword
                                      );
    findService.PreAuthenticate = true;
 
    FindFilter findFilter = new FindFilter();
 
    //http://msdn.microsoft.com/en-us/library/cc534903.aspx
    findFilter.EntityTypeName = _entityDictionary[dupQuery.Text]; 
    
    //only display the display name for each search result
    findFilter.PropertyNames = new string[]{"DisplayName"};
 
    MapPoint.Service.LatLong latLong = new MapPoint.Service.LatLong();
    latLong.Latitude = Double.Parse(txtLatitude.Text);
    latLong.Longitude = Double.Parse(txtLongitude.Text);
 
 
    MapPoint.Service.FindNearbySpecification findSpecification = 
                     new MapPoint.Service.FindNearbySpecification();
    findSpecification.DataSourceName = _registrySettings.MapPointDataSource;
    //If the user has not selected a datasource then we will
    //use the NavTech North American datasource
    if (findSpecification.DataSourceName.Length == 0)
        findSpecification.DataSourceName = "NavTech.NA";
    findSpecification.LatLong = latLong;
    findSpecification.Distance = 100;
    findSpecification.Filter = findFilter;
 
    FindResults resultList = null;
    try
    {
        resultList = findService.FindNearby(findSpecification);
        if (resultList.Results != null)
        {
            DisplayResults(resultList.Results);
        }
    }
    catch(Exception exc)
    {
        System.Diagnostics.Debug.WriteLine(exc.ToString());
    }
}

显示地图

从 Virtual Earth 检索地图是一个多步骤过程。使用 Common 服务,必须获取令牌并随每个请求提交。可以将令牌视为与会话 cookie 相同的东西。它必须随许多其他请求一起提交,并在一定时间后过期。一旦您拥有令牌,您就可以请求地图 URI;该调用需要您希望地图中心区域的坐标、缩放级别,以及可选地,您希望在地图上显示的图钉的坐标。结果是一个包含图像 URI 的字符串。获取地图的第三个请求是下载您收到的 URL 处的图像。为了指导您完成这些步骤,请打开项目 J2i.Net.MapPoint.RenderMap。

J2i.Net.MapPoint.RenderMap 包含对两个服务的 Web 引用:公共服务和图像服务。由于我使用的是开发账户而不是商业账户,我请求的地图上将带有“Staging”水印。

RenderMap Example

重新审视 FindMe

2007 年 8 月,我发布了一个基于 GPS 的 Windows Mobile 程序,它可以在收到特殊短信时通过电子邮件或短信回复我的位置。该程序的一个缺点是,为了使用程序的响应,您必须能够访问完整的浏览器。即使现在,完整的浏览器也不是 Windows Mobile 设备上的标准组件(尽管在接下来的几个月内会发生变化)。在文章的最后,我曾表示有兴趣在程序中添加地图,以便在没有计算机辅助的情况下也可以使用其响应。

这个程序的第一个版本只会发送手机位置,但绝不会接收其他人的位置信息。在这个更新的版本中,程序将同时发送位置并显示从其他设备接收到的位置信息。我还取消了请求他人位置所需的 PIN 码的使用。位置信息的权限完全通过添加到联系人条目中的属性来控制。

使用模拟器省钱

虽然您家里有几部 Windows Mobile 手机可以用于测试这个程序,但您会希望在模拟器上测试这个程序的某些部分。在我看来,短信对于在真实硬件上进行所有测试来说太昂贵了。在美国,消息的发送方和接收方都会被收费。目前,一条短信的费用在 0.20 到 0.30 美元之间(我们姑且说它花费 0.25 美元)。在一次交互中,一部手机向另一部手机发送消息然后收到响应,总共会有四项费用。发送手机在发送请求时产生费用,接收手机在接收请求时产生另一项费用,然后接收手机响应又产生另一项费用,而原始手机收到响应又是一项费用。仅一次交互就花费 1.00 美元。我的手机有一个短信套餐,允许在产生费用之前发送有限数量的消息。但是,消耗完我分配的消息不会花很长时间

模拟器在这里帮助很大。可以使用 Windows Mobile 蜂窝模拟器测试短信相关代码,并且不会产生任何费用。WPSAPI 和蜂窝塔定位代码在模拟器中无法工作,但可以通过使用 FakeGPS(GPS 模拟器)或在真实硬件上进行最终测试之前在模拟器中硬编码位置来轻松解决这个问题。

通过 WiFi 调试

我手机中的 WiFi 适配器通常是关闭的。在调试此程序时将其打开时,我遇到了一个烦人的行为。我手机中的 WiFi 适配器连接到我的家庭网络,当我开始调试时,Visual Studio 通过 WiFi 而不是 USB 数据线与我的设备通信。我通过断开 USB 数据线验证了这一点。断开连接后,我仍然可以单步执行代码并启动和停止我的程序。这听起来像是一个不错的功能,但通过 WiFi 调试速度极慢。在调试此程序时,我从手机中删除了路由器的设置以防止此行为。

代码组织

代码分为三个不同的部分:接口、UI 类和处理类。我为几个(但不是所有)类定义了接口,以确保某些类彼此之间不会太紧密耦合。这使得替换实现变得更容易。例如,有一个名为 `IGeoManager` 的接口,它声明了一些用于地理编码和检索地图的方法。`VirtualEarthGeoManager` 实现此接口以从 Microsoft 的 Virtual Earth Web 服务检索地图。如果我决定使用另一个地图和地理编码提供商,我只需要定义另一个实现此接口的类,并实例化它而不是 `VirtualEarthGeomanager`。

大多数 UI 类,通常除了您期望在表单中找到的内容之外,没什么可说的。`MapDisplay` 类是一个例外。它是一个自定义类,用于以手指友好的方式显示地图。提供给此控件的地图图像通常会大于控件本身的大小,因此您可以在此视图中拖动地图。一些 UI 类也使用异步加载。列出所有联系人或下载地图等任务可能需要一些时间,并且以同步方式执行这些任务会导致程序在一段时间内显得冻结。在完整的框架中,可以通过委托上的 `BeginInvoke` 和 `EndInvoke` 方法异步调用方法。这些方法存在于 Compact Framework 委托上,但它们未实现;尝试调用它们会导致 `NotImplemented` 异常。因此,我没有使用 `BeginInvoke`,而是使用 `ThreadPool` 类在另一个线程上运行任务,并使用 `Control.Invoke` 在任务完成后将 UI 更改封送到主线程。

处理类都在一个名为 *Common* 的文件夹中。功能被分到几个类中,所以应该很容易找到包含特定功能片段的类。以下是一些最重要的类及其用途的列表。

描述
CellTowerLocationProvider 使用 *OpenCellID.org* 进行基于蜂窝塔的定位
GpsLocationProvider 使用 GPS 硬件确定您的位置
WiFiLocationProvider 使用 Skyhook Wireless 的 WPSAPI 从附近的路由器确定位置。
HubLocationProvider 从 `GpsLocationProvider`、`WiFiLocationProvider` 和 `CellTowerLocationProvider` 收集位置信息,并返回最精确且尚未过期的坐标
EmailLocationResponder 使用电子邮件发送位置信息
LocationResponder 包含一些用于创建响应消息的实现的抽象类
ContactApproval 用于确定联系人是否有权限查看您的位置
MapRequest 包含请求的地图信息
SqlLocationLogger 用于将接收到的位置记录到本地 SQL 数据库并检索这些位置以供查看
Session 封装了程序的大部分状态信息
设置 包含用户偏好设置

选择位置

此程序中定义了三种位置提供程序:GPS、WPS 和基于蜂窝塔的位置。这三者是在 Skyhook Wireless SDK 的最新版本 2.7 发布时定义的。版本 2.7 不包含 XPS,所以我不得不从这三个来源收集位置信息,然后自行聚合。随着 3.0 SDK 的发布,WPS 将自动聚合这些位置来源,因此其他位置提供程序将不再使用。但我决定将它们保留在代码中,特别是对于那些希望使用 OpenCellID 蜂窝塔定位系统并需要如何调用示例的人。位置信息已通过名为 `HubLocationPRovider` 的类进行聚合。从每种定位技术返回的位置信息都有一个优先级。GPS 具有最高优先级,因为它最精确。WPS 次之,蜂窝塔位置最后。这些服务的每种位置信息还被分配了一个过期时间;如果我短暂地丢失了 GPS 信号,那么在短时间内,最后报告的 GPS 位置可能比 WPS 或蜂窝塔位置更准确。`HubLocationProvider` 不会考虑下一个可用的提供程序,直到更高优先级的提供程序过期。

//variable for holding our list of providers
ProviderExpiration[] _providerList;
 
public HubLocationProvider()
{
    //Add the providers to the provider list by order of priority
    _providerList = new ProviderExpiration[3] ;
 
    _providerList[0] = new ProviderExpiration();
    _providerList[0].ExpirationTime = new TimeSpan(0, 0, 30);
    _providerList[0].LocationProvider = _gpsProvider;
 
    _providerList[1] = new ProviderExpiration();
    _providerList[1].ExpirationTime = new TimeSpan(0, 1, 15);
    _providerList[1].LocationProvider = _wifiLocationProvider;
 
    _providerList[2] = new ProviderExpiration();
    _providerList[2].ExpirationTime = new TimeSpan(0, 2, 0);
    _providerList[2].LocationProvider = _cellProvider;
}
 
public Location LastLocation
{
    get 
    {
        // Go through all of the available location providers by order of
        //priority and return the highest priority non-expired value
        for (int i = 0; i < _providerList.Length; ++i)
        {
            if (!_providerList[i].IsExpired())
                return _providerList[i].LocationProvider.LastLocation;
        }
        return null;
    }
}

检测位置请求

位置请求通过 SMS 发送。使用 `MessageInterceptor` 类,任何满足特定要求的消息都将被路由到 FindMe 程序。我们感兴趣的消息属性被封装在 `MessageCondition` 类的实例中。

除了将消息路由到我的程序之外,我还希望我的程序在收到消息时自动启动(如果它尚未运行)。如果您启用应用程序启动并提供一个 ID 字符串来识别您的应用程序,`MessageInterceptor` 类将为您处理此问题。如果您的程序因传入 SMS 而启动,您可以使用您的应用程序的 ID 字符串创建 `MessageInterceptor` 来检索消息,并在事件处理程序附加到对象时接收消息。此代码位于 `SmsMessageReceiver` 类中。对于我的消息规则,我关心任何包含字符串“findme://”的消息。

void EnsureMessageInterceptor()
{
    if (_messageInterceptor == null)
    {
        //If the application launcher is enable then 
        //the system already has our message interception
        //rules. We recreate the message interceptor using our Application ID string
        if (MessageInterceptor.IsApplicationLauncherEnabled(ApplicationLaunchID))
        {
            _messageInterceptor = new MessageInterceptor(ApplicationLaunchID, 
                                      _session.UserSettings.EnableAutostart);
        }
        else
        {
            //The application launcher isn't enabled, so we need
            //to create a new interceptor and give it rules. 
            _messageInterceptor = new MessageInterceptor();
            MessageCondition messageCondition = 
              new MessageCondition(
                  MessageProperty.Body,
                  MessagePropertyComparisonType.Contains,"findme://"

                  );
            _messageInterceptor.InterceptionAction = 
                                InterceptionAction.NotifyAndDelete;
            _messageInterceptor.MessageCondition = messageCondition;
            
            //Enable application launching if the user's settings allow for it.
            if(_session.UserSettings.EnableAutostart)
               _messageInterceptor.EnableApplicationLauncher(ApplicationLaunchID);
        }                
    }
}

收到消息后,我需要决定如何处理它。这个程序可以接收两种类型的消息:用户的位置,或者来自另一个用户请求接收位置的请求。如果消息包含用户的位置,它将通过在“findme://”字符串后立即附加纬度、经度以及可能的评论来打包(findme://latitude,longitude,comment)。如果用户正在请求位置,那么“findme://”字符串将立即跟着“whereareyou”。为了解析这些消息,我使用了正则表达式。

static Regex LocationRegularExpression = 
  new  Regex(@"findme://(?<<atitude>(\-|\+)?\d+(\.\d*)?),"+
             "(?<longitude>(\-|\+)?\d+(\.\d*)?),(?<comment>.*)?", 
             RegexOptions.IgnoreCase|RegexOptions.Singleline);
static Regex LocationRequestRegularExpression = 
  new Regex(@"findme://whereareyou(\?format=(?<format>[a-z]*))?", 
            RegexOptions.IgnoreCase | RegexOptions.Singleline);

我可以通过在 `Match` 结果上使用 `Groups` 集合从正则表达式匹配中提取我需要的信息。在下面的示例代码中,我正在提取接收到的纬度和经度。

Match m = LocationRegularExpression.Match(smsMessage.Body);
if (m.Success)
{
    playAlert = true;
    double latitude = double.Parse(m.Groups["latitude"].Value);
    double longitude = double.Parse(m.Groups["longitude"].Value);
    string comment = m.Groups["Comment"].Value;
    
}

在 *findme://whereareyou* 消息的情况下,会立即使用 *findme://latitude,longitude,comment* 格式发送一条短信。代码还将检查消息后面可选的格式参数。格式参数的值可以是 `LocationFormat`(`PlainText`、`LiveMapsLink`、`ShortLiveMapsLink` 或 `FindMeString`)枚举值中的任何一个。如果我想要一个显示用户位置的 Microsoft Live Map 链接,我会发送 *findme://whereareyou?format=LiveMapsLink*。该程序不支持发送此类请求,但如果您想访问它,该功能是存在的。

安全

任何人都不希望一个这样的程序在不首先考虑应用程序用户是否愿意允许请求者查看其位置的情况下报告信息。我第一次实现这个程序时,有两个因素控制着程序是否会发送响应。第一个因素是请求者必须知道要发送到设备的 PIN。第二个因素是用户必须授予请求者查看其位置的权限。我已经取消了 PIN 的使用,现在完全依赖联系人权限。

`Contact` 类有一个名为 `Properties` 的成员,可用于存储用户的自定义信息。如果联系人被授予查看某人位置的权限,我通过向联系人的条目添加自定义属性并将其设置为“Yes”来保存此信息。可以通过清除此值来撤销权限。

partial class ContactApproval: IContactApproval
{
 
    const string AllowLocationViewPropertyName = "Approved for Find Me";
    const string AllowLocationViewPropertyValue = "Yes";
 
    public void Allow(Contact c)
    {
        if (!c.Properties.Contains(AllowLocationViewPropertyName))
            c.Properties.Add(AllowLocationViewPropertyName);
        c.Properties[AllowLocationViewPropertyName] = 
            AllowLocationViewPropertyValue;
        c.Update();
    }
 
    public void Revoke(Contact c)
    {
        if (c.Properties.Contains(AllowLocationViewPropertyName))
            c.Properties[AllowLocationViewPropertyName] = null;
    }
 
    public bool IsAllowed(Contact c)
    {
        if ((c==null)||(c.ItemId.ToString().Equals("0")))
            return false;
        if (c.Properties.Contains(AllowLocationViewPropertyName))
        {
            object o = c.Properties[AllowLocationViewPropertyName];
            string allowSetting = o  as string;
            if (AllowLocationViewPropertyValue.Equals(allowSetting))
                return true;
        }
        return false;
    }
}

记录用户位置

程序收到用户位置后做的第一件事就是将位置保存到日志中。保存位置后,可以通过从程序主菜单中选择“朋友的位置”来查看。此屏幕上的所有信息都直接从日志中提取,除了距离列。距离是根据当前位置计算的。用于计算距离的公式如下所示

public static double CalcDistance(double lat1, double lng1, double lat2, double lng2, 
              double radius /*radius of earth in the unit of your choosing*/)
{
 
    return radius * 2 * Math.Asin(Math.Min(1, Math.Sqrt(
       (Math.Pow(Math.Sin((DiffRadian(lat1, lat2)) / 2.0), 2.0) + 
        Math.Cos(ToRadian(lat1)) * Math.Cos(ToRadian(lat2)) * 
        Math.Pow(Math.Sin((DiffRadian(lng1, lng2)) / 2.0), 2.0)))));
}

当您选择在地图上查看用户位置的选项时,会在您的位置和用户的位置都放置一个图钉。根据您与用户之间的距离,可能需要缩小地图才能看到地图上的两个标记。

Received Position

未来版本

这是我第一次成功尝试创建 WPSAPI 包装器,但绝不是最后一次。包装器提供了我尚未使用的功能,这些功能需要进行更多测试。我还想对内存分配方式进行一些更改,以减少在包装器使用会话生命周期内发生的垃圾回收周期次数。我将在接下来的几周内更多地研究包装器,并计划在Skyhook Wireless 讨论组中提供最终版本。

历史

  • 2008 年 1 月 6 日 - 基于 Skyhook Wireless 27 SDK 的首次发布。
  • 2008 年 1 月 9 日 - 添加 WpsProxy 作为内容文件。
  • 2008 年 1 月 18 日 - 更新代码和文章以适应新发布的 3.0 SDK
© . All rights reserved.