import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:jiffy/jiffy.dart'; import 'package:tp5/core/core.dart'; import 'package:tp5/providers/airports.dart'; import 'package:tp5/roster/models/duty.dart'; import 'package:timezone/timezone.dart' as tz; class Ftl { Duration minimumrest(String? station) => Duration(hours: station == base ? 12 : 12, minutes: 0, seconds: 0); static Duration postflight = const Duration(minutes: 30); static Duration preflight = const Duration(minutes: 60); static Duration maxdutylast7 = const Duration(hours: 60); static Duration maxdutylast14 = const Duration(hours: 110); static Duration maxdutylast28 = const Duration(hours: 190); static Duration maxfltlast28 = const Duration(hours: 100); static Duration maxfltlastyear = const Duration(hours: 900); static Duration maxfltlast12 = const Duration(hours: 1000); Duration travelling(String station) { if (base == station) return Duration.zero; switch (station) { case "DSS": return const Duration(minutes: 60); case "ORY": return const Duration(minutes: 5); default: return const Duration(minutes: 30); } } String base; List dutiesDetails = []; List clDuties = []; List duties = []; // List dutiesLength = []; List get dutiesAsInterval => duties .map((e) => (e.type != FtlDutyType.other) ? DTInterval(e.start!, e.end!) : null) .whereNotNull() .toList(); List get stdbyAsInterval => dutiesDetails .map((e) => (e.type == FtlDutyDetailsType.standby) ? DTInterval(e.start!, e.end!) : null) .whereNotNull() .toList(); List get fltsAsInterval => dutiesDetails .map((e) => (e.type == FtlDutyDetailsType.flight) ? DTInterval(e.start!, e.end!) : null) .whereNotNull() .toList(); bool frms; Ftl({required this.dutiesDetails, required this.base, this.frms = false}); Ftl.fromCrewlink( {required this.clDuties, required this.base, this.frms = false}) { for (Duty one in clDuties) { switch (one.type) { case "flight": dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, placeStart: one.data["dep"], placeEnd: one.data["des"], type: FtlDutyDetailsType.flight, label: one.data["label"], ), ); case "dhflight": dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, placeStart: one.data["dep"], placeEnd: one.data["des"], type: FtlDutyDetailsType.dhflight, label: one.data["label"], ), ); case "dhlimo": dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, placeStart: one.data["dep"], placeEnd: one.data["des"], type: FtlDutyDetailsType.dhlimo, label: one.data["label"], ), ); case "ground": if (["SBY1", "SBY2", "SBY3"].contains(one.data["label"])) { dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, placeStart: one.data["dep"], // placeEnd: one.data["des"], type: FtlDutyDetailsType.standby, label: one.data["label"], ), ); } else if (["R0"].contains(one.data["label"])) { //reserve is not duty dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, placeStart: one.data["dep"], // placeEnd: one.data["des"], type: FtlDutyDetailsType.reserve, label: one.data["label"], ), ); } else if ((one.data["actype"] ?? "") != "") { dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, placeStart: one.data["dep"], // placeEnd: one.data["des"], type: FtlDutyDetailsType.sim, label: one.data["label"], ), ); } else if (one.start != null && one.end != null && one.end!.diff(one.start!, unit: Unit.hour).abs() < 12) { dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, placeStart: one.data["dep"], // placeEnd: one.data["des"], type: FtlDutyDetailsType.ground, label: one.data["label"], ), ); } else { // print("ftl: constr: unknown ${one.date} ${one.type} ${one.data}"); } case "checkin": dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, placeStart: one.data["dep"], // placeEnd: one.data["des"], type: FtlDutyDetailsType.preflight, ), ); case "checkout": dutiesDetails.add( FtlDutyDetails( start: one.start, end: one.end, // placeStart: one.data["dep"], placeEnd: one.data["des"], type: FtlDutyDetailsType.postflight, ), ); case "credit": case "wholeday": break; default: // print("ftl: unk: ${one.date} ${one.type} ${one.data}"); } } } Jiffy? _calcPostflight(FtlDutyDetails xduty) { if (xduty.end != null && [FtlDutyDetailsType.flight, FtlDutyDetailsType.dhflight] .contains(xduty.type)) { return xduty.end!.addDuration(postflight); } else { return xduty.end; } } Jiffy? _calcPreflight(FtlDutyDetails xduty) { if (xduty.start != null && [ FtlDutyDetailsType.flight, FtlDutyDetailsType.dhflight, FtlDutyDetailsType.dhlimo, FtlDutyDetailsType.sim, ].contains(xduty.type)) { return (base == (xduty.placeStart ?? "")) ? xduty.start!.subtractDuration(const Duration(minutes: 60)) : (xduty.start!.subtractDuration(preflight)); } else { return xduty.start; } } final List _fdpList = [ FtlDutyDetailsType.flight, FtlDutyDetailsType.dhflight, FtlDutyDetailsType.dhlimo ]; //calcul duties calcduties({Jiffy? date}) { log("FTL: start calcduties"); duties.clear(); final t1 = Jiffy.now(); _calcDuty(); log("FTL: start _calcfdpmax"); _calcfdpmax(); log("FTL: start _clacdutytotal"); // _calcDutyTotal(); log("FTL: start calclegal"); _calcLegal(date: date); final t4 = Jiffy.now(); print( "ftl: calcduties calculation : ${t4.diff(t1, unit: Unit.second, asFloat: true)} ms"); log("FTL: finish calcduties"); // return [duties, dutiesDetails, dutiesLength]; return [duties, dutiesDetails]; } _calcDuty() { for (int i = 0; i < dutiesDetails.length; i++) { var last = i > 0 ? dutiesDetails[i - 1] : null; final checkin = ((last?.type == FtlDutyDetailsType.preflight) ? dutiesDetails[i - 1].start : null); final newduty = _addDutyDetail(index: i, duty: FtlDuty(), checkin: checkin); //print("${dutiesDetails[i].type} $newduty"); if (newduty != null) { if (duties.isEmpty) { //first duty duties.add(newduty); } else if ((newduty.start!.isAfter(duties.last.end!)) && (duties.last.type == FtlDutyType.other || newduty.type == FtlDutyType.other)) { //last and new are not duty nor fdp (other=>stdby or rsrv) duties.add(newduty); } else if ((newduty.start! .subtractDuration(travelling(newduty.placeStart ?? base)) .isAfter( duties.last.end! .addDuration(travelling(duties.last.placeEnd ?? base)), )) && // duties.last.type == FtlDutyType.duty && newduty.type == FtlDutyType.duty) { //last is duty duties.add(newduty); } else if (newduty.start!.diff(duties.last.end!, unit: Unit.minute) < minimumrest(duties.last.placeEnd).inMinutes) { //diff < minimumrest //ilhim final mergedduty = _addDutyDetail( index: i, duty: duties.isNotEmpty ? duties.last : FtlDuty(), checkin: checkin); duties[duties.length - 1] = mergedduty!; } else { //jdida duties.add(newduty); } } } } _calcfdpmax() { for (var i = 0; i < duties.length; i++) { FtlDuty duty = duties[i]; FtlDuty? lastduty = i == 0 ? null : duties[i - 1]; if (duty.type == FtlDutyType.fdp) { String acclim = _acclimatized( tzoffset: tzDiff(duty.placeStart!, lastduty?.placeEnd ?? base, duty.start!.dateTime), timeElapsed: lastduty?.end!.dateTime.difference(duty.start!.dateTime) ?? const Duration()); duties[i].acclim = acclim; duties[i].reftime = changeTz(duty.start!, duty.placeStart ?? base).Hm; Jiffy reftime = duty.start!; if (duty.reportdelay2 != null && duty.reportdelay1 != null) { if (duty.start!.dateTime .difference(duty.reportdelay2!.dateTime) .inMinutes < (4 * 60)) { reftime = duty.start!; duty.start = duty.notification2!.add(hours: 1).min(duty.reportdelay1!); } else { reftime = duty.reportdelay2!; duty.start = duty.reportdelay2!; } } else if (duty.reportdelay1 != null) { if (duty.start!.dateTime .difference(duty.reportdelay1!.dateTime) .inMinutes < (4 * 60)) { reftime = duty.start!; duty.start = duty.reportdelay1!; } else { reftime = duty.reportdelay1!; duty.start = duty.reportdelay1!; } } switch (acclim) { case "D": // print("${duty.start?.yMMMd} ${duty.start?.toUtc().Hm}"); duties[i].fdpMax = fdpMaxBasic( reftime: reftime, iata: duty.placeStart, sectors: duty.sectors); duties[i].fdpExtMax = fdpMaxExtBasic( reftime: reftime, iata: duty.placeStart, sectors: duty.sectors); case "B": duties[i].fdpMax = fdpMaxBasic( reftime: reftime, iata: lastduty?.placeEnd ?? lastduty?.placeStart ?? base, sectors: duty.sectors); duties[i].fdpExtMax = fdpMaxExtBasic( reftime: reftime, iata: lastduty?.placeEnd ?? lastduty?.placeStart ?? base, sectors: duty.sectors); case "X": if (frms) { duties[i].fdpMax = fdpMaxUnk( reftime: reftime, iata: duty.placeStart, sectors: duty.sectors); } else { duties[i].fdpMax = fdpMaxUnkFrms( reftime: reftime, iata: duty.placeStart, sectors: duty.sectors); } break; default: } } } } _calcLegal({Jiffy? date}) { if (date == null) { for (var i = 0; i < duties.length; i++) { _checklegal(index: i); } } else { duties.forEachIndexed((i, e) { if (duties[i] .start! .isSameOrAfter(date.startOf(Unit.day).subtract(days: 1))) { _checklegal(index: i); } }); } } Duration _sumDuration(List x) => Duration(milliseconds: x.map((e) => e.duration.inMilliseconds).sum); List breaks(FtlDuty duty) { List out = []; for (var e in duty.dutiesDetailsIndex) { final dutydetail = dutiesDetails[e]; final deptravel = (base == (dutydetail.placeStart ?? "")) ? Duration.zero : travelling(dutydetail.placeStart ?? ""); final destravel = (base == (dutydetail.placeEnd ?? "")) ? Duration.zero : travelling(dutydetail.placeEnd ?? dutydetail.placeStart ?? ""); final start = _calcPreflight(dutydetail)!.subtractDuration(deptravel); final finish = _calcPostflight(dutydetail)!.addDuration(destravel); if (finish.isAfter(start)) { out.add(DTInterval(start, finish)); } } return duty.interval.minusmany(out); } _checklegal({required int index}) { FtlDuty duty = duties[index]; final date = changeTz(duty.start!, base).endOf(Unit.day); // log("ftl: dutylenght date=${date.format(pattern: "ddMMMyy HHmm")} \n date-7=${date.subtract(days: 7 - 1).startOf(Unit.day).format(pattern: "ddMMMyy HHmm")}"); // print("${date.yMEd}"); final dutyLength = FtlDutyTotal( date: date, dutyLength: Duration.zero, fltLength: Duration.zero) ..dutylast7 = _sumDuration( DTInterval(date.subtract(days: 7 - 1).startOf(Unit.day), date) .intersectionmany(dutiesAsInterval)) .add(_sumDuration( DTInterval(date.subtract(days: 7 - 1).startOf(Unit.day), date) .intersectionmany(stdbyAsInterval)) .multiply(0.25)) ..dutylast14 = _sumDuration( DTInterval(date.subtract(days: 14 - 1).startOf(Unit.day), date) .intersectionmany(dutiesAsInterval)) .add(_sumDuration(DTInterval( date.subtract(days: 14 - 1).startOf(Unit.day), date) .intersectionmany(stdbyAsInterval)) .multiply(0.25)) ..dutylast28 = _sumDuration( DTInterval(date.subtract(days: 28 - 1).startOf(Unit.day), date) .intersectionmany(dutiesAsInterval)) .add(_sumDuration(DTInterval( date.subtract(days: 28 - 1).startOf(Unit.day), date) .intersectionmany(stdbyAsInterval)) .multiply(0.25)) ..fltlast28 = _sumDuration( DTInterval(date.subtract(days: 28 - 1).startOf(Unit.day), date) .intersectionmany(fltsAsInterval)) ..fltlast12 = _sumDuration(DTInterval( date.endOf(Unit.month).subtract(months: 12).startOf(Unit.month), date) .intersectionmany(fltsAsInterval)) ..fltyear = _sumDuration(DTInterval(date.startOf(Unit.year), date) .intersectionmany(fltsAsInterval)); duty.dutyTotal = dutyLength; // print("${date.yMEd} ${dutyLength.fltyear?.tohhmm}"); // dutylast7, if ((dutyLength.dutylast7?.inMinutes ?? 0) > maxdutylast7.inMinutes) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.dutylast7, legalCauseMsg: "Max duty in 7 days ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.dutylast7?.tohhmm} / ${maxdutylast7.inHours}h")); } // dutylast14, if ((dutyLength.dutylast14?.inMinutes ?? 0) > maxdutylast14.inMinutes) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.dutylast14, legalCauseMsg: "Max duty in 14 days ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.dutylast14?.tohhmm} / ${maxdutylast14.inHours}h")); } // dutylast28, if ((dutyLength.dutylast28?.inMinutes ?? 0) > maxdutylast28.inMinutes) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.dutylast28, legalCauseMsg: "Max duty in 28 days ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.dutylast28?.tohhmm} / ${maxdutylast28.inHours}h")); } // fltlast28, if ((dutyLength.fltlast28?.inMinutes ?? 0) > maxfltlast28.inMinutes) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.fltlast28, legalCauseMsg: "Max flight hours in 28 days ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.fltlast28?.tohhmm} / ${maxfltlast28.inHours}h")); } // fltyear, if ((dutyLength.fltyear?.inMinutes ?? 0) > maxfltlastyear.inMinutes) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.fltyear, legalCauseMsg: "Max flight hours in calendar year ${duty.start?.format(pattern: "yyyy")} exceeded: ${dutyLength.fltyear?.tohhmm} / ${maxfltlastyear.inHours}h")); } // fltlast12, if ((dutyLength.fltlast12?.inMinutes ?? 0) > maxfltlast12.inMinutes) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.fltlast12, legalCauseMsg: "Max flight hours in 12 months ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.fltlast12?.tohhmm} / ${maxfltlast12.inHours}h")); } // fdpmax, if (duty.type == FtlDutyType.fdp && duty.fdpMax != null) { if (duty.fdpLength.inMinutes <= duty.fdpMax!.inMinutes) { } else if (duty.fdpExtMax != null && duty.fdpLength.inMinutes <= duty.fdpExtMax!.inMinutes) { int nbFdpExt = 0; for (var i = index; i >= 0 && DTInterval( changeTz(duty.start!, base) .startOf(Unit.day) .subtract(days: 7), changeTz(duty.end!, base)) .isOverlap(DTInterval(duties[i].start!, duties[i].end!)); i--) { if (duties[i].fdpExt) nbFdpExt++; } if (nbFdpExt < 2) { duty.fdpExt = true; duties[index] = duty; } else { //nbext more than2 in 7days duty.legals.add(FtlLegal( legalCause: FtlLegalCause.fdpmax, legalCauseMsg: "FDP Ext used more than twice in 7 days. ${duty.fdpLength.tohhmm} / ${duty.fdpMax?.tohhmm} / ext${duty.fdpExtMax?.tohhmm}")); } } else if (breaks(duty).any((e) => (e.duration.inHours > 3) //&& // ((duty.fdpLength.inMinutes) <= // (e.duration // .multiply(0.5) // .add(duty.fdpMax ?? Duration.zero) // .inMinutes)))) { )) { //split duty check final fdpmaxbreak = breaks(duty) .map((e) => ((e.duration.inHours > 3) ? ((e.duration.multiply(0.5).add(duty.fdpMax ?? Duration.zero))) : null)) .whereNotNull(); if (fdpmaxbreak.isNotEmpty && duty.fdpLength < fdpmaxbreak.first) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.fdpmax, condition: "A break on ground must be considered to extend FDP Max to ${fdpmaxbreak.first.tohhmm}, ", legalCauseMsg: "FDP is: ${duty.fdpLength.tohhmm}")); } else { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.fdpmax, legalCauseMsg: "FDP Max is excedeed even with ground break of ${breaks(duty).map((e) => ((e.duration.inHours > 3) ? (e.duration) : null)).whereNotNull().first.tohhmm}. FDP:${duty.fdpLength.tohhmm}/Max:${fdpmaxbreak.first.tohhmm}")); } } else { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.fdpmax, legalCauseMsg: "FDP is ${duty.fdpLength.tohhmm}/Max:${duty.fdpMax?.tohhmm}")); } } // restfdp without ext, final FtlDuty? lastduty = (index > 0) ? duties[index - 1] : null; // log("ftl: checking duty <${duty}> lastduty ended <${lastduty}>"); if (lastduty != null && duty.type == FtlDutyType.fdp && (lastduty.type == FtlDutyType.fdp || lastduty.type == FtlDutyType.duty || lastduty.dutiesDetailsIndex.any( (e) => dutiesDetails[e].type == FtlDutyDetailsType.standby))) { final rest = minimumrest(lastduty.placeEnd) .max(duty.end!.dateTime.difference(duty.start!.dateTime)); duties[index - 1].restends = lastduty.end!.addDuration(rest); if (lastduty.restends!.isAfter(duty.start!)) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.restfdp, legalCauseMsg: "Need more rest before starting FDP. rest: ${duty.start!.dateTime.difference(lastduty.end!.dateTime).tohhmm} / ${rest.tohhmm}")); } } // restfdp with ext, duty = duties[index]; FtlDuty? nextduty = (index < duties.length - 1) ? duties[index + 1] : null; if (duty.fdpExt) { final restbefore = (lastduty == null) ? null : minimumrest(lastduty.placeEnd) .max(lastduty.end!.dateTime.difference(lastduty.start!.dateTime)); final restafter = (nextduty == null) ? null : minimumrest(duty.placeEnd) .max(duty.end!.dateTime.difference(duty.start!.dateTime)); //+2h rest before && 2h rest after if ((lastduty == null || duty.start!.dateTime .difference(lastduty.end!.dateTime) .inMinutes >= (restbefore?.inMinutes ?? 0) + (2 * 60)) && (nextduty == null || nextduty.start!.dateTime .difference(duty.end!.dateTime) .inMinutes >= (restafter?.inMinutes ?? 0) + (60 * 2))) { } //+4h rest after else if ((nextduty == null || nextduty.start!.dateTime.difference(duty.end!.dateTime).inMinutes >= (restafter?.inMinutes ?? 0) + (60 * 4))) { } //not enough rest else { duty.restends = duty.end!.addDuration(restafter!).add(hours: 4); duties[index] = duty; nextduty.legals.add(FtlLegal( legalCause: FtlLegalCause.restfdp, legalCauseMsg: "Need more rest after ExtFDP before starting FDP on ${nextduty.start?.format(pattern: "ddMMMyy HH:mm")} rest: ${nextduty.start?.dateTime.difference(duty.end!.dateTime).tohhmm} / ${restafter.tohhmm}")); duties[index + 1] = nextduty; } } // restrecurrent, // 36h inc 2 local nights or (60h if 4 disruptive) Duration maxdutyrecrest = const Duration(hours: 168); Duration minrecrest = const Duration(hours: 36); DTInterval rest = DTInterval(duty.start!, duty.start!); var i = index; int nbdisruptive = 0; while (i >= 1 && !(rest.duration.inMinutes >= const Duration(hours: 36).inMinutes && rest.contains(DTInterval.fromHm( apartir: changeTz(rest.start, duties[i].placeStart!), h: 23, m: 0, duration: const Duration(hours: 7, minutes: 59))) && rest.contains(DTInterval.fromHm( apartir: changeTz(rest.start.add(hours: 24), duties[i].placeStart!), h: 23, m: 0, duration: const Duration(hours: 7, minutes: 59))))) { final thisduty = duties[i]; if (_isEarlyStart(thisduty) || _isLateFinish(thisduty) || _isNightDuty(thisduty)) { // log("${thisduty}"); nbdisruptive++; if (nbdisruptive == 4) { minrecrest = const Duration(hours: 60); } } i--; rest = DTInterval(duties[i].end!, duties[i + 1].start!); } if (DTInterval(rest.end, duty.end!).duration.inMinutes > maxdutyrecrest.inMinutes) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.restrecurrent, legalCauseMsg: "Before duty, need ${minrecrest.inHours}h rest including 2 local nights. from: ${rest.end.format(pattern: "ddMMMyy HH:mm")} ${DTInterval(rest.end, duty.end!).duration.tohhmm}/${maxdutyrecrest.tohhmm} ")); } // 48h inc 2 local days final startmonth = changeTz(duty.start!.startOf(Unit.month), base); final endmonth = startmonth.endOf(Unit.month); if (duty.interval.isOverlap( DTInterval(endmonth.subtract(days: 4).add(minutes: 1), endmonth))) { int nb48inmonth = 0; List dutiesmonth = duties .where((e) => DTInterval(startmonth, endmonth) .isOverlap(DTInterval(e.start!, e.end!))) .toList(); List restmonth = DTInterval(startmonth, endmonth).minusmany( dutiesmonth .map((e) => DTInterval(changeTz(e.start!, e.placeStart!), changeTz(e.end!, e.placeStart!))) .toList()); // print(restmonth); while (restmonth.isNotEmpty) { final e = restmonth.removeAt(0); final localdays = DTInterval.fromHm( apartir: e.start, h: 0, m: 0, duration: const Duration(hours: 47, minutes: 59)); if (e.contains(localdays)) { nb48inmonth++; // print( // "2localdays: ${localdays.start.format(pattern: "ddMMMyy HH:mm")} ${localdays.end.format(pattern: "ddMMMyy HH:mm")}"); // print( // "in rest ${e.start.format(pattern: "ddMMMyy HH:mm")} ${e.end.format(pattern: "ddMMMyy HH:mm")}"); restmonth = [...e.minus(localdays), ...restmonth]; } } if (nb48inmonth == 0) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.restrecurrent, legalCauseMsg: "Before duty, need 2x2 local days rest from: ${startmonth.format(pattern: "ddMMMyy HH:mm")} to: ${endmonth.format(pattern: "ddMMMyy HH:mm")}")); } else if (nb48inmonth == 1 && DTInterval(endmonth.subtract(days: 2).add(minutes: 1), endmonth) .isOverlap(duty.interval)) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.restrecurrent, legalCauseMsg: "Before duty, need 2x2 local days rest from: ${startmonth.format(pattern: "ddMMMyy HH:mm")} to: ${endmonth.format(pattern: "ddMMMyy HH:mm")}")); } else if (nb48inmonth == 1 && DTInterval(endmonth.subtract(days: 4).add(minutes: 1), endmonth.subtract(days: 2)) .isOverlap(duty.interval) && DTInterval(endmonth.subtract(days: 4).add(minutes: 1), endmonth.subtract(days: 2)) .intersectionmany( [...dutiesAsInterval, ...stdbyAsInterval]).isEmpty) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.restrecurrent, legalCauseMsg: "Before duty, need 2x2 local days rest from: ${startmonth.format(pattern: "ddMMMyy HH:mm")} to: ${endmonth.format(pattern: "ddMMMyy HH:mm")}")); } } // disruptive duty.lateFinish = _isLateFinish(duty); duty.earlyStart = _isEarlyStart(duty); duty.nightDuty = _isNightDuty(duty); if (duty.fdpStart != null && lastduty != null && base == lastduty.placeEnd && (_isLateFinish(lastduty) || _isNightDuty(lastduty)) && _isEarlyStart(duty)) { // log("ftl: found late finish or nighty <${duty.start!.format(pattern: "ddMMMyy HH:mm")}"); if (!DTInterval(lastduty.end!, duty.start!).contains(DTInterval.fromHm( apartir: changeTz(lastduty.end!, lastduty.placeEnd!), h: 23, m: 0, duration: const Duration(hours: 7, minutes: 59)))) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.disruptive, legalCauseMsg: "Disruptive duty; one local night of rest is required before early departure.")); } } //consecutive night duty if (_isConsecutiveNight(duty, lastduty)) { var i = index; final monthint = DTInterval( changeTz(duty.start!.startOf(Unit.month), base), changeTz(duty.start!.endOf(Unit.month), base)); int nbconsnight = 0; while (i > 0 && duties[i].interval.isOverlap(monthint)) { if (_isConsecutiveNight(duties[i], duties[i - 1])) { nbconsnight++; } i--; } if (nbconsnight > 1) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.disruptive, legalCauseMsg: "Consecutive nights is allowed only once per civil month.")); } else { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.disruptive, condition: "Two consecutive night duties; crew member agreement is required.", legalCauseMsg: "")); } } //local day rest if night duty if (lastduty != null && _isNightDuty(lastduty)) { if (DTInterval.fromHm( apartir: changeTz(lastduty.end!, lastduty.placeEnd!), h: 0, m: 0, duration: const Duration(hours: 23, minutes: 59)) .isOverlap(duty.interval)) { duty.legals.add(FtlLegal( legalCause: FtlLegalCause.disruptive, condition: "You can request a local day off after last night duty.", legalCauseMsg: "")); } } // if (duty.legals.isNotEmpty) { // log("${duty.legals.map((e) => e.legalCauseMsg)}"); // } // duties[index] = duty; } bool _isConsecutiveNight(FtlDuty duty, FtlDuty? lastduty) => _isNightDuty(duty) && lastduty != null && DTInterval.fromHm( apartir: changeTz(lastduty.end!, lastduty.placeEnd!), h: 2, m: 0, duration: const Duration(hours: 2, minutes: 59)) .isOverlap(lastduty.interval); FtlDuty? _addDutyDetail( {required int index, required FtlDuty duty, Jiffy? checkin}) { final one = dutiesDetails[index]; if (_fdpList.contains(one.type)) { duty.placeStart = duty.placeStart ?? one.placeStart; duty.placeEnd = one.placeEnd; //duty.start = duty.start ?? _calcPreflight(one).latest(checkin); duty.start = duty.start ?? checkin ?? _calcPreflight(one); duty.end = _calcPostflight(one); if (one.type == FtlDutyDetailsType.flight) { duty.type = FtlDutyType.fdp; duty.sectors++; duty.fdpStart = duty.start; duty.fdpEnd = one.end; } else { duty.type = duty.type ?? FtlDutyType.duty; } duty.dutiesDetailsIndex.add(index); } else if ([FtlDutyDetailsType.standby, FtlDutyDetailsType.reserve] .contains(one.type)) { duty.placeStart = duty.placeStart ?? one.placeStart; duty.placeEnd = one.placeStart; duty.start = duty.start ?? _calcPreflight(one); duty.end = one.end; duty.type = FtlDutyType.other; duty.dutiesDetailsIndex.add(index); } else if ([FtlDutyDetailsType.ground, FtlDutyDetailsType.sim] .contains(one.type)) { duty.placeStart = duty.placeStart ?? one.placeStart; duty.placeEnd = one.placeStart; duty.start = duty.start ?? _calcPreflight(one); duty.end = one.end; duty.type = duty.type ?? FtlDutyType.duty; duty.dutiesDetailsIndex.add(index); } else { return null; } return duty; } static bool _isEarlyStart(FtlDuty x) { //500 559 // print( // "$x is earlystart: ${DTInterval.fromHm(apartir: changeTz(x.start!, x.placeStart!), h: 5, m: 0, duration: const Duration(minutes: 59)).include(x.start!)}"); return (x.fdpStart != null && DTInterval.fromHm( apartir: changeTz(x.start!.subtract(hours: 24), x.placeStart!), h: 5, m: 0, duration: const Duration(minutes: 59)) .include(x.start!)); } static bool _isLateFinish(FtlDuty x) { //2300 159 // print( // "$x is latefinish: ${DTInterval.fromHm(apartir: changeTz(x.start!, x.placeStart!), h: 23, m: 0, duration: const Duration(hours: 2, minutes: 59)).include(x.end!)}"); return (x.type != FtlDutyType.other) && (DTInterval.fromHm( apartir: changeTz(x.start!, x.placeStart!), h: 23, m: 0, duration: const Duration(hours: 2, minutes: 59)) .include(x.end!)); } static bool _isNightDuty(FtlDuty x) { //200 459 // print( // "$x is Night duty: ${DTInterval.fromHm(apartir: changeTz(x.start!, x.placeStart!), h: 2, m: 0, duration: const Duration(hours: 2, minutes: 59)).include(x.end!)}"); return (x.type != FtlDutyType.other) && (DTInterval.fromHm( apartir: changeTz(x.start!, x.placeStart!), h: 2, m: 0, duration: const Duration(hours: 2, minutes: 59))) .isOverlap(x.interval); } Duration tzDiff(String iata1, String iata2, DateTime sourceDateTime) { // print(Airports.instance.find(iata1)!.timezoneid); // print(Airports.instance.find(iata2)!.timezoneid); final iata1Location = tz.getLocation(Airports.find(iata1)!.timezoneid); final iata2Location = tz.getLocation(Airports.find(iata2)!.timezoneid); tz.TZDateTime iata1TZDateTime = tz.TZDateTime.from(sourceDateTime, iata1Location); tz.TZDateTime iata2TZDateTime = tz.TZDateTime.from(sourceDateTime, iata2Location); return Duration( minutes: (iata2TZDateTime.timeZoneOffset.inMinutes - iata1TZDateTime.timeZoneOffset.inMinutes) .abs()); } String _acclimatized( {required Duration tzoffset, required Duration timeElapsed}) { if (tzoffset.inHours <= 2) { return "D"; } else if (timeElapsed.inHours < 48) { return "B"; } else if (tzoffset.inHours < 4 && timeElapsed.inHours >= 48) { return "D"; } else if (tzoffset.inHours <= 6 && timeElapsed.inHours >= 72) { return "D"; } else if (tzoffset.inHours <= 9 && timeElapsed.inHours >= 96) { return "D"; } else if (tzoffset.inHours <= 12 && timeElapsed.inHours >= 120) { return "D"; } else { return "X"; } } static Jiffy changeTz(Jiffy jiffy, String iata) { return Jiffy.parseFromDateTime(tz.TZDateTime.from(jiffy.dateTime, tz.getLocation(Airports.find(iata)?.timezoneid ?? "UTC"))); } Duration _fdpMax( {required reftime, String? iata, required int sectors, required Map> tab}) { Jiffy reftjiffy = Jiffy.now(); String reft = ""; if (reftime is Jiffy && iata != null) { reftjiffy = reftime; reftjiffy = changeTz(reftjiffy, iata); reft = reftjiffy.format(pattern: "HHmm"); } else if (reftime is DateTime && iata != null) { reftjiffy = Jiffy.parseFromDateTime(reftime); reftjiffy = changeTz(reftjiffy, iata); reft = reftjiffy.format(pattern: "HHmm"); } else if (reftime is String && reftime.length == 4) { reft = reftime; } else if (reftime is String && iata != null) { reftjiffy = Jiffy.parse(reftime); reftjiffy = changeTz(reftjiffy, iata); reft = reftjiffy.format(pattern: "HHmm"); } else { throw ("_fdpMax type of reftime unrecognized!!!"); } // print("$iata $reft ${reftjiffy.toUtc().Hm}"); for (String key in tab.keys) { List intervalle = key.split("-"); int cmph1 = reft.compareTo(intervalle[0]); int cmph2 = reft.compareTo(intervalle[1]); bool inint = (intervalle[0].compareTo(intervalle[1]) < 0) ? (cmph1 >= 0 && cmph2 <= 0) : ((cmph1 >= 0 || cmph2 <= 0)); if (inint) { final List? lres = tab[key]; final int nrow = ((sectors == 1) ? 2 : sectors) - 2; final String? res = (lres != null && nrow < lres.length) ? lres[nrow] : null; if (res != null && res != "Not allowed") { return Duration( hours: int.parse(res.split(".")[0]), minutes: int.parse(res.split(".")[1])); } } } return Duration.zero; } Duration fdpMaxBasic( {required reftime, String? iata, required int sectors}) => _fdpMax(reftime: reftime, iata: iata, sectors: sectors, tab: _fdpMaxTab); Duration fdpMaxExtBasic( {required reftime, String? iata, required int sectors}) => _fdpMax( reftime: reftime, iata: iata, sectors: sectors, tab: _fdpMaxExtTab); Duration fdpMaxUnk({required reftime, String? iata, required int sectors}) => _fdpMax(reftime: reftime, iata: iata, sectors: sectors, tab: _fdpUnkTab); Duration fdpMaxUnkFrms( {required reftime, String? iata, required int sectors}) => _fdpMax( reftime: reftime, iata: iata, sectors: sectors, tab: _fdpUnkFrmsTab); //acclim final Map> _fdpMaxTab = { "0600-1329": [ "13.00", "12.30", "12.00", "11.30", "11.00", "10.30", "10.00", "09.30", "09.00", ], "1330-1359": [ "12.45", "12.15", "11.45", "11.15", "10.45", "10.15", "09.45", "09.15", "09.00", ], "1400-1429": [ "12.30", "12.00", "11.30", "11.00", "10.30", "10.00", "09.30", "09.00", "09.00", ], "1430-1459": [ "12.15", "11.45", "11.15", "10.45", "10.15", "09.45", "09.15", "09.00", "09.00", ], "1500-1529": [ "12.00", "11.30", "11.00", "10.30", "10.00", "09.30", "09.00", "09.00", "09.00", ], "1530-1559": [ "11.45", "11.15", "10.45", "10.15", "09.45", "09.15", "09.00", "09.00", "09.00", ], "1600-1629": [ "11.30", "11.00", "10.30", "10.00", "09.30", "09.00", "09.00", "09.00", "09.00", ], "1630-1659": [ "11.15", "10.45", "10.15", "09.45", "09.15", "09.00", "09.00", "09.00", "09.00", ], "1700-0459": [ "11.00", "10.30", "10.00", "09.30", "09.00", "09.00", "09.00", "09.00", "09.00", ], "0500-0514": [ "12.00", "11.30", "11.00", "10.30", "10.00", "09.30", "09.00", "09.00", "09.00", ], "0515-0529": [ "12.15", "11.45", "11.15", "10.45", "10.15", "09.45", "09.15", "09.00", "09.00", ], "0530-0544": [ "12.30", "12.00", "11.30", "11.00", "10.30", "10.00", "09.30", "09.00", "09.00", ], "0545-0559": [ "12.45", "12.15", "11.45", "11.15", "10.45", "10.15", "09.45", "09.15", "09.00", ], }; final Map> _fdpMaxExtTab = { "0600-0614": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0615-0629": [ "13.15", "12.45", "12.15", "11.45", ], "0630-0644": [ "13.30", "13.00", "12.30", "12.00", ], "0645-0659": [ "13.45", "13.15", "12.45", "12.15", ], "0700-1329": [ "14.00", "13.30", "13.00", "12.30", ], "1330-1359": [ "13.45", "13.15", "12.45", "Not allowed", ], "1400-1429": [ "13.30", "13.00", "12.30", "Not allowed", ], "1430-1459": [ "13.15", "12.45", "12.15", "Not allowed", ], "1500-1529": [ "13.00", "12.30", "12.00", "Not allowed", ], "1530-1559": [ "12.45", "Not allowed", "Not allowed", "Not allowed", ], "1600-1629": [ "12.30", "Not allowed", "Not allowed", "Not allowed", ], "1630-1659": [ "12.15", "Not allowed", "Not allowed", "Not allowed", ], "1700-1729": [ "12.00", "Not allowed", "Not allowed", "Not allowed", ], "1730-1759": [ "11.45", "Not allowed", "Not allowed", "Not allowed", ], "1800-1829": [ "11.30", "Not allowed", "Not allowed", "Not allowed", ], "1830-1859": [ "11.15", "Not allowed", "Not allowed", "Not allowed", ], "1900-0359": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0400-0414": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0415-0429": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0430-0444": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0445-0459": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0500-0514": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0515-0529": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0530-0544": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ], "0545-0559": [ "Not allowed", "Not allowed", "Not allowed", "Not allowed", ] }; final Map> _fdpUnkTab = { "0000-2359": ["11.00", "10:30", "10.00", "09.30", "09.00", "09.00", "09.00"] }; final Map> _fdpUnkFrmsTab = { "0000-2359": ["12.00", "11.30", "11.00", "10.30", "10.00", "09.30", "09.00"] }; } class FtlDuty { FtlDuty({this.start, this.end, this.placeStart, this.placeEnd, this.type}); @override String toString() { return "${start?.yMEd} ${start?.Hm} >>> ${end?.Hm} : ${fdpLength.inMinutes > 0 ? "FDP" : "DUTY"} ${dutiesDetailsIndex.length}leg(s)"; } Jiffy? start; Jiffy? end; DTInterval get interval => DTInterval(start!, end!); String? placeStart; String? placeEnd; FtlDutyType? type; List dutiesDetailsIndex = []; Jiffy? fdpStart; Jiffy? fdpEnd; int sectors = 0; Duration? fdpMax; Duration? fdpExtMax; //Duration? fdpIfRExtMax;//if inflight rest rest bool fdpExt = false; List legals = []; Duration get dutyLength => (start != null && end != null) ? end!.dateTime.difference(start!.dateTime) : Duration.zero; Duration get fdpLength => (fdpStart != null && fdpEnd != null) ? fdpEnd!.dateTime.difference(fdpStart!.dateTime) : Duration.zero; // Duration? get rest => (dutyLength.max(Ftl.minimumrest(placeEnd))); Jiffy? restends; String? acclim; String? reftime; FtlDutyTotal? dutyTotal; //Jiffy? report; Jiffy? reportdelay1; Jiffy? notification1; Jiffy? reportdelay2; Jiffy? notification2; bool earlyStart = false; bool lateFinish = false; bool nightDuty = false; } class FtlLegal { FtlLegal( {required this.legalCause, required this.legalCauseMsg, this.legalIndex, this.condition}); FtlLegalCause legalCause; String legalCauseMsg; int? legalIndex; String? condition; } enum FtlDutyType { duty, fdp, other } class FtlDutyDetails { FtlDutyDetails({ this.start, this.end, this.placeStart, this.placeEnd, this.type, this.label, }); Jiffy? start; Jiffy? end; DTInterval get interval => DTInterval(start!, end!); Duration get duration => end!.dateTime.difference(start!.dateTime); String? placeStart; String? placeEnd; FtlDutyDetailsType? type; String get typeString { switch (type) { case FtlDutyDetailsType.flight: return "FLT"; case FtlDutyDetailsType.dhflight: return "DH-FLT"; case FtlDutyDetailsType.dhlimo: return "DH-LIMO"; case FtlDutyDetailsType.sim: return "SIM"; case FtlDutyDetailsType.standby: return "STBY"; case FtlDutyDetailsType.reserve: return "RSRV"; case FtlDutyDetailsType.ground: return "GRND"; default: return "UNK"; } } String? label; } enum FtlDutyDetailsType { preflight, flight, postflight, dhflight, dhlimo, ground, standby, reserve, sim, // training, // other } class FtlDutyTotal { FtlDutyTotal( {required this.date, required this.dutyLength, required this.fltLength}); Jiffy date; Duration dutyLength; Duration fltLength; Duration? dutylast7; Duration? dutylast14; Duration? dutylast28; Duration? fltlast28; Duration? fltyear; Duration? fltlast12; } enum FtlLegalCause { dutylast7, dutylast14, dutylast28, fltlast28, fltyear, fltlast12, fdpmax, restfdp, restrecurrent, disruptive //check duty length && flt length //check fdp leength //check rest before fdp //check recurrent rest //check disruptive sched }