@osfarm/itineraire-technique 1.1.8 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/js/chart-render.js +240 -184
  2. package/package.json +1 -1
@@ -39,17 +39,35 @@ class RotationRenderer {
39
39
  }
40
40
 
41
41
  fixRotationData(rotationData) {
42
+ if (!rotationData || typeof rotationData !== 'object') return rotationData;
43
+
42
44
  if (rotationData.options == undefined)
43
45
  rotationData.options = {};
44
46
 
45
47
  if (rotationData.options?.view == undefined || rotationData?.options?.view == '')
46
48
  rotationData.options.view = 'horizontal';
47
49
 
50
+ // Safety check for steps array
51
+ if (!rotationData.steps || !Array.isArray(rotationData.steps)) {
52
+ rotationData.steps = [];
53
+ return rotationData;
54
+ }
55
+
48
56
  // Map rotationData items to make sure that the startDate and endDate are proper Date objects
49
57
  rotationData.steps.map((item) => {
58
+ if (!item) return item;
59
+
50
60
  item.startDate = new Date(item.startDate);
51
61
  item.endDate = new Date(item.endDate);
52
62
 
63
+ // Safety check for invalid dates
64
+ if (isNaN(item.startDate.getTime())) {
65
+ item.startDate = new Date();
66
+ }
67
+ if (isNaN(item.endDate.getTime()) || item.endDate <= item.startDate) {
68
+ item.endDate = new Date(item.startDate.getTime() + (30 * 24 * 60 * 60 * 1000)); // Default to 30 days later
69
+ }
70
+
53
71
  // Add a duration in months
54
72
  item.duration = Math.round((item.endDate - item.startDate) / (30 * 1000 * 60 * 60 * 24));
55
73
 
@@ -340,9 +358,14 @@ class RotationRenderer {
340
358
 
341
359
  // Avoid words that are too long
342
360
  testWidth = echarts.format.getTextRect(line).width;
343
- while (testWidth > maxWidth) {
361
+ let trimCount = 0;
362
+ const maxTrimCount = line.length; // Prevent infinite loop
363
+
364
+ while (testWidth > maxWidth && line.length > 0 && trimCount < maxTrimCount) {
344
365
  line = line.slice(0, -1);
366
+ if (line.length === 0) break; // Safety check
345
367
  testWidth = echarts.format.getTextRect(line).width;
368
+ trimCount++;
346
369
  }
347
370
  }
348
371
 
@@ -376,101 +399,221 @@ class RotationRenderer {
376
399
  let maxXPositions = new Map();
377
400
 
378
401
  function renderItem(params, api) {
402
+ // Safety checks to prevent crashes
403
+ if (!params || !api) return null;
404
+
405
+ try {
406
+ var categoryIndex = api.value(0);
407
+ var start = api.coord([api.value(1), categoryIndex]);
408
+ var end = api.coord([api.value(2), categoryIndex]);
409
+ var name = api.value(3);
410
+ var type = api.value(4);
411
+ let secondary_crop = api.value(5);
412
+ let bHasSecondaryCrops = api.value(6);
413
+
414
+ // Safety check for invalid coordinates
415
+ if (!start || !end || start.length < 2 || end.length < 2) return null;
416
+ if (isNaN(start[0]) || isNaN(start[1]) || isNaN(end[0]) || isNaN(end[1])) return null;
417
+
418
+ const x = start[0];
419
+ let y = start[1];
420
+
421
+ const style = api.style();
422
+ style.opacity = 0.5;
423
+
424
+ if (params.context.rendered == undefined) {
425
+ // Start of a new rendering round
426
+ maxXPositions = new Map();
427
+
428
+ for (let catIndex = 0; catIndex < 3; catIndex++) {
429
+ for (let track = 0; track < 3; track++) {
430
+ maxXPositions.set('track_left_' + catIndex + '_' + track, params.coordSys.width);
431
+ }
432
+ }
433
+ }
379
434
 
380
- var categoryIndex = api.value(0);
381
- var start = api.coord([api.value(1), categoryIndex]);
382
- var end = api.coord([api.value(2), categoryIndex]);
383
- var name = api.value(3);
384
- var type = api.value(4);
385
- let secondary_crop = api.value(5);
386
- let bHasSecondaryCrops = api.value(6);
435
+ params.context.rendered = true;
387
436
 
388
- const x = start[0];
389
- let y = start[1];
437
+ // Remove the default emphasis style
438
+ api.styleEmphasis({});
390
439
 
391
- const style = api.style();
392
- style.opacity = 0.5;
440
+ if (type == 'rotation_item') {
393
441
 
394
- if (params.context.rendered == undefined) {
395
- // Start of a new rendering round
396
- maxXPositions = new Map();
442
+ // start[0] // abscisse gauche de l'élément (après zoom)
443
+ // start[1] // ordonnée gauche de l'élément
444
+ // end[0] // abscisse droite de l'élément (après zoom)
445
+ // end[1] // ordonnée droite de l'élément
446
+ // height // Hauteur de l'élément
397
447
 
398
- for (let catIndex = 0; catIndex < 3; catIndex++) {
399
- for (let track = 0; track < 3; track++) {
400
- maxXPositions.set('track_left_' + catIndex + '_' + track, params.coordSys.width);
448
+ // params.coordSys.x // début du canva
449
+ // params.coordSys.y // début du canva
450
+ // params.coordSys.width, // largeur du canva
451
+ // params.coordSys.height // hauteur du canva
452
+
453
+ let height = self.barHeight - 20; // 20 px margin top and bottom
454
+ let top = y - height / 2;
455
+ let textXMargin = 2;
456
+ let textYMargin = 10;
457
+
458
+ if (bHasSecondaryCrops) {
459
+ height = self.barHeight - 40; // 20 px margin top and bottom
460
+ top = y - height / 2 - 15;
461
+ textXMargin = 2;
462
+ textYMargin = 10;
401
463
  }
402
- }
403
- }
404
464
 
405
- params.context.rendered = true;
465
+ if (secondary_crop) {
466
+ // Move secondary crops a bit down and reduce their size
467
+ top = top + height + 5;
468
+ height = height / 3;
469
+ textXMargin = 5;
470
+ textYMargin = 5;
471
+ }
406
472
 
407
- // Remove the default emphasis style
408
- api.styleEmphasis({});
473
+ const arrowWidth = height / 3;
474
+ const border = 3;
409
475
 
410
- if (type == 'rotation_item') {
476
+ var points = [
477
+ [x, top],
478
+ [end[0] - border, top],
479
+ [end[0] + arrowWidth - border, top + height / 2],
480
+ [end[0] - border, top + height],
481
+ [x, top + height],
482
+ [x + arrowWidth, top + height / 2],
483
+ ];
411
484
 
412
- // start[0] // abscisse gauche de l'élément (après zoom)
413
- // start[1] // ordonnée gauche de l'élément
414
- // end[0] // abscisse droite de l'élément (après zoom)
415
- // end[1] // ordonnée droite de l'élément
416
- // height // Hauteur de l'élément
485
+ //const itemLabelWidth = echarts.format.getTextRect(name).width + textMargin * 2;
486
+ const itemWidth = end[0] - x;
417
487
 
418
- // params.coordSys.x // début du canva
419
- // params.coordSys.y // début du canva
420
- // params.coordSys.width, // largeur du canva
421
- // params.coordSys.height // hauteur du canva
488
+ // if (itemLabelWidth > itemWidth)
489
+ // name = ''; // Hide the label as we won't have the room to show it
422
490
 
423
- let height = self.barHeight - 20; // 20 px margin top and bottom
424
- let top = y - height / 2;
425
- let textXMargin = 2;
426
- let textYMargin = 10;
491
+ name = wrapText(echarts, name, itemWidth - arrowWidth, height);
427
492
 
428
- if (bHasSecondaryCrops) {
429
- height = self.barHeight - 40; // 20 px margin top and bottom
430
- top = y - height / 2 - 15;
431
- textXMargin = 2;
432
- textYMargin = 10;
433
- }
493
+ // See this for clip regions : https://stackoverflow.com/questions/71735038/setting-border-and-label-in-custom-apache-echarts
494
+ // https://stackoverflow.com/questions/73653691/how-to-draw-a-custom-triangle-in-renderitem-in-apache-echarts
434
495
 
435
- if (secondary_crop) {
436
- // Move secondary crops a bit down and reduce their size
437
- top = top + height + 5;
438
- height = height / 3;
439
- textXMargin = 5;
440
- textYMargin = 5;
496
+ return (
497
+ {
498
+ type: 'polygon',
499
+ transition: ['shape'],
500
+ shape: {
501
+ points: points
502
+ },
503
+ style: style,
504
+ emphasis: {
505
+ style: {
506
+ shadowBlur: 4,
507
+ shadowOffsetX: 1,
508
+ shadowOffsetY: 2,
509
+ shadowColor: 'rgba(0, 0, 0, 0.2)'
510
+ },
511
+ },
512
+ textConfig: {
513
+ position: [arrowWidth + textXMargin, textYMargin]
514
+ },
515
+ textContent: {
516
+ style: {
517
+ text: name,
518
+ fill: '#000',
519
+ width: 80,
520
+ fontWeight: 'bold'
521
+ }
522
+ }
523
+ }
524
+ );
441
525
  }
442
526
 
443
- const arrowWidth = height / 3;
444
- const border = 3;
527
+ if (type == 'intervention_bottom' || type == 'intervention_top') {
528
+
529
+ const height = 20;
530
+ const margin = 10;
531
+ const textMargin = 5;
445
532
 
446
- var points = [
447
- [x, top],
448
- [end[0] - border, top],
449
- [end[0] + arrowWidth - border, top + height / 2],
450
- [end[0] - border, top + height],
451
- [x, top + height],
452
- [x + arrowWidth, top + height / 2],
453
- ];
533
+ // Maintain a list of max x for each row. If the max x is further right than the label we try to push,
534
+ // use another row. If for all rows the space is taken, just drop this item
454
535
 
455
- //const itemLabelWidth = echarts.format.getTextRect(name).width + textMargin * 2;
456
- const itemWidth = end[0] - x;
536
+ let trackToUse = null;
537
+ const itemLabelWidth = echarts.format.getTextRect(name).width + textMargin * 2;
457
538
 
458
- // if (itemLabelWidth > itemWidth)
459
- // name = ''; // Hide the label as we won't have the room to show it
539
+ for (let track = 0; track < 3; track++) {
540
+ if (!maxXPositions.has('track_right_' + categoryIndex + '_' + track)) {
541
+ // Situation where the track is empty
542
+ trackToUse = track;
543
+ break;
544
+ }
460
545
 
461
- name = wrapText(echarts, name, itemWidth - arrowWidth, height);
546
+ let trackLeft = maxXPositions.get('track_left_' + categoryIndex + '_' + track);
547
+ if (trackLeft > (x + itemLabelWidth)) {
548
+ // Situation where the drawing has started right of the current element
549
+ trackToUse = track;
550
+ break;
551
+ }
462
552
 
463
- // See this for clip regions : https://stackoverflow.com/questions/71735038/setting-border-and-label-in-custom-apache-echarts
464
- // https://stackoverflow.com/questions/73653691/how-to-draw-a-custom-triangle-in-renderitem-in-apache-echarts
553
+ let trackRight = maxXPositions.get('track_right_' + categoryIndex + '_' + track);
554
+ if (trackRight < x) {
555
+ // Situation where the last painted element is sufficiently far on the left
556
+ trackToUse = track;
557
+ break;
558
+ }
559
+ }
465
560
 
466
- return (
467
- {
561
+ if (trackToUse == null)
562
+ return null;
563
+
564
+ let currentRight = maxXPositions.get('track_right_' + categoryIndex + '_' + trackToUse);
565
+ if (currentRight == undefined || currentRight < x + itemLabelWidth)
566
+ maxXPositions.set('track_right_' + categoryIndex + '_' + trackToUse, x + itemLabelWidth);
567
+
568
+ let currentLeft = maxXPositions.get('track_left_' + categoryIndex + '_' + trackToUse);
569
+ if (currentLeft > x)
570
+ maxXPositions.set('track_left_' + categoryIndex + '_' + trackToUse, x);
571
+
572
+ // A nicer solution could be to draw large items in a reduced format until there is enough space for
573
+ // drawing them fully. Unfortunately that would require a two pass drawing which does not exist with Echarts ?
574
+
575
+ const arrowWidth = 3;
576
+
577
+ let arrowTop = y + 55;
578
+ let arrowBottom = y - 55;
579
+
580
+ y = margin + y + trackToUse * (height + margin) - (self.barHeight / 2);
581
+
582
+ var points = [];
583
+ var textPosition = [];
584
+
585
+ if (type == 'intervention_top') {
586
+ textPosition = [textMargin, textMargin];
587
+ points = [
588
+ [x, y],
589
+ [x + itemLabelWidth, y],
590
+ [x + itemLabelWidth, y + height],
591
+ [x + arrowWidth, y + height],
592
+ [x, arrowTop]
593
+ ];
594
+ }
595
+ else {
596
+ textPosition = [textMargin, textMargin + y - arrowBottom];
597
+ points = [
598
+ [x, arrowBottom],
599
+ [x + arrowWidth, y],
600
+ [x + itemLabelWidth, y],
601
+ [x + itemLabelWidth, y + height],
602
+ [x, y + height]
603
+ ];
604
+ }
605
+
606
+ return ({
468
607
  type: 'polygon',
469
608
  transition: ['shape'],
470
609
  shape: {
471
610
  points: points
472
611
  },
473
- style: style,
612
+ style: api.style({
613
+ fill: style.fill,
614
+ stroke: style.fill,
615
+ textFill: '#000',
616
+ }),
474
617
  emphasis: {
475
618
  style: {
476
619
  shadowBlur: 4,
@@ -480,130 +623,21 @@ class RotationRenderer {
480
623
  },
481
624
  },
482
625
  textConfig: {
483
- position: [arrowWidth + textXMargin, textYMargin]
626
+ position: textPosition
484
627
  },
485
628
  textContent: {
486
629
  style: {
487
630
  text: name,
488
631
  fill: '#000',
489
- width: 80,
490
- fontWeight: 'bold'
491
632
  }
492
633
  }
493
634
  }
494
- );
495
- }
496
-
497
- if (type == 'intervention_bottom' || type == 'intervention_top') {
498
-
499
- const height = 20;
500
- const margin = 10;
501
- const textMargin = 5;
502
-
503
- // Maintain a list of max x for each row. If the max x is further right than the label we try to push,
504
- // use another row. If for all rows the space is taken, just drop this item
505
-
506
- let trackToUse = null;
507
- const itemLabelWidth = echarts.format.getTextRect(name).width + textMargin * 2;
508
-
509
- for (let track = 0; track < 3; track++) {
510
- if (!maxXPositions.has('track_right_' + categoryIndex + '_' + track)) {
511
- // Situation where the track is empty
512
- trackToUse = track;
513
- break;
514
- }
635
+ );
515
636
 
516
- let trackLeft = maxXPositions.get('track_left_' + categoryIndex + '_' + track);
517
- if (trackLeft > (x + itemLabelWidth)) {
518
- // Situation where the drawing has started right of the current element
519
- trackToUse = track;
520
- break;
521
- }
522
-
523
- let trackRight = maxXPositions.get('track_right_' + categoryIndex + '_' + track);
524
- if (trackRight < x) {
525
- // Situation where the last painted element is sufficiently far on the left
526
- trackToUse = track;
527
- break;
528
- }
529
637
  }
530
-
531
- if (trackToUse == null)
532
- return null;
533
-
534
- let currentRight = maxXPositions.get('track_right_' + categoryIndex + '_' + trackToUse);
535
- if (currentRight == undefined || currentRight < x + itemLabelWidth)
536
- maxXPositions.set('track_right_' + categoryIndex + '_' + trackToUse, x + itemLabelWidth);
537
-
538
- let currentLeft = maxXPositions.get('track_left_' + categoryIndex + '_' + trackToUse);
539
- if (currentLeft > x)
540
- maxXPositions.set('track_left_' + categoryIndex + '_' + trackToUse, x);
541
-
542
- // A nicer solution could be to draw large items in a reduced format until there is enough space for
543
- // drawing them fully. Unfortunately that would require a two pass drawing which does not exist with Echarts ?
544
-
545
- const arrowWidth = 3;
546
-
547
- let arrowTop = y + 55;
548
- let arrowBottom = y - 55;
549
-
550
- y = margin + y + trackToUse * (height + margin) - (self.barHeight / 2);
551
-
552
- var points = [];
553
- var textPosition = [];
554
-
555
- if (type == 'intervention_top') {
556
- textPosition = [textMargin, textMargin];
557
- points = [
558
- [x, y],
559
- [x + itemLabelWidth, y],
560
- [x + itemLabelWidth, y + height],
561
- [x + arrowWidth, y + height],
562
- [x, arrowTop]
563
- ];
564
- }
565
- else {
566
- textPosition = [textMargin, textMargin + y - arrowBottom];
567
- points = [
568
- [x, arrowBottom],
569
- [x + arrowWidth, y],
570
- [x + itemLabelWidth, y],
571
- [x + itemLabelWidth, y + height],
572
- [x, y + height]
573
- ];
574
- }
575
-
576
- return ({
577
- type: 'polygon',
578
- transition: ['shape'],
579
- shape: {
580
- points: points
581
- },
582
- style: api.style({
583
- fill: style.fill,
584
- stroke: style.fill,
585
- textFill: '#000',
586
- }),
587
- emphasis: {
588
- style: {
589
- shadowBlur: 4,
590
- shadowOffsetX: 1,
591
- shadowOffsetY: 2,
592
- shadowColor: 'rgba(0, 0, 0, 0.2)'
593
- },
594
- },
595
- textConfig: {
596
- position: textPosition
597
- },
598
- textContent: {
599
- style: {
600
- text: name,
601
- fill: '#000',
602
- }
603
- }
604
- }
605
- );
606
-
638
+ } catch (error) {
639
+ console.error('Error in renderItem:', error);
640
+ return null;
607
641
  }
608
642
  }
609
643
 
@@ -823,8 +857,29 @@ class RotationRenderer {
823
857
  ];
824
858
 
825
859
  let monthsPerYear = new Map();
860
+
861
+ // Safety check to prevent infinite loops
862
+ if (!steps || steps.length === 0) {
863
+ series.push(months);
864
+ return series;
865
+ }
866
+
826
867
  let startMonth = new Date(steps.at(0).startDate.valueOf());
827
- while (startMonth < steps.at(-1).endDate) {
868
+ let endDate = steps.at(-1).endDate;
869
+
870
+ // Safety check for valid dates and reasonable duration (max 20 years)
871
+ if (isNaN(startMonth.getTime()) ||
872
+ isNaN(endDate.getTime()) ||
873
+ startMonth >= endDate ||
874
+ (endDate - startMonth) > (20 * 365 * 24 * 60 * 60 * 1000)) {
875
+ series.push(months);
876
+ return series;
877
+ }
878
+
879
+ let loopCount = 0;
880
+ const maxLoops = 240; // Max 20 years * 12 months
881
+
882
+ while (startMonth < endDate && loopCount < maxLoops) {
828
883
  const monthName = startMonth.toLocaleDateString(undefined, { month: 'short' });
829
884
  const year = startMonth.getFullYear();
830
885
 
@@ -843,6 +898,7 @@ class RotationRenderer {
843
898
 
844
899
  // increment the current month
845
900
  startMonth.setMonth(startMonth.getMonth() + 1);
901
+ loopCount++;
846
902
  }
847
903
 
848
904
  series.push(months);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfarm/itineraire-technique",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "A visualisation tool to show agricultural technical itineraries based on Echarts",
5
5
  "main": "editor.html",
6
6
  "scripts": {