/// <summary>
        /// Executes the request. If there are more waypoints than the batchSize value (default 25), only a MaxSolutions is set to 1, and Tolerances is set to null.
        /// </summary>
        /// <param name="remainingTimeCallback">A callback function in which the estimated remaining time in seconds is sent.</param>
        /// <returns>A response containing the requested data.</returns>
        public override async Task <Response> Execute(Action <int> remainingTimeCallback)
        {
            Response response = null;

            if (WaypointOptimization != null && WaypointOptimization.HasValue && Waypoints.Count >= 2)
            {
                var wpHash = ServiceHelper.GetSequenceHashCode <SimpleWaypoint>(Waypoints);

                var      optionHash = Enum.GetName(typeof(TspOptimizationType), WaypointOptimization);
                var      mode       = TravelModeType.Driving;
                DateTime?depart     = null;

                if (RouteOptions != null)
                {
                    optionHash += "|" + Enum.GetName(typeof(RouteTimeType), RouteOptions.TimeType);

                    if (RouteOptions.TimeType == RouteTimeType.Departure && RouteOptions.DateTime != null && RouteOptions.DateTime.HasValue)
                    {
                        depart      = RouteOptions.DateTime;
                        optionHash += "|" + RouteOptions.DateTime.ToString();
                    }

                    mode = RouteOptions.TravelMode;

                    optionHash += "|" + Enum.GetName(typeof(TravelModeType), mode);
                }
                else
                {
                    optionHash += "|" + Enum.GetName(typeof(RouteTimeType), RouteTimeType.Departure);
                }

                //Check to see if the waypoints have changed since they were last optimized.
                if (waypointsHash != wpHash || string.Compare(optimizationOptionHash, optionHash) != 0)
                {
                    var tspResult = await TravellingSalesmen.Solve(Waypoints, mode, WaypointOptimization, depart, BingMapsKey);

                    Waypoints = tspResult.OptimizedWaypoints;

                    //Update the stored hashes to prevent unneeded optimizations in the future if not needed.
                    waypointsHash          = ServiceHelper.GetSequenceHashCode <SimpleWaypoint>(Waypoints);
                    optimizationOptionHash = optionHash;
                }
            }

            var requestUrl = GetRequestUrl();

            if (RouteOptions != null && RouteOptions.TravelMode == TravelModeType.Truck)
            {
                var requestBody = GetTruckPostRequestBody();

                response = await ServiceHelper.MakeAsyncPostRequest <Route>(requestUrl, requestBody, remainingTimeCallback);
            }
            else
            {
                if (Waypoints.Count <= batchSize)
                {
                    using (var responseStream = await ServiceHelper.GetStreamAsync(new Uri(requestUrl)))
                    {
                        return(ServiceHelper.DeserializeStream <Response>(responseStream));
                    }
                }

                //There is more waypoints than the batchSize value (default 25), break it up into multiple requests. Only allow a single route in the response and no tolerances.
                if (RouteOptions != null)
                {
                    if (RouteOptions.MaxSolutions > 1)
                    {
                        RouteOptions.MaxSolutions = 1;
                    }

                    RouteOptions.Tolerances = null;
                }

                if (Waypoints == null)
                {
                    throw new Exception("Waypoints not specified.");
                }
                else if (Waypoints.Count < 2)
                {
                    throw new Exception("Not enough Waypoints specified.");
                }
                else if (Waypoints[0].IsViaPoint || Waypoints[Waypoints.Count - 1].IsViaPoint)
                {
                    throw new Exception("Start and end waypoints must not be ViaWaypoints.");
                }

                int startIdx = 0;
                int endIdx   = 0;

                var requestUrls = new List <string>();

                while (endIdx < Waypoints.Count - 1)
                {
                    requestUrls.Add(GetRequestUrl(startIdx, out endIdx));
                    startIdx = endIdx - 1;
                }

                var      routes        = new Route[requestUrls.Count];
                Response errorResponse = null;

                Parallel.For(0, requestUrls.Count, (i) =>
                {
                    try
                    {
                        //Make the call synchronously as we are in a parrallel for loop and need this to block, otherwise the for loop will exist before the async code has completed.
                        using (var responseStream = ServiceHelper.GetStreamAsync(new Uri(requestUrls[i])).GetAwaiter().GetResult())
                        {
                            var r = ServiceHelper.DeserializeStream <Response>(responseStream);

                            if (r != null)
                            {
                                if (r.ErrorDetails != null && r.ErrorDetails.Length > 0)
                                {
                                    errorResponse = r;
                                }
                                else if (r.ResourceSets != null && r.ResourceSets.Length > 0 &&
                                         r.ResourceSets[0].Resources != null && r.ResourceSets[0].Resources.Length > 0)
                                {
                                    routes[i] = r.ResourceSets[0].Resources[0] as Route;
                                }
                            }

                            if (i == 0)
                            {
                                response = r;
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        errorResponse = new Response()
                        {
                            ErrorDetails = new string[]
                            {
                                ex.Message
                            }
                        };
                    }
                });

                //If any of the responses failed to process, do not merge results, return the error info.
                if (errorResponse != null)
                {
                    return(errorResponse);
                }

                response.ResourceSets[0].Resources[0] = await MergeRoutes(routes);
            }

            return(response);
        }
        /// <summary>
        /// Executes the request. If there are more waypoints than the batchSize value (default 25), only a MaxSolutions is set to 1, and Tolerances is set to null.
        /// </summary>
        /// <param name="remainingTimeCallback">A callback function in which the estimated remaining time in seconds is sent.</param>
        /// <returns>A response containing the requested data.</returns>
        public override async Task <Response> Execute(Action <int> remainingTimeCallback)
        {
            Response response = null;

            if (WaypointOptimization != null && WaypointOptimization.HasValue && Waypoints.Count >= 2)
            {
                var wpHash = ServiceHelper.GetSequenceHashCode <SimpleWaypoint>(Waypoints);

                var      optionHash = Enum.GetName(typeof(TspOptimizationType), WaypointOptimization);
                var      mode       = TravelModeType.Driving;
                DateTime?depart     = null;

                if (RouteOptions != null)
                {
                    optionHash += "|" + Enum.GetName(typeof(RouteTimeType), RouteOptions.TimeType);

                    if (RouteOptions.TimeType == RouteTimeType.Departure && RouteOptions.DateTime != null && RouteOptions.DateTime.HasValue)
                    {
                        depart      = RouteOptions.DateTime;
                        optionHash += "|" + RouteOptions.DateTime.ToString();
                    }

                    mode = RouteOptions.TravelMode;

                    optionHash += "|" + Enum.GetName(typeof(TravelModeType), mode);
                }
                else
                {
                    optionHash += "|" + Enum.GetName(typeof(RouteTimeType), RouteTimeType.Departure);
                }

                //Check to see if the waypoints have changed since they were last optimized.
                if (waypointsHash != wpHash || string.Compare(optimizationOptionHash, optionHash) != 0)
                {
                    var tspResult = await TravellingSalesmen.Solve(Waypoints, mode, WaypointOptimization, depart, BingMapsKey);

                    Waypoints = tspResult.OptimizedWaypoints;

                    //Update the stored hashes to prevent unneeded optimizations in the future if not needed.
                    waypointsHash          = ServiceHelper.GetSequenceHashCode <SimpleWaypoint>(Waypoints);
                    optimizationOptionHash = optionHash;
                }
            }

            var requestUrl = GetRequestUrl();

            int startIdx = 0;
            int endIdx   = 0;

            if (Waypoints.Count <= batchSize)
            {
                if (RouteOptions != null && RouteOptions.TravelMode == TravelModeType.Truck)
                {
                    var requestBody = GetTruckPostRequestBody(startIdx, out endIdx);

                    response = await ServiceHelper.MakeAsyncPostRequest(requestUrl, requestBody, remainingTimeCallback);
                }
                else
                {
                    remainingTimeCallback?.Invoke(1);

                    using (var responseStream = await ServiceHelper.GetStreamAsync(new Uri(requestUrl)))
                    {
                        response = ServiceHelper.DeserializeStream <Response>(responseStream);
                    }
                }
            }
            else
            {
                //There is more waypoints than the batchSize value (default 25), break it up into multiple requests. Only allow a single route in the response and no tolerances.
                if (RouteOptions != null)
                {
                    if (RouteOptions.MaxSolutions > 1)
                    {
                        RouteOptions.MaxSolutions = 1;
                    }

                    RouteOptions.Tolerances = null;
                }

                if (Waypoints == null)
                {
                    throw new Exception("Waypoints not specified.");
                }
                else if (Waypoints.Count < 2)
                {
                    throw new Exception("Not enough Waypoints specified.");
                }
                else if (Waypoints[0].IsViaPoint || Waypoints[Waypoints.Count - 1].IsViaPoint)
                {
                    throw new Exception("Start and end waypoints must not be ViaWaypoints.");
                }

                var requestUrls   = new List <string>();
                var requestBodies = new List <string>();

                while (endIdx < Waypoints.Count - 1)
                {
                    if (RouteOptions != null && RouteOptions.TravelMode == TravelModeType.Truck)
                    {
                        requestUrls.Add(requestUrl);
                        requestBodies.Add(GetTruckPostRequestBody(startIdx, out endIdx));
                    }
                    else
                    {
                        requestUrls.Add(GetRequestUrl(startIdx, out endIdx));
                        requestBodies.Add(null);
                    }

                    startIdx = endIdx - 1;
                }

                if (remainingTimeCallback != null)
                {
                    int batchProcessingTime = (int)Math.Ceiling((double)requestUrls.Count / (double)ServiceManager.QpsLimit);

                    if (RouteOptions != null && RouteOptions.TravelMode == TravelModeType.Truck)
                    {
                        //Use an average of 4 seconds per batch for processing truck routes as multiplier for the processing time.
                        //Other routes typically take less than a second and as such 1 second is used for those but isn't needed as a multiplier.
                        batchProcessingTime *= 4;
                    }

                    remainingTimeCallback(batchProcessingTime);
                }

                var routes     = new Route[requestUrls.Count];
                var routeTasks = new List <Task>();

                for (var i = 0; i < routes.Length; i++)
                {
                    routeTasks.Add(CalculateRoute(requestUrls[i], requestBodies[i], i, routes));
                }

                if (routeTasks.Count > 0)
                {
                    await ServiceHelper.WhenAllTaskLimiter(routeTasks);
                }

                try
                {
                    response = new Response()
                    {
                        StatusCode        = 200,
                        StatusDescription = "OK",
                        ResourceSets      = new ResourceSet[]
                        {
                            new ResourceSet()
                            {
                                Resources = new Resource[]
                                {
                                    await MergeRoutes(routes)
                                }
                            }
                        }
                    };
                }
                catch (Exception ex)
                {
                    return(new Response()
                    {
                        StatusCode = 500,
                        StatusDescription = "Error",
                        ErrorDetails = new string[]
                        {
                            ex.Message
                        }
                    });
                }
            }

            return(response);
        }