123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- var rowId = [];
- var tree, persons, pairs, groups;
- var group = null;
- var panZoom;
- window.onload = function() {
- const parameterList = new URLSearchParams(window.location.search);
- group = parameterList.get("group");
- tree = new SVGTree(document.getElementById('chart'), 'svg0');
- persons = JSON.parse(document.getElementById('person-data').textContent);
- pairs = JSON.parse(document.getElementById('pair-data').textContent);
- groups = JSON.parse(document.getElementById('group-data').textContent);
-
- // construct persons
- for (let i=0; i<persons.length; i++) {
- const p = persons[i];
- persons[i] = {
- id: p[0],
- name: p[1],
- parent_id: p[2],
- birth_date: p[3] ? new Date(p[3]) : null,
- birth_town: p[4],
- death_date: p[5] ? new Date(p[5]) : null,
- death_town: p[6],
- comment: p[7],
- image: p[8],
- color: p[9],
- group_id: p[10]
- };
- }
- // construct pairs
- for (let i = 0; i < pairs.length; i++) {
- const p = pairs[i];
- pairs[i] = {
- id: p[0],
- from_person_id: p[1],
- to_person_id: p[2]
- };
- }
- // find partners
- for(const pair of pairs) {
- const p1 = persons.find(p => p.id === pair.from_person_id);
- const p2 = persons.find(p => p.id === pair.to_person_id);
- pair.from_person = p1;
- pair.to_person = p2;
- p1.partner = p2;
- p1.pair = pair;
- p2.partner = p1;
- p2.pair = pair;
- }
-
- // find group, parents and children
- for(const person of persons) {
- const group = groups.find(g => g.id == person.group_id);
- const parent = persons.find(p => p.id === person.parent_id && p.id != person.id);
- const children = persons.filter(p => p.parent_id === person.id && p.id != person.id);
- person.group = group;
- person.parent = parent;
- person.children = children;
- }
- // hide persons without tree
- for (const person of persons) {
- person.hasCard =
- person.hasCard ||
- !person.partner ||
- !!person.parent ||
- person.children.length > 0;
- if (person.partner) {
- person.partner.hasCard =
- !person.hasCard ||
- !!person.partner.parent ||
- person.partner.children.length > 0;
- }
- }
- // order persons to allow shifting down level pairs and get related children closer
- persons.sort(function(a, b) {
- if(a.group_id != b.group_id && a.group_id != null)
- return a.group_id - b.group_id;
- function findRelation(p) {
- if (!p.hasCard)
- return null;
- if (p.partner && p.partner.hasCard == true)
- return p.pair.id;
- var ret = null;
- for (const child of p.children){
- ret = findRelation(child);
- if(ret !== null)
- return ret;
- }
- return null;
- }
- if(!a.parent || !b.parent) {
- var relationId = findRelation(a) - findRelation(b);
- if(relationId != null)
- return relationId;
- }
- const dt1 = a.birth_date || a.death_date || "9999-01-01";
- const dt2 = b.birth_date || b.death_date || "9999-01-01";
- return new Date(dt1) - new Date(dt2);
- });
- // add groups
- for(const row of groups) {
- $('#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>`);
- }
- $('#loader').hide();
-
- // define levels by the maximum birth date in that level
- function calculateLevels(personList) {
- personList = personList.concat().sort((a, b) => {
- const dateA = a.birth_date || a.death_date
- const dateB = b.birth_date || b.death_date
- return dateA - dateB
- })
- let levels = []
- let currentPersons = []
- for(const person of personList) {
- const date = person.birth_date || person.death_date
- if(!date || !person.hasCard)
- continue
- if(person.parent && currentPersons.find(p => person.parent.id === p.id)) {
- levels.push(date - 1)
- currentPersons = [person]
- } else {
- currentPersons.push(person)
- }
- }
- levels.push(+personList[personList.length - 1].birth_date || +personList[personList.length - 1].death_date)
- return levels
- }
- const levels = calculateLevels(persons)
- /**
- * calculate position of cards
- * @param {*} personList current persons
- * @param {*} levels reference to levels
- * @param {*} level row of the current card
- * @param {*} accX maximum X positions per layer
- * @returns width of the tree
- */
- function assignCoords(personList, levels, level=0, accX=[0]) {
-
- personList = personList.filter(p => p.hasCard);
- const margin = 20;
-
- let startX = 0, endX = 0;
- for (let j=0; j<personList.length; j++) {
- const person = personList[j];
- // set y
- const date = person.birth_date || person.death_date
- person.level = Math.max(level, levels.findIndex(l => l >= +date && date != null));
- if(person.partner && person.partner.level > person.level) {
- person.level = person.partner.level;
- }
- person.y = person.level * (tree.cardHeight + 40);
- // set x
- accX[person.level + 1] = accX[person.level + 1] || 0;
- // fill in skipped layers
- for(var i=level; i<=person.level; i++)
- accX[i] = accX.slice(level,person.level+1).reduce((acc, x) => Math.max(acc, x), 0);
- const width = tree.cardWidth * (person.partner == null ? 1 : 2);
- const origAccX = accX.slice();
- const subWidth = assignCoords(person.children, levels, person.level + 1, accX);
- const requiredForSiblings = personList.slice(j + 1).reduce((acc, p) => acc + tree.cardWidth * (person.partner == null ? 1 : 2) + margin, 0);
- const remainingWidthAboveChildren = accX[person.level+1] - accX[person.level];
- const idealX = subWidth > 0 ? accX[person.level + 1] - subWidth / 2 - width / 2 : accX[person.level] + margin;
- if (idealX >= accX[person.level] + margin) {
- // perfect fit
- person.x = idealX;
- } else if(remainingWidthAboveChildren > margin + width) {
- // still fits above children
- person.x = accX[person.level] + margin;
- } else if(subWidth == 0) {
- // no children
- person.x = accX[person.level] + margin;
- } else if (subWidth < width) {
- // center children under this card to avoid overlap
- person.x = accX[person.level] + margin;
- for (var i = person.level + 1; i < accX.length; i++)
- accX[i] = Math.max(origAccX[i], person.x + width/2 - subWidth/2 - margin);
- assignCoords(person.children, levels, person.level + 1, accX);
- } else {
- // we need to move children under this card to avoid overlap
- person.x = accX[person.level] + margin;
- for (var i = person.level + 1; i < accX.length; i++)
- accX[i] = Math.max(origAccX[i], person.x + width - subWidth - margin);
- assignCoords(person.children, levels, person.level + 1, accX);
- // center this node again
- person.x = Math.max(origAccX[person.level] + margin, accX[person.level + 1] - subWidth / 2 - width / 2);
- }
- if(!startX)
- startX = person.x;
- endX = person.x + width;
- accX[person.level] = person.x + width;
- // fill in skipped layers
- for (var i = level; i <= person.level; i++)
- accX[i] = accX.slice(level, person.level + 1).reduce((acc, x) => Math.max(acc, x), 0);
- }
- return endX - startX;
- }
- assignCoords(persons.filter(p => p.parent_id == null), levels);
-
- // draw links
- for(const link of pairs) {
- const minId = Math.min(link.from_person_id, link.to_person_id);
- const maxId = Math.max(link.from_person_id, link.to_person_id);
- if (!document.getElementById(`link_line_${minId}_${maxId}`)) {
- if (link.from_person.hasCard && link.to_person.hasCard)
- tree.drawLink(link);
- }
- }
- let drag = false;
- for(const person of persons) {
- if(!person.hasCard)
- continue;
- let color = person.color;
- if(!person.partner || !person.partner.hasCard) {
- color = null;
- }
-
- const card = tree.drawCard(person, person.group ? person.group.color : "red", color);
- card.addEventListener('mousedown', () => drag = false);
- card.addEventListener('mousemove', () => drag = true);
- card.addEventListener('mouseover', onMouseover);
- card.addEventListener('click', onSelect);
- }
- for(const person of persons) {
- if(person.hasCard)
- tree.drawLines(person, person.children, person.group ? person.group.color : "#ff0000");
- }
- function onMouseover(event) {
- const person_id = this.id.replace('card_', '');
- const link = pairs.find(r => r.from_person_id == person_id || r.to_person_id == person_id);
- if(link == null)
- return;
-
- if (group === null || link.from_person.group_id === link.to_person.group_id) {
- const minId = Math.min(link.from_person_id, link.to_person_id);
- const maxId = Math.max(link.from_person_id, link.to_person_id);
- $('.link_lines').hide();
- $(`#link_line_${minId}_${maxId}`).show();
- }
- }
- function onSelect(event) {
- if(drag) {
- event.preventDefault();
- } else {
- const person = persons.find(p => p.id == this.id.replace('card_', ''));
- $('#img_name').html(`${person.name} (${person.group_id})` + (person.partner != null ? '<br>' + person.partner.name : ''));
- $('#name').val(person.name);
- $('#form').attr('action', `/stammbaum/person/${person.id}/upload`);
- $('.info').css('display', 'inline-block');
- }
- }
- panZoom = panZoomInit();
- display(group);
- var saveData = (function () {
- var a = document.createElement("a");
- document.body.appendChild(a);
- a.style = "display: none";
- return function (data, fileName) {
- var blob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
- var url = window.URL.createObjectURL(blob);
- a.href = url;
- a.download = fileName;
- a.click();
- window.URL.revokeObjectURL(url);
- };
- }());
- document.getElementById('save').addEventListener('click', function() {
- $('.link_lines').hide();
- var svg = document.getElementById('svg0').cloneNode(true);
- function iterRemove(node) {
- for(let child of node.children)
- iterRemove(child);
- if(node.style.display === "none") {
- console.log(node);
- node.remove();
- }
- }
- iterRemove(svg);
- var data = (new XMLSerializer()).serializeToString(svg);
- saveData(data, "stammbaum.svg");
- });
- }
- function display(g) {
- group = g;
- history.replaceState({}, 'Stammbaum ' + (group + 1), '/stammbaum/' + (group !== null ? '?group=' + group : ''));
-
- for(const person of persons) {
- if(!person.hasCard)
- continue;
- if(group === null || person.group_id == group) {
- document.getElementById(`card_${person.id}`).style.display = 'inline';
- document.getElementById(`line_${person.id}`).style.display = 'inline';
- } else {
- document.getElementById(`card_${person.id}`).style.display = 'none';
- document.getElementById(`line_${person.id}`).style.display = 'none';
- }
- }
- $('.link_lines').hide();
- panZoom.updateBBox();
- panZoom.fit();
- panZoom.center();
- }
- function panZoomInit() {
- return svgPanZoom('#svg0', {
- maxZoom: 100,
- dblClickZoomEnabled: false,
- zoomScaleSensitivity: 0.3,
- beforePan: function (oldPan, newPan) {
- var stopHorizontal = false
- , stopVertical = false
- , gutterWidth = 100
- , gutterHeight = 100
- // Computed variables
- , sizes = this.getSizes()
- , leftLimit = -((sizes.viewBox.x + sizes.viewBox.width) * sizes.realZoom) + gutterWidth
- , rightLimit = sizes.width - gutterWidth - (sizes.viewBox.x * sizes.realZoom)
- , topLimit = -((sizes.viewBox.y + sizes.viewBox.height) * sizes.realZoom) + gutterHeight
- , bottomLimit = sizes.height - gutterHeight - (sizes.viewBox.y * sizes.realZoom)
- customPan = {}
- customPan.x = Math.max(leftLimit, Math.min(rightLimit, newPan.x))
- customPan.y = Math.max(topLimit, Math.min(bottomLimit, newPan.y))
- return customPan
- },
- customEventsHandler: {
- haltEventListeners: ['touchstart', 'touchend', 'touchmove', 'touchleave', 'touchcancel']
- , init: function(options) {
- var instance = options.instance
- , initialScale = 1
- , pannedX = 0
- , pannedY = 0
- // Init Hammer
- // Listen only for pointer and touch events
- this.hammer = Hammer(options.svgElement, {
- inputClass: Hammer.SUPPORT_POINTER_EVENTS ? Hammer.PointerEventInput : Hammer.TouchInput
- })
- // Enable pinch
- this.hammer.get('pinch').set({enable: true})
- // Handle double tap
- this.hammer.on('doubletap', function(ev){
- instance.zoomIn()
- })
- // Handle pan
- this.hammer.on('panstart panmove', function(ev){
- // On pan start reset panned variables
- if (ev.type === 'panstart') {
- pannedX = 0
- pannedY = 0
- }
- // Pan only the difference
- instance.panBy({x: ev.deltaX - pannedX, y: ev.deltaY - pannedY})
- pannedX = ev.deltaX
- pannedY = ev.deltaY
- })
- // Handle pinch
- this.hammer.on('pinchstart pinchmove', function(ev){
- // On pinch start remember initial zoom
- if (ev.type === 'pinchstart') {
- initialScale = instance.getZoom()
- instance.zoomAtPoint(initialScale * ev.scale, {x: ev.center.x, y: ev.center.y})
- }
- instance.zoomAtPoint(initialScale * ev.scale, {x: ev.center.x, y: ev.center.y})
- })
- // Prevent moving the page on some devices when panning over SVG
- options.svgElement.addEventListener('touchmove', function(e){ e.preventDefault(); });
- }
- , destroy: function(){
- this.hammer.destroy()
- }
- }
- });
- }
|