6)计算最佳路线。执行线路计算的核心算法定义在AntSystem.h和AntSystem.cpp中,这些文件定义了AntSystem命名空间,这个命名空间不包含WinRT上的依附元素,所以并不使用C++/CX。AntSystem.h定义了LatLong,Node和Edge结构体,同时也定义了OptimizeRoute函数。
LatLong结构体表示地图上一个点的经纬度,代码如下。
struct LatLong
{
explicit LatLong(double latitude, double longitude)
: Latitude(latitude)
, Longitude(longitude)
{
}
// 位置坐标
double Latitude;
double Longitude;
};
Node结构体表示图像中一个节点,包含位置的名称,经纬度,也含从Bing Maps服务中而来的替换名称,代码如下。
struct Node
{
explicit Node(const std::wstring& inputName)
: InputName(inputName)
, ResolvedLocation(0.0, 0.0)
{
}
// 用户提供的位置名称
std::wstring InputName;
// Bing Maps位置服务提供的位置经纬度
LatLong ResolvedLocation;
std::wstring ResolvedName;
//
//匹配当前输入所有可能位置的平行数组(数组中包含名称字符串,经纬度等)
//比如在输入一个城市名称后,显示一系列和此城市名相近的(匹配的)具体位置名称。
// 位置数组会包含对应的每个位置经纬度
std::vector<std::wstring> Names;
std::vector<LatLong> Locations;
};
Edge结构体连接两个节点,并包含点间距离,同样也包含用作蚁群优化算法的数据,代码如下。
struct Edge
{
explicit Edge(std::shared_ptr<Node> pointA, std::shared_ptr<Node> pointB)
: PointA(pointA)
, PointB(pointB)
, Pheromone(0.0)
, TravelDistance(-1.0)
{
}
// 开始节点
std::shared_ptr<Node> PointA;
// 结束节点
std::shared_ptr<Node> PointB;
//棱边的信息数
double Pheromone;
// 始末节点的距离
double TravelDistance;
};
C++组件为每个线路中的位置创建一个Node对象,为每对位置间创建一个Edge对象。在组件集合了所有从Bing Maps web服务上获取的必要信息后,它就会调用OptimizeRoute计算最佳路线,代码如下。
// 计算所给出节点间距离,算出最短距离
// 这个方法提供Node集合
std::vector<size_t> OptimizeRoute(
std::vector<std::shared_ptr<Node>>& nodes,
std::vector<std::shared_ptr<Edge>>& edges,
double alpha,
double beta,
double rho,
unsigned int iterations,
Concurrency::cancellation_token cancellationToken,
std::function<void(unsigned int)>* progressCallback = nullptr,
bool parallel = true);
算法使用的一个重要方面是利用并发性,蚁群优化算法执行3个基本步骤的几次迭代,代码如下。
// 执行几次蚁群算法模拟
auto startTime = GetTickCount64();
for (unsigned int i = 0; i < iterations; ++i)
{
// 不定期检查是否取消
auto time = GetTickCount64();
if (time - startTime > 100) {
if (cancellationToken.is_canceled()) {
// 返回空集合
return vector<size_t>();
}
startTime = time;
}
// 发送进度
if (progressCallback != nullptr) {
(*progressCallback)(i);
}
// 注意这个操作可并行执行
// 这个步骤不包含共享数据或依附性计算
if (parallel) {
parallel_for_each(begin(ants), end(ants), [&](Ant& blitz) {
blitz.Explore();
});
}
else {
for_each(begin(ants), end(ants), [&](Ant& blitz) {
blitz.Explore();
});
}
//
for_each(begin(edges), end(edges), [rho](shared_ptr<Edge> edge) {
edge->Pheromone *= (1.0 - rho);
});
//图像中回溯
// 注意这个操作不是并行执行的,因为每个棱边的值的更新。
// 这里回溯操作相对来说不是很长
for_each(begin(ants), end(ants), [&](Ant& blitz) {
blitz.Backtrack();
});
}
7)处理取消操作。IAsyncAction,IAsyncActionWithProgress<TProgress>,IAsyncOperation<TResult>,和IAsyncOperationWithProgress<TResult, TProgress>中的每一个接口都会提供一个取消异步操作的“Cancel”方法。可以将任务的取消和WinRT的“Cancel”方法以两种方式关联,首先可以定义传递给create_async的功能函数得到一个Concurrency::cancellation_token对象,当“Cancel”方法被调用时,取消标记(cancellation token)会被撤销。如果没有cancellation_token对象,下层的任务对象会隐式地定义一个,当需要协同响应在功能函数中的取消操作,就定义一个cancellation_token对象。
取消操作是用户在JavaScript应用中选择取消按钮,或一个不可恢复错误发生时出现的,保持界面在优化任务中响应,是为了让用户能够使用取消,而取消不会立刻发生。C++组件使用Concurrency::cancellation_token_source和Concurrency::cancellation_token标志取消并随机地检测取消操作。从一个执行性能的角度讲,如果应用检测取消的操作过于频繁,或者说检查取消的时间多于应用执行正常工作的时间,对应用的性能会产生影响。C++组件以几种方式检测取消,第一种方式是在每个优化阶段,调用Concurrency::is_task_cancellation_requested函数以确定当前任务是否接受到取消执行的请求之后,出现Continuation任务,如果请求了取消,Continuation调用Concurrency::cancel_current_task 将当前任务置于已撤销状态,下面的代码在从Bing Maps上获取路径之前,获取位置之后的时候检测取消操作。
//
// 阶段二: 为每对位置获取路由信息
//
// 报告进度
reporter.report("Retrieving routes (0% complete)...");
auto tasks = RetrieveRoutes(params, cancellationToken, reporter);
// 在所有当前任务完成后前往下一阶段
return when_all(begin(tasks), end(tasks))
.then([=](task<void>) -> task<IMap<String^, IVector<String^>^>^> {
//如果有取消则返回
if (is_task_cancellation_requested()) {
cancel_current_task();
return task_from_result<IMap<String^, IVector<String^>^>^>(nullptr);
}
// 进度报告
reporter.report("Retrieving routes (100% complete)...");
// 记录HTTP消耗时间
params->HttpTime = GetTickCount64() - params->HttpTime;
第二种方式是在组件收到其他任务的结果时,通过捕获Concurrency::task_canceled检测取消操作。当每个HTTP请求结束时,会在XML数据处理前检查结果代码,下面的代码展示了TripOptimizerImpl::RetrieveLocations方法怎样执行这样的检查,如果取消操作,Concurrency::task::get方法会抛出task_canceled,这样Continuation会返回并不再处理文档内容。
// 下载完成后创建Continuation任务填写位置信息
tasks.push_back(downloadTask.then([=](task<tuple<HRESULT, wstring>> response) {
try {
// 如果下载操作失败,抛出COMException异常
HRESULT hr = get<0>(response.get());
if (FAILED(hr)) {
throw ref new COMException(hr);
}
}
// 操作取消了就不再处理文档
catch (task_canceled&) {
return;
}
try {
// 创建并加载来自响应中的XML文档
XmlDocument^ xmlDocument = ref new XmlDocument();
xmlDocument->LoadXml(ref new String(get<1>(response.get()).c_str()));
// 填写位置信息
ProcessLocation(node, xmlDocument, params->UnresolvedLocations);
}
catch (Exception^) {
// 错误发生,取消任何活跃的操作
m_cancellationTokenSource.cancel();
// 重新抛出异常
throw;
}
// 进度报告
wstringstream progress;
progress << L"Retrieving locations ("
<< static_cast<long>(100.0 * (params->Nodes.size() - params->RequestsPending) / params->Nodes.size())
<< L"% complete)...";
reporter.report(ref new String(progress.str().c_str()));
InterlockedDecrement(¶ms->RequestsPending);
}, cancellationToken));
TripOptimizerImpl::RetrieveRoutes方法在每个HTTP请求结束之后,执行了一个相似的检查。接着上面说第三种组件检测取消的方式,是通过调用Concurrency::cancellation_token::is_canceled方法,路线优化算法(即对应的AntSystem::OptimizeRoute函数)以100ms时间为间隔检测取消,代码如下。
// 执行几次模拟
auto startTime = GetTickCount64();
for (unsigned int i = 0; i < iterations; ++i)
{
// 随机检测取消
auto time = GetTickCount64();
if (time - startTime > 100) {
if (cancellationToken.is_canceled()) {
// 返回空集合
return vector<size_t>();
}
startTime = time;
}
与以上类似,HttpRequestCallback类使用Concurrency::cancellation_token::register_callback方法注册一个在取消标记被撤销时调用的回调函数,也可以支持操作取消。这个方法很有用,因为IXMLHTTPRequest2接口执行我们控制之外的异步工作,当取消标记被撤销时,回调函数终止HTTP请求并设置任务结束事件,代码如下。
HttpRequestCallback(IXMLHTTPRequest2 *request, cancellation_token cancellationToken)
: m_dwRef(1)
, m_request(request)
, m_cancellationToken(cancellationToken)
{
// 注册当取消标记被撤销时,中止HTTP操作的回调函数
m_aborted = false;
m_registrationToken = m_cancellationToken.register_callback([this]() {
// 设置中止标志
m_aborted = true;
if (m_request != nullptr) {
m_request->Abort();
}
m_completionEvent.set(make_tuple<HRESULT, wstring>(S_OK, wstring()));
});
}
cancellation_token::register_callback返回一个Concurrency::cancellation_token_registration对象来识别回调注册。HttpRequest类的析构函数使用这个注册对象,来取消回调函数的注册。最好在不再需要回调函数时取消对其的注册,这样保证在回调函数被调用时所有的对象都是有效的,代码如下。
~HttpRequestCallback()
{
// Unregister the callback.
m_cancellationToken.deregister_callback(m_registrationToken);
}
遇到不可恢复错误时,任何遗留的任务都会被取消,例如如果无法处理一个XML文档,全部的操作会被取消并抛出异常,代码如下。
try {
// Create and load the XML document from the response.
XmlDocument^ xmlDocument = ref new XmlDocument();
xmlDocument->LoadXml(ref new String(get<1>(response.get()).c_str()));
// Fill in location information.
ProcessLocation(node, xmlDocument, params->UnresolvedLocations);
}
catch (Exception^) {
// An error occurred. Cancel any active operations.
m_cancellationTokenSource.cancel();
// Rethrow the exception.
throw;
}