diff --git a/AsbCloudInfrastructure/Services/ProcessMap/ProcessMapReportTemplate.xlsx b/AsbCloudInfrastructure/Services/ProcessMaps/Files/DrillingProcessMapReportTemplate.xlsx similarity index 100% rename from AsbCloudInfrastructure/Services/ProcessMap/ProcessMapReportTemplate.xlsx rename to AsbCloudInfrastructure/Services/ProcessMaps/Files/DrillingProcessMapReportTemplate.xlsx diff --git a/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/Data/ParamStat.cs b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/Data/ParamStat.cs new file mode 100644 index 00000000..aab2ed52 --- /dev/null +++ b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/Data/ParamStat.cs @@ -0,0 +1,112 @@ +using System; +using AsbCloudApp.Data.ProcessMaps.Report; +using AsbCloudApp.Data.SAUB; + +namespace AsbCloudInfrastructure.Services.ProcessMaps.WellDrillingProcessMap.Report.Data; + +internal class ParamStat +{ + private double spWSum; + private double pvWSum; + private double limitMaxWSum; + + private double deltaDepthSum; + + private readonly Func getterSp; + private readonly Func getterPv; + private readonly Func? getterLimitMax; + + private readonly int idFeedRegulator; + private readonly int idMode; + private TelemetryDataSaubStatDto? previous; + + public double SpUsageDepth { get; private set; } + private static double spUsageTotal; + + public ParamStat(Func getterSp, + Func getterPv, + Func? getterLimitMax, + int idFeedRegulator, + int idMode) + { + this.getterSp = getterSp; + this.getterPv = getterPv; + this.getterLimitMax = getterLimitMax; + this.idFeedRegulator = idFeedRegulator; + this.idMode = idMode; + spUsageTotal = 0d; + } + + public void UpdateStat(TelemetryDataSaubStatDto current) + { + if (previous is not null) + { + var deltaDepth = current.WellDepthMin - previous.WellDepthMin; + if (deltaDepth > 0) + { + var deltaDepthHalf = deltaDepth / 2; + + double CalculateWeight(Func getter) => + (getter(previous!) + getter(current)) * deltaDepthHalf; + + spWSum += CalculateWeight(getterSp); + pvWSum += CalculateWeight(getterPv); + if (getterLimitMax is not null) + limitMaxWSum += CalculateWeight(getterLimitMax!); + + if (current.IdFeedRegulator is not null) + { + if (current.IdFeedRegulator == idFeedRegulator) + { + SpUsageDepth += deltaDepth; + spUsageTotal += deltaDepth; + } + } + else + { + var pvErr = (getterSp(current) - getterPv(current)) / getterSp(current); + if (pvErr < 0.03d) //3% + { + SpUsageDepth += deltaDepth; + spUsageTotal += deltaDepth; + } + } + + deltaDepthSum += deltaDepth; + } + } + + previous = current; + } + + public WellDrillingProcessMapReportParamsDto MakeParams(double? spPlan) + { + var result = new WellDrillingProcessMapReportParamsDto + { + SetpointPlan = spPlan, + Fact = DivideValByDepth(pvWSum), + }; + + if (idMode == 0) + { + result.SetpointFact = null; + result.Limit = null; + result.SetpointUsage = null; + } + else + { + result.SetpointFact = DivideValByDepth(spWSum); + result.Limit = getterLimitMax is not null ? DivideValByDepth(limitMaxWSum) : null; + result.SetpointUsage = deltaDepthSum > 0d ? 100d * SpUsageDepth / spUsageTotal : null; + } + + return result; + } + + private double? DivideValByDepth(double? val) + { + if (val is null || val == 0d || deltaDepthSum == 0d) + return null; + return val / deltaDepthSum; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/Data/TelemetryStat.cs b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/Data/TelemetryStat.cs new file mode 100644 index 00000000..34daa65e --- /dev/null +++ b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/Data/TelemetryStat.cs @@ -0,0 +1,82 @@ +using System; +using AsbCloudApp.Data.SAUB; + +namespace AsbCloudInfrastructure.Services.ProcessMaps.WellDrillingProcessMap.Report.Data; + +internal class TelemetryStat +{ + public ParamStat Pressure { get; } + public ParamStat AxialLoad { get; } + public ParamStat RotorTorque { get; } + public ParamStat BlockSpeed { get; } + + private TelemetryDataSaubStatDto? previous; + private double depthSum = 0d; + private double hoursSum = 0d; + + public double? Rop => hoursSum == 0d ? null : depthSum / hoursSum; + + private double depthWithSaub = 0d; + public double UsageSaub { get; } + public double UsagePredictPlan { get; } + public DateTime DateStart { get; } + public float DeltaDepth { get; } + public int IdMode { get; } + public string ModeName { get; } + public double MechDrillingHours { get; } + + public TelemetryStat(Span telemetry) + { + var telemetryFirst = telemetry[0]; + var telemetryLast = telemetry[^1]; + + IdMode = telemetryFirst.IdMode; + ModeName = GetModeName(IdMode); + DateStart = telemetryFirst.DateMin; + DeltaDepth = telemetryLast.WellDepthMax - telemetryFirst.WellDepthMin; + MechDrillingHours = (telemetryLast.DateMax - telemetryFirst.DateMin).TotalHours; + + BlockSpeed = new(t => t.BlockSpeedSp, t => t.BlockSpeed, null, 1, IdMode); + Pressure = new(t => t.PressureSpDelta, t => t.PressureDelta, t => t.PressureDeltaLimitMax, 2, IdMode); + RotorTorque = new(t => t.RotorTorqueSp, t => t.RotorTorque, t => t.RotorTorqueLimitMax, 3, IdMode); + AxialLoad = new(t => t.AxialLoadSp, t => t.AxialLoad, t => t.AxialLoadLimitMax, 4, IdMode); + + foreach (var t in telemetry) + UpdateStat(t); + + UsageSaub = 100d * depthWithSaub / depthSum; + UsagePredictPlan = IdMode != 0 ? 100d : 0d; + } + + private void UpdateStat(TelemetryDataSaubStatDto current) + { + if (previous is not null) + { + var deltaDepth = current.WellDepthMin - previous.WellDepthMin; + + if (deltaDepth > 0) + { + var deltaHours = (current.DateMin - previous.DateMax).TotalHours; + depthSum += deltaDepth; + hoursSum += deltaHours; + + if (current.IdMode == 1 || current.IdMode == 3) + depthWithSaub += deltaDepth; + } + } + + previous = current; + Pressure.UpdateStat(current); + AxialLoad.UpdateStat(current); + RotorTorque.UpdateStat(current); + BlockSpeed.UpdateStat(current); + } + + private static string GetModeName(int idMode) + => idMode switch + { + 1 => "Ротор", + 3 => "Слайд", + _ => "Ручной", + }; +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/WellDrillingProcessMapReportExportService.cs b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/WellDrillingProcessMapReportExportService.cs new file mode 100644 index 00000000..58d3e4f2 --- /dev/null +++ b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/WellDrillingProcessMapReportExportService.cs @@ -0,0 +1,225 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data.ProcessMaps.Report; +using AsbCloudApp.Services; +using AsbCloudApp.Services.ProcessMaps; +using AsbCloudApp.Services.ProcessMaps.WellDrillingProcessMap; +using ClosedXML.Excel; + +namespace AsbCloudInfrastructure.Services.ProcessMaps.WellDrillingProcessMap.Report; + +public class WellDrillingProcessMapReportExportService : IProcessMapReportExportService +{ + const int firstColumn = 2; + const int lastColumn = 42; + + const int headerRowsCount = 5; + + private readonly IWellService wellService; + private readonly IWellDrillingProcessMapReportService wellDrillingProcessMapReportService; + + public WellDrillingProcessMapReportExportService(IWellService wellService, + IWellDrillingProcessMapReportService wellDrillingProcessMapReportService) + { + this.wellService = wellService; + this.wellDrillingProcessMapReportService = wellDrillingProcessMapReportService; + } + + + public async Task<(string Name, Stream File)?> ExportAsync(int idWell, CancellationToken cancellationToken) + { + var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken); + + if (well is null) + return null; + + var stream = GetExcelTemplateStream(); + using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled); + + var data = await wellDrillingProcessMapReportService.GetAsync(idWell, cancellationToken); + + FillProcessMapToWorkbook(workbook, data); + + MemoryStream memoryStream = new(); + workbook.SaveAs(memoryStream, new SaveOptions { }); + memoryStream.Seek(0, SeekOrigin.Begin); + + var name = $"РТК бурение. Отчёт по скважине {well.Caption} куст {well.Cluster}.xlsx"; + + return (name, memoryStream); + } + + private static void FillProcessMapToWorkbook(XLWorkbook workbook, + IEnumerable data) + { + var sheet = workbook.Worksheets.FirstOrDefault(); + if (sheet is null) + return; + var dataBySections = data.GroupBy(p => p.IdWellSectionType); + FillSheet(sheet, dataBySections); + } + + private static void FillSheet(IXLWorksheet sheet, + IEnumerable> dataBySections) + { + var startRow = headerRowsCount + 1; + foreach (var sectionData in dataBySections) + { + if (sectionData.Any()) + startRow = FillSection(sheet, sectionData, startRow); + } + } + + private static int FillSection(IXLWorksheet sheet, IGrouping sectionData, + int row) + { + var rowStart = row; + var sectionName = sectionData.FirstOrDefault()?.WellSectionTypeName + ?? sectionData.Key.ToString(); + + sheet.Range(row, firstColumn, row, lastColumn) + .Merge() + .FirstCell() + .SetVal(sectionName) + .Style + .Fill.SetBackgroundColor(XLColor.LightGray); + row++; + foreach (var interval in sectionData) + row = FillIntervalData(sheet, interval, row); + + var sectionStyle = sheet.Range(rowStart, firstColumn, row - 1, lastColumn).Style; + SetBorders(sectionStyle); + return row; + } + + private static int FillIntervalData(IXLWorksheet sheet, WellDrillingProcessMapReportDto interval, int row) + { + const int columnDepth = firstColumn + 1; + const int columnDate = firstColumn + 2; + const int columnRopTime = firstColumn + 3; + const int columnMode = firstColumn + 4; + + sheet.Cell(row, firstColumn) + .SetVal(interval.DepthStart, "0.0"); + + sheet.Cell(row, columnDepth) + .SetVal(interval.DepthEnd, "0.0"); + + sheet.Cell(row, columnDate) + .SetVal(interval.DateStart); + + sheet.Cell(row, columnRopTime) + .SetVal(interval.MechDrillingHours); + + row = FillIntervalModeData(sheet, interval, columnMode, row); + + return row; + } + + private static int FillIntervalModeData(IXLWorksheet sheet, WellDrillingProcessMapReportDto modeData, + int column, int row) + { + int columnDeltaDepth = column + 1; + int columnPressure = columnDeltaDepth + 1; + int columnLoad = columnPressure + 5; + int columnTorque = columnLoad + 5; + int columnSpeed = columnTorque + 5; + int columnUsagePlan = columnSpeed + 5; + int columnUsageFact = columnUsagePlan + 1; + int columnRop = columnUsageFact + 12; + + sheet.Cell(row, column) + .SetVal(modeData.DrillingMode); + + sheet.Cell(row, columnDeltaDepth) + .SetVal(modeData.DeltaDepth); + + FillIntervalModeDataParam(sheet, modeData.PressureDiff, columnPressure, row); + FillIntervalModeDataParam(sheet, modeData.AxialLoad, columnLoad, row); + FillIntervalModeDataParam(sheet, modeData.TopDriveTorque, columnTorque, row); + FillIntervalModeDataSpeed(sheet, modeData.SpeedLimit, columnSpeed, row); + + sheet.Cell(row, columnUsagePlan) + .SetVal(modeData.UsagePlan); + + sheet.Cell(row, columnUsageFact) + .SetVal(modeData.UsageFact); + + sheet.Cell(row, columnRop) + .SetVal(modeData.Rop); + + return row + 1; + } + + private static void FillIntervalModeDataParam(IXLWorksheet sheet, + WellDrillingProcessMapReportParamsDto dataParam, int column, int row) + { + const int columnOffsetSpPlan = 0; + const int columnOffsetSpFact = 1; + const int columnOffsetFact = 2; + const int columnOffsetLimit = 3; + const int columnOffsetPercent = 4; + + sheet.Cell(row, column + columnOffsetSpPlan) + .SetVal(dataParam.SetpointPlan); + + sheet.Cell(row, column + columnOffsetSpFact) + .SetVal(dataParam.SetpointFact); + + sheet.Cell(row, column + columnOffsetFact) + .SetVal(dataParam.Fact); + + sheet.Cell(row, column + columnOffsetLimit) + .SetVal(dataParam.Limit); + + sheet.Cell(row, column + columnOffsetPercent) + .SetVal(dataParam.SetpointUsage, format: "0.0"); + } + + private static void FillIntervalModeDataSpeed(IXLWorksheet sheet, + WellDrillingProcessMapReportParamsDto dataParam, int column, int row) + { + const int columnOffsetSpPlan = 0; + const int columnOffsetSpFact = 1; + const int columnOffsetFact = 2; + const int columnOffsetLimit = 3; + const int columnOffsetPercent = 4; + + sheet.Cell(row, column + columnOffsetSpPlan) + .SetVal(dataParam.SetpointPlan); + + sheet.Cell(row, column + columnOffsetSpFact) + .SetVal(dataParam.SetpointFact); + + sheet.Cell(row, column + columnOffsetFact) + .SetVal(dataParam.Fact); + + sheet.Cell(row, column + columnOffsetLimit) + .SetVal(dataParam.Limit); + + sheet.Cell(row, column + columnOffsetPercent) + .SetVal(dataParam.SetpointUsage, format: "0.0"); + } + + private static Stream GetExcelTemplateStream() + { + var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream( + "AsbCloudInfrastructure.Services.ProcessMaps.Files.DrillingProcessMapReportTemplate.xlsx"); + + return stream!; + } + + private static void SetBorders(IXLStyle style) + { + style.Border.RightBorder = XLBorderStyleValues.Thin; + style.Border.LeftBorder = XLBorderStyleValues.Thin; + style.Border.TopBorder = XLBorderStyleValues.Thin; + style.Border.BottomBorder = XLBorderStyleValues.Thin; + style.Border.InsideBorder = XLBorderStyleValues.Thin; + } +} \ No newline at end of file diff --git a/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/WellDrillingProcessMapReportService.cs b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/WellDrillingProcessMapReportService.cs new file mode 100644 index 00000000..e38f839b --- /dev/null +++ b/AsbCloudInfrastructure/Services/ProcessMaps/WellDrillingProcessMap/Report/WellDrillingProcessMapReportService.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data.ProcessMaps; +using AsbCloudApp.Data.ProcessMaps.Report; +using AsbCloudApp.Data.SAUB; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Repositories; +using AsbCloudApp.Services; +using AsbCloudApp.Services.ProcessMaps.WellDrillingProcessMap; +using AsbCloudInfrastructure.Services.ProcessMaps.WellDrillingProcessMap.Report.Data; + +namespace AsbCloudInfrastructure.Services.ProcessMaps.WellDrillingProcessMap.Report; + +public class WellDrillingProcessMapReportService : IWellDrillingProcessMapReportService +{ + private readonly IWellService wellService; + private readonly IWellDrillingProcessMapRepository wellDrillingProcessMapRepository; + private readonly ITelemetryDataSaubService telemetryDataSaubService; + private readonly IWellOperationRepository wellOperationRepository; + + public WellDrillingProcessMapReportService(IWellService wellService, + IWellDrillingProcessMapRepository wellDrillingProcessMapRepository, + ITelemetryDataSaubService telemetryDataSaubService, + IWellOperationRepository wellOperationRepository) + { + this.wellService = wellService; + this.wellDrillingProcessMapRepository = wellDrillingProcessMapRepository; + this.telemetryDataSaubService = telemetryDataSaubService; + this.wellOperationRepository = wellOperationRepository; + } + + public async Task> GetAsync(int idWell, + CancellationToken token) + { + var well = await wellService.GetOrDefaultAsync(idWell, token) + ?? throw new ArgumentInvalidException(nameof(idWell), $"Скважина с Id: {idWell} не найдена"); + + if (!well.IdTelemetry.HasValue) + throw new ArgumentInvalidException(nameof(idWell), $"Скважина с Id: {idWell} не имеет телеметрии"); + + var wellDrillingProcessMaps = await wellDrillingProcessMapRepository.GetByIdWellAsync(idWell, token); + + if (!wellDrillingProcessMaps.Any()) + return Enumerable.Empty(); + + var telemetryDataStat = + (await telemetryDataSaubService.GetTelemetryDataStatAsync(well.IdTelemetry.Value, token)).ToArray(); + + if (!telemetryDataStat.Any()) + return Enumerable.Empty(); + + var result = CalcByIntervals(wellDrillingProcessMaps, telemetryDataStat); + + return result; + } + + private IEnumerable CalcByIntervals( + IEnumerable wellDrillingProcessMaps, + TelemetryDataSaubStatDto[] telemetryDataStat) + { + var processMapIntervals = CalcDepthIntervals(wellDrillingProcessMaps); + + var result = new List(processMapIntervals.Count() * 4); + + var telemetryIndexStart = + Array.FindIndex(telemetryDataStat, t => t.WellDepthMin >= processMapIntervals.First().DepthStart); + if (telemetryIndexStart < 0) + return Enumerable.Empty(); + + IDictionary sectionTypes = wellOperationRepository + .GetSectionTypes() + .ToDictionary(s => s.Id, s => s.Caption); + + foreach (var interval in processMapIntervals) + { + var processMapPlanInterval = wellDrillingProcessMaps + .Where(p => p.DepthStart <= interval.DepthEnd && p.DepthEnd >= interval.DepthStart); + + if (!processMapPlanInterval.Any()) + continue; + + var telemetryIndexEnd = Array.FindIndex(telemetryDataStat, telemetryIndexStart, + t => t.WellDepthMin >= interval.DepthEnd); + if (telemetryIndexEnd < 0) + telemetryIndexEnd = telemetryDataStat.Length - 1; + var telemetryDataInterval = + telemetryDataStat.AsSpan(telemetryIndexStart, telemetryIndexEnd - telemetryIndexStart); + + IEnumerable subIntervalsResult = + CalcSubIntervals(interval, processMapPlanInterval, telemetryDataInterval, sectionTypes); + + result.AddRange(subIntervalsResult); + telemetryIndexStart = telemetryIndexEnd; + } + + return result; + } + + private static IEnumerable<(double DepthStart, double DepthEnd)> CalcDepthIntervals( + IEnumerable wellDrillingProcessMaps) + { + if (!wellDrillingProcessMaps.Any()) + yield break; + + var intervalStarts = wellDrillingProcessMaps + .OrderBy(i => i.DepthStart) + .Select(p => p.DepthStart) + .Distinct() + .ToArray(); + + for (var i = 1; i < intervalStarts.Length; i++) + yield return (intervalStarts[i - 1], intervalStarts[i]); + + yield return (intervalStarts[^1], wellDrillingProcessMaps.Max(p => p.DepthEnd)); + } + + private static IEnumerable CalcSubIntervals( + (double DepthStart, double DepthEnd) interval, + IEnumerable wellDrillingProcessMapInterval, + Span telemetryDataInterval, + IDictionary sectionTypes) + { + var telemetryDataIntervalLength = telemetryDataInterval.Length; + if (telemetryDataInterval.Length == 0) + return Enumerable.Empty(); + + var result = new List(); + var telemetryIndexStart = 0; + var subInterval = interval; + + for (var i = telemetryIndexStart + 1; i < telemetryDataIntervalLength; i++) + { + if (IsDifferent(telemetryDataInterval[telemetryIndexStart], telemetryDataInterval[i])) + { + subInterval.DepthEnd = telemetryDataInterval[i - 1].WellDepthMax; + var telemetryRowSpan = telemetryDataInterval[telemetryIndexStart..(i - 1)]; + + if (!telemetryRowSpan.IsEmpty) + { + var intervalReportRow = CalcSubIntervalReportRow(subInterval, wellDrillingProcessMapInterval, + telemetryRowSpan, sectionTypes); + result.Add(intervalReportRow); + } + + telemetryIndexStart = i; + subInterval.DepthStart = subInterval.DepthEnd; + } + } + + subInterval.DepthEnd = interval.DepthEnd; + var intervalReportRowLast = CalcSubIntervalReportRow(subInterval, wellDrillingProcessMapInterval, + telemetryDataInterval[telemetryIndexStart..telemetryDataIntervalLength], sectionTypes); + result.Add(intervalReportRowLast); + return result; + } + + private static WellDrillingProcessMapReportDto CalcSubIntervalReportRow( + (double DepthStart, double DepthEnd) subInterval, + IEnumerable wellDrillingProcessMaps, + Span telemetryRowSpan, + IDictionary sectionTypes) + { + var telemetryStat = new TelemetryStat(telemetryRowSpan); + var processMapByMode = wellDrillingProcessMaps.FirstOrDefault(p => p.IdMode == telemetryStat.IdMode); + var processMapFirst = wellDrillingProcessMaps.First(); + var idWellSectionType = processMapByMode?.IdWellSectionType ?? processMapFirst.IdWellSectionType; + + var result = new WellDrillingProcessMapReportDto + { + IdWell = processMapByMode?.IdWell ?? processMapFirst.IdWell, + IdWellSectionType = idWellSectionType, + WellSectionTypeName = sectionTypes[idWellSectionType], + + DepthStart = subInterval.DepthStart, + DepthEnd = subInterval.DepthEnd, + DateStart = telemetryStat.DateStart, + + MechDrillingHours = telemetryStat.MechDrillingHours, + DrillingMode = telemetryStat.ModeName, + + DeltaDepth = telemetryStat.DeltaDepth, + + PressureDiff = telemetryStat.Pressure.MakeParams(processMapByMode?.Pressure.Plan), + AxialLoad = telemetryStat.AxialLoad.MakeParams(processMapByMode?.AxialLoad.Plan), + TopDriveTorque = telemetryStat.RotorTorque.MakeParams(processMapByMode?.TopDriveTorque.Plan), + SpeedLimit = telemetryStat.BlockSpeed.MakeParams(processMapByMode?.RopPlan), + + Rop = telemetryStat.Rop, + UsagePlan = processMapByMode?.UsageSaub ?? telemetryStat.UsagePredictPlan, + UsageFact = telemetryStat.UsageSaub, + }; + return result; + } + + private static bool IsDifferent(TelemetryDataSaubStatDto intervalStart, TelemetryDataSaubStatDto current) + { + if (intervalStart.WellDepthMin > current.WellDepthMin) + return true; + + if (intervalStart.IdMode != current.IdMode) + return true; + + if (Math.Abs(intervalStart.PressureSp - current.PressureSp) > 5d) + return true; + + if (Math.Abs(intervalStart.AxialLoadSp - current.AxialLoadSp) > 1d) + return true; + + if (Math.Abs(intervalStart.RotorTorqueSp - current.RotorTorqueSp) > 5d) + return true; + + var blockSpeedSpDiff = Math.Abs(intervalStart.BlockSpeedSp - current.BlockSpeedSp); + if (!(blockSpeedSpDiff > 5d)) + return false; + + switch (intervalStart.BlockSpeedSp) + { + case <= 30: + case > 30 when blockSpeedSpDiff > 15d: + case > 80 when blockSpeedSpDiff > 20d: + return true; + } + + return false; + } +} \ No newline at end of file