/// <summary>
        /// Returns a list of AirportLists given a route.  "=>" is the separator between routes.  This hits the database exactly once, for efficiency, and excludes duplicates.
        /// </summary>
        /// <param name="rgRoutes">The array of routes.</param>
        /// <param name="aplMaster">The "master" airportlist (contains all of the airports)</param>
        /// <returns>A list of airportlists; each contains at least one airport</returns>
        public static ListsFromRoutesResults ListsFromRoutes(IEnumerable <string> rgRoutes)
        {
            List <AirportList> lst = new List <AirportList>();

            if (rgRoutes == null || !rgRoutes.Any())
            {
                return(new ListsFromRoutesResults(lst, new AirportList(string.Empty)));
            }

            // Create a single "worker" airportlist from which we will create the others.
            AirportList aplMaster = new AirportList(String.Join(" ", rgRoutes.ToArray()));

            Dictionary <string, AirportList> dictRoutes = new Dictionary <string, AirportList>();

            // Now create the resulting airport lists.  Exclude duplicate routes
            foreach (string szRoute in rgRoutes)
            {
                string[] rgApCodeNormal = AirportList.NormalizeAirportList(szRoute);
                string   szRouteKey     = String.Join("", rgApCodeNormal);

                // skip duplicate routes
                if (dictRoutes.ContainsKey(szRouteKey))
                {
                    continue;
                }

                AirportList al = aplMaster.CloneSubset(rgApCodeNormal);
                if (al.m_rgAirports.Count > 0)
                {
                    lst.Add(al);
                    dictRoutes.Add(szRouteKey, al);
                }
            }
            return(new ListsFromRoutesResults(lst, aplMaster));
        }
        /// <summary>
        /// Returns a KML respresentation of all of the flights represented by the specified query
        /// </summary>
        /// <param name="fq">The flight query</param>
        /// <param name="s">The stream to which to write</param>
        /// <param name="error">Any error</param>
        /// <param name="lstIDs">The list of specific flight IDs to request</param>
        /// <returns>KML string for the matching flights.</returns>
        public static void AllFlightsAsKML(FlightQuery fq, Stream s, out string error, IEnumerable <int> lstIDs = null)
        {
            if (fq == null)
            {
                throw new ArgumentNullException(nameof(fq));
            }
            if (String.IsNullOrEmpty(fq.UserName) && (lstIDs == null || !lstIDs.Any()))
            {
                throw new MyFlightbookException("Don't get all flights as KML for an empty user!!");
            }

            if (lstIDs != null)
            {
                fq.EnumeratedFlights = lstIDs;
            }

            // Get the master airport list
            AirportList alMaster = AllFlightsAndNavaids(fq);

            using (KMLWriter kw = new KMLWriter(s))
            {
                kw.BeginKML();

                error = LookAtAllFlights(
                    fq,
                    LogbookEntryCore.LoadTelemetryOption.LoadAll,
                    (le) =>
                {
                    if (le.Telemetry.HasPath)
                    {
                        using (FlightData fd = new FlightData())
                        {
                            try
                            {
                                fd.ParseFlightData(le.Telemetry.RawData, le.Telemetry.MetaData);
                                if (fd.HasLatLongInfo)
                                {
                                    kw.AddPath(fd.GetTrajectory(), String.Format(CultureInfo.CurrentCulture, "{0:d} - {1}", le.Date, le.Comment), fd.SpeedFactor);
                                    return;
                                }
                            }
                            catch (Exception ex) when(!(ex is OutOfMemoryException))
                            {
                            }                                                                   // eat any error and fall through below
                        }
                    }
                    // No path was found above.
                    AirportList al = alMaster.CloneSubset(le.Route);
                    kw.AddRoute(al.GetNormalizedAirports(), String.Format(CultureInfo.CurrentCulture, "{0:d} - {1}", le.Date, le.Route));
                },
                    lstIDs != null && lstIDs.Any());
                kw.EndKML();
            }
        }
        /// <summary>
        /// Estimates the total distance flown by the user for the subset of flights described by the query
        /// </summary>
        /// <param name="fq">The flight query</param>
        /// <param name="fAutofillDistanceFlown">True to autofill the distance flown property if not found.</param>
        /// <param name="error">Any error</param>
        /// <returns>Distance flown, in nm</returns>
        public static double DistanceFlownByUser(FlightQuery fq, bool fAutofillDistanceFlown, out string error)
        {
            if (fq == null)
            {
                throw new ArgumentNullException(nameof(fq));
            }
            if (String.IsNullOrEmpty(fq.UserName))
            {
                throw new MyFlightbookException("Don't estimate distance for an empty user!!");
            }

            double distance = 0.0;

            // Get the master airport list
            AirportList alMaster = AllFlightsAndNavaids(fq);

            error = LookAtAllFlights(
                fq,
                LogbookEntryCore.LoadTelemetryOption.MetadataOrDB,
                (le) =>
            {
                double distThisFlight = 0;

                // If the trajectory had a distance, use it; otherwise, use airport-to-airport.
                double dPath = le.Telemetry.Distance();
                if (dPath > 0)
                {
                    distThisFlight = dPath;
                }
                else if (!String.IsNullOrEmpty(le.Route))
                {
                    distThisFlight = alMaster.CloneSubset(le.Route).DistanceForRoute();
                }

                distance += distThisFlight;

                if (fAutofillDistanceFlown && distThisFlight > 0 && !le.CustomProperties.PropertyExistsWithID(CustomPropertyType.KnownProperties.IDPropDistanceFlown))
                {
                    le.CustomProperties.Add(CustomFlightProperty.PropertyWithValue(CustomPropertyType.KnownProperties.IDPropDistanceFlown, (decimal)distThisFlight));
                    le.FCommit();
                }
            });

            return(distance);
        }