schemard 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,867 @@
1
+
2
+ // テーブル・リレーションを格納するコンテナクラス
3
+ //
4
+ class tvFigures {
5
+ constructor(){
6
+ this.tables = {};
7
+ this.relations = {};
8
+ }
9
+ addTable(name, tableObject){
10
+ this.tables[name] = tableObject;
11
+ }
12
+ addRelation(name, relationObject){
13
+ this.relations[name] = relationObject;
14
+ }
15
+ getTable(name){
16
+ return this.tables[name];
17
+ }
18
+ static generateRelationName(parentTable, childTable){
19
+ return "relation-" + parentTable + "-" + childTable;
20
+ }
21
+ getRelation(name){
22
+ return this.relations[name];
23
+ }
24
+ getRelationsAs(as, tableName){
25
+ return Object.values(this.relations).filter((relObj)=>{ return relObj[as].name == tableName });
26
+ }
27
+ getRelationsAsChild(childTableName){
28
+ return this.getRelationsAs("child", childTableName);
29
+ }
30
+ getRelationsAsParent(parentTableName){
31
+ return this.getRelationsAs("parent", parent);
32
+ }
33
+ overlayExceptOptions(relation){
34
+ return { exceptTable: [relation.parent.name, relation.child.name], exceptRelation: [relation.name] };
35
+ }
36
+ getOverlayedRelations(){
37
+ return Object.values(this.relations).filter((relation)=>{
38
+ return this.isOverlayAboutLines(relation.getFigure().getLines(), this.overlayExceptOptions(relation));
39
+ })
40
+ }
41
+ isNotOverlay(relationFigure, options){
42
+ let overlay = true;
43
+ while(overlay){
44
+ overlay = this.isOverlayAboutLines(relationFigure.getLines(), options);
45
+ if(!overlay) break;
46
+ if(!relationFigure.nextOffset()) break;
47
+ }
48
+ if(!overlay){
49
+ relationFigure.fixOffset();
50
+ }
51
+ return !overlay;
52
+ }
53
+ isOverlayAboutLines(lines, options){
54
+ const MARGIN = 5; // ギリギリずれている場合に、重なっていると判定するための余白
55
+ return Object.keys(this.tables)
56
+ .some((tblName)=>{
57
+ let { top, bottom, left, right } = this.tables[tblName].position;
58
+ [ top, bottom, left, right ] = [ top + 1, bottom - 1, left + 1, right - 1 ];
59
+
60
+ return lines.some((line)=>{ return line.isOverlay(top, bottom, left, right) })
61
+
62
+ }) || Object.keys(this.tables).filter((tblName => !options.exceptTable.includes(tblName)))
63
+ .some((tblName)=>{
64
+ let { top, bottom, left, right } = this.tables[tblName].position;
65
+ [ top, bottom, left, right ] = [ top - MARGIN, bottom + MARGIN, left - MARGIN, right + MARGIN ];
66
+
67
+ return lines.some((line)=>{ return line.isOverlay(top, bottom, left, right) })
68
+
69
+ }) || Object.keys(this.relations).filter(relName => !options.exceptRelation.includes(relName))
70
+ .some((relName)=>{
71
+ let relationLines = this.relations[relName].getFigure().getLines();
72
+ return lines.some((line)=>{
73
+ return relationLines.some((relationLine)=>{
74
+ if(line.isVertical == relationLine.isVertical){
75
+ // 平行な線
76
+ return line.fixedPosition == relationLine.fixedPosition
77
+ && line.start - MARGIN < relationLine.end && relationLine.start < line.end + MARGIN;
78
+ }else{
79
+ // 交差する線
80
+ return relationLine.start < line.fixedPosition && line.fixedPosition < relationLine.end
81
+ && line.start - MARGIN < relationLine.fixedPosition && relationLine.fixedPosition < line.end + MARGIN;
82
+ }
83
+ })
84
+ })
85
+ })
86
+ }
87
+ }
88
+
89
+ var figures = new tvFigures();
90
+
91
+ // 線 を表すクラス
92
+ //
93
+ class tvLine {
94
+ constructor(direction, fixedPosition){
95
+ this.isVertical = (direction == "vertical");
96
+ this.fixedPosition = fixedPosition;
97
+ }
98
+ setStartEnd(start, end){
99
+ this.start = parseFloat(start < end ? start : end);
100
+ this.end = parseFloat(start < end ? end : start);
101
+ return this;
102
+ }
103
+ getPointOf(startOrEnd){
104
+ if(this.isVertical){
105
+ return { left: this.fixedPosition, top: this[startOrEnd] };
106
+ }else{
107
+ return { left: this[startOrEnd], top: this.fixedPosition };
108
+ }
109
+ }
110
+ get startPoint(){ return this.getPointOf("start") }
111
+ get endPoint(){ return this.getPointOf("end") }
112
+ get centerPoint(){
113
+ let fixed = this.fixedPosition, center = (this.start + this.end)/2;
114
+ return this.isVertical ? { left: fixed, top: center } : { left: center, top: fixed };
115
+ }
116
+ length(){ return Math.abs(this.start - this.end) }
117
+
118
+ pointsOnLine(){
119
+ if(this.points) return this.points;
120
+
121
+ let [fixed, moving] = this.isVertical ? ["left", "top"]: ["top", "left"];
122
+
123
+ let center = (this.start + this.end) / 2;
124
+ this.points = [ { [fixed]: this.fixedPosition, [moving]: center } ];
125
+
126
+ // 前後 20px ずつずらしていく。ただし両端 20px は空けておく
127
+ for(let offset = 20; center - (offset + 20) > this.start; offset += 20){
128
+ this.points.push({ [fixed]: this.fixedPosition, [moving]: center - offset });
129
+ this.points.push({ [fixed]: this.fixedPosition, [moving]: center + offset });
130
+ }
131
+ return this.points;
132
+ }
133
+ isOverlay(top, bottom, left, right){
134
+ let [fixedMin, fixedMax] = this.isVertical ? [left, right] : [top, bottom];
135
+ let [startEndMin, startEndMax] = this.isVertical ? [top, bottom] : [left, right];
136
+ if(this.fixedPosition < fixedMin || fixedMax < this.fixedPosition) return false;
137
+ if(this.end < startEndMin || startEndMax < this.start) return false;
138
+ return true;
139
+ }
140
+ }
141
+ class tvVerticalLine extends tvLine {
142
+ constructor(fixedPosition){
143
+ super("vertical", fixedPosition)
144
+ }
145
+ }
146
+ class tvHorizontalLine extends tvLine {
147
+ constructor(fixedPosition){
148
+ super("horizontal", fixedPosition)
149
+ }
150
+ }
151
+ // テーブルの枠線 を表すクラス(sideプロパティで四辺のいずれか(top,right,bottom,left)を識別する)
152
+ //
153
+ class tvTableLine extends tvLine {
154
+ constructor(tableName, side, fixedPosition){
155
+ super((side=="top"||side=="bottom")? "horizontal" : "vertical", fixedPosition)
156
+ this.tableName = tableName;
157
+ this.side = side;
158
+ }
159
+ searchBindPoint(anotherLine){
160
+ if(!this.points){
161
+ this.points = this.pointsOnLine();
162
+ this.boundPoints = new Set();
163
+ }
164
+ this.temporaryBound = [];
165
+ return this.nextBindPoint(anotherLine);
166
+ }
167
+ nextBindPoint(anotherLine){
168
+ let topOrLeft = this.isVertical ? "top" : "left";
169
+ let centerPosition = this.centerPoint[topOrLeft];
170
+ // anotherLine が指定されている場合は、検索方向を固定する(中央から端まで)
171
+ let nextDirection;
172
+ if(anotherLine){
173
+ nextDirection = { centerToEnd: centerPosition < anotherLine.centerPoint[topOrLeft] };
174
+ nextDirection.centerToStart = !nextDirection.centerToEnd;
175
+ }
176
+ for(let i=0; i < this.points.length; i++){
177
+ // 検索方向が指定されている場合
178
+ if(nextDirection){
179
+ if(nextDirection.centerToEnd && centerPosition > this.points[i][topOrLeft]) continue;
180
+ if(nextDirection.centerToStart && centerPosition < this.points[i][topOrLeft]) continue;
181
+ }
182
+ if(this.boundPoints.has(i)) continue;
183
+ if(this.temporaryBound.includes(i)) continue;
184
+
185
+ this.temporaryBound.push(i);
186
+ return this.points[i];
187
+ }
188
+ // anotherLineの指定なしで呼び出す
189
+ if(anotherLine){
190
+ return this.nextBindPoint();
191
+ }
192
+ // それでも見つからない場合は undefined
193
+ }
194
+ // temporaryBound を確定する
195
+ fixBindPoint(){
196
+ if(this.temporaryBound.length > 0){
197
+ this.boundPoints.add(this.temporaryBound[this.temporaryBound.length - 1]);
198
+ }
199
+ }
200
+ // boundPoints を解除する
201
+ unbind(point){
202
+ for(let i of this.boundPoints){
203
+ if(this.points[i].left == point.left && this.points[i].top == point.top){
204
+ this.boundPoints.delete(i);
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ // テーブル を表すクラス
211
+ //
212
+ class tvTable {
213
+ constructor(name, left, top, width, height){
214
+ this.name = name;
215
+ this.width = parseFloat(width);
216
+ this.height = parseFloat(height);
217
+ this.moveTo(parseFloat(left), parseFloat(top));
218
+ }
219
+ moveTo(left, top){
220
+ this.left = parseFloat(left);
221
+ this.top = parseFloat(top);
222
+ this.center = { left: this.left + this.width/2, top: this.top + this.height/2 };
223
+ this.position = {
224
+ left: this.left, right: this.left + this.width,
225
+ top: this.top, bottom: this.top + this.height,
226
+ };
227
+ this.sideLines = {}; // clear
228
+ }
229
+ getLineOf(side){
230
+ if(this.sideLines[side]) return this.sideLines[side];
231
+
232
+ this.sideLines[side] = new tvTableLine(this.name, side, this.position[side])
233
+ if(side=="top" || side=="bottom"){
234
+ this.sideLines[side].setStartEnd(this.position.left, this.position.right);
235
+ }else{
236
+ this.sideLines[side].setStartEnd(this.position.top, this.position.bottom);
237
+ }
238
+ return this.sideLines[side];
239
+ }
240
+ // "top", "bottom", "left", "right" のいずれかを返す
241
+ getSideOf(line){
242
+ if(line.isVertical){
243
+ return line.fixedPosition == this.position.left ? "left" : "right"
244
+ }else{
245
+ return line.fixedPosition == this.position.top ? "top" : "bottom"
246
+ }
247
+ }
248
+ getVerticalLines(){
249
+ return [ this.getLineOf("left"), this.getLineOf("right") ];
250
+ }
251
+ getHorizontalLines(){
252
+ return [ this.getLineOf("top"), this.getLineOf("bottom") ];
253
+ }
254
+ // 指定されたテーブル側の辺(1つか2つ)を返す
255
+ nearlySideLines(otherTable){
256
+ let otherPosition = otherTable.position;
257
+ let lines = [];
258
+ if(this.position.top > otherPosition.bottom) lines.push(this.getLineOf("top"));
259
+ if(this.position.bottom < otherPosition.top) lines.push(this.getLineOf("bottom"));
260
+ if(this.position.left > otherPosition.right) lines.push(this.getLineOf("left"));
261
+ if(this.position.right < otherPosition.left) lines.push(this.getLineOf("right"));
262
+ return lines;
263
+ }
264
+ // 指定されたテーブルの中央の距離のうち、近い方を返す。
265
+ // ただし、2つのテーブルの垂直位置が少しでも重なる場合は、水平距離は返さない(垂直距離を返す)
266
+ // また、2つのテーブルの水平位置が少しでも重なる場合は、垂直距離は返さない(水平距離を返す)
267
+ nearlyCenterPositionDistance(otherTable){
268
+ let distances = {
269
+ left: Math.abs(this.center.left - otherTable.center.left),
270
+ top: Math.abs(this.center.top - otherTable.center.top),
271
+ };
272
+ if(this.position.top < otherTable.position.bottom && this.position.bottom > otherTable.position.top)
273
+ return distances.top;
274
+ if(this.position.left < otherTable.position.right && this.position.right > otherTable.position.left)
275
+ return distances.left;
276
+ else
277
+ return Object.values(distances).sort((a, b)=> a - b)[0];
278
+ }
279
+ static create(name, options){
280
+ let table = new tvTable(name, options.left, options.top, options.width, options.height);
281
+ figures.addTable(name, table);
282
+ return table;
283
+ }
284
+ }
285
+
286
+ // リレーションを表すクラス
287
+ //
288
+ class tvRelation {
289
+ constructor(name, parentTbl, childTbl, childCardinality){
290
+ this.name = name;
291
+ this.parent = parentTbl;
292
+ this.child = childTbl;
293
+ this.childCardinality = childCardinality;
294
+ }
295
+ unbind(){
296
+ this.parentSideLine && this.parentSideLine.unbind(this.parentPoint);
297
+ this.childSideLine && this.childSideLine.unbind(this.childPoint);
298
+ delete this.relationFigure;
299
+ }
300
+ calcLinePosition(){
301
+ this.unbind();
302
+ // 選択された辺により折れ線の種類・始点/終点を決定
303
+ let parentLines = this.parent.nearlySideLines(this.child);
304
+ let childLines = this.child.nearlySideLines(this.parent);
305
+
306
+ let TOO_NEAR_LIMIT = 30;
307
+
308
+ if(parentLines.length == 2){
309
+
310
+ // 中央位置より優先する辺(方向:垂直/水平)を選択
311
+ let isPriorVertical = false;
312
+ if(Math.abs(this.parent.center.left - this.child.center.left)
313
+ > Math.abs(this.parent.center.top - this.child.center.top)){
314
+ isPriorVertical = true;
315
+ }
316
+ let paLine = parentLines.find(v => v.isVertical === isPriorVertical);
317
+ let chLine = childLines.find(v => v.isVertical === isPriorVertical);
318
+
319
+ /// 平行線が近すぎず、かつ点が見つかれば処理終了
320
+ if(Math.abs(paLine.fixedPosition - chLine.fixedPosition) > TOO_NEAR_LIMIT){
321
+ if(this.searchPoint(paLine, chLine)) return true;
322
+ }
323
+ // parentLines のもう片方を選択
324
+ let altPaLine = parentLines.find(v => v.isVertical !== isPriorVertical);
325
+ let altChLine = childLines.find(v => v.isVertical !== isPriorVertical);
326
+ /// 平行線が近すぎず、かつ点が見つかれば処理終了
327
+ if(altChLine && Math.abs(altPaLine.fixedPosition - altChLine.fixedPosition) > TOO_NEAR_LIMIT){
328
+ if(this.searchPoint(altPaLine, altChLine)) return true;
329
+ }
330
+ // それでも点が見つからない場合、交差する組み合わせを試す
331
+ if(this.searchPoint(paLine, altChLine)) return true;
332
+ if(this.searchPoint(altPaLine, chLine)) return true;
333
+
334
+ }else if(parentLines.length == 1){
335
+ /// 一組しか線が選択されなかった場合、平行線が近すぎず、かつ点が見つかれば処理終了
336
+ if(Math.abs(parentLines[0].fixedPosition - childLines[0].fixedPosition) > TOO_NEAR_LIMIT){
337
+ if(this.searchPoint(parentLines[0], childLines[0])) return true;
338
+ }
339
+
340
+ }else{
341
+ // テーブル同士が重なっている場合, とりあえずtop同士でつなぐ
342
+ return this.searchPoint(this.parent.getLineOf("top"), this.child.getLineOf("top"), true);
343
+ }
344
+
345
+ // 選択すべき点がない場合、平行でない辺を選択
346
+ let paLine = parentLines[0];
347
+ let chLine = childLines[0];
348
+ let otherDirection = paLine.isVertical ? "Horizontal": "Vertical";
349
+
350
+ // まず長さが短い方の辺を, 平行でない辺に変更
351
+ if(paLine.length() < chLine.length()){
352
+ paLine = this.selectOtherSideLineFor("parent", otherDirection);
353
+ }else{
354
+ chLine = this.selectOtherSideLineFor("child", otherDirection);
355
+ }
356
+ // 点が見つかれば処理終了
357
+ if(paLine && chLine && this.searchPoint(paLine, chLine)) return true;
358
+
359
+ // それでも見つからなければ長さが長い方の辺を変更(長い方の辺は取得できない場合がある)
360
+ if(paLine.length() >= chLine.length()){
361
+ paLine = this.selectOtherSideLineFor("parent", otherDirection);
362
+ }else{
363
+ chLine = this.selectOtherSideLineFor("child", otherDirection);
364
+ }
365
+ // 点が見つかれば処理終了
366
+ if(paLine && chLine && this.searchPoint(paLine, chLine)) return true;
367
+
368
+ // それでも見つからない場合は 0 番目のLineで決定とする
369
+ return this.searchPoint(parentLines[0], childLines[0], true);
370
+ }
371
+ // targetTable の direction (=Vertical or Horizontal) の辺を取得する
372
+ selectOtherSideLineFor(targetTable, direction){
373
+ let nonTargetTable = targetTable == "parent" ? "child" : "parent";
374
+
375
+ let targetLines = this[targetTable][`get${direction}Lines`]();
376
+ let nonTargetLines = this[nonTargetTable][`get${direction}Lines`]();
377
+
378
+ if(targetLines[0].fixedPosition < nonTargetLines[1].fixedPosition
379
+ && targetLines[1].fixedPosition > nonTargetLines[0].fixedPosition){
380
+ // 重なっている場合, nonTargetLines[0] と nonTargetLines[1] の間にある辺を返す
381
+ return targetLines.find((line)=>{
382
+ return nonTargetLines[0].fixedPosition <= line.fixedPosition
383
+ && line.fixedPosition <= nonTargetLines[1].fixedPosition;
384
+ });
385
+ }else{
386
+ // 重なってない場合, nonTargetLine[0] に近い方の辺を返す
387
+ return targetLines[targetLines[0].fixedPosition > nonTargetLines[0].fixedPosition ? 0 : 1];
388
+ }
389
+ }
390
+ // 線を引き、他の何かを重なってないか判定
391
+ // 重なっていれば、点を選択しなおす(選択すべき点がなければfalseを返す)
392
+ searchPoint(paLine, chLine, noSearch = false){
393
+ // 選択された辺により折れ線の種類・始点/終点を決定
394
+ this.parentSideLine = paLine;
395
+ this.childSideLine = chLine;
396
+ this.parentPoint = paLine.searchBindPoint(chLine);
397
+ this.childPoint = chLine.searchBindPoint(paLine);
398
+
399
+ let found = noSearch;
400
+ let relationFigure = noSearch && this.getFigure();
401
+
402
+ //DEBUG if(noSearch) console.log("noSearch!!!!!!!!!!!! " + this.name);
403
+
404
+ while(!found){
405
+ if(!this.parentPoint || !this.childPoint) break;
406
+ if(found = figures.isNotOverlay(relationFigure = this.getFigure(), figures.overlayExceptOptions(this))) break;
407
+
408
+ this.parentPoint = paLine.nextBindPoint(chLine);
409
+
410
+ if(!this.parentPoint) break;
411
+ if(found = figures.isNotOverlay(relationFigure = this.getFigure(), figures.overlayExceptOptions(this))) break;
412
+
413
+ this.childPoint = chLine.nextBindPoint(paLine);
414
+ }
415
+ if(found){
416
+ this.parentSideLine.fixBindPoint();
417
+ this.childSideLine.fixBindPoint();
418
+ this.fixFigure(relationFigure.fixOffset());
419
+ }
420
+ return found;
421
+ }
422
+ fixFigure(figure){
423
+ this.relationFigure = figure;
424
+ }
425
+ getFigure(){
426
+ if(this.relationFigure) return this.relationFigure;
427
+
428
+ // Parent, Child が完全に重なっている場合など、this.parentPoint, this.childPointが undefined になっている。
429
+ // とりあえず中央点にする
430
+ this.parentPoint = this.parentPoint || this.parentSideLine.centerPoint;
431
+ this.childPoint = this.childPoint || this.childSideLine.centerPoint
432
+
433
+ if(this.parentSideLine.isVertical == this.childSideLine.isVertical){
434
+ ///// └┐, ┌┘, │ のいずれか
435
+ let prop = this.parentSideLine.isVertical ? "top" : "left";
436
+
437
+ if(this.parentPoint[prop] == this.childPoint[prop]){
438
+ return new tvRelationFigure1(this.parentPoint, this.childPoint)
439
+ }else{
440
+ return new tvRelationFigure3(this.parentPoint, this.childPoint, ! this.parentSideLine.isVertical)
441
+ }
442
+ }else{
443
+ ///// └, ┘, ┌, ┐ のいずれか
444
+ // 上にある方を posA、下にある方を posB とする
445
+ let [posA, posB] = [this.parentPoint, this.childPoint]
446
+ if(this.parentPoint.top > this.childPoint.top){
447
+ [posA, posB] = [posB, posA];
448
+ }
449
+ let posASideLine = this.parentPoint.top < this.childPoint.top ? this.parentSideLine : this.childSideLine;
450
+ if(posASideLine.isVertical){
451
+ // ┌, ┐ のいずれか
452
+ return new tvRelationFigure2(posA, posB, { top: posA.top, left: posB.left });
453
+ }else{
454
+ // └, ┘ のいずれか
455
+ return new tvRelationFigure2(posA, posB, { top: posB.top, left: posA.left });
456
+ }
457
+ }
458
+ }
459
+ get parentEdge(){
460
+ if(!this.parentSideLine || !this.parentPoint) return;
461
+ return { point: this.parentPoint, side: this.parent.getSideOf(this.parentSideLine) };
462
+ }
463
+ get childEdge(){
464
+ if(!this.childSideLine || !this.childPoint) return;
465
+ return { point: this.childPoint, side: this.child.getSideOf(this.childSideLine), cardinality: this.childCardinality };
466
+ }
467
+ static create(name, parentTbl, childTbl, childCardinality){
468
+ let relation = new tvRelation(name, parentTbl, childTbl, childCardinality);
469
+ figures.addRelation(name, relation);
470
+ return relation;
471
+ }
472
+ }
473
+
474
+ // 直線図形のリレーション
475
+ class tvRelationFigure1 {
476
+ constructor(posA, posB){
477
+ if(posA.left == posB.left){
478
+ this.line = new tvLine("vertical", posA.left).setStartEnd(posA.top, posB.top)
479
+ }else{
480
+ this.line = new tvLine("horizontal", posA.top).setStartEnd(posA.left, posB.left)
481
+ }
482
+ }
483
+ getLines(){
484
+ return [ this.line ];
485
+ }
486
+ nextOffset(){ return false }
487
+ fixOffset(){ return this }
488
+ displayInfo(){
489
+ return [ this.line.isVertical ?
490
+ { top: this.line.start, left: this.line.fixedPosition, width: 10, height: this.line.length(), borders: ["left"] } :
491
+ { top: this.line.fixedPosition, left: this.line.start, width: this.line.length(), height: 10, borders: ["top"] }
492
+ ];
493
+ }
494
+ }
495
+ // 折線(角1つ)図形のリレーション
496
+ class tvRelationFigure2 {
497
+ constructor(posA, posB, corner){
498
+ this.lines = [];
499
+ if(posA.top == corner.top){
500
+ this.lines.push(new tvHorizontalLine(posA.top).setStartEnd(posA.left, corner.left))
501
+ this.lines.push(new tvVerticalLine(posB.left).setStartEnd(posB.top, corner.top))
502
+ }else{
503
+ this.lines.push(new tvVerticalLine(posA.left).setStartEnd(posA.top, corner.top))
504
+ this.lines.push(new tvHorizontalLine(posB.top).setStartEnd(posB.left, corner.left))
505
+ }
506
+ }
507
+ getLines(){
508
+ return this.lines;
509
+ }
510
+ nextOffset(){ return false }
511
+ fixOffset(){ return this }
512
+ displayInfo(){
513
+ let [vline, hline] = this.lines[0].isVertical ? this.lines : this.lines.concat().reverse();
514
+ let borders = [];
515
+ borders.push(vline.fixedPosition == hline.start ? "left" : "right")
516
+ borders.push(hline.fixedPosition == vline.start ? "top" : "bottom")
517
+ return [{
518
+ top: vline.start, left: hline.start, width: hline.length(), height: vline.length(), borders: borders
519
+ }];
520
+ }
521
+ }
522
+ // 折線(角2つ)図形のリレーション
523
+ class tvRelationFigure3 {
524
+ constructor(posA, posB, isVertical){
525
+ this.isVertical = isVertical;
526
+ this.offset = 0;
527
+ if(isVertical){
528
+ // 上にある方を start とする
529
+ [this.start, this.end] = posA.top < posB.top ? [posA, posB] : [posB, posA];
530
+ }else{
531
+ // 左にある方を start とする
532
+ [this.start, this.end] = posA.left < posB.left ? [posA, posB] : [posB, posA];
533
+ }
534
+ }
535
+ get center(){
536
+ let prop = this.isVertical ? "top" : "left";
537
+ return parseInt((this.start[prop] + this.end[prop]) / 2) + this.offset;
538
+ }
539
+ getLines(){
540
+ if(this.lines) return this.lines;
541
+ let lines = [];
542
+ if(this.isVertical){
543
+ lines.push(new tvVerticalLine(this.start.left).setStartEnd(this.start.top, this.center));
544
+ lines.push(new tvHorizontalLine(this.center).setStartEnd(this.start.left, this.end.left));
545
+ lines.push(new tvVerticalLine(this.end.left).setStartEnd(this.center, this.end.top));
546
+ }else{
547
+ lines.push(new tvHorizontalLine(this.start.top).setStartEnd(this.start.left, this.center));
548
+ lines.push(new tvVerticalLine(this.center).setStartEnd(this.start.top, this.end.top));
549
+ lines.push(new tvHorizontalLine(this.end.top).setStartEnd(this.center, this.end.left));
550
+ }
551
+ return lines;
552
+ }
553
+ // 折れ線の位置を 0, 10, -10, 20, -20,, とずらしていく
554
+ // 終端まで到達した場合は、false を返す
555
+ nextOffset(){
556
+ let newOffset = this.offset <= 0 ? Math.abs(this.offset) + 10 : this.offset * -1;
557
+
558
+ let prop = this.isVertical ? "top" : "left";
559
+ let centerLinePosition = parseInt((this.start[prop] + this.end[prop]) / 2) + newOffset;
560
+
561
+ if(this.start[prop] < centerLinePosition - 15 && centerLinePosition + 15 < this.end[prop]){
562
+ this.offset = newOffset;
563
+ return true;
564
+ }
565
+ return false;
566
+ }
567
+ fixOffset(){
568
+ this.lines = this.getLines();
569
+ return this;
570
+ }
571
+ displayInfo(){
572
+ function toggle(dir){ return { left: "right", right: "left", top: "bottom", bottom: "top" }[dir] }
573
+ if(this.isVertical){
574
+ // └┐, ┌┘ のどちらか
575
+ let [vline, hline, vline2] = this.lines;
576
+ let borders = [ vline.fixedPosition == hline.start ? "left" : "right", "bottom" ];
577
+ let base = { left: hline.start, width: hline.length() };
578
+ return [
579
+ Object.assign(Object.create(base), { top: vline.start, height: vline.length(), borders: borders }),
580
+ Object.assign(Object.create(base), { top: vline2.start, height: vline2.length(), borders: [ toggle(borders[0]) ] })
581
+ ];
582
+ }else{
583
+ // ┌ ┐
584
+ // ┘, └ のいずれか
585
+ let [hline, vline, hline2] = this.lines;
586
+ let borders = [ hline.fixedPosition == vline.start ? "top" : "bottom", "right" ];
587
+ let base = { top: vline.start, height: vline.length() };
588
+ return [
589
+ Object.assign(Object.create(base), { left: hline.start, width: hline.length(), borders: borders }),
590
+ Object.assign(Object.create(base), { left: hline2.start, width: hline2.length(), borders: [ toggle(borders[0]) ] })
591
+ ];
592
+ }
593
+ }
594
+ }
595
+ var Custom = {
596
+
597
+ apply: function(){
598
+ $('.table').bind('moved', Custom.saveTablePosition);
599
+ $('.table .title').on('click.table_show', Custom.moveToShowPage);
600
+ $('.editable input').bind('click', Custom.setEditMode);
601
+ },
602
+
603
+ initLayout: function(){
604
+ $(".table").each(function(){
605
+
606
+ if($(this).hasClass("default-position")){
607
+ $(this).attr("data-pos-left", $(this).offset().left);
608
+ $(this).attr("data-pos-top", $(this).offset().top);
609
+ }
610
+
611
+ $(this).css({
612
+ "width": $(this).width() + 20,
613
+ "left": $(this).attr("data-pos-left") + "px",
614
+ "top": $(this).attr("data-pos-top") + "px"
615
+ });
616
+
617
+ tvTable.create($(this).attr("data-table-name"), {
618
+ left: $(this).attr("data-pos-left"), top: $(this).attr("data-pos-top"),
619
+ width: parseInt($(this).outerWidth(true)), height: parseInt($(this).outerHeight(true))
620
+ });
621
+ });
622
+ $(".table")
623
+ .filter(function(){ return $(this).hasClass("default-position") })
624
+ .each(function(){ return $(this).removeClass("default-position") })
625
+
626
+ $(".table")
627
+ .filter(function(){ return $(this).attr("data-relation-to") })
628
+ .sort((tblA, tblB)=>{
629
+ return $(tblB).attr("data-relation-to").split(",").length
630
+ - $(tblA).attr("data-relation-to").split(",").length
631
+ })
632
+ .each(function(){ Custom.private.refreshRelationLines($(this)) });
633
+
634
+ // 描画順によって重なってしまう場合があるため、重複している線について再描画を試みる
635
+ figures.getOverlayedRelations().forEach((relation)=>{
636
+ Custom.private.drawRelationLines(relation.name, relation);
637
+ });
638
+
639
+ $("a[data-link-url]").each(function(){
640
+ if($(this).attr("href")) return;
641
+ $(this).attr("href", Custom.URLSuffix($(this).attr("data-link-url")));
642
+ });
643
+ },
644
+
645
+ saveTablePosition: function(e){
646
+ var position = $(this).offset().left+","+$(this).offset().top;
647
+ $.ajax({
648
+ type: 'POST',
649
+ url: "/tables/" + $(this).attr("data-table-name"),
650
+ data: { layout: position }
651
+ })
652
+ .done(function( data, textStatus, jqXHR ) {
653
+ // message.
654
+ })
655
+ },
656
+
657
+ moveToShowPage: function(e){
658
+ window.location.href =
659
+ Custom.URLSuffix("tables/" + $(this).parent().attr("data-table-name"));
660
+ },
661
+
662
+ URLSuffix: function(url){
663
+ var suffix = /.+\.html/.test(window.location.href)? ".html": "";
664
+ return url == "/" && suffix == ".html" ? "./..": url + suffix;
665
+ },
666
+
667
+ setEditMode: function(){
668
+ if($(this).is(':checked')){
669
+ Custom.dragable(".table");
670
+ $(".table").addClass("editing");
671
+ $('.table .title').off('click.table_show');
672
+ }else{
673
+ Custom.unDragable(".table");
674
+ $(".table").removeClass("editing");
675
+ $('.table .title').on('click.table_show', Custom.moveToShowPage);
676
+ }
677
+ },
678
+
679
+ unDragable: function(cssSelector){
680
+ $(document).off('mousedown.draggable', cssSelector);
681
+ $(document).off('mouseup.draggable');
682
+ $(document).off('mousemove.draggable');
683
+ },
684
+
685
+ dragable: function(cssSelector){
686
+ $(document).on('mousedown.draggable', cssSelector, function(e){
687
+ $(document).data("drag-target", this);
688
+ $(this).data("dragStartX", e.pageX);
689
+ $(this).data("dragStartY", e.pageY);
690
+ $(this).data("startLeft", $(this).offset().left);
691
+ $(this).data("startTop", $(this).offset().top);
692
+ $(this).addClass("dragging");
693
+ });
694
+
695
+ $(document).on('mouseup.draggable', function(e){
696
+ var target = $(document).data("drag-target");
697
+ if(target){
698
+ Custom.private.moveTo($($(target).data("drag-target")), e);
699
+ }
700
+ $(document).data("drag-target", false);
701
+ $(target).removeClass("dragging");
702
+ $(target).trigger("moved");
703
+ });
704
+ $(document).on('mousemove.draggable', function(e){
705
+ if($(document).data("drag-target")){
706
+ Custom.private.moveTo($($(document).data("drag-target")), e);
707
+ }
708
+ });
709
+ },
710
+
711
+ private: {
712
+
713
+ moveTo: function($obj, e){
714
+ var startX = $obj.data("dragStartX");
715
+ var startY = $obj.data("dragStartY");
716
+ if(!startX || !startY)
717
+ return;
718
+
719
+ $obj.css({
720
+ "left": $obj.data("startLeft") + (e.pageX - startX),
721
+ "top": $obj.data("startTop") + (e.pageY - startY)
722
+ });
723
+ figures.getTable($obj.attr("data-table-name")).moveTo(
724
+ $obj.data("startLeft") + (e.pageX - startX), $obj.data("startTop") + (e.pageY - startY)
725
+ )
726
+ // 親テーブルとしての関連を再描画
727
+ Custom.private.refreshRelationLines($obj);
728
+ // 子テーブルとしての関連を再描画
729
+ figures.getRelationsAsChild($obj.attr("data-table-name")).forEach((relObj)=>{
730
+ Custom.private.refreshRelationLines($(`.table[data-table-name=${relObj.parent.name}]`));
731
+ })
732
+ // 親テーブルとして関連しているテーブルを再描画
733
+ figures.getRelationsAsParent($obj.attr("data-table-name")).forEach((relObj)=>{
734
+ Custom.private.refreshRelationLines($(`.table[data-table-name=${relObj.child.name}]`));
735
+ })
736
+ },
737
+
738
+ refreshRelationLines: function($obj){
739
+ let tableObject = figures.getTable($obj.attr("data-table-name"));
740
+ // 関連テーブルを取得
741
+ let relationTableObjects = $obj.attr("data-relation-to").split(",")
742
+ .filter(v => v).map(rel => figures.getTable(rel));
743
+
744
+ let relationCardinalities = $obj.attr("data-relation-cardinality").split(",")
745
+ .reduce((sum, cardinality)=>{
746
+ sum[cardinality.split(":")[0]] = cardinality.split(":")[1];
747
+ return sum;
748
+ }, {});
749
+
750
+ // 関連テーブルを中心の絶対座長(X or Y)の差異が小さい順でソート
751
+ relationTableObjects.sort((relTblA, relTblB)=>{
752
+ return relTblA.nearlyCenterPositionDistance(tableObject)
753
+ - relTblB.nearlyCenterPositionDistance(tableObject)
754
+ })
755
+ .forEach((relTable, index)=>{
756
+ var relName = tvFigures.generateRelationName(tableObject.name, relTable.name);
757
+ if($(`#${relName}`).length == 0){
758
+ $("body").append("<div id=" + relName + " class='relation-line' ></div>")
759
+ }
760
+ let relation = figures.getRelation(relName) ||
761
+ tvRelation.create(relName, tableObject, relTable, relationCardinalities[relTable.name]);
762
+ Custom.private.drawRelationLines(`#${relName}`, relation);
763
+ })
764
+ },
765
+ drawRelationLines: function(relId, relation){
766
+ relation.calcLinePosition();
767
+
768
+ let displayInfo = relation.getFigure().displayInfo();
769
+ // └┐, ┌┘ のように2つの角をもつ線の場合
770
+ if(displayInfo.length == 2){
771
+ if($(relId).children().length == 0){
772
+ $(relId).append("<div class=\"child-1\"></div><div class=\"child-2\"></div>");
773
+ }
774
+ $(relId).css({ top: displayInfo[0].top, left: displayInfo[0].left, border: "none" });
775
+ $(relId).find(".child-1").css({ top: 0, left: 0, width: displayInfo[0].width, height: displayInfo[0].height });
776
+ ["top", "bottom", "left", "right"].forEach((border)=>{
777
+ $(relId).find(".child-1").css(`border-${border}`, displayInfo[0].borders.includes(border) ? "solid 1px black" : "none");
778
+ });
779
+ $(relId).find(".child-2").css({ width: displayInfo[1].width, height: displayInfo[1].height });
780
+ $(relId).find(".child-2").css({
781
+ left: displayInfo[1].left - displayInfo[0].left, top: displayInfo[1].top - displayInfo[0].top
782
+ });
783
+ ["top", "bottom", "left", "right"].forEach((border)=>{
784
+ $(relId).find(".child-2").css(`border-${border}`, displayInfo[1].borders.includes(border) ? "solid 1px black" : "none");
785
+ });
786
+
787
+ }else{
788
+ // ┌, ┘, |, のように1 or 0個の角をもつ線の場合
789
+ if($(relId).children().length > 0){
790
+ $(relId).empty();
791
+ }
792
+ $(relId).css(displayInfo[0]);
793
+ ["top", "bottom", "left", "right"].forEach((border)=>{
794
+ $(relId).css(`border-${border}`, displayInfo[0].borders.includes(border) ? "solid 1px black" : "none");
795
+ })
796
+ }
797
+ // 親側の記号を描画
798
+ Custom.private.drawRelationEdge_1(`${relId}-parent-edge`, relation.parentEdge);
799
+ // 子側の記号を描画
800
+ if(relation.childCardinality == "1"){
801
+ Custom.private.drawRelationEdge_1(`${relId}-child-edge`, relation.childEdge);
802
+ }else{
803
+ Custom.private.drawRelationEdge_N(`${relId}-child-edge`, relation.childEdge);
804
+ }
805
+ },
806
+ drawRelationEdge_1: function(edgeId, edge){
807
+ let { top: offsetTop, left: offsetLeft } = {
808
+ top: { top: -5, left: -5 }, bottom: { top: 0, left: -5 },
809
+ left: { top: -5, left: -5 }, right: { top: -5, left: 0 }
810
+ }[edge.side];
811
+
812
+ if($(edgeId).length == 0){
813
+ $("body").append("<div id='" + edgeId.substring(1) + "' class='relation-edge' ></div>");
814
+ }
815
+ $(edgeId).css({ top: edge.point.top + offsetTop, left: edge.point.left + offsetLeft });
816
+ $(edgeId).css(
817
+ ["left", "right"].includes(edge.side) ? { width: 3, height: 10 } : { width: 10, height: 3 });
818
+
819
+ ["top", "bottom", "left", "right"].forEach((border)=>{
820
+ $(edgeId).css(`border-${border}`, edge.side == border ? "solid 1px black" : "none");
821
+ })
822
+ },
823
+ drawRelationEdge_N: function(edgeId, edge){
824
+ let offsets = {
825
+ top: [{ left: -6 }, { top: -9, left: 1 }, { left: 7 }],
826
+ bottom: [{ left: -5, top: -1 }, { top: 7 }, { left: 5, top: -1 }],
827
+ left: [{ top: -6 }, { left: -8, top: -1 }, { top: 4 }],
828
+ right: [{ top: -6 }, { left: 8, top: -1 }, { top: 4 }],
829
+ }[edge.side];
830
+
831
+ let points = offsets.map((offset)=>{
832
+ let point = Object.assign({}, edge.point);
833
+ if("top" in offset) Object.assign(point, { top: offset.top + edge.point.top });
834
+ if("left" in offset) Object.assign(point, { left: offset.left + edge.point.left });
835
+ return point;
836
+ });
837
+
838
+ if($(`${edgeId}-1`).length == 0){
839
+ $("body").append("<div id='" + edgeId.substring(1) + "-1' class='relation-edge-diagonal' ></div>")
840
+ $("body").append("<div id='" + edgeId.substring(1) + "-2' class='relation-edge-diagonal' ></div>")
841
+ }
842
+
843
+ let drawDiagonalLine = (rectId, p1, p2)=>{
844
+ let pos = p1.left < p2.left? p1: p2;
845
+ let width = Math.abs(p1.left - p2.left);
846
+ let height = Math.abs(p1.top - p2.top);
847
+ let isUpToDown = ((p1.top - p2.top) * (p1.left - p2.left) < 0);
848
+
849
+ var deg = Math.atan(height / width) / (Math.PI / 180) * (isUpToDown ? -1: 1);
850
+
851
+ $(rectId).css({
852
+ top: pos.top,
853
+ left: pos.left,
854
+ width: Math.sqrt((width * width) + (height * height)),
855
+ height: 1,
856
+ transform: "rotate(" + deg + "deg)",
857
+ "transform-origin": "left top"
858
+ });
859
+ };
860
+ drawDiagonalLine(`${edgeId}-1`, points[0], points[1]);
861
+ drawDiagonalLine(`${edgeId}-2`, points[1], points[2]);
862
+ },
863
+ }
864
+ }
865
+
866
+ $(document).ready(Custom.initLayout);
867
+ $(document).ready(Custom.apply);