forked from gramps-project/addons-source
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDenominoViso.py
3202 lines (2996 loc) · 149 KB
/
DenominoViso.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# DenominoViso - a plugin for GRAMPS, the GTK+/GNOME based genealogy program,
# that creates an Ancestor Chart Map.
#
# Copyright (C) 2007-2011 Michiel D. Nauta
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# version 2.3
# fixed bug-nr: 4681
# The basic idea of this plugin is very simple: create an ancestry map in SVG
# supply each rectangle with an event handler and the data of a person packed
# in a JavaScript object. The head section of the document should contain
# the necessary functions to unpack the JavaScript object and represent
# it as HTML.
# TODO
# use ReportUtils.get_address_str() in Gramps2.4.5
# add keyboard navigation.
# Add method to allow user to see data as GEDCOM/Gramps-xml.
# Add option to prefer baptism above birth
# Apply privacy on people still alive.
# Hourglass-mode
# Add repositories
# CONTENT
# _cnsts
# polar2cart
# hex2int_color
# DenominoViso
# __init__
# get_event_attribute_types
# write_report
# walk_the_tree_depth_asc
# walk_the_tree_depth_desc
# walk_the_tree_asc
# walk_the_tree_desc
# sort_family_list
# sort_child_list
# fan_segment
# growthspiral_segment
# matree_segment
# pytree_segment
# get_birth_confidence
# relationship_line
# generation2coord
# add_personal_data
# mouse_event_handler
# pack_person_img
# pack_person_url
# pack_birth_death_data
# unpack_birth_death_data
# pack_event_data
# sort_event_ref_list
# get_event_description
# event_is_birth
# event_is_death
# event_is_wanted
# get_death_estimate
# unpack_event_data
# pack_attribute_data
# unpack_attribute_data
# pack_address_data
# unpack_address_data
# pack_note_data
# unpack_note_data
# pack_source_data
# unpack_source_data
# get_family_event_refs
# get_death_ref_or_fallback
# event_role2names
# marriage_event2spouse_name
# marriage_event2parent_names
# witnesses2JS
# event_source2JS
# event_roles2JS
# event_attributes2JS
# photo2JS
# get_copied_photo_name
# img_attr_check
# relpathA2B
# privacy_filter
# escbacka
# start_page
# get_css_style
# get_javascript_functions
# end_page
# get_html_search_options
# write_old_browser_output
# DenominoVisoOptions
# __init__
# set_new_options
# add_user_options
# array2table
# img_toggled
# dash_toggled
# on_dash_length_edited
# on_inter_dash_length_edited
# on_conf_color_edited
# parse_user_options
# DenominoVisoDialog
# __init__
# setup_style_frame
# etc.
#-------------------------------------------------------------------------
#
# python modules
#
#-------------------------------------------------------------------------
import os
import shutil
import re
from urllib.parse import quote, splittype
from urllib.request import pathname2url
from xml.sax.saxutils import escape, quoteattr
from math import sin,cos,exp,sqrt,e,pi
import codecs
#-------------------------------------------------------------------------
#
# gtk
#
#-------------------------------------------------------------------------
from gi.repository import GObject
from gi.repository import Gtk
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from gramps.gen.sort import Sort
from gramps.gen.plug.report import Report
from gramps.gen.plug.report import MenuReportOptions
import gramps.gen.plug.report.utils as ReportUtils
#from ReportBase._CommandLineReport import CommandLineReport
from gramps.gen.errors import DatabaseError
from gramps.gui.dialog import ErrorDialog, WarningDialog
from gramps.gui.plug.report._fileentry import FileEntry
from gramps.gen.plug.menu import NumberOption, BooleanOption, TextOption, PersonOption, EnumeratedListOption, ColorOption, DestinationOption, StringOption
from gramps.gen.display.name import displayer as _nd
from gramps.gen.display.place import displayer as place_displayer
import gramps.gen.datehandler as datehandler
from gramps.gui.autocomp import fill_combo
from gramps.gen.lib import EventType, EventRoleType, ChildRefType, AttributeType, Date
from gramps.gen.utils.string import conf_strings as confidence
from gramps.gen.utils.file import media_path_full
from gramps.gen.plug.menu import Option as PlugOption
from gramps.gen.proxy import PrivateProxyDb
from gramps.gen.utils.db import get_birth_or_fallback
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.const import USER_HOME
#-------------------------------------------------------------------------
#
# constants
#
#-------------------------------------------------------------------------
try:
_trans = glocale.get_addon_translator(__file__)
except ValueError:
_trans = glocale.translation
_ = _trans.gettext
ext_confidence = confidence.copy()
ext_confidence[len(confidence)] = _('No Source')
class _cnsts:
ANCESTOR = 0
DESCENDANT = 1
REGULAR = 0
FAN = 1
GROWTHSPIRAL = 2
MATREE = 3
PYTREE = 4
ONCLICK = 0
ONMOUSEOVER = 1
RIGHT2LEFT = 0
LEFT2RIGHT = 1
TOP2BOTTOM = 2
BOTTOM2TOP = 3
BIRTH_REL_COLUMN = 0
USE_DASH_COLUMN = 1
DASH_LENGTH_COLUMN = 2
INTER_DASH_LENGTH_COLUMN = 3
CONFIDENCE_COLUMN = 0
COLOR_COLUMN = 1
mouse_events = [
(ONCLICK, _("onclick")),
(ONMOUSEOVER, _("onmouseover"))
]
chart_mode = [
(ANCESTOR, _('Ancestor')),
(DESCENDANT,_('Descendant'))
]
chart_type = [
(REGULAR, _('Regular')),
(FAN, _('Fan')),
(GROWTHSPIRAL, _('Growth Spiral')),
(MATREE, _('Mandelbrot Tree')),
(PYTREE, _('Pythagoras Tree'))
]
time_direction = [
(RIGHT2LEFT, _('right to left')),
(LEFT2RIGHT, _('left to right')),
(TOP2BOTTOM, _('top to bottom')),
(BOTTOM2TOP, _('bottom to top'))
]
def polar2cart(r,phi):
x = r*cos(phi)
y = r*sin(phi)
return x,y
def hex2int_color(x):
"""Return the decimal representation of a given hex color string.
x: #e112ff a color in hex notation"""
return ",".join([str(int(x[i+1:i+3],16)) for i in [0,2,4]])
def list_of_strings2list_of_lists(data):
# If the option (DNMdash_child_rel or DNMconf_color) is read from the
# report_options.xml it is a list of strings, while if it comes from
# the widget or the default it is a list of lists.
if type(data[0]) == type([]):
return data
else:
rv = []
for line in data:
# remove older Python 'L' on ints
line = re.sub(r'(\d+)L', r'\1', line)
if line[0] != '[' or line[-1] != ']':
raise TypeError('invalid list-option value')
rv.append(eval(line))
return rv
class DenominoVisoReport(Report):
def __init__(self, database, options_class, user):
Report.__init__(self, database, options_class, user)
self.options = {}
menu = options_class.menu
self.database = database
for name in menu.get_all_option_names():
self.options[name] = menu.get_option_by_name(name).get_value()
self.options['DNMexl_private'] = not self.options['DNMuse_privacy']
self.start_person = database.get_person_from_gramps_id(self.options['DNMpid'])
self.rect_xdist = 100.0
self.rect_width = self.options['DNMrect_width']*self.rect_xdist
if self.options['DNMrect_height'] >= self.options['DNMrect_ydist']:
self.options['DNMrect_height'] = 0.9*self.options['DNMrect_ydist']
self.rect_height = self.options['DNMrect_height']*self.rect_xdist
self.rect_ydist = self.options['DNMrect_ydist']*self.rect_xdist
self.advance = 1.0
self.target_path = self.options['DNMfilename']
self.open_subwindow = self.options['DNMtree_width'] > 90
self.copyright = '\n'.join(self.options['DNMcopyright'])
# split megawidget options
# IncAttributeOption into DNMinc_attributes en DNMinc_att_list
(self.options['DNMinc_attributes'], self.options['DNMinc_att_list']) =\
self.options['DNMinc_attributes_m'].split(', ',1)
self.options['DNMinc_attributes'] = self.options['DNMinc_attributes'] == 'True'
# CopyImgOption into DNMcopy_img and DNMcopy_dir
(self.options['DNMcopy_img'], self.options['DNMcopy_dir']) = \
self.options['DNMcopy_img_m'].split(', ',1)
self.options['DNMcopy_img'] = self.options['DNMcopy_img'] == 'True'
# ImageIncludeAttrOption into DNMinexclude_img, DNMimg_attr4inex, DNMimg_attr_val4inex
(self.options['DNMinexclude_img'], self.options['DNMimg_attr4inex'],
self.options['DNMimg_attr_val4inex']) = self.options['DNMimg_attr_m'].split(', ',2)
self.options['DNMinexclude_img'] = int(
self.options['DNMinexclude_img'].replace('L', ''))
# HtmlWrapperOption into DNMold_browser_output and DNMfilename4old_browser
# MouseHandlerOption
# LineStyleOption
# ConfidenceColorOption
# old_browser_output is deprecated because IE9 can display svg.
#(self.options['DNMold_browser_output'], self.options['DNMfilename4old_browser']) = \
# self.options['DNMold_browser_output_m'].split(', ',1)
#self.options['DNMold_browser_output'] = self.options['DNMold_browser_output'] == 'True'
self.options['DNMdash_child_rel'] = list_of_strings2list_of_lists(
self.options['DNMdash_child_rel'])
self.options['DNMconf_color'] = list_of_strings2list_of_lists(
self.options['DNMconf_color'])
self.event_format = '\n'.join(self.options['DNMevent_format'])
placeholders = re.findall('<.+?>',self.event_format)
placeholders = set(placeholders)
placeholders -= set(['<' + _('type') + '>', \
'<' + _('role') + '>', \
'<' + _('date') + '>', \
'<' + _('place') + '>', \
'<' + _('description') + '>', \
'<' + _('witnesses') + '>', \
'<' + _('source') + '>'])
placeholders = map(lambda x: x.strip('<>'),placeholders)
placeholders = set(placeholders)
#roles = RelLib.EventRoleType().get_standard_names()
roles = EventRoleType().get_standard_names()
roles.extend(self.database.get_event_roles())
roles = set(roles)
self.event_format_roles = placeholders & roles
placeholders -= self.event_format_roles
# perhaps remove Family,Custom,Unknown from self.event_format_roles?
attributes = AttributeType().get_standard_names()
#attributes = RelLib.AttributeType().get_standard_names()
attributes.extend(self.get_event_attribute_types())
attributes = set(attributes)
self.event_format_attributes = placeholders & attributes
# if the user wants to see the source of events, she will probably
# also want to see the source of attributes.
self.options_inc_attr_source = ('<' + _('source') + '>') in \
self.event_format
self.options_inc_addr_source = self.options_inc_attr_source
source_ref_formats = [i for i in enumerate(self.options['DNMevent_format']) \
if ('<' + _('source') + '>') in i[1]]
if len(source_ref_formats) > 0:
# add the character on the line before the <source> line
# if it is a punctuation mark such a newline, comma, semi-colon.
self.source_ref_format = '\n' + source_ref_formats[0][1]
if source_ref_formats[0][0] > 0:
try:
end_char_line_before = self.options['DNMevent_format'][source_ref_formats[0][0]-1][-1]
# assume end_char_line_b is printable then this = [:punct:]
if (not end_char_line_before.isalnum() and not \
end_char_line_before.isspace() and \
end_char_line_before != '>'):
add_char = end_char_line_before
else:
add_char = ''
except IndexError:
add_char = '\n'
self.source_ref_format = add_char + self.source_ref_format
else:
self.source_ref_format = ''
self.source_format = '\n'.join(self.options['DNMsource_format'])
# if any conf_color deviates from the default, use colors
self.colorcode_confidence = len([i[_cnsts.COLOR_COLUMN] for i in \
self.options['DNMconf_color'] if i[_cnsts.COLOR_COLUMN] != \
self.options['DNMconf_color'][-1][_cnsts.COLOR_COLUMN]]) > 0
self.person_srcs = []
self.person_imgs = set([]) # makes images being shown only once.
self.person_img_srcs = []
self.copied_imgs = {}
self.search_subjects = {}
self.sort = Sort(self.database)
def get_event_attribute_types(self):
"""There should be a function GrampsDb/_GrampsDbBase that does this!"""
rv = set()
for handle in self.database.get_event_handles():
event= self.database.get_event_from_handle(handle)
if event:
for attr in event.get_attribute_list():
if attr.type.is_custom() and str(attr.type):
rv.add(str(attr.get_type()))
return rv
def write_report(self):
# old_browser_output is depricated because IE9 can display svg.
#if self.options['DNMold_browser_output']:
# self.write_old_browser_output()
try:
with codecs.open(self.target_path, 'w', encoding='UTF-8') as f:
startup = {}
startup[_cnsts.FAN] = ((0,-pi,pi), (0,0,2*pi), \
(0,pi/2,5*pi/2), (0,-pi/2,3*pi/2))
startup[_cnsts.GROWTHSPIRAL] = ((10,50,-pi/2), (10,50,pi/2), \
(10,50,pi), (10,50,0))
startup[_cnsts.MATREE] = ((0,-50,0,50), (0,50,0,-50), \
(-50,0,50,0), (50,0,-50,0))
startup[_cnsts.PYTREE] = ((0,50,0,-50), (0,-50,0,50), \
(50,0,-50,0), (-50,0,50,0))
self.start_page(f)
if self.start_person:
start_handle = self.start_person.get_handle()
else:
ErrorDialog(_('Failure writing %s: %s') % (self.target_path,
_('No central person selected')))
return
if self.options['DNMchart_type'] == _cnsts.REGULAR:
if self.options['DNMchart_mode'] == _cnsts.DESCENDANT:
self.walk_the_tree_depth_desc(f, start_handle, 0)
else:
self.walk_the_tree_depth_asc(f, start_handle, 0)
else:
if self.options['DNMchart_mode'] == _cnsts.DESCENDANT:
self.walk_the_tree_desc(f, start_handle, 0,\
startup[self.options['DNMchart_type']][self.options['DNMtime_dir']])
else:
self.walk_the_tree_asc(f, start_handle, 0,\
startup[self.options['DNMchart_type']][self.options['DNMtime_dir']])
self.end_page(f)
except IOError as msg:
ErrorDialog(_('Failure writing %s: %s') % (self.target_path,str(msg)))
return
# I need four tree-walking routines: for ascestor/descendant mode and for
# depth-first or not. So they are all quite similar but not similar enough
# to merge them.
def walk_the_tree_depth_asc(self,f,person_handle,generation):
"""Traverse the ancestor tree depth first and call the necessary
functions to write the data to file.
Arguments:
f Fileobject for output.
person_handle Database handle of the present person.
generation Integer indicating the generation of person (x-coord).
It takes one step in the recursive traversal, returning the new \
cross-coordinate."""
if not person_handle:
return
if self.options['DNMtime_dir'] >= _cnsts.TOP2BOTTOM:
advance_incr = self.rect_xdist
else:
advance_incr = self.rect_ydist
cross_coord = self.advance # coord perpendicular to generation-coord.
person = self.database.get_person_from_handle(person_handle)
family_handle = person.get_main_parents_family_handle()
birth_confidence = ""
if self.colorcode_confidence:
birth_confidence = self.get_birth_confidence(person)
if family_handle and \
generation < (self.options['DNMmax_generations'] - 1):
family = self.database.get_family_from_handle(family_handle)
relationList = [(ref.get_mother_relation(),ref.get_father_relation())\
for ref in family.get_child_ref_list() if \
('Person',person_handle) in ref.get_referenced_handles()]
if len(relationList) > 0:
(mrel,frel) = relationList[0]
else:
raise DatabaseError("Can't find person " +
person.get_gramps_id()+" in family "+family.get_gramps_id())
father_coord = self.walk_the_tree_depth_asc(f,family.get_father_handle(),\
generation+1)
self.advance += advance_incr
mother_coord = self.walk_the_tree_depth_asc(f,family.get_mother_handle(),\
generation+1)
if father_coord and mother_coord:
cross_coord = (father_coord + mother_coord)/2.0
self.relationship_line(f,generation,cross_coord, \
father_coord,frel,birth_confidence)
self.relationship_line(f,generation,cross_coord, \
mother_coord,mrel,birth_confidence)
elif father_coord:
cross_coord = father_coord + advance_incr/2.0
self.relationship_line(f,generation,cross_coord, \
father_coord,frel,birth_confidence)
elif mother_coord:
cross_coord = mother_coord - advance_incr/2.0
self.relationship_line(f,generation,cross_coord, \
mother_coord,mrel,birth_confidence)
self.add_personal_data(f,person,generation,(cross_coord,))
return cross_coord
# should add birth_confidence
def walk_the_tree_depth_desc(self,f,person_handle,generation,parent_in_middle=False):
"""Traverse the tree in a depth first manner getting the children of
person person_handle."""
if not person_handle:
return
cross_coord = self.advance
cross_coord_child = []
prel = []
person = self.database.get_person_from_handle(person_handle)
family_handle_list = person.get_family_handle_list()
family_handle_list.sort(key=self.sort_family_list)
for family_handle in family_handle_list:
if family_handle and generation > (-self.options['DNMmax_generations'] + 1):
family = self.database.get_family_from_handle(family_handle)
person_is_mother = family.get_mother_handle() == person_handle
child_ref_list = family.get_child_ref_list()
if len(child_ref_list) > 0:
child_ref_list.sort(key=self.sort_child_list)
for child_ref in child_ref_list:
if child_ref:
if person_is_mother:
prel.append(child_ref.get_mother_relation())
else:
prel.append(child_ref.get_father_relation())
cross_coord_child.append(self.walk_the_tree_depth_desc(f,child_ref.ref,generation-1))
if len(cross_coord_child) > 0:
if len(cross_coord_child) > 1 and parent_in_middle:
cross_coord = (cross_coord_child[0] + cross_coord_child[-1])/2.0
else:
cross_coord = cross_coord_child[0]
for xy,rel in zip(cross_coord_child,prel):
self.relationship_line(f,generation,cross_coord,xy,rel,0)
self.add_personal_data(f,person,generation,(cross_coord,))
if self.options['DNMtime_dir'] < _cnsts.TOP2BOTTOM:
self.advance += self.rect_ydist
else:
self.advance += self.rect_xdist
return cross_coord
def walk_the_tree_asc(self,f,person_handle,generation,attachment_segment):
"""Tree walker that draws the node first before going to the parents."""
if not person_handle:
return
person = self.database.get_person_from_handle(person_handle)
family_handle = person.get_main_parents_family_handle()
if family_handle and generation < (self.options['DNMmax_generations']-1):
family = self.database.get_family_from_handle(family_handle)
nr_attachees = 2
attach_points = self.add_personal_data(f,person,generation,\
attachment_segment,nr_attachees)
self.walk_the_tree_asc(f,family.get_father_handle(),generation+1,\
attach_points[0])
self.walk_the_tree_asc(f,family.get_mother_handle(),generation+1,\
attach_points[1])
else:
self.add_personal_data(f,person,generation,attachment_segment)
return
def walk_the_tree_desc(self,f,person_handle,generation,attachment_segment):
"""Tree walker that draws the node first before going to the kids."""
if not person_handle:
return
nr_attachees = 0
child_refs = []
person = self.database.get_person_from_handle(person_handle)
family_handles = person.get_family_handle_list()
family_handles.sort(key=self.sort_family_list)
for family_handle in family_handles:
if family_handle and generation > (-self.options['DNMmax_generations']+1):
family = self.database.get_family_from_handle(family_handle)
child_ref_list = family.get_child_ref_list()
nr_attachees += len(child_ref_list)
child_ref_list.sort(key=self.sort_child_list)
child_refs.extend(child_ref_list)
if nr_attachees == 0:
self.add_personal_data(f,person,generation,attachment_segment)
else:
attach_points = self.add_personal_data(f,person,generation,\
attachment_segment,nr_attachees)
for i,ref in enumerate(child_refs):
self.walk_the_tree_desc(f,ref.ref,generation-1,attach_points[i])
return
def sort_family_list(self, family_id):
"""Function called by a sort to order the families according to
marriage date."""
if not family_id:
return Date()
family = self.database.get_family_from_handle(family_id)
if not family:
return Date()
event_ref_list = family.get_event_ref_list()
marriage = None
for ref in event_ref_list:
event = self.database.get_event_from_handle(ref.ref)
if event.get_type() == EventType.MARRIAGE:
marriage = event
break
if not marriage:
return Date()
return marriage.get_date_object()
def sort_child_list(self, child_id):
"""Function called by a sort to order the children according to birth
date."""
if not child_id:
return Date()
child = self.database.get_person_from_handle(child_id.ref)
if not child:
return Date()
birth_ref = child.get_birth_ref()
if not birth_ref:
return Date()
birth = self.database.get_event_from_handle(birth_ref.ref)
if not birth:
return Date()
return birth.get_date_object()
def fan_segment(self,nr_ret_attach_points,att_rad,att_phi1,att_phi2):
"""Return the svg-pathstring to draw a fan-segment which is to be
attached to a circle with radius att_rad from angle att_ph1 to
att_phi2. Also return the attachment parameters for the next
parent/child segments."""
delta_r = 100
rel_att = [] # attachment segments for relatives
if att_rad == 0:
path_str = "M-100,0A100,100 0 1,1 -100,1"
outer_rad = delta_r
else:
x1,y1 = polar2cart(att_rad,att_phi1)
x2,y2 = polar2cart(att_rad,att_phi2)
outer_rad = att_rad + delta_r
x3,y3 = polar2cart(outer_rad,att_phi2)
x4,y4 = polar2cart(outer_rad,att_phi1)
path_str = "M%d,%dA%d,%d 0 0,1 %d,%dL%d,%dA%d,%d 0 0,0 %d,%dZ" % \
(x1,y1,att_rad,att_rad,x2,y2,x3,y3,outer_rad,outer_rad,x4,y4)
if nr_ret_attach_points > 0:
delta_phi = (att_phi2-att_phi1)/nr_ret_attach_points
for i in range(0,nr_ret_attach_points):
rel_att.append((outer_rad,att_phi1+i*delta_phi,\
att_phi1+(i+1)*delta_phi))
return (path_str,rel_att)
def growthspiral_segment(self,nr_attach_points,att_rad1,att_rad2,att_phi):
# r = r0*exp(0.69*phi)
rel_att = []
angular_step = 1.0
x1,y1 = polar2cart(att_rad1,att_phi)
r1 = att_rad1*exp(0.69*angular_step)
x2,y2 = polar2cart(r1,att_phi+angular_step)
r2 = att_rad2*exp(0.69*angular_step)
x3,y3 = polar2cart(r2,att_phi+angular_step)
x4,y4 = polar2cart(att_rad2,att_phi)
path_str = "M%d,%dL%d,%dL%d,%dL%d,%dZ" % (x1,y1,x2,y2,x3,y3,x4,y4)
if nr_attach_points > 0:
delta_r = (r2-r1)/nr_attach_points
for i in range(nr_attach_points-1,-1,-1):
rel_att.append((r1+i*delta_r,r1+(i+1)*delta_r,att_phi+angular_step))
return (path_str,rel_att)
def matree_segment(self,x1,y1,x2,y2):
"""Mandelbrot tree segment, make a drawing to understand what's below"""
twig_length = 5 # self.twig_length
shrink = 1.47 # theoretically this number should be larger
rel_att = []
if y1 == y2:
width = abs(x2-x1)
height = twig_length*width
if x1 < x2: # |-|
x = x1 #
y = y1 - height # | |
rel_att.append((x,y+width/shrink,x,y)) # | |
rel_att.append((x2,y,x2,y+width/shrink)) # |-|
else: # |-|
x = x2 # | |
y = y2 # | |
rel_att.append((x1,y+height-width/shrink,x1,y+height)) #
rel_att.append((x,y+height,x,y+height-width/shrink)) # |-|
else:
height = abs(y2-y1)
width = twig_length*height
if y1 < y2: # ----- -
x = x1 # | |
y = y1 # ----- -
rel_att.append((x+width-height/shrink,y,x+width,y))
rel_att.append((x+width,y2,x+width-height/shrink,y2))
else: # - -----
x = x2 - width # | |
y = y2 # - -----
rel_att.append((x1-width+height/shrink,y1,x1-width,y1))
rel_att.append((x2-width,y2,x2-width+height/shrink,y2))
path_str = 'rect x="%d" y="%d" width="%d" height="%d"' % \
(x,y,width,height)
return (path_str,rel_att)
def pytree_segment(self,x1,y1,x2,y2):
"""Pythagoras tree segment"""
twig_length = 4 # self.twig_length?
shrink = 0.68 # value between 0.5 and 1
rel_att = []
x3 = x2 + twig_length*(y1-y2)
y3 = y2 + twig_length*(x2-x1)
x5 = x1 + twig_length*(y1-y2)
y5 = y1 + twig_length*(x2-x1)
x4 = (x3+x5)/2 + sqrt(shrink**2-0.25)*(y5-y3)
y4 = (y3+y5)/2 + sqrt(shrink**2-0.25)*(x3-x5)
path_str = 'M%d,%dL%d,%dL%d,%dL%d,%dL%d,%dZ' % \
(x1,y1,x2,y2,x3,y3,x4,y4,x5,y5)
rel_att.append((x4,y4,x3,y3))
rel_att.append((x5,y5,x4,y4))
return (path_str,rel_att)
def get_birth_confidence(self,person):
"""Return the numerical confidence level of the source of the
birth-event or an empty string"""
rv = ""
birth = get_birth_or_fallback(self.database,person)
if birth:
citations = birth.get_citation_list()
if citations:
citation = self.database.get_citation_from_handle(citations[0])
rv = citation.get_confidence_level()
return rv
def relationship_line(self,f,generation,y_person,y_relative,rel,birth_conf):
"""Write the SVG to create a line connecting a parent and child.
f Fileobject for output.
generation integer for the generation of the child.
y_person y_coordinate of the person.
y_relative y_coordinate of the relavive.
rel object of type ChildRelType indicating kind of
parent-child relationship
birth_conf integer indicating confidence in relationship.
If the rel is one that is drawn with dashes or if the confidence
is of a color that deviates from the default, the line is assigned
to specific classes.
It is not enough to set the color of every birth_conf but one
needs to actually remove the class assignment because it might
otherwise leak how the user thinks about certain sources."""
cls = ""
if self.options['DNMchart_mode'] == _cnsts.DESCENDANT:
mode = -1
else:
mode = 1
if birth_conf != "":
if self.options['DNMconf_color'][birth_conf][_cnsts.COLOR_COLUMN] \
!= self.options['DNMconf_color'][-1][_cnsts.COLOR_COLUMN]:
cls += ext_confidence[birth_conf].replace(' ','')
if rel.xml_str() in [i[_cnsts.BIRTH_REL_COLUMN] for i in \
self.options['DNMdash_child_rel'] if i[_cnsts.USE_DASH_COLUMN]]:
cls += ' ' + rel.xml_str().replace(' ','')
if self.options['DNMtime_dir'] < _cnsts.TOP2BOTTOM:
x1 = self.generation2coord(generation)
y1 = y_person
x2 = self.generation2coord(generation + mode*1)
y2 = y_relative
if (self.options['DNMchart_mode'] == _cnsts.ANCESTOR and \
self.options['DNMtime_dir'] == _cnsts.RIGHT2LEFT) or \
(self.options['DNMchart_mode'] == _cnsts.DESCENDANT and \
self.options['DNMtime_dir'] == _cnsts.LEFT2RIGHT) :
f.write('<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" class="%s"/>\n' \
% (x1+self.rect_width/2,y1,x2-self.rect_width/2,y2,cls.strip()))
else:
f.write('<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" class="%s"/>\n' \
% (x1-self.rect_width/2,y1,x2+self.rect_width/2,y2,cls.strip()))
else: # time_dir >= TOP2BOTTOM
x1 = y_person
y1 = self.generation2coord(generation)
x2 = y_relative
y2 = self.generation2coord(generation + mode*1)
if (self.options['DNMchart_mode'] == _cnsts.ANCESTOR and \
self.options['DNMtime_dir'] == _cnsts.TOP2BOTTOM) or \
(self.options['DNMchart_mode'] == _cnsts.DESCENDANT and \
self.options['DNMtime_dir'] == _cnsts.BOTTOM2TOP):
f.write('<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" class="%s"/>\n' \
% (x1,y1-self.rect_height/2,x2,y2+self.rect_height/2,cls.strip()))
else:
f.write('<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" class="%s" />\n' \
% (x1,y1+self.rect_height/2,x2,y2-self.rect_height/2,cls.strip()))
return
def generation2coord(self,generation):
if self.options['DNMtime_dir'] == _cnsts.RIGHT2LEFT:
return generation*self.rect_xdist
elif self.options['DNMtime_dir'] == _cnsts.LEFT2RIGHT:
return -generation*self.rect_xdist
elif self.options['DNMtime_dir'] == _cnsts.TOP2BOTTOM:
return -generation*self.rect_ydist
elif self.options['DNMtime_dir'] == _cnsts.BOTTOM2TOP:
return generation*self.rect_ydist
else:
ErrorDialog(_('Unknown time direction'),self.options['DNMtime_dir'])
return
def add_personal_data(self,f,person,generation,attachment_segment, \
nr_ret_attachment_points=2):
"""
Write the data on the specified person to file.
Arguments:
f Fileobject for output.
person The person to write the data about.
generation Measure of the x-coordinate of the person.
attachment List of coordinates to which the svg-shape is to be glued.
nr_ret_attachment_points Number of attachment points the svg-shape should offer for relatives.
In practice this means an svg-shape is created with an onmouse
event handler that has a JavaScript object as argument packed with
the persons details."""
child_att = []
self.person_srcs = []
self.person_imgs.clear()
self.person_img_srcs = []
person_name = self.escbacka(_nd.display(person))
person_txt = "{person_name:'" + person_name + "'"
self.search_subjects['Name'] = 'person_name'
person_img = self.pack_person_img(person)
if person_img:
person_txt += ',' + person_img
person_url = self.pack_person_url(person)
if person_url:
person_txt += ',' + person_url
event_data = self.pack_event_data(person)
if event_data:
person_txt += "," + event_data
attribute_data = self.pack_attribute_data(person)
if attribute_data:
person_txt += "," + attribute_data
address_data = self.pack_address_data(person)
if address_data:
person_txt += "," + address_data
note_data = self.pack_note_data(person)
if note_data:
person_txt += "," + note_data
source_data = self.pack_source_data(person)
if source_data:
person_txt += "," + source_data
if len(self.person_img_srcs) > 0:
person_txt += "," + "img_sources:['" + \
"','".join(map(self.escbacka,self.person_img_srcs)) + "']"
person_txt = "activate(this," + person_txt + "})"
person_txt = quoteattr(person_txt)
person_name = escape(person_name)
person_gender = ['female','male','unknown'][int(person.get_gender())]
if self.options['DNMchart_type'] == _cnsts.FAN:
path_str,child_att = self.fan_segment(nr_ret_attachment_points, \
*attachment_segment)
f.write("""<path d="%s" class="%s" %s><title>%s</title></path>\n""" % (path_str, \
person_gender, self.mouse_event_handler(person_txt), \
person_name))
elif self.options['DNMchart_type'] == _cnsts.GROWTHSPIRAL:
path_str,child_att = self.growthspiral_segment(\
nr_ret_attachment_points,*attachment_segment)
f.write("""<path d="%s" class="%s" %s><title>%s</title></path>\n""" % (path_str, \
person_gender, self.mouse_event_handler(person_txt), \
person_name))
elif self.options['DNMchart_type'] == _cnsts.MATREE:
path_str,child_att = self.matree_segment(*attachment_segment)
f.write("""<%s class="%s" %s><title>%s</title></rect>\n""" % (path_str, person_gender, \
self.mouse_event_handler(person_txt), person_name))
elif self.options['DNMchart_type'] == _cnsts.PYTREE:
path_str,child_att = self.pytree_segment(*attachment_segment)
f.write("""<path d="%s" class="%s" %s><title>%s</title></path>\n""" % (path_str, \
person_gender, self.mouse_event_handler(person_txt), \
person_name))
else:
if self.options['DNMtime_dir'] < _cnsts.TOP2BOTTOM:
f.write("""<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f"
class="%s" %s><title>%s</title></rect>\n""" % \
(self.generation2coord(generation)-self.rect_width/2.0, attachment_segment[0]-self.rect_height/2.0,\
self.rect_width, self.rect_height, person_gender, \
self.mouse_event_handler(person_txt), person_name))
else:
f.write("""<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f"
class="%s" %s><title>%s</title></rect>\n""" % \
(attachment_segment[0]-self.rect_width/2.0,self.generation2coord(generation)-self.rect_height/2.0,self.rect_width, self.rect_height, person_gender, self.mouse_event_handler(person_txt), person_name))
return child_att
def mouse_event_handler(self,person_txt):
if self.options['DNMclick_over'] == _cnsts.ONCLICK:
return "onclick=%s" % person_txt
else:
return "onmouseover=%s" % person_txt
def pack_person_img(self,person):
"""Return a string that will be part of the JavaScript object
describing the main/portret photo of a person, e.g.:
person_img:{img_path:'...'}"""
rv = ""
if self.options['DNMinc_img']:
plist = [x for x in person.get_media_list() if self.privacy_filter(x)]
if (len(plist) > 0):
media = self.database.get_media_from_handle(\
plist[0].get_reference_handle())
pJS = self.photo2JS(media)
if pJS:
rv += "person_img:" + pJS
return rv
def pack_person_url(self,person):
"""Return a string that will be part of the JavaScript object
describing the main URL of a person, e.g.:
url:{url_path:'...',url_desc:'...'}"""
# Is there any need to support more than 1 uri?
rv = ""
if self.options['DNMinc_url']:
ulist = [x for x in person.get_url_list() if self.privacy_filter]
if len(ulist) > 0:
(type,path) = splittype(ulist[0].get_path())
if not path: return rv
if type:
rv += "url:{url_path:'" + type + ":" + quote(path) + "'"
else:
rv += "url:{url_path:'http://" + quote(path) + "'"
if self.options['DNMinc_url_desc']:
rv += ",url_desc:'" + \
self.escbacka(ulist[0].get_description()) + "'"
self.search_subjects['URL description'] = "url_desc"
if rv:
rv += "}"
return rv
# pack_birth_death_data was moved into pack_event_data, this is just a
# modification of what pack_event_data returns.
def pack_birth_death_data(self,type,event_str):
"""Return a string that will be part of the JavaScript object
describing a birth or death event, e.g.:
birth:{event_type:'...',birth_date:'...',birth_place:'...',
event_witnesses:['...',...],event_img:{...}}
Date and place are treated specially to allow searching on them.
event_str also can contain description, the unpack routine does
nothing with this; event_role should not occur because it is Primary"""
# I set here the search_subjects. I should actually remove
# search_subjects event_date and event_place if there are no such
# events, but I leave it in, gets too complicated.
rv = ''
if event_str:
tmp = event_str.replace(',event_date', ','+type+'_date')
if tmp != event_str:
self.search_subjects[type.capitalize() + ' Date'] = type+'_date'
rv = tmp.replace(',event_place', ','+type+'_place')
if rv != tmp:
self.search_subjects[type.capitalize()+' Place'] = type+'_place'
rv = type + ':{' + rv[1:] + '}'
return rv
def unpack_birth_death_data(self,bir_dea):
"""Return a string that is a JavaScript function suitable to unpack
the birth or death data packed into the person-JS-object."""
JSfunction = """
function %(bd)s2html(person,containerDL) {
if (person.%(bd)s != undefined) {
var eventDT = document.createElement('dt');
eventDT.appendChild(document.createTextNode(person.%(bd)s.event_type + ":"));
containerDL.appendChild(eventDT);
var eventDD = document.createElement('dd');
if (person.%(bd)s.event_imgs != undefined) {
var imgDIV = document.createElement('div');
imgDIV.setAttribute('class','imgTable');
var imgTABLE = document.createElement('table');
var imgTR = document.createElement('tr');
for (var j=0; j<person.%(bd)s.event_imgs.length; j++) {
var imgTD = document.createElement('td');
imgTD.appendChild(photo2html(person.%(bd)s.event_imgs[j]));
imgTR.appendChild(imgTD);
}
imgTABLE.appendChild(imgTR);
imgDIV.appendChild(imgTABLE);
eventDD.appendChild(imgDIV);
}
var event_str = "%(event_format)s"
event_str = replaceSubstring(event_str,"<%(date)s>",
person.%(bd)s.%(bd)s_date);
event_str = replaceSubstring(event_str,"<%(place)s>",
person.%(bd)s.%(bd)s_place);
event_str = replaceSubstring(event_str,"<%(role)s>","");
event_str = replaceSubstring(event_str,"<%(type)s>","");
event_str = replaceSubstring(event_str,"<%(description)s>","");
event_str = replaceSubstring(event_str,"<%(witnesses)s>",
witness_array2string_array(person.%(bd)s.event_witnesses));
if ('event_source' in person.%(bd)s) {
event_str = replaceSubstring(event_str,"<%(source)s>",
person.%(bd)s.event_source.source_page);
if ('source_conf' in person.%(bd)s.event_source) {
var src_cls = '<%(source)s ' + person.%(bd)s.event_source.source_conf + ">";
event_str = event_str.replace('<%(source)s>',src_cls);
}
} else {
event_str = replaceSubstring(event_str,"<%(source)s>","");
}
""" % {'bd':bir_dea, \
'event_format':self.event_format.replace('\n','\\n').replace('"','\\"'),
'date':_('date'), 'place':_('place'), 'role':_('role'), \
'type':_('type'), 'description':_('description'), \
'witnesses':_('witnesses'), 'source':_('source')}
for i in self.event_format_roles:
JSfunction += 'event_str = replaceSubstring(event_str,"<%s>",person.%s.event_%s);' % (i,bir_dea,i.replace(' ','_'))
for i in self.event_format_attributes:
JSfunction += 'event_str = replaceSubstring(event_str,"<%s>",person.%s.event_%s);' % (i,bir_dea,i.replace(' ','_'))
JSfunction += """
event_str2html(event_str,eventDD);
containerDL.appendChild(eventDD);
}
return;
}
"""
return JSfunction
def pack_event_data(self,person):
"""Return a string that will be part of the JavaScript object
describing all events of a person, e.g.:
events:[{event_type:'...',event_date:'...',event_place:'...',
event_desc:'...',event_witnesses:[...],event_img:{...}},...]
Also returns the string needed for birth/death events by calling
pack_birth_death_data."""
# Events are related to person being alive. Some events indicate the
# person is still alive (e.g. witness), others should be suppressed
# when the person is dead (e.g. death-spouse).
rv = ""