public static string AvailabilityMap(DateTime dtStart, int clubID, int limitAircraft = Aircraft.idAircraftUnknown, int cDays = 1)
        {
            if (!IsValidCaller(clubID, PrivilegeLevel.ReadOnly))
            {
                return(null);
            }

            int minutes                 = cDays == 1 ? 15 : cDays == 2 ? 60 : 180;
            int intervalsPerDay         = (24 * 60) / minutes;
            int cellsPerHeader          = (minutes < 60) ? (60 / minutes) : Math.Max(intervalsPerDay / 2, 1);
            int totalIntervals          = cDays * intervalsPerDay;
            IDictionary <int, bool[]> d = ScheduledEvent.ComputeAvailabilityMap(dtStart, clubID, out Club club, limitAircraft = Aircraft.idAircraftUnknown, cDays, minutes);
            DateTime dtStartLocal       = new DateTime(dtStart.Year, dtStart.Month, dtStart.Day, 0, 0, 0, DateTimeKind.Local);

            CultureInfo ciCurrent = util.SessionCulture ?? CultureInfo.CurrentCulture;

            // We have no Page, so things like Page_Load don't get called.
            // We fix this by faking a page and calling Server.Execute on it.  This sets up the form and - more importantly - causes Page_load to be called on loaded controls.
            using (Page p = new FormlessPage())
            {
                p.Controls.Add(new HtmlForm());
                using (StringWriter sw1 = new StringWriter(ciCurrent))
                    HttpContext.Current.Server.Execute(p, sw1, false);

                // Build the map, one day at a time in 15-minute increments.

                // Our map is created - iterate through it now.
                Table t = new Table()
                {
                    CssClass = "tblAvailablityMap"
                };
                p.Form.Controls.Add(t);

                // Header row
                // Date first
                TableHeaderRow thrDate = new TableHeaderRow();
                t.Rows.Add(thrDate);
                thrDate.Cells.Add(new TableHeaderCell());  // upper left corner is blank cell
                // Add days
                for (int i = 0; i < cDays; i++)
                {
                    DateTime dt = dtStartLocal.AddDays(i);
                    thrDate.Cells.Add(new TableHeaderCell()
                    {
                        Text = dt.ToString("d", ciCurrent), ColumnSpan = intervalsPerDay, CssClass = "dateHeader"
                    });
                }

                // Now times
                TableHeaderRow thrTimes = new TableHeaderRow();
                t.Rows.Add(thrTimes);
                thrTimes.Cells.Add(new TableHeaderCell());  // upper left corner is blank cell

                // We want the time minus any minutes, but keep it localized.
                string szTimeFormat = Regex.Replace(ciCurrent.DateTimeFormat.ShortTimePattern, ":m+", string.Empty);

                for (int iHeaderCol = 0; iHeaderCol < totalIntervals; iHeaderCol += cellsPerHeader)
                {
                    DateTime dt = dtStartLocal.AddMinutes(iHeaderCol * minutes);
                    thrTimes.Cells.Add(new TableHeaderCell()
                    {
                        Text          = (dt.Minute == 0) ? dt.ToString(szTimeFormat, ciCurrent) : String.Empty,
                        ColumnSpan    = cellsPerHeader,
                        VerticalAlign = VerticalAlign.Bottom,
                        CssClass      = "timeHeader"
                    });
                }

                // Sort the aircraft by tail
                List <Aircraft> lstAc = new List <Aircraft>(club.MemberAircraft);
                lstAc.Sort((ac1, ac2) => { return(String.Compare(ac1.DisplayTailnumber, ac2.DisplayTailnumber, true, ciCurrent)); });
                foreach (Aircraft aircraft in lstAc)
                {
                    if (!d.ContainsKey(aircraft.AircraftID))    // sanity check - but should not happen
                    {
                        continue;
                    }

                    TableRow trAircraft = new TableRow();
                    t.Rows.Add(trAircraft);
                    trAircraft.Cells.Add(new TableCell()
                    {
                        Text = aircraft.DisplayTailnumber, CssClass = "avmResource"
                    });

                    for (int i = 0; i < d[aircraft.AircraftID].Length; i++)
                    {
                        bool b = d[aircraft.AircraftID][i];
                        trAircraft.Cells.Add(new TableCell()
                        {
                            CssClass = b ? "avmBusy" : (i % cellsPerHeader == 0) ? "avmAvail" : "avmAvail avmSubHour"
                        });
                    }
                }

                // Now, write it out.
                StringBuilder sb = new StringBuilder();
                using (StringWriter sw = new StringWriter(sb, ciCurrent))
                {
                    using (HtmlTextWriter htmlTW = new HtmlTextWriter(sw))
                    {
                        try
                        {
                            t.RenderControl(htmlTW);
                            return(sb.ToString());
                        }
                        catch (ArgumentException ex) when(ex is ArgumentOutOfRangeException)
                        {
                        }                                                                         // don't write bogus or incomplete HTML
                    }
                }
            }
            return(String.Empty);
        }