Example #1
0
        private RegulatedHoursDetail GetRegulationRuleAtEventTime(SensorAndPaymentReportEngine.CommonSensorAndPaymentEvent currentReportableEvent)
        {
            RegulatedHoursDetail result = null;

            // Try to obtain the regulated hours applicable to this meter
            RegulatedHoursGroup regulatedHours = this._ReportEngine.GetBestGroupForMeter(this._CustomerConfig.CustomerId, currentReportableEvent.BayInfo.AreaID_PreferLibertyBeforeInternal, currentReportableEvent.BayInfo.MeterID);

            // If no regulated hour defintions came back, then we are unable to calculate any overstay violation, so just exit
            if ((regulatedHours == null) || (regulatedHours.Details == null) || (regulatedHours.Details.Count == 0))
            {
                return(result);
            }

            DateTime ruleStart = DateTime.MinValue;
            DateTime ruleEnd   = DateTime.MinValue;

            TimeSlot OccupiedSegment = new TimeSlot(currentReportableEvent.SensorEvent_Start, currentReportableEvent.SensorEvent_End);

            // We need to check if this single occupancy event is an overstay violation for multiple rules, or even for more than one day, etc.
            while (OccupiedSegment.Start < currentReportableEvent.SensorEvent_End)
            {
                // Determine the day of week that is involved
                int dayOfWeek = (int)OccupiedSegment.Start.DayOfWeek;

                // Loop through the daily rules and see which ones overlap with our occupied period
                foreach (RegulatedHoursDetail detail in regulatedHours.Details)
                {
                    // Skip this one if its for a different day of the week
                    if (detail.DayOfWeek != dayOfWeek)
                    {
                        continue;
                    }

                    // Determine if the occupied timeslot overlaps with the rule's timeslot
                    ruleStart = new DateTime(OccupiedSegment.Start.Year, OccupiedSegment.Start.Month, OccupiedSegment.Start.Day, detail.StartTime.Hour, detail.StartTime.Minute, 0);
                    ruleEnd   = new DateTime(OccupiedSegment.Start.Year, OccupiedSegment.Start.Month, OccupiedSegment.Start.Day, detail.EndTime.Hour, detail.EndTime.Minute, 59);
                    TimeSlot RuleSegment = new TimeSlot(ruleStart, ruleEnd);

                    // We only care about this overlapping rule if the MaxStayMinutes is greater than zero (zero or less means there is no MaxStay that is enforced),
                    // or if it's explicitly set as a "No Parking" regulation
                    if ((RuleSegment.OverlapsWith(OccupiedSegment) == true) &&
                        ((detail.MaxStayMinutes > 0) || (string.Compare(detail.Type, "No Parking", true) == 0))
                        )
                    {
                        result = detail;
                        return(result);
                    }
                }

                // Nothing more to do, so break out of loop!
                break;
            }

            return(result);
        }
        protected void FindCurrentOverstayRule(SpaceStatusModel model)
        {
            // We need to try to find the regulation rule that is currently in effect at the customer's current timezone
            // Resolve the associated area for the meter
            int areaID = ResolveAreaIDForMeterID(model.MeterID);

            RegulatedHoursGroupRepository.Repository = new RegulatedHoursGroupRepository();
            // Try to obtain the regulated hours applicable to this meter
            RegulatedHoursGroup regulatedHours = RegulatedHoursGroupRepository.Repository.GetBestGroupForMeter(this._CustomerConfig.CustomerId, areaID, model.MeterID);

            // If no regulated hour defintions came back, then we are unable to calculate any overstay violation, so just exit
            if ((regulatedHours == null) || (regulatedHours.Details == null) || (regulatedHours.Details.Count == 0))
            {
                return;
            }

            DateTime NowAtDestination = Convert.ToDateTime(this._CustomerConfig.DestinationTimeZoneDisplayName);
            // Determine the day of week that is involved
            int dayOfWeek = (int)NowAtDestination.DayOfWeek;

            // Loop through the daily rules and see which ones overlap with our occupied period
            foreach (RegulatedHoursDetail detail in regulatedHours.Details)
            {
                // Skip this one if its for a different day of the week
                if (detail.DayOfWeek != dayOfWeek)
                {
                    continue;
                }

                TimeSlot OccupiedSegment = new TimeSlot(NowAtDestination);

                // Determine if the occupied timeslot overlaps with the rule's timeslot
                DateTime ruleStart   = new DateTime(NowAtDestination.Year, NowAtDestination.Month, NowAtDestination.Day, detail.StartTime.Hour, detail.StartTime.Minute, 0);
                DateTime ruleEnd     = new DateTime(NowAtDestination.Year, NowAtDestination.Month, NowAtDestination.Day, detail.EndTime.Hour, detail.EndTime.Minute, 59);
                TimeSlot RuleSegment = new TimeSlot(ruleStart, ruleEnd);

                if (RuleSegment.OverlapsWith(OccupiedSegment) == true)
                {
                    model.ActiveRegulationPeriod = detail;
                    break;
                }
            }
        }
        public static string GetFormEditDefaultsForDay(RegulatedHoursGroup group, DayOfWeek day, CustomerConfig customerCfg)
        {
            StringBuilder sb = new StringBuilder();

            string delim = string.Empty;

            sb.AppendLine("$(\"#itemDetails_" + day.ToString().Substring(0, 3) + "\").EnableMultiField({");
            sb.AppendLine("   data:[");

            foreach (RegulatedHoursDetail nextDetail in group.Details)
            {
                // Skip if its not for the day we are interested in
                if (nextDetail.DayOfWeek != (int)day)
                {
                    continue;
                }

                if (string.IsNullOrEmpty(delim))
                {
                    delim = ",";
                }
                else
                {
                    sb.AppendLine(delim);
                }

                sb.AppendLine("   {");
                sb.AppendLine("      " + "detail_Type : " + "\"" + nextDetail.Type.ToString() + "\",");
                sb.AppendLine("      " + "detail_StartTime : " + "\"" + nextDetail.StartTime.ToString("hh:mm:ss tt") + "\",");
                sb.AppendLine("      " + "detail_EndTime : " + "\"" + nextDetail.EndTime.ToString("hh:mm:ss tt") + "\",");
                sb.AppendLine("      " + "detail_DayOfWeek : " + "\"" + Convert.ToString((int)nextDetail.DayOfWeek) + "\",");
                sb.AppendLine("      " + "detail_MaxStayMinutes : " + "\"" + nextDetail.MaxStayMinutes.ToString() + "\",");
                sb.AppendLine("      " + "detail_RegulatedHoursDetailID : " + "\"" + nextDetail.RegulatedHoursDetailID.ToString() + "\"");
                sb.Append("   }");
            }
            sb.AppendLine();
            sb.AppendLine("   ],");
            sb.AppendLine("   addEventCallback : function(newElem, clonnedFrom){commonCloneFormFieldsEvent(newElem, clonnedFrom);} });");

            return(sb.ToString());
        }
        protected void AnalyzeSpaceStatusModelForOverstayViolation(SpaceStatusModel model)
        {
            // There is nothing to do if this event is not for occupied status
            if ((model.BayOccupancyState == OccupancyState.Empty) || (model.BayOccupancyState == OccupancyState.NotAvailable) ||
                (model.BayOccupancyState == OccupancyState.OutOfDate) || (model.BayOccupancyState == OccupancyState.Unknown))
            {
                return;
            }

            // Find the space asset associated with this data model.  If the space is "inactive" (based on the "IsActive" column of "HousingMaster" table in database),
            // then we will not consider the space to be in a violating state, because the sensor is effectively marked as bad/untrusted
            SpaceAsset spcAsset = GetSpaceAsset(model.MeterID, model.BayID);

            if (spcAsset != null)
            {
                // Nothing more to do if the space isn't active
                if (spcAsset.IsActive == false)
                {
                    return;
                }
            }

            // Resolve the associated area for the meter
            int areaID = ResolveAreaIDForMeterID(model.MeterID);

            // Try to obtain the regulated hours applicable to this meter
            RegulatedHoursGroup regulatedHours = RegulatedHoursGroupRepository.Repository.GetBestGroupForMeter(this._CustomerConfig.CustomerId, areaID, model.MeterID);

            // If no regulated hour defintions came back, then we are unable to calculate any overstay violation, so just exit
            if ((regulatedHours == null) || (regulatedHours.Details == null) || (regulatedHours.Details.Count == 0))
            {
                return;
            }

            DateTime NowAtDestination = Convert.ToDateTime(this._CustomerConfig.DestinationTimeZoneDisplayName);
            DateTime ruleStart        = DateTime.MinValue;
            DateTime ruleEnd          = DateTime.MinValue;

            TimeSlot OccupiedSegment = new TimeSlot(model.BayVehicleSensingTimestamp, NowAtDestination);

            // We need to check if this single occupancy event is an overstay violation for multiple rules, or even for more than one day, etc.
            while (OccupiedSegment.Start < NowAtDestination)
            {
                // Determine the day of week that is involved
                int dayOfWeek = (int)OccupiedSegment.Start.DayOfWeek;

                // Loop through the daily rules and see which ones overlap with our occupied period
                foreach (RegulatedHoursDetail detail in regulatedHours.Details)
                {
                    // Skip this one if its for a different day of the week
                    if (detail.DayOfWeek != dayOfWeek)
                    {
                        continue;
                    }

                    // Determine if the occupied timeslot overlaps with the rule's timeslot
                    ruleStart = new DateTime(OccupiedSegment.Start.Year, OccupiedSegment.Start.Month, OccupiedSegment.Start.Day, detail.StartTime.Hour, detail.StartTime.Minute, 0);
                    ruleEnd   = new DateTime(OccupiedSegment.Start.Year, OccupiedSegment.Start.Month, OccupiedSegment.Start.Day, detail.EndTime.Hour, detail.EndTime.Minute, 59);
                    TimeSlot RuleSegment = new TimeSlot(ruleStart, ruleEnd);

                    // We only care about this overlapping rule if the MaxStayMinutes is greater than zero (zero or less means there is no MaxStay that is enforced),
                    // or if it's explicitly set as a "No Parking" regulation
                    if ((RuleSegment.OverlapsWith(OccupiedSegment) == true) &&
                        ((detail.MaxStayMinutes > 0) || (string.Compare(detail.Type, "No Parking", true) == 0))
                        )
                    {
                        // Normally we will use the verbatim value of the max stay minutes, but if its a "No Parking", we will always take that to mean 0 minutes is the actual max
                        int timeLimitMinutes = detail.MaxStayMinutes;
                        if (string.Compare(detail.Type, "No Parking", true) == 0)
                        {
                            timeLimitMinutes = 0;
                        }

                        // Get the intersection of the overlaps so we know how long the vehicle has been occupied during this rule
                        TimeSlot OccupiedIntersection = RuleSegment.GetIntersection(OccupiedSegment);

                        // Determine if the vehicle has been occupied during this rule segment in excess of the MaxStayMinutes
                        if (OccupiedIntersection != null)
                        {
                            if (OccupiedIntersection.Duration.TotalMinutes >= timeLimitMinutes)
                            {
                                // We will check to see if this violated regulated period matches the current regulated period.
                                // But since it could be occupied for a long time, we must also check the current date, in addition to day of week and time of day!
                                bool     currDetailMatchesCurrentRegulatedPeriod = false;
                                DateTime TodayAtDestination = Convert.ToDateTime(this._CustomerConfig.DestinationTimeZoneDisplayName); //UtilityClasses.TimeZoneInfo.ConvertTimeZoneToTimeZone(DateTime.Now, this._CustomerConfig.ServerTimeZone, this._CustomerConfig.CustomerTimeZone).Date;
                                if (OccupiedSegment.Start >= TodayAtDestination)
                                {
                                    if (model.ActiveRegulationPeriod != null)
                                    {
                                        RegulatedHoursDetailLogicalComparer comparer = new RegulatedHoursDetailLogicalComparer();
                                        currDetailMatchesCurrentRegulatedPeriod = (comparer.Compare(model.ActiveRegulationPeriod, detail) == 0);
                                    }
                                }

                                // Create a new Overstay Vio Info object and add to the overall list of violations
                                OverstayViolationInfo overstay = new OverstayViolationInfo();
                                overstay.IsForCurrentRegulationPeriod = currDetailMatchesCurrentRegulatedPeriod;
                                overstay.Regulation_Date                   = new DateTime(OccupiedSegment.Start.Year, OccupiedSegment.Start.Month, OccupiedSegment.Start.Day);
                                overstay.Regulation_DayOfWeek              = detail.DayOfWeek;
                                overstay.Regulation_EndTime                = detail.EndTime;
                                overstay.Regulation_MaxStayMinutes         = detail.MaxStayMinutes; // Instead of using our calculated time limit, we will record the configured max stay minutes here, because it will be displayed
                                overstay.Regulation_StartTime              = detail.StartTime;
                                overstay.Regulation_Type                   = detail.Type;
                                overstay.DateTime_StartOfOverstayViolation = new DateTime(OccupiedIntersection.Start.Ticks).AddMinutes(timeLimitMinutes);
                                overstay.DurationOfTimeBeyondStayLimits    = new TimeSpan(OccupiedIntersection.Duration.Ticks).Add(new TimeSpan(0, (-1) * timeLimitMinutes, 0));

                                // Add this overstay info to the model's list
                                model.AllOverstayViolations.Add(overstay);

                                // If its also for the current regulation period, retain a reference to it
                                if (overstay.IsForCurrentRegulationPeriod == true)
                                {
                                    model.CurrentOverstayViolation = overstay;
                                }

                                // Mark as "Overstay Violation" status if we have a current overstay violation.  Otherwise mark
                                // as "Discretionary" because there is an overstay violation, but not for the current enforcement period
                                if (model.CurrentOverstayViolation != null)
                                {
                                    model.BayEnforcementState = EnforcementState.OverstayViolation;
                                }
                                else
                                {
                                    model.BayEnforcementState = EnforcementState.Discretionary;
                                }
                            }
                        }
                    }
                }

                // Rules for current day of week have been processed.  So now we will advance to beginning of next day and see if there are more violations that we will use
                // to add accumulated time in violation state...
                OccupiedSegment = new TimeSlot(new DateTime(OccupiedSegment.Start.Year, OccupiedSegment.Start.Month, OccupiedSegment.Start.Day).AddDays(1),
                                               NowAtDestination);
            }
        }
Example #5
0
        public void GetReportAsExcelSpreadsheet(List <int> listOfMeterIDs, MemoryStream ms,
                                                ActivityRestrictions activityRestriction, string scopedAreaName, string scopedMeter)
        {
            // Start diagnostics timer
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            sw.Start();

            DateTime NowAtDestination = Convert.ToDateTime(this._CustomerConfig.DestinationTimeZoneDisplayName);

            this._ActivityRestriction = activityRestriction;
            this.GatherReportData(listOfMeterIDs);

            OfficeOpenXml.ExcelWorksheet ws = null;
            int rowIdx = -1;

            using (OfficeOpenXml.ExcelPackage pck = new OfficeOpenXml.ExcelPackage())
            {
                // Let's create a report coversheet and overall summary page, with hyperlinks to the other worksheets
                // Create the worksheet
                ws = pck.Workbook.Worksheets.Add("Summary");

                // Render the header row
                rowIdx = 1; // Excel uses 1-based indexes
                ws.Cells[rowIdx, 1].Value = "Asset Listings Report";
                using (OfficeOpenXml.ExcelRange rng = ws.Cells[rowIdx, 1, rowIdx, 10])
                {
                    rng.Merge                     = true; //Merge columns start and end range
                    rng.Style.Font.Bold           = true;
                    rng.Style.Font.Italic         = true;
                    rng.Style.Font.Size           = 22;
                    rng.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
                    rng.Style.Fill.PatternType    = OfficeOpenXml.Style.ExcelFillStyle.Solid;
                    rng.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(23, 55, 93));
                    rng.Style.Font.Color.SetColor(System.Drawing.Color.White);
                }

                rowIdx++;
                ws.Cells[rowIdx, 1].IsRichText = true;
                OfficeOpenXml.Style.ExcelRichTextCollection rtfCollection = ws.Cells[rowIdx, 1].RichText;
                AddRichTextNameAndValue(rtfCollection, "Client: ", this._CustomerConfig.CustomerName);
                AddRichTextNameAndValue(rtfCollection, ",  Generated: ", NowAtDestination.ToShortDateString() + "  " + NowAtDestination.ToShortTimeString());
                ws.Cells[rowIdx, 1, rowIdx, 10].Merge = true;

                rowIdx++;
                rtfCollection = ws.Cells[rowIdx, 1].RichText;
                AddRichTextNameAndValue(rtfCollection, "Included Activity: ", "Asset Listing");
                ws.Cells[rowIdx, 1, rowIdx, 10].Merge = true;

                if (!string.IsNullOrEmpty(scopedAreaName))
                {
                    rowIdx++;
                    rtfCollection = ws.Cells[rowIdx, 1].RichText;
                    AddRichTextNameAndValue(rtfCollection, "Report limited to area: ", scopedAreaName);
                    ws.Cells[rowIdx, 1, rowIdx, 10].Merge = true;
                }

                if (!string.IsNullOrEmpty(scopedMeter))
                {
                    rowIdx++;
                    rtfCollection = ws.Cells[rowIdx, 1].RichText;
                    AddRichTextNameAndValue(rtfCollection, "Report limited to meter: ", scopedMeter);
                    ws.Cells[rowIdx, 1, rowIdx, 10].Merge = true;
                }

                rowIdx++;

                using (OfficeOpenXml.ExcelRange rng = ws.Cells[2, 1, rowIdx, 10])
                {
                    rng.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
                    rng.Style.Font.Color.SetColor(System.Drawing.Color.White);
                    rng.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(207, 221, 237));  //Set color to lighter blue FromArgb(184, 204, 228)
                }


                rowIdx++;
                int hyperlinkstartRowIdx = rowIdx;

                rowIdx++;
                rowIdx++;

                using (OfficeOpenXml.ExcelRange rng = ws.Cells[hyperlinkstartRowIdx, 1, rowIdx, 13])
                {
                    rng.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;                 //Set Pattern for the background to Solid
                    rng.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.White);
                }

                // Render the header row
                rowIdx = 7; // Excel uses 1-based indexes

                // have to start at column 2, does not work when start column is 1. Will come back when more time is avail
                using (OfficeOpenXml.ExcelRange rng = ws.Cells[rowIdx, 2, rowIdx, 6])
                {
                    rng.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
                    rng.Merge           = true; //Merge columns start and end range
                    rng.Style.Font.Bold = true;
                }
                ws.Cells[rowIdx, 2].Value = "Site Details";

                using (OfficeOpenXml.ExcelRange rng = ws.Cells[rowIdx, 1, rowIdx, 6])
                {
                    rng.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
                    rng.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(23, 55, 93));
                    rng.Style.Font.Color.SetColor(System.Drawing.Color.White);
                }

                using (OfficeOpenXml.ExcelRange rng = ws.Cells[rowIdx, 7, rowIdx, 13])
                {
                    rng.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
                    rng.Merge           = true; //Merge columns start and end range
                    rng.Style.Font.Bold = true;
                    rng.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(207, 221, 237));
                }
                ws.Cells[rowIdx, 7].Value = "Regulations";

                rowIdx++;

                ws.Cells[rowIdx, 1].Value  = "Meter ID";
                ws.Cells[rowIdx, 2].Value  = "Space ID";
                ws.Cells[rowIdx, 3].Value  = "Area #";
                ws.Cells[rowIdx, 4].Value  = "Site Details Area";
                ws.Cells[rowIdx, 5].Value  = "Co-Ordinates Lat";
                ws.Cells[rowIdx, 6].Value  = "Co-Ordinates Long";
                ws.Cells[rowIdx, 7].Value  = "Regulations - Sun";
                ws.Cells[rowIdx, 8].Value  = "Regulations - Mon";
                ws.Cells[rowIdx, 9].Value  = "Regulations - Tues";
                ws.Cells[rowIdx, 10].Value = "Regulations - Wed";
                ws.Cells[rowIdx, 11].Value = "Regulations - Thurs";
                ws.Cells[rowIdx, 12].Value = "Regulations - Fri";
                ws.Cells[rowIdx, 13].Value = "Regulations - Sat";

                // Format the header row
                using (OfficeOpenXml.ExcelRange rng = ws.Cells[1, 1, 1, 6])
                {
                    rng.Style.Font.Bold        = true;
                    rng.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;                 //Set Pattern for the background to Solid
                    rng.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.FromArgb(79, 129, 189));  //Set color to dark blue
                    rng.Style.Font.Color.SetColor(System.Drawing.Color.White);
                }

                // Increment the row index, which will now be the 1st row of our data
                rowIdx++;

                foreach (AssetListing_Space meterStat in this._ReportDataModel.SpaceDetailsList)
                {
                    #region Unused code, but useful examples
                    // Example of how we could automatically render a dataset to worksheet

                    /*
                     * // Load the datatable into the sheet, starting from cell A1. Print the column names on row 1
                     * ws.Cells["A1"].LoadFromDataTable(nextTable, true);
                     */

                    // Example of how we could automatically render a strongly-typed collection of objects to worksheet

                    /*
                     * List<MeterStatisticObj> statObjects = new List<MeterStatisticObj>();
                     * statObjects.Add(meterStatCollection.MeterStats_Summary);
                     * ws.Cells["A1"].LoadFromCollection(statObjects, true);
                     */

                    // Example of formatting a column for Date/Time

                    /*
                     * ws.Column(3).Width = 20;
                     * using (OfficeOpenXml.ExcelRange col = ws.Cells[2, 3, 2 + nextTable.Rows.Count, 3])
                     * {
                     *  col.Style.Numberformat.Format = "mm/dd/yyyy hh:mm:ss tt";
                     *  col.Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right;
                     * }
                     */

                    // Example of using RichText in a cell for advanced formatting possibilites

                    /*
                     * ws.Cells[rowIdx, 1].IsRichText = true;
                     * ws.Cells[rowIdx, 1].Style.WrapText = true; // Need this if we want multi-line
                     * OfficeOpenXml.Style.ExcelRichTextCollection rtfCollection = ws.Cells[rowIdx, 1].RichText;
                     * OfficeOpenXml.Style.ExcelRichText ert = rtfCollection.Add(areaStat.AreaName + "\r\n");
                     *
                     * ert = rtfCollection.Add(" (ID=" + areaStat.AreaID.ToString() + ")");
                     * ert.Bold = false;
                     * ert.Italic = true;
                     * ert.Size = 8;
                     */
                    #endregion

                    // Output row values for data
                    ws.Cells[rowIdx, 1].Value = meterStat.MeterID;
                    ws.Cells[rowIdx, 2].Value = meterStat.SpaceID;
                    ws.Cells[rowIdx, 3].Value = meterStat.AreaID;
                    ws.Cells[rowIdx, 4].Value = meterStat.Location;
                    ws.Cells[rowIdx, 5].Value = meterStat.Latitude;
                    ws.Cells[rowIdx, 6].Value = meterStat.Longitude;
                    RegulatedHoursGroupRepository.Repository = new RegulatedHoursGroupRepository();
                    RegulatedHoursGroup regulatedHours = RegulatedHoursGroupRepository.Repository.GetBestGroupForMeter(this._CustomerConfig.CustomerId,
                                                                                                                       meterStat.AreaID, meterStat.MeterID);

                    // If no regulated hour defintions came back, then we will default to assumption that regulated period is 24-hours a day
                    if ((regulatedHours == null) || (regulatedHours.Details == null) || (regulatedHours.Details.Count == 0))
                    {
                        rowIdx++;
                        continue;
                    }

                    // Loop through the daily rules and see if the timestamp falls within a Regulated or No Parking timeslot for the appropriate day
                    foreach (RegulatedHoursDetail detail in regulatedHours.Details)
                    {
                        string regulationTxt =
                            detail.StartTime.ToString("hh:mm:ss tt") + " - " +
                            detail.EndTime.ToString("hh:mm:ss tt") + ", " +
                            detail.MaxStayMinutes.ToString() + " mins";

                        if (string.Compare(detail.Type, "Unregulated", true) == 0)
                        {
                            regulationTxt = "(Unregulated) " +
                                            detail.StartTime.ToString("hh:mm:ss tt") + " - " +
                                            detail.EndTime.ToString("hh:mm:ss tt") + ", " +
                                            detail.MaxStayMinutes.ToString() + " mins";
                        }
                        else if (string.Compare(detail.Type, "No Parking", true) == 0)
                        {
                            regulationTxt = "(No Parking) " +
                                            detail.StartTime.ToString("hh:mm:ss tt") + " - " +
                                            detail.EndTime.ToString("hh:mm:ss tt");
                        }
                        else if (detail.MaxStayMinutes < 1)
                        {
                            regulationTxt = "(No Limit) " +
                                            detail.StartTime.ToString("hh:mm:ss tt") + " - " +
                                            detail.EndTime.ToString("hh:mm:ss tt");
                        }

                        // Determine which column of the spreadsheet is used for this day of the week
                        int columnIdxForDayOfWeek = 7 + detail.DayOfWeek;

                        // If the cell is empty, just add the regulation text.  If something is already there, append the regulation text
                        // (There might be more than one regulated period for the same day)
                        if ((ws.Cells[rowIdx, columnIdxForDayOfWeek].Value == null) || ((ws.Cells[rowIdx, columnIdxForDayOfWeek].Value as string) == null))
                        {
                            ws.Cells[rowIdx, columnIdxForDayOfWeek].Value = regulationTxt;
                        }
                        else
                        {
                            ws.Cells[rowIdx, columnIdxForDayOfWeek].Value = (ws.Cells[rowIdx, columnIdxForDayOfWeek].Value as string) + Environment.NewLine + regulationTxt;

                            // And increment the row height also
                            ws.Row(rowIdx).Height = ws.Row(rowIdx).Height + ws.DefaultRowHeight;
                            ws.Cells[rowIdx, columnIdxForDayOfWeek].Style.WrapText = true;

                            using (OfficeOpenXml.ExcelRange rowrange = ws.Cells[rowIdx, 1, rowIdx, 14])
                            {
                                rowrange.Style.VerticalAlignment = ExcelVerticalAlignment.Top;
                            }
                        }
                    }

                    // Increment the row index, which will now be the next row of our data
                    rowIdx++;
                }

                // We will add autofilters to our headers so user can sort the columns easier
                using (OfficeOpenXml.ExcelRange rng = ws.Cells[8, 1, 8, 13])
                {
                    rng.AutoFilter = true;
                }

                // Column 1 is numeric integer (Meter ID)
                ApplyNumberStyleToColumn(ws, 1, 2, rowIdx, "########0", ExcelHorizontalAlignment.Left);

                // And now lets size the columns
                for (int autoSizeColIdx = 1; autoSizeColIdx <= 13; autoSizeColIdx++)
                {
                    using (OfficeOpenXml.ExcelRange col = ws.Cells[1, autoSizeColIdx, rowIdx, autoSizeColIdx])
                    {
                        col.AutoFitColumns();
                    }
                }

                // All cells in spreadsheet are populated now, so render (save the file) to a memory stream
                byte[] bytes = pck.GetAsByteArray();
                ms.Write(bytes, 0, bytes.Length);
            }

            // Stop diagnostics timer
            sw.Stop();
            System.Diagnostics.Debug.WriteLine("Occupancy Report Generation took: " + sw.Elapsed.ToString());
        }