ftl copy3.dart.bak 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557
  1. import 'dart:developer';
  2. import 'package:collection/collection.dart';
  3. import 'package:jiffy/jiffy.dart';
  4. import 'package:tp5/core/core.dart';
  5. import 'package:tp5/providers/airports.dart';
  6. import 'package:tp5/roster/models/duty.dart';
  7. import 'package:timezone/timezone.dart' as tz;
  8. class Ftl {
  9. Duration minimumrest(String? station) =>
  10. Duration(hours: station == base ? 12 : 12, minutes: 0, seconds: 0);
  11. static Duration postflight = const Duration(minutes: 30);
  12. static Duration preflight = const Duration(minutes: 60);
  13. static Duration maxdutylast7 = const Duration(hours: 60);
  14. static Duration maxdutylast14 = const Duration(hours: 110);
  15. static Duration maxdutylast28 = const Duration(hours: 190);
  16. static Duration maxfltlast28 = const Duration(hours: 100);
  17. static Duration maxfltlastyear = const Duration(hours: 900);
  18. static Duration maxfltlast12 = const Duration(hours: 1000);
  19. Duration travelling(String station) {
  20. if (base == station) return Duration.zero;
  21. switch (station) {
  22. case "DSS":
  23. return const Duration(minutes: 60);
  24. case "ORY":
  25. return const Duration(minutes: 5);
  26. default:
  27. return const Duration(minutes: 30);
  28. }
  29. }
  30. String base;
  31. List<FtlDutyDetails> dutiesDetails = [];
  32. List<Duty> clDuties = [];
  33. List<FtlDuty> duties = [];
  34. // List<FtlDutyTotal> dutiesLength = [];
  35. List<DTInterval> get dutiesAsInterval => duties
  36. .map((e) =>
  37. (e.type != FtlDutyType.other) ? DTInterval(e.start!, e.end!) : null)
  38. .whereNotNull()
  39. .toList();
  40. List<DTInterval> get stdbyAsInterval => dutiesDetails
  41. .map((e) => (e.type == FtlDutyDetailsType.standby)
  42. ? DTInterval(e.start!, e.end!)
  43. : null)
  44. .whereNotNull()
  45. .toList();
  46. List<DTInterval> get fltsAsInterval => dutiesDetails
  47. .map((e) => (e.type == FtlDutyDetailsType.flight)
  48. ? DTInterval(e.start!, e.end!)
  49. : null)
  50. .whereNotNull()
  51. .toList();
  52. bool frms;
  53. Ftl({required this.dutiesDetails, required this.base, this.frms = false});
  54. Ftl.fromCrewlink(
  55. {required this.clDuties, required this.base, this.frms = false}) {
  56. for (Duty one in clDuties) {
  57. switch (one.type) {
  58. case "flight":
  59. dutiesDetails.add(
  60. FtlDutyDetails(
  61. start: one.start,
  62. end: one.end,
  63. placeStart: one.data["dep"],
  64. placeEnd: one.data["des"],
  65. type: FtlDutyDetailsType.flight,
  66. label: one.data["label"],
  67. ),
  68. );
  69. case "dhflight":
  70. dutiesDetails.add(
  71. FtlDutyDetails(
  72. start: one.start,
  73. end: one.end,
  74. placeStart: one.data["dep"],
  75. placeEnd: one.data["des"],
  76. type: FtlDutyDetailsType.dhflight,
  77. label: one.data["label"],
  78. ),
  79. );
  80. case "dhlimo":
  81. dutiesDetails.add(
  82. FtlDutyDetails(
  83. start: one.start,
  84. end: one.end,
  85. placeStart: one.data["dep"],
  86. placeEnd: one.data["des"],
  87. type: FtlDutyDetailsType.dhlimo,
  88. label: one.data["label"],
  89. ),
  90. );
  91. case "ground":
  92. if (["SBY1", "SBY2", "SBY3"].contains(one.data["label"])) {
  93. dutiesDetails.add(
  94. FtlDutyDetails(
  95. start: one.start,
  96. end: one.end,
  97. placeStart: one.data["dep"],
  98. // placeEnd: one.data["des"],
  99. type: FtlDutyDetailsType.standby,
  100. label: one.data["label"],
  101. ),
  102. );
  103. } else if (["R0"].contains(one.data["label"])) {
  104. //reserve is not duty
  105. dutiesDetails.add(
  106. FtlDutyDetails(
  107. start: one.start,
  108. end: one.end,
  109. placeStart: one.data["dep"],
  110. // placeEnd: one.data["des"],
  111. type: FtlDutyDetailsType.reserve,
  112. label: one.data["label"],
  113. ),
  114. );
  115. } else if ((one.data["actype"] ?? "") != "") {
  116. dutiesDetails.add(
  117. FtlDutyDetails(
  118. start: one.start,
  119. end: one.end,
  120. placeStart: one.data["dep"],
  121. // placeEnd: one.data["des"],
  122. type: FtlDutyDetailsType.sim,
  123. label: one.data["label"],
  124. ),
  125. );
  126. } else if (one.start != null &&
  127. one.end != null &&
  128. one.end!.diff(one.start!, unit: Unit.hour).abs() < 12) {
  129. dutiesDetails.add(
  130. FtlDutyDetails(
  131. start: one.start,
  132. end: one.end,
  133. placeStart: one.data["dep"],
  134. // placeEnd: one.data["des"],
  135. type: FtlDutyDetailsType.ground,
  136. label: one.data["label"],
  137. ),
  138. );
  139. } else {
  140. // print("ftl: constr: unknown ${one.date} ${one.type} ${one.data}");
  141. }
  142. case "checkin":
  143. dutiesDetails.add(
  144. FtlDutyDetails(
  145. start: one.start,
  146. end: one.end,
  147. placeStart: one.data["dep"],
  148. // placeEnd: one.data["des"],
  149. type: FtlDutyDetailsType.preflight,
  150. ),
  151. );
  152. case "checkout":
  153. dutiesDetails.add(
  154. FtlDutyDetails(
  155. start: one.start,
  156. end: one.end,
  157. // placeStart: one.data["dep"],
  158. placeEnd: one.data["des"],
  159. type: FtlDutyDetailsType.postflight,
  160. ),
  161. );
  162. case "credit":
  163. case "wholeday":
  164. break;
  165. default:
  166. // print("ftl: unk: ${one.date} ${one.type} ${one.data}");
  167. }
  168. }
  169. }
  170. Jiffy? _calcPostflight(FtlDutyDetails xduty) {
  171. if (xduty.end != null &&
  172. [FtlDutyDetailsType.flight, FtlDutyDetailsType.dhflight]
  173. .contains(xduty.type)) {
  174. return xduty.end!.addDuration(postflight);
  175. } else {
  176. return xduty.end;
  177. }
  178. }
  179. Jiffy? _calcPreflight(FtlDutyDetails xduty) {
  180. if (xduty.start != null &&
  181. [
  182. FtlDutyDetailsType.flight,
  183. FtlDutyDetailsType.dhflight,
  184. FtlDutyDetailsType.dhlimo,
  185. FtlDutyDetailsType.sim,
  186. ].contains(xduty.type)) {
  187. return (base == (xduty.placeStart ?? ""))
  188. ? xduty.start!.subtractDuration(const Duration(minutes: 60))
  189. : (xduty.start!.subtractDuration(preflight));
  190. } else {
  191. return xduty.start;
  192. }
  193. }
  194. final List<FtlDutyDetailsType> _fdpList = [
  195. FtlDutyDetailsType.flight,
  196. FtlDutyDetailsType.dhflight,
  197. FtlDutyDetailsType.dhlimo
  198. ];
  199. //calcul duties
  200. calcduties({Jiffy? date}) {
  201. log("FTL: start calcduties");
  202. duties.clear();
  203. final t1 = Jiffy.now();
  204. _calcDuty();
  205. log("FTL: start _calcfdpmax");
  206. _calcfdpmax();
  207. log("FTL: start _clacdutytotal");
  208. // _calcDutyTotal();
  209. log("FTL: start calclegal");
  210. _calcLegal(date: date);
  211. final t4 = Jiffy.now();
  212. print(
  213. "ftl: calcduties calculation : ${t4.diff(t1, unit: Unit.second, asFloat: true)} ms");
  214. log("FTL: finish calcduties");
  215. // return [duties, dutiesDetails, dutiesLength];
  216. return [duties, dutiesDetails];
  217. }
  218. _calcDuty() {
  219. for (int i = 0; i < dutiesDetails.length; i++) {
  220. var last = i > 0 ? dutiesDetails[i - 1] : null;
  221. final checkin = ((last?.type == FtlDutyDetailsType.preflight)
  222. ? dutiesDetails[i - 1].start
  223. : null);
  224. final newduty =
  225. _addDutyDetail(index: i, duty: FtlDuty(), checkin: checkin);
  226. //print("${dutiesDetails[i].type} $newduty");
  227. if (newduty != null) {
  228. if (duties.isEmpty) {
  229. //first duty
  230. duties.add(newduty);
  231. } else if ((newduty.start!.isAfter(duties.last.end!)) &&
  232. (duties.last.type == FtlDutyType.other ||
  233. newduty.type == FtlDutyType.other)) {
  234. //last and new are not duty nor fdp (other=>stdby or rsrv)
  235. duties.add(newduty);
  236. } else if ((newduty.start!
  237. .subtractDuration(travelling(newduty.placeStart ?? base))
  238. .isAfter(
  239. duties.last.end!
  240. .addDuration(travelling(duties.last.placeEnd ?? base)),
  241. )) &&
  242. // duties.last.type == FtlDutyType.duty &&
  243. newduty.type == FtlDutyType.duty) {
  244. //last is duty
  245. duties.add(newduty);
  246. } else if (newduty.start!.diff(duties.last.end!, unit: Unit.minute) <
  247. minimumrest(duties.last.placeEnd).inMinutes) {
  248. //diff < minimumrest
  249. //ilhim
  250. final mergedduty = _addDutyDetail(
  251. index: i,
  252. duty: duties.isNotEmpty ? duties.last : FtlDuty(),
  253. checkin: checkin);
  254. duties[duties.length - 1] = mergedduty!;
  255. } else {
  256. //jdida
  257. duties.add(newduty);
  258. }
  259. }
  260. }
  261. }
  262. _calcfdpmax() {
  263. for (var i = 0; i < duties.length; i++) {
  264. FtlDuty duty = duties[i];
  265. FtlDuty? lastduty = i == 0 ? null : duties[i - 1];
  266. if (duty.type == FtlDutyType.fdp) {
  267. String acclim = _acclimatized(
  268. tzoffset: tzDiff(duty.placeStart!, lastduty?.placeEnd ?? base,
  269. duty.start!.dateTime),
  270. timeElapsed:
  271. lastduty?.end!.dateTime.difference(duty.start!.dateTime) ??
  272. const Duration());
  273. duties[i].acclim = acclim;
  274. duties[i].reftime = changeTz(duty.start!, duty.placeStart ?? base).Hm;
  275. Jiffy reftime = duty.start!;
  276. if (duty.reportdelay2 != null && duty.reportdelay1 != null) {
  277. if (duty.start!.dateTime
  278. .difference(duty.reportdelay2!.dateTime)
  279. .inMinutes <
  280. (4 * 60)) {
  281. reftime = duty.start!;
  282. duty.start =
  283. duty.notification2!.add(hours: 1).min(duty.reportdelay1!);
  284. } else {
  285. reftime = duty.reportdelay2!;
  286. duty.start = duty.reportdelay2!;
  287. }
  288. } else if (duty.reportdelay1 != null) {
  289. if (duty.start!.dateTime
  290. .difference(duty.reportdelay1!.dateTime)
  291. .inMinutes <
  292. (4 * 60)) {
  293. reftime = duty.start!;
  294. duty.start = duty.reportdelay1!;
  295. } else {
  296. reftime = duty.reportdelay1!;
  297. duty.start = duty.reportdelay1!;
  298. }
  299. }
  300. switch (acclim) {
  301. case "D":
  302. // print("${duty.start?.yMMMd} ${duty.start?.toUtc().Hm}");
  303. duties[i].fdpMax = fdpMaxBasic(
  304. reftime: reftime, iata: duty.placeStart, sectors: duty.sectors);
  305. duties[i].fdpExtMax = fdpMaxExtBasic(
  306. reftime: reftime, iata: duty.placeStart, sectors: duty.sectors);
  307. case "B":
  308. duties[i].fdpMax = fdpMaxBasic(
  309. reftime: reftime,
  310. iata: lastduty?.placeEnd ?? lastduty?.placeStart ?? base,
  311. sectors: duty.sectors);
  312. duties[i].fdpExtMax = fdpMaxExtBasic(
  313. reftime: reftime,
  314. iata: lastduty?.placeEnd ?? lastduty?.placeStart ?? base,
  315. sectors: duty.sectors);
  316. case "X":
  317. if (frms) {
  318. duties[i].fdpMax = fdpMaxUnk(
  319. reftime: reftime,
  320. iata: duty.placeStart,
  321. sectors: duty.sectors);
  322. } else {
  323. duties[i].fdpMax = fdpMaxUnkFrms(
  324. reftime: reftime,
  325. iata: duty.placeStart,
  326. sectors: duty.sectors);
  327. }
  328. break;
  329. default:
  330. }
  331. }
  332. }
  333. }
  334. _calcLegal({Jiffy? date}) {
  335. if (date == null) {
  336. for (var i = 0; i < duties.length; i++) {
  337. _checklegal(index: i);
  338. }
  339. } else {
  340. duties.forEachIndexed((i, e) {
  341. if (duties[i]
  342. .start!
  343. .isSameOrAfter(date.startOf(Unit.day).subtract(days: 1))) {
  344. _checklegal(index: i);
  345. }
  346. });
  347. }
  348. }
  349. Duration _sumDuration(List<DTInterval> x) =>
  350. Duration(milliseconds: x.map((e) => e.duration.inMilliseconds).sum);
  351. List<DTInterval> breaks(FtlDuty duty) {
  352. List<DTInterval> out = [];
  353. for (var e in duty.dutiesDetailsIndex) {
  354. final dutydetail = dutiesDetails[e];
  355. final deptravel = (base == (dutydetail.placeStart ?? ""))
  356. ? Duration.zero
  357. : travelling(dutydetail.placeStart ?? "");
  358. final destravel = (base == (dutydetail.placeEnd ?? ""))
  359. ? Duration.zero
  360. : travelling(dutydetail.placeEnd ?? dutydetail.placeStart ?? "");
  361. final start = _calcPreflight(dutydetail)!.subtractDuration(deptravel);
  362. final finish = _calcPostflight(dutydetail)!.addDuration(destravel);
  363. if (finish.isAfter(start)) {
  364. out.add(DTInterval(start, finish));
  365. }
  366. }
  367. return duty.interval.minusmany(out);
  368. }
  369. _checklegal({required int index}) {
  370. FtlDuty duty = duties[index];
  371. final date = changeTz(duty.start!, base).endOf(Unit.day);
  372. // log("ftl: dutylenght date=${date.format(pattern: "ddMMMyy HHmm")} \n date-7=${date.subtract(days: 7 - 1).startOf(Unit.day).format(pattern: "ddMMMyy HHmm")}");
  373. // print("${date.yMEd}");
  374. final dutyLength = FtlDutyTotal(
  375. date: date, dutyLength: Duration.zero, fltLength: Duration.zero)
  376. ..dutylast7 = _sumDuration(
  377. DTInterval(date.subtract(days: 7 - 1).startOf(Unit.day), date)
  378. .intersectionmany(dutiesAsInterval))
  379. .add(_sumDuration(
  380. DTInterval(date.subtract(days: 7 - 1).startOf(Unit.day), date)
  381. .intersectionmany(stdbyAsInterval))
  382. .multiply(0.25))
  383. ..dutylast14 = _sumDuration(
  384. DTInterval(date.subtract(days: 14 - 1).startOf(Unit.day), date)
  385. .intersectionmany(dutiesAsInterval))
  386. .add(_sumDuration(DTInterval(
  387. date.subtract(days: 14 - 1).startOf(Unit.day), date)
  388. .intersectionmany(stdbyAsInterval))
  389. .multiply(0.25))
  390. ..dutylast28 = _sumDuration(
  391. DTInterval(date.subtract(days: 28 - 1).startOf(Unit.day), date)
  392. .intersectionmany(dutiesAsInterval))
  393. .add(_sumDuration(DTInterval(
  394. date.subtract(days: 28 - 1).startOf(Unit.day), date)
  395. .intersectionmany(stdbyAsInterval))
  396. .multiply(0.25))
  397. ..fltlast28 = _sumDuration(
  398. DTInterval(date.subtract(days: 28 - 1).startOf(Unit.day), date)
  399. .intersectionmany(fltsAsInterval))
  400. ..fltlast12 = _sumDuration(DTInterval(
  401. date.endOf(Unit.month).subtract(months: 12).startOf(Unit.month),
  402. date)
  403. .intersectionmany(fltsAsInterval))
  404. ..fltyear = _sumDuration(DTInterval(date.startOf(Unit.year), date)
  405. .intersectionmany(fltsAsInterval));
  406. duty.dutyTotal = dutyLength;
  407. // print("${date.yMEd} ${dutyLength.fltyear?.tohhmm}");
  408. // dutylast7,
  409. if ((dutyLength.dutylast7?.inMinutes ?? 0) > maxdutylast7.inMinutes) {
  410. duty.legals.add(FtlLegal(
  411. legalCause: FtlLegalCause.dutylast7,
  412. legalCauseMsg:
  413. "Max duty in 7 days ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.dutylast7?.tohhmm} / ${maxdutylast7.inHours}h"));
  414. }
  415. // dutylast14,
  416. if ((dutyLength.dutylast14?.inMinutes ?? 0) > maxdutylast14.inMinutes) {
  417. duty.legals.add(FtlLegal(
  418. legalCause: FtlLegalCause.dutylast14,
  419. legalCauseMsg:
  420. "Max duty in 14 days ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.dutylast14?.tohhmm} / ${maxdutylast14.inHours}h"));
  421. }
  422. // dutylast28,
  423. if ((dutyLength.dutylast28?.inMinutes ?? 0) > maxdutylast28.inMinutes) {
  424. duty.legals.add(FtlLegal(
  425. legalCause: FtlLegalCause.dutylast28,
  426. legalCauseMsg:
  427. "Max duty in 28 days ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.dutylast28?.tohhmm} / ${maxdutylast28.inHours}h"));
  428. }
  429. // fltlast28,
  430. if ((dutyLength.fltlast28?.inMinutes ?? 0) > maxfltlast28.inMinutes) {
  431. duty.legals.add(FtlLegal(
  432. legalCause: FtlLegalCause.fltlast28,
  433. legalCauseMsg:
  434. "Max flight hours in 28 days ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.fltlast28?.tohhmm} / ${maxfltlast28.inHours}h"));
  435. }
  436. // fltyear,
  437. if ((dutyLength.fltyear?.inMinutes ?? 0) > maxfltlastyear.inMinutes) {
  438. duty.legals.add(FtlLegal(
  439. legalCause: FtlLegalCause.fltyear,
  440. legalCauseMsg:
  441. "Max flight hours in calendar year ${duty.start?.format(pattern: "yyyy")} exceeded: ${dutyLength.fltyear?.tohhmm} / ${maxfltlastyear.inHours}h"));
  442. }
  443. // fltlast12,
  444. if ((dutyLength.fltlast12?.inMinutes ?? 0) > maxfltlast12.inMinutes) {
  445. duty.legals.add(FtlLegal(
  446. legalCause: FtlLegalCause.fltlast12,
  447. legalCauseMsg:
  448. "Max flight hours in 12 months ending ${duty.start?.format(pattern: "ddMMMyy")} exceeded: ${dutyLength.fltlast12?.tohhmm} / ${maxfltlast12.inHours}h"));
  449. }
  450. // fdpmax,
  451. if (duty.type == FtlDutyType.fdp && duty.fdpMax != null) {
  452. if (duty.fdpLength.inMinutes <= duty.fdpMax!.inMinutes) {
  453. } else if (duty.fdpExtMax != null &&
  454. duty.fdpLength.inMinutes <= duty.fdpExtMax!.inMinutes) {
  455. int nbFdpExt = 0;
  456. for (var i = index;
  457. i >= 0 &&
  458. DTInterval(
  459. changeTz(duty.start!, base)
  460. .startOf(Unit.day)
  461. .subtract(days: 7),
  462. changeTz(duty.end!, base))
  463. .isOverlap(DTInterval(duties[i].start!, duties[i].end!));
  464. i--) {
  465. if (duties[i].fdpExt) nbFdpExt++;
  466. }
  467. if (nbFdpExt < 2) {
  468. duty.fdpExt = true;
  469. duties[index] = duty;
  470. } else {
  471. //nbext more than2 in 7days
  472. duty.legals.add(FtlLegal(
  473. legalCause: FtlLegalCause.fdpmax,
  474. legalCauseMsg:
  475. "FDP Ext used more than twice in 7 days. ${duty.fdpLength.tohhmm} / ${duty.fdpMax?.tohhmm} / ext${duty.fdpExtMax?.tohhmm}"));
  476. }
  477. } else if (breaks(duty).any((e) => (e.duration.inHours > 3)
  478. //&&
  479. // ((duty.fdpLength.inMinutes) <=
  480. // (e.duration
  481. // .multiply(0.5)
  482. // .add(duty.fdpMax ?? Duration.zero)
  483. // .inMinutes)))) {
  484. )) {
  485. //split duty check
  486. final fdpmaxbreak = breaks(duty)
  487. .map((e) => ((e.duration.inHours > 3)
  488. ? ((e.duration.multiply(0.5).add(duty.fdpMax ?? Duration.zero)))
  489. : null))
  490. .whereNotNull();
  491. if (fdpmaxbreak.isNotEmpty && duty.fdpLength < fdpmaxbreak.first) {
  492. duty.legals.add(FtlLegal(
  493. legalCause: FtlLegalCause.fdpmax,
  494. condition:
  495. "A break on ground must be considered to extend FDP Max to ${fdpmaxbreak.first.tohhmm}, ",
  496. legalCauseMsg: "FDP is: ${duty.fdpLength.tohhmm}"));
  497. } else {
  498. duty.legals.add(FtlLegal(
  499. legalCause: FtlLegalCause.fdpmax,
  500. legalCauseMsg:
  501. "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}"));
  502. }
  503. } else {
  504. duty.legals.add(FtlLegal(
  505. legalCause: FtlLegalCause.fdpmax,
  506. legalCauseMsg:
  507. "FDP is ${duty.fdpLength.tohhmm}/Max:${duty.fdpMax?.tohhmm}"));
  508. }
  509. }
  510. // restfdp without ext,
  511. final FtlDuty? lastduty = (index > 0) ? duties[index - 1] : null;
  512. // log("ftl: checking duty <${duty}> lastduty ended <${lastduty}>");
  513. if (lastduty != null &&
  514. duty.type == FtlDutyType.fdp &&
  515. (lastduty.type == FtlDutyType.fdp ||
  516. lastduty.type == FtlDutyType.duty ||
  517. lastduty.dutiesDetailsIndex.any(
  518. (e) => dutiesDetails[e].type == FtlDutyDetailsType.standby))) {
  519. final rest = minimumrest(lastduty.placeEnd)
  520. .max(duty.end!.dateTime.difference(duty.start!.dateTime));
  521. duties[index - 1].restends = lastduty.end!.addDuration(rest);
  522. if (lastduty.restends!.isAfter(duty.start!)) {
  523. duty.legals.add(FtlLegal(
  524. legalCause: FtlLegalCause.restfdp,
  525. legalCauseMsg:
  526. "Need more rest before starting FDP. rest: ${duty.start!.dateTime.difference(lastduty.end!.dateTime).tohhmm} / ${rest.tohhmm}"));
  527. }
  528. }
  529. // restfdp with ext,
  530. duty = duties[index];
  531. FtlDuty? nextduty = (index < duties.length - 1) ? duties[index + 1] : null;
  532. if (duty.fdpExt) {
  533. final restbefore = (lastduty == null)
  534. ? null
  535. : minimumrest(lastduty.placeEnd)
  536. .max(lastduty.end!.dateTime.difference(lastduty.start!.dateTime));
  537. final restafter = (nextduty == null)
  538. ? null
  539. : minimumrest(duty.placeEnd)
  540. .max(duty.end!.dateTime.difference(duty.start!.dateTime));
  541. //+2h rest before && 2h rest after
  542. if ((lastduty == null ||
  543. duty.start!.dateTime
  544. .difference(lastduty.end!.dateTime)
  545. .inMinutes >=
  546. (restbefore?.inMinutes ?? 0) + (2 * 60)) &&
  547. (nextduty == null ||
  548. nextduty.start!.dateTime
  549. .difference(duty.end!.dateTime)
  550. .inMinutes >=
  551. (restafter?.inMinutes ?? 0) + (60 * 2))) {
  552. }
  553. //+4h rest after
  554. else if ((nextduty == null ||
  555. nextduty.start!.dateTime.difference(duty.end!.dateTime).inMinutes >=
  556. (restafter?.inMinutes ?? 0) + (60 * 4))) {
  557. }
  558. //not enough rest
  559. else {
  560. duty.restends = duty.end!.addDuration(restafter!).add(hours: 4);
  561. duties[index] = duty;
  562. nextduty.legals.add(FtlLegal(
  563. legalCause: FtlLegalCause.restfdp,
  564. legalCauseMsg:
  565. "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.add(const Duration(hours: 4)).tohhmm}"));
  566. duties[index + 1] = nextduty;
  567. }
  568. }
  569. // restrecurrent,
  570. // 36h inc 2 local nights or (60h if 4 disruptive)
  571. const Duration maxdutyrecrest = Duration(hours: 168);
  572. Duration minrecrest = const Duration(hours: 36);
  573. bool foundrecrest = false;
  574. var i = index;
  575. int nbdisruptive = 0;
  576. DTInterval rest = DTInterval(duty.start!.toUtc(), duty.start!.toUtc());
  577. String restPlace = duty.placeStart!;
  578. DTInterval dutyl = DTInterval(duty.start!.toUtc(), duty.end!.toUtc());
  579. Jiffy lastdutystart = duty.start!.toUtc();
  580. //find last recrest
  581. while (i >= 1 && !(foundrecrest)) {
  582. i--;
  583. lastdutystart = duties[i + 1].start!.toUtc();
  584. dutyl = DTInterval(lastdutystart, duty.end!.toUtc());
  585. bool disruptive = (_isEarlyStart(duties[i + 1]) ||
  586. _isLateFinish(duties[i + 1]) ||
  587. _isNightDuty(duties[i + 1]));
  588. while (i >= 1 &&
  589. duties[i]
  590. .legals
  591. .any((e) => e.legalCause == FtlLegalCause.restrecurrent)) {
  592. i--;
  593. }
  594. rest = DTInterval(duties[i].end!.toUtc(), lastdutystart);
  595. restPlace = duties[i].placeStart!;
  596. foundrecrest = (rest.duration.inMinutes >= minrecrest.inMinutes) &&
  597. rest.contains(DTInterval.fromHm(
  598. apartir: rest.start.toUtc(),
  599. h: 23,
  600. m: 0,
  601. duration: const Duration(hours: 7, minutes: 59),
  602. ap: restPlace)) &&
  603. rest.contains(DTInterval.fromHm(
  604. apartir: rest.start.toUtc().add(days: 1),
  605. h: 23,
  606. m: 0,
  607. duration: const Duration(hours: 7, minutes: 59),
  608. ap: restPlace));
  609. if (foundrecrest) print("ftl: recrest 36h: $rest");
  610. if (!foundrecrest && disruptive) {
  611. nbdisruptive++;
  612. }
  613. }
  614. //check if 60h rest needed
  615. //find recrest before last recrest
  616. int j = i;
  617. final duty2 = duties[j];
  618. bool foundrecrest2 = false;
  619. int nbdisruptive2 = 0;
  620. DTInterval rest2 = DTInterval(duty2.start!.toUtc(), duty2.start!.toUtc());
  621. String restPlace2 = duty2.placeStart!;
  622. DTInterval dutyl2 = DTInterval(duty2.start!.toUtc(), duty2.end!.toUtc());
  623. Jiffy lastdutystart2 = duty2.start!.toUtc();
  624. if (duty.start!.isBefore(rest.start.add(hours: 60))) {
  625. print("ftl 60h: $duty");
  626. while (j >= 1 && !(foundrecrest2)) {
  627. j--;
  628. lastdutystart2 = duties[j + 1].start!.toUtc();
  629. dutyl2 = DTInterval(lastdutystart2, duty2.end!.toUtc());
  630. bool disruptive2 = (_isEarlyStart(duties[j + 1]) ||
  631. _isLateFinish(duties[j + 1]) ||
  632. _isNightDuty(duties[j + 1]));
  633. while (j >= 1 &&
  634. duties[j]
  635. .legals
  636. .any((e) => e.legalCause == FtlLegalCause.restrecurrent)) {
  637. j--;
  638. }
  639. rest2 = DTInterval(duties[j].end!.toUtc(), lastdutystart2);
  640. restPlace2 = duties[j].placeStart!;
  641. foundrecrest2 = (rest2.duration.inMinutes >= minrecrest.inMinutes) &&
  642. rest2.contains(DTInterval.fromHm(
  643. apartir: rest2.start.toUtc(),
  644. h: 23,
  645. m: 0,
  646. duration: const Duration(hours: 7, minutes: 59),
  647. ap: restPlace2)) &&
  648. rest2.contains(DTInterval.fromHm(
  649. apartir: rest2.start.toUtc().add(days: 1),
  650. h: 23,
  651. m: 0,
  652. duration: const Duration(hours: 7, minutes: 59),
  653. ap: restPlace2));
  654. // if (foundrecrest) print(rest);
  655. if (!foundrecrest2 && disruptive2) {
  656. nbdisruptive2++;
  657. }
  658. }
  659. print("ftl: duty 60h: nbdisruptive: $nbdisruptive2");
  660. }
  661. if (nbdisruptive2 >= 4) {
  662. const restlength = Duration(hours: 60);
  663. final lastduty = ((index >= 2 &&
  664. duties[index - 1]
  665. .legals
  666. .any((e) => e.legalCause == FtlLegalCause.restrecurrent))
  667. ? duties[index - 2]
  668. : duties[index - 1]);
  669. duty.legals.add(FtlLegal(
  670. legalCause: FtlLegalCause.restrecurrent,
  671. legalCauseMsg:
  672. "More than 4 disruptive duties found before recurrent rest. Need ${restlength.inHours}h rest. Next duty can be started after <${lastduty.end!.addDuration(restlength).max(DTInterval.fromHm(apartir: lastduty.end!.toUtc(), h: 23, m: 0, duration: const Duration(hours: 7, minutes: 59), ap: lastduty.placeEnd!).end.add(days: 1)).format(pattern: "ddMMMyy HH:mm")}>"));
  673. } else {
  674. // if (DTInterval(duties[i].start!, duty.end!).duration.inMinutes >
  675. // print("ftl nb disruptive $nbdisruptive");
  676. if (dutyl.duration.inMinutes > maxdutyrecrest.inMinutes) {
  677. final restlength =
  678. ((nbdisruptive <= 4) ? minrecrest : const Duration(hours: 60));
  679. final lastduty = ((index >= 2 &&
  680. duties[index - 1]
  681. .legals
  682. .any((e) => e.legalCause == FtlLegalCause.restrecurrent))
  683. ? duties[index - 2]
  684. : duties[index - 1]);
  685. duty.legals.add(FtlLegal(
  686. legalCause: FtlLegalCause.restrecurrent,
  687. legalCauseMsg:
  688. "More than ${maxdutyrecrest.inHours}h without recurrent rest. from <${rest.end.format(pattern: "ddMMMyy HH:mm")}> to <${duty.end!.format(pattern: "ddMMMyy HH:mm")}> ${DTInterval(rest.end, duty.end!).duration.tohhmm}. ${maxdutyrecrest.inHours}h ends at <${rest.end.addDuration(maxdutyrecrest).format(pattern: "ddMMMyy HH:mm")}>. Need ${restlength.inHours}h rest. Next duty can be started after <${lastduty.end!.addDuration(restlength).max(DTInterval.fromHm(apartir: lastduty.end!.toUtc(), h: 23, m: 0, duration: const Duration(hours: 7, minutes: 59), ap: lastduty.placeEnd!).end.add(days: 1)).format(pattern: "ddMMMyy HH:mm")}>"));
  689. }
  690. }
  691. // 48h inc 2 local days
  692. final startmonth = changeTz(duty.start!.startOf(Unit.month), base);
  693. final endmonth = startmonth.endOf(Unit.month);
  694. if (duty.interval.isOverlap(
  695. DTInterval(endmonth.subtract(days: 4).add(minutes: 1), endmonth))) {
  696. int nb48inmonth = 0;
  697. List<FtlDuty> dutiesmonth = duties
  698. .where((e) => DTInterval(startmonth, endmonth)
  699. .isOverlap(DTInterval(e.start!, e.end!)))
  700. .toList();
  701. List<DTInterval> restmonth = DTInterval(startmonth, endmonth).minusmany(
  702. dutiesmonth
  703. .map((e) => DTInterval(changeTz(e.start!, e.placeStart!),
  704. changeTz(e.end!, e.placeStart!)))
  705. .toList());
  706. // print(restmonth);
  707. while (restmonth.isNotEmpty) {
  708. final e = restmonth.removeAt(0);
  709. final localdays = DTInterval.fromHm(
  710. apartir: e.start.toUtc(),
  711. h: 0,
  712. m: 0,
  713. duration: const Duration(hours: 47, minutes: 59),
  714. ap: base);
  715. if (e.contains(localdays)) {
  716. nb48inmonth++;
  717. // print(
  718. // "2localdays: ${localdays.start.format(pattern: "ddMMMyy HH:mm")} ${localdays.end.format(pattern: "ddMMMyy HH:mm")}");
  719. // print(
  720. // "in rest ${e.start.format(pattern: "ddMMMyy HH:mm")} ${e.end.format(pattern: "ddMMMyy HH:mm")}");
  721. restmonth = [...e.minus(localdays), ...restmonth];
  722. }
  723. }
  724. if (nb48inmonth == 0) {
  725. duty.legals.add(FtlLegal(
  726. legalCause: FtlLegalCause.restrecurrent,
  727. legalCauseMsg:
  728. "Before duty, need 2x2 local days rest from: ${startmonth.format(pattern: "ddMMMyy HH:mm")} to: ${endmonth.format(pattern: "ddMMMyy HH:mm")}"));
  729. } else if (nb48inmonth == 1 &&
  730. DTInterval(endmonth.subtract(days: 2).add(minutes: 1), endmonth)
  731. .isOverlap(duty.interval)) {
  732. duty.legals.add(FtlLegal(
  733. legalCause: FtlLegalCause.restrecurrent,
  734. legalCauseMsg:
  735. "Before duty, need 2x2 local days rest from: ${startmonth.format(pattern: "ddMMMyy HH:mm")} to: ${endmonth.format(pattern: "ddMMMyy HH:mm")}"));
  736. } else if (nb48inmonth == 1 &&
  737. DTInterval(endmonth.subtract(days: 4).add(minutes: 1),
  738. endmonth.subtract(days: 2))
  739. .isOverlap(duty.interval) &&
  740. DTInterval(endmonth.subtract(days: 4).add(minutes: 1),
  741. endmonth.subtract(days: 2))
  742. .intersectionmany(
  743. [...dutiesAsInterval, ...stdbyAsInterval]).isEmpty) {
  744. duty.legals.add(FtlLegal(
  745. legalCause: FtlLegalCause.restrecurrent,
  746. legalCauseMsg:
  747. "Before duty, need 2x2 local days rest from: ${startmonth.format(pattern: "ddMMMyy HH:mm")} to: ${endmonth.format(pattern: "ddMMMyy HH:mm")}"));
  748. }
  749. }
  750. // disruptive
  751. duty.lateFinish = _isLateFinish(duty);
  752. duty.earlyStart = _isEarlyStart(duty);
  753. duty.nightDuty = _isNightDuty(duty);
  754. if (duty.fdpStart != null &&
  755. lastduty != null &&
  756. base == lastduty.placeEnd &&
  757. (_isLateFinish(lastduty) || _isNightDuty(lastduty)) &&
  758. _isEarlyStart(duty)) {
  759. // log("ftl: found late finish or nighty <${duty.start!.format(pattern: "ddMMMyy HH:mm")}");
  760. if (!DTInterval(lastduty.end!, duty.start!).contains(DTInterval.fromHm(
  761. apartir: changeTz(lastduty.end!, lastduty.placeEnd!).toUtc(),
  762. h: 23,
  763. m: 0,
  764. duration: const Duration(hours: 7, minutes: 59),
  765. ap: duty.placeStart!))) {
  766. duty.legals.add(FtlLegal(
  767. legalCause: FtlLegalCause.disruptive,
  768. legalCauseMsg:
  769. "Disruptive duty; one local night of rest is required before early departure."));
  770. }
  771. }
  772. //consecutive night duty
  773. if (_isConsecutiveNight(duty, lastduty)) {
  774. var i = index;
  775. final monthint = DTInterval(
  776. changeTz(duty.start!.startOf(Unit.month), base),
  777. changeTz(duty.start!.endOf(Unit.month), base));
  778. int nbconsnight = 0;
  779. while (i > 0 &&
  780. duties[i]
  781. .interval
  782. .isOverlap(changeTzDt(monthint, duty.placeStart!))) {
  783. if (_isConsecutiveNight(duties[i], duties[i - 1])) {
  784. nbconsnight++;
  785. }
  786. i--;
  787. }
  788. if (nbconsnight > 1) {
  789. duty.legals.add(FtlLegal(
  790. legalCause: FtlLegalCause.disruptive,
  791. legalCauseMsg:
  792. "Consecutive nights is allowed only once per civil month."));
  793. } else {
  794. duty.legals.add(FtlLegal(
  795. legalCause: FtlLegalCause.disruptive,
  796. condition:
  797. "Two consecutive night duties; crew member agreement is required.",
  798. legalCauseMsg: ""));
  799. }
  800. }
  801. //local day rest if night duty
  802. if (lastduty != null && _isNightDuty(lastduty)) {
  803. if (DTInterval.fromHm(
  804. apartir: changeTz(lastduty.end!, lastduty.placeEnd!).toUtc(),
  805. h: 0,
  806. m: 0,
  807. duration: const Duration(hours: 23, minutes: 59),
  808. ap: lastduty.placeEnd!)
  809. .isOverlap(changeTzDt(duty.interval, duty.placeStart!))) {
  810. duty.legals.add(FtlLegal(
  811. legalCause: FtlLegalCause.disruptive,
  812. condition: "You can request a local day off after last night duty.",
  813. legalCauseMsg: ""));
  814. }
  815. }
  816. // if (duty.legals.isNotEmpty) {
  817. // log("${duty.legals.map((e) => e.legalCauseMsg)}");
  818. // }
  819. // duties[index] = duty;
  820. }
  821. bool _isConsecutiveNight(FtlDuty duty, FtlDuty? lastduty) =>
  822. _isNightDuty(duty) &&
  823. lastduty != null &&
  824. DTInterval.fromHm(
  825. apartir: changeTz(lastduty.end!, lastduty.placeEnd!).toUtc(),
  826. h: 2,
  827. m: 0,
  828. duration: const Duration(hours: 2, minutes: 59),
  829. ap: lastduty.placeEnd!)
  830. .isOverlap(lastduty.interval.toUtc());
  831. FtlDuty? _addDutyDetail(
  832. {required int index, required FtlDuty duty, Jiffy? checkin}) {
  833. final one = dutiesDetails[index];
  834. if (_fdpList.contains(one.type)) {
  835. duty.placeStart = duty.placeStart ?? one.placeStart;
  836. duty.placeEnd = one.placeEnd;
  837. //duty.start = duty.start ?? _calcPreflight(one).latest(checkin);
  838. duty.start = duty.start ?? checkin ?? _calcPreflight(one);
  839. duty.end = _calcPostflight(one);
  840. if (one.type == FtlDutyDetailsType.flight) {
  841. duty.type = FtlDutyType.fdp;
  842. duty.sectors++;
  843. duty.fdpStart = duty.start;
  844. duty.fdpEnd = one.end;
  845. } else {
  846. duty.type = duty.type ?? FtlDutyType.duty;
  847. }
  848. duty.dutiesDetailsIndex.add(index);
  849. } else if ([FtlDutyDetailsType.standby, FtlDutyDetailsType.reserve]
  850. .contains(one.type)) {
  851. duty.placeStart = duty.placeStart ?? one.placeStart;
  852. duty.placeEnd = one.placeStart;
  853. duty.start = duty.start ?? _calcPreflight(one);
  854. duty.end = one.end;
  855. duty.type = FtlDutyType.other;
  856. duty.dutiesDetailsIndex.add(index);
  857. } else if ([FtlDutyDetailsType.ground, FtlDutyDetailsType.sim]
  858. .contains(one.type)) {
  859. duty.placeStart = duty.placeStart ?? one.placeStart;
  860. duty.placeEnd = one.placeStart;
  861. duty.start = duty.start ?? _calcPreflight(one);
  862. duty.end = one.end;
  863. duty.type = duty.type ?? FtlDutyType.duty;
  864. duty.dutiesDetailsIndex.add(index);
  865. } else {
  866. return null;
  867. }
  868. return duty;
  869. }
  870. static bool _isEarlyStart(FtlDuty x) {
  871. //500 559
  872. // print(
  873. // "$x is earlystart: ${DTInterval.fromHm(apartir: changeTz(x.start!, x.placeStart!), h: 5, m: 0, duration: const Duration(minutes: 59)).include(x.start!)}");
  874. return (x.fdpStart != null &&
  875. DTInterval.fromHm(
  876. apartir: changeTz(x.start!.subtract(hours: 24), x.placeStart!)
  877. .toUtc(),
  878. h: 5,
  879. m: 0,
  880. duration: const Duration(minutes: 59),
  881. ap: x.placeStart!)
  882. .include(changeTz(x.start!, x.placeStart!)));
  883. }
  884. static bool _isLateFinish(FtlDuty x) {
  885. //2300 159
  886. // print(
  887. // "$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!)}");
  888. return (x.type != FtlDutyType.other) &&
  889. (DTInterval.fromHm(
  890. apartir: changeTz(x.start!, x.placeStart!).toUtc(),
  891. h: 23,
  892. m: 0,
  893. duration: const Duration(hours: 2, minutes: 59),
  894. ap: x.placeStart!)
  895. .include(changeTz(x.end!, x.placeStart!)));
  896. }
  897. static bool _isNightDuty(FtlDuty x) {
  898. //200 459
  899. // print(
  900. // "$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!)}");
  901. return (x.type != FtlDutyType.other) &&
  902. ((DTInterval.fromHm(
  903. apartir: x.start!.toUtc(),
  904. h: 2,
  905. m: 0,
  906. duration: const Duration(hours: 2, minutes: 59),
  907. ap: x.placeStart!))
  908. .isOverlap(x.interval.toUtc()) ||
  909. (DTInterval.fromHm(
  910. apartir: x.start!.subtract(days: 1).toUtc(),
  911. h: 2,
  912. m: 0,
  913. duration: const Duration(hours: 2, minutes: 59),
  914. ap: x.placeStart!))
  915. .isOverlap(x.interval.toUtc()));
  916. }
  917. Duration tzDiff(String iata1, String iata2, DateTime sourceDateTime) {
  918. // print(Airports.instance.find(iata1)!.timezoneid);
  919. // print(Airports.instance.find(iata2)!.timezoneid);
  920. final iata1Location = tz.getLocation(Airports.find(iata1)!.timezoneid);
  921. final iata2Location = tz.getLocation(Airports.find(iata2)!.timezoneid);
  922. tz.TZDateTime iata1TZDateTime =
  923. tz.TZDateTime.from(sourceDateTime, iata1Location);
  924. tz.TZDateTime iata2TZDateTime =
  925. tz.TZDateTime.from(sourceDateTime, iata2Location);
  926. return Duration(
  927. minutes: (iata2TZDateTime.timeZoneOffset.inMinutes -
  928. iata1TZDateTime.timeZoneOffset.inMinutes)
  929. .abs());
  930. }
  931. String _acclimatized(
  932. {required Duration tzoffset, required Duration timeElapsed}) {
  933. if (tzoffset.inHours <= 2) {
  934. return "D";
  935. } else if (timeElapsed.inHours < 48) {
  936. return "B";
  937. } else if (tzoffset.inHours < 4 && timeElapsed.inHours >= 48) {
  938. return "D";
  939. } else if (tzoffset.inHours <= 6 && timeElapsed.inHours >= 72) {
  940. return "D";
  941. } else if (tzoffset.inHours <= 9 && timeElapsed.inHours >= 96) {
  942. return "D";
  943. } else if (tzoffset.inHours <= 12 && timeElapsed.inHours >= 120) {
  944. return "D";
  945. } else {
  946. return "X";
  947. }
  948. }
  949. static Jiffy changeTz(Jiffy jiffy, String iata) {
  950. return Jiffy.parseFromDateTime(tz.TZDateTime.from(jiffy.dateTime,
  951. tz.getLocation(Airports.find(iata)?.timezoneid ?? "UTC")));
  952. }
  953. static DTInterval changeTzDt(DTInterval dt, String iata) {
  954. return DTInterval(changeTz(dt.start, iata), changeTz(dt.end, iata));
  955. }
  956. Duration _fdpMax(
  957. {required reftime,
  958. String? iata,
  959. required int sectors,
  960. required Map<String, List<String>> tab}) {
  961. Jiffy reftjiffy = Jiffy.now();
  962. String reft = "";
  963. if (reftime is Jiffy && iata != null) {
  964. reftjiffy = reftime;
  965. reftjiffy = changeTz(reftjiffy, iata);
  966. reft = reftjiffy.format(pattern: "HHmm");
  967. } else if (reftime is DateTime && iata != null) {
  968. reftjiffy = Jiffy.parseFromDateTime(reftime);
  969. reftjiffy = changeTz(reftjiffy, iata);
  970. reft = reftjiffy.format(pattern: "HHmm");
  971. } else if (reftime is String && reftime.length == 4) {
  972. reft = reftime;
  973. } else if (reftime is String && iata != null) {
  974. reftjiffy = Jiffy.parse(reftime);
  975. reftjiffy = changeTz(reftjiffy, iata);
  976. reft = reftjiffy.format(pattern: "HHmm");
  977. } else {
  978. throw ("_fdpMax type of reftime unrecognized!!!");
  979. }
  980. // print("$iata $reft ${reftjiffy.toUtc().Hm}");
  981. for (String key in tab.keys) {
  982. List<String> intervalle = key.split("-");
  983. int cmph1 = reft.compareTo(intervalle[0]);
  984. int cmph2 = reft.compareTo(intervalle[1]);
  985. bool inint = (intervalle[0].compareTo(intervalle[1]) < 0)
  986. ? (cmph1 >= 0 && cmph2 <= 0)
  987. : ((cmph1 >= 0 || cmph2 <= 0));
  988. if (inint) {
  989. final List<String>? lres = tab[key];
  990. final int nrow = ((sectors == 1) ? 2 : sectors) - 2;
  991. final String? res =
  992. (lres != null && nrow < lres.length) ? lres[nrow] : null;
  993. if (res != null && res != "Not allowed") {
  994. return Duration(
  995. hours: int.parse(res.split(".")[0]),
  996. minutes: int.parse(res.split(".")[1]));
  997. }
  998. }
  999. }
  1000. return Duration.zero;
  1001. }
  1002. Duration fdpMaxBasic(
  1003. {required reftime, String? iata, required int sectors}) =>
  1004. _fdpMax(reftime: reftime, iata: iata, sectors: sectors, tab: _fdpMaxTab);
  1005. Duration fdpMaxExtBasic(
  1006. {required reftime, String? iata, required int sectors}) =>
  1007. _fdpMax(
  1008. reftime: reftime, iata: iata, sectors: sectors, tab: _fdpMaxExtTab);
  1009. Duration fdpMaxUnk({required reftime, String? iata, required int sectors}) =>
  1010. _fdpMax(reftime: reftime, iata: iata, sectors: sectors, tab: _fdpUnkTab);
  1011. Duration fdpMaxUnkFrms(
  1012. {required reftime, String? iata, required int sectors}) =>
  1013. _fdpMax(
  1014. reftime: reftime, iata: iata, sectors: sectors, tab: _fdpUnkFrmsTab);
  1015. //acclim
  1016. final Map<String, List<String>> _fdpMaxTab = {
  1017. "0600-1329": [
  1018. "13.00",
  1019. "12.30",
  1020. "12.00",
  1021. "11.30",
  1022. "11.00",
  1023. "10.30",
  1024. "10.00",
  1025. "09.30",
  1026. "09.00",
  1027. ],
  1028. "1330-1359": [
  1029. "12.45",
  1030. "12.15",
  1031. "11.45",
  1032. "11.15",
  1033. "10.45",
  1034. "10.15",
  1035. "09.45",
  1036. "09.15",
  1037. "09.00",
  1038. ],
  1039. "1400-1429": [
  1040. "12.30",
  1041. "12.00",
  1042. "11.30",
  1043. "11.00",
  1044. "10.30",
  1045. "10.00",
  1046. "09.30",
  1047. "09.00",
  1048. "09.00",
  1049. ],
  1050. "1430-1459": [
  1051. "12.15",
  1052. "11.45",
  1053. "11.15",
  1054. "10.45",
  1055. "10.15",
  1056. "09.45",
  1057. "09.15",
  1058. "09.00",
  1059. "09.00",
  1060. ],
  1061. "1500-1529": [
  1062. "12.00",
  1063. "11.30",
  1064. "11.00",
  1065. "10.30",
  1066. "10.00",
  1067. "09.30",
  1068. "09.00",
  1069. "09.00",
  1070. "09.00",
  1071. ],
  1072. "1530-1559": [
  1073. "11.45",
  1074. "11.15",
  1075. "10.45",
  1076. "10.15",
  1077. "09.45",
  1078. "09.15",
  1079. "09.00",
  1080. "09.00",
  1081. "09.00",
  1082. ],
  1083. "1600-1629": [
  1084. "11.30",
  1085. "11.00",
  1086. "10.30",
  1087. "10.00",
  1088. "09.30",
  1089. "09.00",
  1090. "09.00",
  1091. "09.00",
  1092. "09.00",
  1093. ],
  1094. "1630-1659": [
  1095. "11.15",
  1096. "10.45",
  1097. "10.15",
  1098. "09.45",
  1099. "09.15",
  1100. "09.00",
  1101. "09.00",
  1102. "09.00",
  1103. "09.00",
  1104. ],
  1105. "1700-0459": [
  1106. "11.00",
  1107. "10.30",
  1108. "10.00",
  1109. "09.30",
  1110. "09.00",
  1111. "09.00",
  1112. "09.00",
  1113. "09.00",
  1114. "09.00",
  1115. ],
  1116. "0500-0514": [
  1117. "12.00",
  1118. "11.30",
  1119. "11.00",
  1120. "10.30",
  1121. "10.00",
  1122. "09.30",
  1123. "09.00",
  1124. "09.00",
  1125. "09.00",
  1126. ],
  1127. "0515-0529": [
  1128. "12.15",
  1129. "11.45",
  1130. "11.15",
  1131. "10.45",
  1132. "10.15",
  1133. "09.45",
  1134. "09.15",
  1135. "09.00",
  1136. "09.00",
  1137. ],
  1138. "0530-0544": [
  1139. "12.30",
  1140. "12.00",
  1141. "11.30",
  1142. "11.00",
  1143. "10.30",
  1144. "10.00",
  1145. "09.30",
  1146. "09.00",
  1147. "09.00",
  1148. ],
  1149. "0545-0559": [
  1150. "12.45",
  1151. "12.15",
  1152. "11.45",
  1153. "11.15",
  1154. "10.45",
  1155. "10.15",
  1156. "09.45",
  1157. "09.15",
  1158. "09.00",
  1159. ],
  1160. };
  1161. final Map<String, List<String>> _fdpMaxExtTab = {
  1162. "0600-0614": [
  1163. "Not allowed",
  1164. "Not allowed",
  1165. "Not allowed",
  1166. "Not allowed",
  1167. ],
  1168. "0615-0629": [
  1169. "13.15",
  1170. "12.45",
  1171. "12.15",
  1172. "11.45",
  1173. ],
  1174. "0630-0644": [
  1175. "13.30",
  1176. "13.00",
  1177. "12.30",
  1178. "12.00",
  1179. ],
  1180. "0645-0659": [
  1181. "13.45",
  1182. "13.15",
  1183. "12.45",
  1184. "12.15",
  1185. ],
  1186. "0700-1329": [
  1187. "14.00",
  1188. "13.30",
  1189. "13.00",
  1190. "12.30",
  1191. ],
  1192. "1330-1359": [
  1193. "13.45",
  1194. "13.15",
  1195. "12.45",
  1196. "Not allowed",
  1197. ],
  1198. "1400-1429": [
  1199. "13.30",
  1200. "13.00",
  1201. "12.30",
  1202. "Not allowed",
  1203. ],
  1204. "1430-1459": [
  1205. "13.15",
  1206. "12.45",
  1207. "12.15",
  1208. "Not allowed",
  1209. ],
  1210. "1500-1529": [
  1211. "13.00",
  1212. "12.30",
  1213. "12.00",
  1214. "Not allowed",
  1215. ],
  1216. "1530-1559": [
  1217. "12.45",
  1218. "Not allowed",
  1219. "Not allowed",
  1220. "Not allowed",
  1221. ],
  1222. "1600-1629": [
  1223. "12.30",
  1224. "Not allowed",
  1225. "Not allowed",
  1226. "Not allowed",
  1227. ],
  1228. "1630-1659": [
  1229. "12.15",
  1230. "Not allowed",
  1231. "Not allowed",
  1232. "Not allowed",
  1233. ],
  1234. "1700-1729": [
  1235. "12.00",
  1236. "Not allowed",
  1237. "Not allowed",
  1238. "Not allowed",
  1239. ],
  1240. "1730-1759": [
  1241. "11.45",
  1242. "Not allowed",
  1243. "Not allowed",
  1244. "Not allowed",
  1245. ],
  1246. "1800-1829": [
  1247. "11.30",
  1248. "Not allowed",
  1249. "Not allowed",
  1250. "Not allowed",
  1251. ],
  1252. "1830-1859": [
  1253. "11.15",
  1254. "Not allowed",
  1255. "Not allowed",
  1256. "Not allowed",
  1257. ],
  1258. "1900-0359": [
  1259. "Not allowed",
  1260. "Not allowed",
  1261. "Not allowed",
  1262. "Not allowed",
  1263. ],
  1264. "0400-0414": [
  1265. "Not allowed",
  1266. "Not allowed",
  1267. "Not allowed",
  1268. "Not allowed",
  1269. ],
  1270. "0415-0429": [
  1271. "Not allowed",
  1272. "Not allowed",
  1273. "Not allowed",
  1274. "Not allowed",
  1275. ],
  1276. "0430-0444": [
  1277. "Not allowed",
  1278. "Not allowed",
  1279. "Not allowed",
  1280. "Not allowed",
  1281. ],
  1282. "0445-0459": [
  1283. "Not allowed",
  1284. "Not allowed",
  1285. "Not allowed",
  1286. "Not allowed",
  1287. ],
  1288. "0500-0514": [
  1289. "Not allowed",
  1290. "Not allowed",
  1291. "Not allowed",
  1292. "Not allowed",
  1293. ],
  1294. "0515-0529": [
  1295. "Not allowed",
  1296. "Not allowed",
  1297. "Not allowed",
  1298. "Not allowed",
  1299. ],
  1300. "0530-0544": [
  1301. "Not allowed",
  1302. "Not allowed",
  1303. "Not allowed",
  1304. "Not allowed",
  1305. ],
  1306. "0545-0559": [
  1307. "Not allowed",
  1308. "Not allowed",
  1309. "Not allowed",
  1310. "Not allowed",
  1311. ]
  1312. };
  1313. final Map<String, List<String>> _fdpUnkTab = {
  1314. "0000-2359": ["11.00", "10:30", "10.00", "09.30", "09.00", "09.00", "09.00"]
  1315. };
  1316. final Map<String, List<String>> _fdpUnkFrmsTab = {
  1317. "0000-2359": ["12.00", "11.30", "11.00", "10.30", "10.00", "09.30", "09.00"]
  1318. };
  1319. }
  1320. class FtlDuty {
  1321. FtlDuty({this.start, this.end, this.placeStart, this.placeEnd, this.type});
  1322. @override
  1323. String toString() {
  1324. return "${start?.yMEd} ${start?.Hm} >>> ${end?.Hm} : ${fdpLength.inMinutes > 0 ? "FDP" : "DUTY"} ${dutiesDetailsIndex.length}leg(s)";
  1325. }
  1326. Jiffy? start;
  1327. Jiffy? end;
  1328. DTInterval get interval => DTInterval(start!, end!);
  1329. String? placeStart;
  1330. String? placeEnd;
  1331. FtlDutyType? type;
  1332. List<int> dutiesDetailsIndex = [];
  1333. Jiffy? fdpStart;
  1334. Jiffy? fdpEnd;
  1335. int sectors = 0;
  1336. Duration? fdpMax;
  1337. Duration? fdpExtMax;
  1338. //Duration? fdpIfRExtMax;//if inflight rest rest
  1339. bool fdpExt = false;
  1340. List<FtlLegal> legals = [];
  1341. Duration get dutyLength => (start != null && end != null)
  1342. ? end!.dateTime.difference(start!.dateTime)
  1343. : Duration.zero;
  1344. Duration get fdpLength => (fdpStart != null && fdpEnd != null)
  1345. ? fdpEnd!.dateTime.difference(fdpStart!.dateTime)
  1346. : Duration.zero;
  1347. // Duration? get rest => (dutyLength.max(Ftl.minimumrest(placeEnd)));
  1348. Jiffy? restends;
  1349. String? acclim;
  1350. String? reftime;
  1351. FtlDutyTotal? dutyTotal;
  1352. //Jiffy? report;
  1353. Jiffy? reportdelay1;
  1354. Jiffy? notification1;
  1355. Jiffy? reportdelay2;
  1356. Jiffy? notification2;
  1357. bool earlyStart = false;
  1358. bool lateFinish = false;
  1359. bool nightDuty = false;
  1360. }
  1361. class FtlLegal {
  1362. FtlLegal(
  1363. {required this.legalCause,
  1364. required this.legalCauseMsg,
  1365. this.legalIndex,
  1366. this.condition});
  1367. FtlLegalCause legalCause;
  1368. String legalCauseMsg;
  1369. int? legalIndex;
  1370. String? condition;
  1371. }
  1372. enum FtlDutyType { duty, fdp, other }
  1373. class FtlDutyDetails {
  1374. FtlDutyDetails({
  1375. this.start,
  1376. this.end,
  1377. this.placeStart,
  1378. this.placeEnd,
  1379. this.type,
  1380. this.label,
  1381. });
  1382. Jiffy? start;
  1383. Jiffy? end;
  1384. DTInterval get interval => DTInterval(start!, end!);
  1385. Duration get duration => end!.dateTime.difference(start!.dateTime);
  1386. String? placeStart;
  1387. String? placeEnd;
  1388. FtlDutyDetailsType? type;
  1389. String get typeString {
  1390. switch (type) {
  1391. case FtlDutyDetailsType.flight:
  1392. return "FLT";
  1393. case FtlDutyDetailsType.dhflight:
  1394. return "DH-FLT";
  1395. case FtlDutyDetailsType.dhlimo:
  1396. return "DH-LIMO";
  1397. case FtlDutyDetailsType.sim:
  1398. return "SIM";
  1399. case FtlDutyDetailsType.standby:
  1400. return "STBY";
  1401. case FtlDutyDetailsType.reserve:
  1402. return "RSRV";
  1403. case FtlDutyDetailsType.ground:
  1404. return "GRND";
  1405. default:
  1406. return "UNK";
  1407. }
  1408. }
  1409. String? label;
  1410. }
  1411. enum FtlDutyDetailsType {
  1412. preflight,
  1413. flight,
  1414. postflight,
  1415. dhflight,
  1416. dhlimo,
  1417. ground,
  1418. standby,
  1419. reserve,
  1420. sim,
  1421. // training,
  1422. // other
  1423. }
  1424. class FtlDutyTotal {
  1425. FtlDutyTotal(
  1426. {required this.date, required this.dutyLength, required this.fltLength});
  1427. Jiffy date;
  1428. Duration dutyLength;
  1429. Duration fltLength;
  1430. Duration? dutylast7;
  1431. Duration? dutylast14;
  1432. Duration? dutylast28;
  1433. Duration? fltlast28;
  1434. Duration? fltyear;
  1435. Duration? fltlast12;
  1436. }
  1437. enum FtlLegalCause {
  1438. dutylast7,
  1439. dutylast14,
  1440. dutylast28,
  1441. fltlast28,
  1442. fltyear,
  1443. fltlast12,
  1444. fdpmax,
  1445. restfdp,
  1446. restrecurrent,
  1447. disruptive
  1448. //check duty length && flt length
  1449. //check fdp leength
  1450. //check rest before fdp
  1451. //check recurrent rest
  1452. //check disruptive sched
  1453. }