Selaa lähdekoodia

Merge branch 'master' into develop

poohr 3 viikkoa sitten
vanhempi
commit
c474cce848

+ 617
- 644
src/app/pages/danka-detail/danka-detail.html
File diff suppressed because it is too large
Näytä tiedosto


+ 40
- 0
src/app/pages/danka-detail/danka-detail.scss Näytä tiedosto

@@ -1264,3 +1264,43 @@
1264 1264
     bottom: -22px;
1265 1265
   }
1266 1266
 }
1267
+
1268
+.family-tree-svg-container {
1269
+  width: 100%;
1270
+  height: 700px;
1271
+
1272
+  overflow: auto;
1273
+
1274
+  border: 1px solid #ddd;
1275
+}
1276
+.family-tree-svg-container {
1277
+
1278
+  width: 100%;
1279
+
1280
+  height: 700px;
1281
+
1282
+  overflow: hidden;
1283
+
1284
+  border: 1px solid #ddd;
1285
+}
1286
+.family-node {
1287
+
1288
+  cursor: pointer;
1289
+
1290
+}
1291
+
1292
+.family-text {
1293
+
1294
+  cursor: pointer;
1295
+
1296
+}
1297
+
1298
+.family-tree-svg-container svg {
1299
+  width: 100%;
1300
+  height: 100%;
1301
+}
1302
+
1303
+.family-text {
1304
+  writing-mode: vertical-rl;
1305
+  text-orientation: upright;
1306
+}

+ 385
- 1
src/app/pages/danka-detail/danka-detail.ts Näytä tiedosto

@@ -1,4 +1,9 @@
1 1
 import { Component } from '@angular/core';
2
+import {
3
+  ElementRef,
4
+  ViewChild,
5
+  AfterViewInit
6
+} from '@angular/core';
2 7
 import { ActivatedRoute, RouterLink } from '@angular/router';
3 8
 import { DankaService } from '../../services/dankaService';
4 9
 import { FamilyService } from '../../services/family-service';
@@ -12,6 +17,21 @@ import { AppSideMenu } from '../../share/side-menu/app-side-menu';
12 17
 import { MarriageRelationService } from '../../services/marriage-relation-service';
13 18
 import { FormsModule } from '@angular/forms';
14 19
 import { EventStatus, EventTarget, EventType } from '../../models/event';
20
+import {
21
+  FamilyTreeBuilderService,
22
+  FamilyTreeNode
23
+} from '../../services/family-tree-builder';
24
+import {
25
+  FamilyTreeLayoutService
26
+} from '../../services/family-tree-layout';
27
+
28
+import {
29
+  LayoutNode
30
+} from '../../models/layoutnode';
31
+import svgPanZoom from 'svg-pan-zoom';
32
+import { FamilyUnitLayout } from '../../models/family-unit-layout';
33
+import { FamilyUnitLayoutService } from '../../services/family-unit-layout';
34
+
15 35
 
16 36
 interface NextMemorial {
17 37
   name: string;
@@ -26,7 +46,7 @@ interface NextMemorial {
26 46
   templateUrl: './danka-detail.html',
27 47
   styleUrl: './danka-detail.scss',
28 48
 })
29
-export class DankaDetail {
49
+export class DankaDetail implements AfterViewInit {
30 50
   danka: Danka | undefined;
31 51
   families: Family[] = [];
32 52
   kakocholist: Kakocho[] = [];
@@ -37,16 +57,39 @@ export class DankaDetail {
37 57
   eventSearchKeyword = '';
38 58
   eventStatuses: EventStatus[] = ['未案内', '案内済'];
39 59
   private eventStatusByTargetId: Record<string, EventStatus> = {};
60
+  treeNodes: FamilyTreeNode[] = [];
61
+  layoutNodes: LayoutNode[] = [];
62
+  layoutNodeMap = new Map<string, LayoutNode>();
63
+  unitLayouts: FamilyUnitLayout[] = [];
64
+  unitLayoutMap = new Map<string, FamilyUnitLayout>();
65
+  deathDateMap = new Map<string, Kakocho>();
66
+  private kakochoByNameMap = new Map<string, Kakocho>();
40 67
 
41 68
   selectedTab: 'basic' | 'family' | 'kakocho' | 'event' | 'familyTree' = 'basic';
42 69
   selectedFamily: Family | undefined = undefined;
43 70
 
71
+  @ViewChild('familyTreeSvg')
72
+  familyTreeSvg?: ElementRef<SVGSVGElement>;
73
+  private panZoomInstance: any;
74
+
75
+  readonly PERSON_WIDTH = 90;
76
+  readonly PERSON_HEIGHT = 140;
77
+  readonly SPOUSE_GAP = 30;
78
+
79
+  viewBox = '0 0 6000 6000';
80
+  readonly DEATH_FONT_SIZE = 10;
81
+  readonly DEATH_LINE_HEIGHT = 10; // 少し余白込み
82
+
83
+
44 84
   constructor(
45 85
     private dankaService: DankaService,
46 86
     private familyService: FamilyService,
47 87
     private kakochoService: KakochoService,
48 88
     private marriageRelationService: MarriageRelationService,
49 89
     private route: ActivatedRoute,
90
+    private familyTreeBuilder: FamilyTreeBuilderService,
91
+    private familyTreeLayout: FamilyTreeLayoutService,
92
+    private familyUnitLayout: FamilyUnitLayoutService,
50 93
   ) {
51 94
     const tab = this.route.snapshot.queryParams['tab'];
52 95
     if (tab === 'family') {
@@ -67,7 +110,121 @@ export class DankaDetail {
67 110
       this.selectedFamily = this.families[0];
68 111
       this.kakocholist = this.kakochoService.getKakochoByDankaId(id);
69 112
       this.nextMemorial = this.getNextMemorial();
113
+
114
+      this.treeNodes =
115
+        this.familyTreeBuilder.build(
116
+          this.families,
117
+          this.marriageRelations
118
+        );
119
+
120
+      const units =
121
+        this.familyTreeBuilder.buildFamilyUnits(
122
+          this.treeNodes
123
+        );
124
+
125
+      const unitTree =
126
+        this.familyTreeBuilder.buildFamilyUnitTree(
127
+          units
128
+        );
129
+
130
+      const unitRoots =
131
+        this.familyTreeBuilder.getUnitRoots(
132
+          unitTree
133
+        );
134
+
135
+      this.unitLayouts =
136
+        this.familyUnitLayout.buildLayout(
137
+          unitRoots
138
+        );
139
+
140
+      const roots =
141
+        this.familyTreeBuilder.getRoots(
142
+          this.treeNodes
143
+        );
144
+
145
+      this.layoutNodes =
146
+        this.familyTreeLayout.buildLayout(
147
+          roots
148
+        );
149
+
150
+      this.rebuildLayoutNodeMap();
151
+
152
+      this.unitLayouts =
153
+        this.familyUnitLayout.buildLayout(
154
+          unitRoots
155
+        );
156
+
157
+      this.rebuildUnitLayoutMap();
158
+
159
+      this.calculateViewBox();
160
+
161
+      this.kakocholist.forEach(kakocho => {
162
+        if (kakocho.familyId) {
163
+          this.deathDateMap.set(
164
+            kakocho.familyId,
165
+            kakocho
166
+          );
167
+        }
168
+      });
169
+
170
+      this.kakocholist.forEach(k => {
171
+        const key = this.normalizeName(k.name) + '_' + k.dankaId;
172
+        this.kakochoByNameMap.set(key, k);
173
+      });
174
+
175
+    }
176
+  }
177
+
178
+  ngAfterViewInit(): void {
179
+    if (!this.familyTreeSvg) {
180
+      return;
70 181
     }
182
+
183
+    this.panZoomInstance = svgPanZoom(
184
+      this.familyTreeSvg.nativeElement,
185
+      {
186
+        fit: true,
187
+        center: true,
188
+        zoomEnabled: true,
189
+        panEnabled: true,
190
+        minZoom: 0.1,
191
+        maxZoom: 50
192
+      }
193
+    );
194
+  }
195
+
196
+  initializePanZoom(): void {
197
+
198
+    if (!this.familyTreeSvg) {
199
+      console.log('SVGなし');
200
+      return;
201
+    }
202
+
203
+    console.log('SVGあり');
204
+
205
+    if (!this.familyTreeSvg) {
206
+      return;
207
+    }
208
+
209
+    if (this.panZoomInstance) {
210
+      return;
211
+    }
212
+
213
+    this.panZoomInstance = svgPanZoom(
214
+      this.familyTreeSvg.nativeElement,
215
+      {
216
+        zoomEnabled: true,
217
+        panEnabled: true,
218
+        fit: true,
219
+        center: true,
220
+      }
221
+    );
222
+  }
223
+  openFamilyTreeTab(): void {
224
+    this.selectedTab = 'familyTree';
225
+    setTimeout(() => {
226
+      this.initializePanZoom();
227
+    });
71 228
   }
72 229
 
73 230
   getAge(birthDate: string): string {
@@ -388,4 +545,231 @@ export class DankaDetail {
388 545
     }
389 546
     return '不明';
390 547
   }
548
+
549
+  getLayoutNode(
550
+    familyId: string
551
+  ): LayoutNode | undefined {
552
+    return this.layoutNodeMap.get(
553
+      familyId
554
+    );
555
+
556
+  }
557
+  getCurrentMarriageLines() {
558
+    return this.marriageRelations.filter(
559
+      x => x.status === 'current'
560
+    );
561
+
562
+  }
563
+
564
+  private rebuildLayoutNodeMap(): void {
565
+    this.layoutNodeMap.clear();
566
+    this.layoutNodes.forEach(node => {
567
+      this.layoutNodeMap.set(
568
+        node.familyId,
569
+        node
570
+      );
571
+    });
572
+  }
573
+
574
+  private rebuildUnitLayoutMap(): void {
575
+    this.unitLayoutMap.clear();
576
+    this.unitLayouts.forEach(layout => {
577
+      this.unitLayoutMap.set(
578
+        layout.node.unit.id,
579
+        layout
580
+      );
581
+    });
582
+  }
583
+
584
+  getUnitLayout(
585
+    unitId: string
586
+  ): FamilyUnitLayout | undefined {
587
+    return this.unitLayoutMap.get(
588
+      unitId
589
+    );
590
+
591
+  }
592
+
593
+  getCenterX(layout: FamilyUnitLayout): number {
594
+    return (
595
+      layout.x +
596
+      this.PERSON_WIDTH +
597
+      this.SPOUSE_GAP / 2
598
+    );
599
+
600
+  }
601
+
602
+  getTopCenterY(
603
+    layout: FamilyUnitLayout
604
+  ): number {
605
+    return layout.y;
606
+  }
607
+
608
+  getBottomCenterY(
609
+    layout: FamilyUnitLayout
610
+  ): number {
611
+    return (
612
+      layout.y +
613
+      this.PERSON_HEIGHT
614
+    );
615
+  }
616
+
617
+  getMiddleY(
618
+    parent: FamilyUnitLayout,
619
+    child: FamilyUnitLayout
620
+  ): number {
621
+
622
+    return (
623
+      this.getBottomCenterY(parent)
624
+      +
625
+      child.y
626
+    ) / 2;
627
+
628
+  }
629
+
630
+  getHusbandX(
631
+    layout: FamilyUnitLayout
632
+  ): number {
633
+
634
+    if (!layout.node.unit.wife) {
635
+      return (
636
+        layout.x +
637
+        (this.PERSON_WIDTH + this.SPOUSE_GAP) / 2
638
+      );
639
+    }
640
+
641
+    return layout.x;
642
+
643
+  }
644
+
645
+  getWifeX(
646
+    layout: FamilyUnitLayout
647
+  ): number {
648
+
649
+    if (!layout.node.unit.husband) {
650
+      return (
651
+        layout.x +
652
+        (this.PERSON_WIDTH + this.SPOUSE_GAP) / 2
653
+      );
654
+    }
655
+
656
+    return (
657
+      layout.x +
658
+      this.PERSON_WIDTH +
659
+      this.SPOUSE_GAP
660
+    );
661
+
662
+  }
663
+
664
+  getHusbandTextX(
665
+    layout: FamilyUnitLayout
666
+  ): number {
667
+    return (
668
+      this.getHusbandX(layout)
669
+      + this.PERSON_WIDTH / 2
670
+    );
671
+
672
+  }
673
+
674
+  getWifeTextX(
675
+    layout: FamilyUnitLayout
676
+  ): number {
677
+    return (
678
+      this.getWifeX(layout)
679
+      + this.PERSON_WIDTH / 2
680
+    );
681
+  }
682
+
683
+  private calculateViewBox(): void {
684
+
685
+    if (this.unitLayouts.length === 0) {
686
+      return;
687
+    }
688
+
689
+    const minX = Math.min(
690
+      ...this.unitLayouts.map(x => x.x)
691
+    );
692
+
693
+    const minY = Math.min(
694
+      ...this.unitLayouts.map(x => x.y)
695
+    );
696
+
697
+    const maxX = Math.max(
698
+      ...this.unitLayouts.map(
699
+        x => x.x + this.PERSON_WIDTH * 2 + this.SPOUSE_GAP
700
+      )
701
+    );
702
+
703
+    const maxY = Math.max(
704
+      ...this.unitLayouts.map(
705
+        x => x.y + this.PERSON_HEIGHT
706
+      )
707
+    );
708
+
709
+    const padding = 100;
710
+
711
+    this.viewBox =
712
+      `${minX - padding}
713
+     ${minY - padding}
714
+     ${maxX - minX + padding * 2}
715
+     ${maxY - minY + padding * 2}`;
716
+  }
717
+
718
+  getKakochoByFamilyId(
719
+    familyId: string
720
+  ): Kakocho | undefined {
721
+    return this.deathDateMap.get(
722
+      familyId
723
+    );
724
+  }
725
+
726
+  getKakochoByFamily(family: Family): Kakocho | undefined {
727
+    const key =
728
+      this.normalizeName(family.name) + '_' + family.dankaId;
729
+    return this.kakochoByNameMap.get(key);
730
+  }
731
+
732
+  getDeathWareki(family: Family): string {
733
+    const kakocho = this.getKakochoByFamily(family);
734
+    if (!kakocho?.deathDate) return '';
735
+    const date = new Date(kakocho.deathDate);
736
+    const wareki = new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
737
+      era: 'long',
738
+      year: 'numeric',
739
+    }).format(date);
740
+
741
+    return `${wareki}${date.getMonth() + 1}月${date.getDate()}日没`;
742
+  }
743
+
744
+  getAgeAtDeathText(family: Family): string {
745
+    const kakocho = this.getKakochoByFamily(family);
746
+    if (!kakocho?.ageAtDeath) return '';
747
+    return `享年${kakocho.ageAtDeath}歳`;
748
+  }
749
+
750
+  getDeathDate(family: Family): string {
751
+    const kakocho = this.getKakochoByFamily(family);
752
+    return kakocho?.deathDate ?? '';
753
+  }
754
+
755
+  /**
756
+   * 縦書きテキストの高さを概算する
757
+   */
758
+  getVerticalTextHeight(text: string | null | undefined): number {
759
+    if (!text) return 0;
760
+    // 縦書きは基本「1文字=1行」
761
+    return text.length * this.DEATH_LINE_HEIGHT;
762
+  }
763
+
764
+  /**
765
+   * テキストをカード下に揃えるためのY座標
766
+   */
767
+  getDeathTextY(layout: FamilyUnitLayout, text: string | null | undefined): number {
768
+    const height = this.getVerticalTextHeight(text);
769
+    return (
770
+      layout.y +
771
+      this.PERSON_HEIGHT - 5 - height
772
+    );
773
+  }
774
+
391 775
 }

Loading…
Peruuta
Tallenna