main.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. var rowId = [];
  2. var tree, persons, pairs, groups;
  3. var group = null;
  4. var panZoom;
  5. window.onload = function() {
  6. const parameterList = new URLSearchParams(window.location.search);
  7. group = parameterList.get("group");
  8. tree = new SVGTree(document.getElementById('chart'), 'svg0');
  9. persons = JSON.parse(document.getElementById('person-data').textContent);
  10. pairs = JSON.parse(document.getElementById('pair-data').textContent);
  11. groups = JSON.parse(document.getElementById('group-data').textContent);
  12. // construct persons
  13. for (let i=0; i<persons.length; i++) {
  14. const p = persons[i];
  15. persons[i] = {
  16. id: p[0],
  17. name: p[1],
  18. parent_id: p[2],
  19. birth_date: p[3] ? new Date(p[3]) : null,
  20. birth_town: p[4],
  21. death_date: p[5] ? new Date(p[5]) : null,
  22. death_town: p[6],
  23. comment: p[7],
  24. image: p[8],
  25. color: p[9],
  26. group_id: p[10]
  27. };
  28. }
  29. // construct pairs
  30. for (let i = 0; i < pairs.length; i++) {
  31. const p = pairs[i];
  32. pairs[i] = {
  33. id: p[0],
  34. from_person_id: p[1],
  35. to_person_id: p[2]
  36. };
  37. }
  38. // find partners
  39. for(const pair of pairs) {
  40. const p1 = persons.find(p => p.id === pair.from_person_id);
  41. const p2 = persons.find(p => p.id === pair.to_person_id);
  42. pair.from_person = p1;
  43. pair.to_person = p2;
  44. p1.partner = p2;
  45. p1.pair = pair;
  46. p2.partner = p1;
  47. p2.pair = pair;
  48. }
  49. // find group, parents and children
  50. for(const person of persons) {
  51. const group = groups.find(g => g.id == person.group_id);
  52. const parent = persons.find(p => p.id === person.parent_id && p.id != person.id);
  53. const children = persons.filter(p => p.parent_id === person.id && p.id != person.id);
  54. person.group = group;
  55. person.parent = parent;
  56. person.children = children;
  57. }
  58. // hide persons without tree
  59. for (const person of persons) {
  60. person.hasCard =
  61. person.hasCard ||
  62. !person.partner ||
  63. !!person.parent ||
  64. person.children.length > 0;
  65. if (person.partner) {
  66. person.partner.hasCard =
  67. !person.hasCard ||
  68. !!person.partner.parent ||
  69. person.partner.children.length > 0;
  70. }
  71. }
  72. // order persons to allow shifting down level pairs and get related children closer
  73. persons.sort(function(a, b) {
  74. if(a.group_id != b.group_id && a.group_id != null)
  75. return a.group_id - b.group_id;
  76. function findRelation(p) {
  77. if (!p.hasCard)
  78. return null;
  79. if (p.partner && p.partner.hasCard == true)
  80. return p.pair.id;
  81. var ret = null;
  82. for (const child of p.children){
  83. ret = findRelation(child);
  84. if(ret !== null)
  85. return ret;
  86. }
  87. return null;
  88. }
  89. if(!a.parent || !b.parent) {
  90. var relationId = findRelation(a) - findRelation(b);
  91. if(relationId != null)
  92. return relationId;
  93. }
  94. const dt1 = a.birth_date || a.death_date || "9999-01-01";
  95. const dt2 = b.birth_date || b.death_date || "9999-01-01";
  96. return new Date(dt1) - new Date(dt2);
  97. });
  98. // add groups
  99. for(const row of groups) {
  100. $('#group').append(`<div class='overlay ${row.id}' style='color: black; background: linear-gradient(rgba(0,0,0,0),${row.color}, rgba(0,0,0,0));' onclick='display(${row.id});'>${row.name}</div>`);
  101. }
  102. $('#loader').hide();
  103. // define levels by the maximum birth date in that level
  104. function calculateLevels(personList) {
  105. personList = personList.concat().sort((a, b) => {
  106. const dateA = a.birth_date || a.death_date
  107. const dateB = b.birth_date || b.death_date
  108. return dateA - dateB
  109. })
  110. let levels = []
  111. let currentPersons = []
  112. for(const person of personList) {
  113. const date = person.birth_date || person.death_date
  114. if(!date || !person.hasCard)
  115. continue
  116. if(person.parent && currentPersons.find(p => person.parent.id === p.id)) {
  117. levels.push(date - 1)
  118. currentPersons = [person]
  119. } else {
  120. currentPersons.push(person)
  121. }
  122. }
  123. levels.push(+personList[personList.length - 1].birth_date || +personList[personList.length - 1].death_date)
  124. return levels
  125. }
  126. const levels = calculateLevels(persons)
  127. /**
  128. * calculate position of cards
  129. * @param {*} personList current persons
  130. * @param {*} levels reference to levels
  131. * @param {*} level row of the current card
  132. * @param {*} accX maximum X positions per layer
  133. * @returns width of the tree
  134. */
  135. function assignCoords(personList, levels, level=0, accX=[0]) {
  136. personList = personList.filter(p => p.hasCard);
  137. const margin = 20;
  138. let startX = 0, endX = 0;
  139. for (let j=0; j<personList.length; j++) {
  140. const person = personList[j];
  141. // set y
  142. const date = person.birth_date || person.death_date
  143. person.level = Math.max(level, levels.findIndex(l => l >= +date && date != null));
  144. if(person.partner && person.partner.level > person.level) {
  145. person.level = person.partner.level;
  146. }
  147. person.y = person.level * (tree.cardHeight + 40);
  148. // set x
  149. accX[person.level + 1] = accX[person.level + 1] || 0;
  150. // fill in skipped layers
  151. for(var i=level; i<=person.level; i++)
  152. accX[i] = accX.slice(level,person.level+1).reduce((acc, x) => Math.max(acc, x), 0);
  153. const width = tree.cardWidth * (person.partner == null ? 1 : 2);
  154. const origAccX = accX.slice();
  155. const subWidth = assignCoords(person.children, levels, person.level + 1, accX);
  156. const requiredForSiblings = personList.slice(j + 1).reduce((acc, p) => acc + tree.cardWidth * (person.partner == null ? 1 : 2) + margin, 0);
  157. const remainingWidthAboveChildren = accX[person.level+1] - accX[person.level];
  158. const idealX = subWidth > 0 ? accX[person.level + 1] - subWidth / 2 - width / 2 : accX[person.level] + margin;
  159. if (idealX >= accX[person.level] + margin) {
  160. // perfect fit
  161. person.x = idealX;
  162. } else if(remainingWidthAboveChildren > margin + width) {
  163. // still fits above children
  164. person.x = accX[person.level] + margin;
  165. } else if(subWidth == 0) {
  166. // no children
  167. person.x = accX[person.level] + margin;
  168. } else if (subWidth < width) {
  169. // center children under this card to avoid overlap
  170. person.x = accX[person.level] + margin;
  171. for (var i = person.level + 1; i < accX.length; i++)
  172. accX[i] = Math.max(origAccX[i], person.x + width/2 - subWidth/2 - margin);
  173. assignCoords(person.children, levels, person.level + 1, accX);
  174. } else {
  175. // we need to move children under this card to avoid overlap
  176. person.x = accX[person.level] + margin;
  177. for (var i = person.level + 1; i < accX.length; i++)
  178. accX[i] = Math.max(origAccX[i], person.x + width - subWidth - margin);
  179. assignCoords(person.children, levels, person.level + 1, accX);
  180. // center this node again
  181. person.x = Math.max(origAccX[person.level] + margin, accX[person.level + 1] - subWidth / 2 - width / 2);
  182. }
  183. if(!startX)
  184. startX = person.x;
  185. endX = person.x + width;
  186. accX[person.level] = person.x + width;
  187. // fill in skipped layers
  188. for (var i = level; i <= person.level; i++)
  189. accX[i] = accX.slice(level, person.level + 1).reduce((acc, x) => Math.max(acc, x), 0);
  190. }
  191. return endX - startX;
  192. }
  193. assignCoords(persons.filter(p => p.parent_id == null), levels);
  194. // draw links
  195. for(const link of pairs) {
  196. const minId = Math.min(link.from_person_id, link.to_person_id);
  197. const maxId = Math.max(link.from_person_id, link.to_person_id);
  198. if (!document.getElementById(`link_line_${minId}_${maxId}`)) {
  199. if (link.from_person.hasCard && link.to_person.hasCard)
  200. tree.drawLink(link);
  201. }
  202. }
  203. let drag = false;
  204. for(const person of persons) {
  205. if(!person.hasCard)
  206. continue;
  207. let color = person.color;
  208. if(!person.partner || !person.partner.hasCard) {
  209. color = null;
  210. }
  211. const card = tree.drawCard(person, person.group ? person.group.color : "red", color);
  212. card.addEventListener('mousedown', () => drag = false);
  213. card.addEventListener('mousemove', () => drag = true);
  214. card.addEventListener('mouseover', onMouseover);
  215. card.addEventListener('click', onSelect);
  216. }
  217. for(const person of persons) {
  218. if(person.hasCard)
  219. tree.drawLines(person, person.children, person.group ? person.group.color : "#ff0000");
  220. }
  221. function onMouseover(event) {
  222. const person_id = this.id.replace('card_', '');
  223. const link = pairs.find(r => r.from_person_id == person_id || r.to_person_id == person_id);
  224. if(link == null)
  225. return;
  226. if (group === null || link.from_person.group_id === link.to_person.group_id) {
  227. const minId = Math.min(link.from_person_id, link.to_person_id);
  228. const maxId = Math.max(link.from_person_id, link.to_person_id);
  229. $('.link_lines').hide();
  230. $(`#link_line_${minId}_${maxId}`).show();
  231. }
  232. }
  233. function onSelect(event) {
  234. if(drag) {
  235. event.preventDefault();
  236. } else {
  237. const person = persons.find(p => p.id == this.id.replace('card_', ''));
  238. $('#img_name').html(`${person.name} (${person.group_id})` + (person.partner != null ? '<br>' + person.partner.name : ''));
  239. $('#name').val(person.name);
  240. $('#form').attr('action', `/stammbaum/person/${person.id}/upload`);
  241. $('.info').css('display', 'inline-block');
  242. }
  243. }
  244. panZoom = panZoomInit();
  245. display(group);
  246. var saveData = (function () {
  247. var a = document.createElement("a");
  248. document.body.appendChild(a);
  249. a.style = "display: none";
  250. return function (data, fileName) {
  251. var blob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
  252. var url = window.URL.createObjectURL(blob);
  253. a.href = url;
  254. a.download = fileName;
  255. a.click();
  256. window.URL.revokeObjectURL(url);
  257. };
  258. }());
  259. document.getElementById('save').addEventListener('click', function() {
  260. $('.link_lines').hide();
  261. var svg = document.getElementById('svg0').cloneNode(true);
  262. function iterRemove(node) {
  263. for(let child of node.children)
  264. iterRemove(child);
  265. if(node.style.display === "none") {
  266. console.log(node);
  267. node.remove();
  268. }
  269. }
  270. iterRemove(svg);
  271. var data = (new XMLSerializer()).serializeToString(svg);
  272. saveData(data, "stammbaum.svg");
  273. });
  274. }
  275. function display(g) {
  276. group = g;
  277. history.replaceState({}, 'Stammbaum ' + (group + 1), '/stammbaum/' + (group !== null ? '?group=' + group : ''));
  278. for(const person of persons) {
  279. if(!person.hasCard)
  280. continue;
  281. if(group === null || person.group_id == group) {
  282. document.getElementById(`card_${person.id}`).style.display = 'inline';
  283. document.getElementById(`line_${person.id}`).style.display = 'inline';
  284. } else {
  285. document.getElementById(`card_${person.id}`).style.display = 'none';
  286. document.getElementById(`line_${person.id}`).style.display = 'none';
  287. }
  288. }
  289. $('.link_lines').hide();
  290. panZoom.updateBBox();
  291. panZoom.fit();
  292. panZoom.center();
  293. }
  294. function panZoomInit() {
  295. return svgPanZoom('#svg0', {
  296. maxZoom: 100,
  297. dblClickZoomEnabled: false,
  298. zoomScaleSensitivity: 0.3,
  299. beforePan: function (oldPan, newPan) {
  300. var stopHorizontal = false
  301. , stopVertical = false
  302. , gutterWidth = 100
  303. , gutterHeight = 100
  304. // Computed variables
  305. , sizes = this.getSizes()
  306. , leftLimit = -((sizes.viewBox.x + sizes.viewBox.width) * sizes.realZoom) + gutterWidth
  307. , rightLimit = sizes.width - gutterWidth - (sizes.viewBox.x * sizes.realZoom)
  308. , topLimit = -((sizes.viewBox.y + sizes.viewBox.height) * sizes.realZoom) + gutterHeight
  309. , bottomLimit = sizes.height - gutterHeight - (sizes.viewBox.y * sizes.realZoom)
  310. customPan = {}
  311. customPan.x = Math.max(leftLimit, Math.min(rightLimit, newPan.x))
  312. customPan.y = Math.max(topLimit, Math.min(bottomLimit, newPan.y))
  313. return customPan
  314. },
  315. customEventsHandler: {
  316. haltEventListeners: ['touchstart', 'touchend', 'touchmove', 'touchleave', 'touchcancel']
  317. , init: function(options) {
  318. var instance = options.instance
  319. , initialScale = 1
  320. , pannedX = 0
  321. , pannedY = 0
  322. // Init Hammer
  323. // Listen only for pointer and touch events
  324. this.hammer = Hammer(options.svgElement, {
  325. inputClass: Hammer.SUPPORT_POINTER_EVENTS ? Hammer.PointerEventInput : Hammer.TouchInput
  326. })
  327. // Enable pinch
  328. this.hammer.get('pinch').set({enable: true})
  329. // Handle double tap
  330. this.hammer.on('doubletap', function(ev){
  331. instance.zoomIn()
  332. })
  333. // Handle pan
  334. this.hammer.on('panstart panmove', function(ev){
  335. // On pan start reset panned variables
  336. if (ev.type === 'panstart') {
  337. pannedX = 0
  338. pannedY = 0
  339. }
  340. // Pan only the difference
  341. instance.panBy({x: ev.deltaX - pannedX, y: ev.deltaY - pannedY})
  342. pannedX = ev.deltaX
  343. pannedY = ev.deltaY
  344. })
  345. // Handle pinch
  346. this.hammer.on('pinchstart pinchmove', function(ev){
  347. // On pinch start remember initial zoom
  348. if (ev.type === 'pinchstart') {
  349. initialScale = instance.getZoom()
  350. instance.zoomAtPoint(initialScale * ev.scale, {x: ev.center.x, y: ev.center.y})
  351. }
  352. instance.zoomAtPoint(initialScale * ev.scale, {x: ev.center.x, y: ev.center.y})
  353. })
  354. // Prevent moving the page on some devices when panning over SVG
  355. options.svgElement.addEventListener('touchmove', function(e){ e.preventDefault(); });
  356. }
  357. , destroy: function(){
  358. this.hammer.destroy()
  359. }
  360. }
  361. });
  362. }